]> kaliko git repositories - mpd-sima.git/blob - sima/utils/config.py
Update file access controls
[mpd-sima.git] / sima / utils / config.py
1 # -*- coding: utf-8 -*-
2 # Copyright (c) 2009-2015, 2019-2021 kaliko <kaliko@azylum.org>
3 # Copyright (c) 2019 sacha <sachahony@gmail.com>
4 #
5 #  This file is part of sima
6 #
7 #  sima is free software: you can redistribute it and/or modify
8 #  it under the terms of the GNU General Public License as published by
9 #  the Free Software Foundation, either version 3 of the License, or
10 #  (at your option) any later version.
11 #
12 #  sima is distributed in the hope that it will be useful,
13 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #  GNU General Public License for more details.
16 #
17 #  You should have received a copy of the GNU General Public License
18 #  along with sima.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 # pylint: disable=bad-continuation
21
22 """
23 Deal with configuration and data files.
24 Parse configuration file and set defaults for missing options.
25 """
26
27 # IMPORTS
28 import configparser
29 import logging
30 import sys
31
32 from configparser import Error
33 from os import (access, makedirs, environ, stat, chmod, W_OK)
34 from os.path import (join, isdir, isfile, dirname, exists)
35 from stat import (S_IMODE, ST_MODE, S_IRWXO, S_IRWXG)
36
37 from . import utils
38
39 # DEFAULTS
40 DIRNAME = 'mpd_sima'
41 CONF_FILE = 'mpd_sima.cfg'
42
43 DEFAULT_CONF = {
44         'MPD': {
45             'host': "localhost",
46             #'password': "",
47             'port': 6600,
48             },
49         'sima': {
50             'internal': "Crop, Lastfm, Random",
51             'contrib': "",
52             'user_db': False,
53             'history_duration': 8,
54             'queue_length': 2,
55             'var_dir': 'empty',
56             'musicbrainzid': True,
57             'repeat_disable_queue': True,
58             'single_disable_queue': True,
59             'mopidy_compat': False,
60             },
61         'daemon': {
62             'daemon': False,
63             'pidfile': "",
64             },
65         'log': {
66             'verbosity': "info",
67             'logfile': "",
68             },
69         'crop': {
70             'consume': 10,
71             'priority': 0,
72             },
73         'lastfm': {
74             'queue_mode': "track",  # TODO control values
75             'max_art': 10,
76             'single_album': False,
77             'track_to_add': 1,
78             'album_to_add': 1,
79             'shuffle_album': False,
80             'track_to_add_from_album': 0,  # <=0 means keep all
81             'depth': 1,
82             'cache': True,
83             'priority': 100,
84             },
85         'random': {
86             'flavour': "sensible",  # in pure, sensible
87             'track_to_add': 1,
88             'priority': 50,
89             },
90         'tags': {
91             'comment': "",
92             'date': "",
93             'genre': "",
94             'label': "",
95             'originaldate': "",
96             'filter': "",
97             'queue_mode': "track",
98             'single_album': False,
99             'track_to_add': 1,
100             'album_to_add': 1,
101             'priority': 80,
102             },
103         'genre': {
104             'queue_mode': "track",
105             'single_album': False,
106             'track_to_add': 1,
107             'album_to_add': 1,
108             'priority': 80,
109             },
110         }
111 #
112
113
114 class ConfMan:  # CONFIG MANAGER CLASS
115     """
116     Configuration manager.
117     Default configuration is stored in DEFAULT_CONF dictionnary.
118     First init_config() run to get config from file.
119     Then control_conf() is run and retrieve configuration from defaults if not
120     set in conf files.
121     These settings are then updated with command line options with
122     supersedes_config_with_cmd_line_options().
123
124     Order of priority for the origin of an option is then (lowest to highest):
125         * DEFAULT_CONF
126         * Env. Var for MPD host, port and password
127         * configuration file (overrides previous)
128         * command line options (overrides previous)
129     """
130
131     def __init__(self, options=None):
132         self.log = logging.getLogger('sima')
133         # options settings priority:
134         # defauts < env. var. < conf. file < command line
135         self.conf_file = options.get('conf_file')
136         self.config = configparser.ConfigParser(inline_comment_prefixes='#')
137         self.config.read_dict(DEFAULT_CONF)
138         # update DEFAULT_CONF with env. var.
139         self.use_envar()
140         self.startopt = options
141
142         ## INIT CALLS
143         self.init_config()
144         self.supersedes_config_with_cmd_line_options()
145         # set dbfile
146         self.config['sima']['db_file'] = join(self.config['sima']['var_dir'], 'sima.db')
147         # Controls files access
148         self.control_facc()
149
150         # Create directories
151         data_dir = self.config['sima']['var_dir']
152         if not isdir(data_dir):
153             self.log.trace('Creating "%s"', data_dir)
154             makedirs(data_dir)
155             chmod(data_dir, 0o700)
156
157     def control_facc(self):
158         """Controls file access.
159         This is relevant only for file provided through the configuration file
160         since files provided on the command line are already checked with
161         argparse. Also add config['sima']['db_file'] contructed here in init
162         """
163         ok = True
164         for op, ftochk in [('logfile', self.config.get('log', 'logfile')),
165                            ('pidfile', self.config.get('daemon', 'pidfile')),
166                            ('db file', self.config.get('sima', 'db_file'))]:
167             if not ftochk:
168                 continue
169             if isdir(ftochk):
170                 self.log.critical('Need a file not a directory: "%s"', ftochk)
171                 ok = False
172             if not exists(ftochk):
173                 # Is parent directory writable then
174                 filedir = dirname(ftochk)
175                 if not access(filedir, W_OK):
176                     self.log.critical('no write access to "%s" (%s)', filedir, op)
177                     ok = False
178             else:
179                 if not access(ftochk, W_OK):
180                     self.log.critical('no write access to "%s" (%s)', ftochk, op)
181                     ok = False
182         if not ok:
183             sys.exit(2)
184
185     def control_mod(self):
186         """
187         Controls conf file permissions.
188         """
189         mode = S_IMODE(stat(self.conf_file)[ST_MODE])
190         self.log.debug('file permission is: %o', mode)
191         if mode & S_IRWXO or mode & S_IRWXG:
192             self.log.warning('File is readable by "other" and/or' +
193                              ' "group" (actual permission %o octal).' %
194                              mode)
195             self.log.warning('Consider setting permissions' +
196                              ' to 600 octal.')
197
198     def supersedes_config_with_cmd_line_options(self):
199         """Updates defaults settings with command line options"""
200         for sec in self.config.sections():
201             for opt in self.config.options(sec):
202                 if opt in list(self.startopt.keys()):
203                     self.config.set(sec, opt, str(self.startopt.get(opt)))
204         # honor MPD_HOST format as in mpc(1)  for command line option --host
205         if self.startopt.get('host'):
206             if '@' in self.startopt.get('host'):
207                 passwd, host = self.startopt.get('host').split('@')
208                 self.config.set('MPD', 'password', passwd)
209                 self.config.set('MPD', 'host', host)
210
211     def use_envar(self):
212         """Use MPD en.var. to set defaults"""
213         mpd_host, mpd_port, passwd = utils.get_mpd_environ()
214         if mpd_host:
215             self.log.info('Env. variable MPD_HOST set to "%s"', mpd_host)
216             self.config['MPD'].update(host=mpd_host)
217         if passwd:
218             self.log.info('Env. variable MPD_HOST contains password.')
219             self.config['MPD'].update(password=passwd)
220         if mpd_port:
221             self.log.info('Env. variable MPD_PORT set to "%s".', mpd_port)
222             self.config['MPD'].update(port=mpd_port)
223
224     def init_config(self):
225         """
226         Use XDG directory standard if exists
227         else use "HOME/(.config|.local/share)/sima/"
228         http://standards.freedesktop.org/basedir-spec/basedir-spec-0.6.html
229         """
230
231         homedir = environ.get('HOME')
232
233         if environ.get('XDG_DATA_HOME'):
234             data_dir = join(environ.get('XDG_DATA_HOME'), DIRNAME)
235         elif homedir and isdir(homedir) and homedir not in ['/']:
236             data_dir = join(homedir, '.local', 'share', DIRNAME)
237         else:
238             self.log.critical('Can\'t find a suitable location for data folder (XDG_DATA_HOME)')
239             self.log.critical('Please use "--var-dir" to set a proper location')
240             sys.exit(1)
241
242         if self.startopt.get('conf_file'):
243             # No need to handle conf file location
244             pass
245         elif environ.get('XDG_CONFIG_HOME'):
246             conf_dir = join(environ.get('XDG_CONFIG_HOME'), DIRNAME)
247             self.conf_file = join(conf_dir, CONF_FILE)
248         elif homedir and isdir(homedir) and homedir not in ['/']:
249             conf_dir = join(homedir, '.config', DIRNAME)
250             self.conf_file = join(conf_dir, CONF_FILE)
251         else:
252             self.log.critical('Can\'t find a suitable location for config folder (XDG_CONFIG_HOME)')
253             self.log.critical('Please use "--config" to locate the conf file')
254             sys.exit(1)
255
256         # Sima sqlite DB
257         self.config['sima']['var_dir'] = join(data_dir)
258
259         # If no conf file present, uses defaults
260         if not isfile(self.conf_file):
261             return
262
263         self.log.info('Loading configuration from:  %s', self.conf_file)
264         self.control_mod()
265
266         try:
267             self.config.read(self.conf_file)
268         except Error as err:
269             self.log.error(err)
270             sys.exit(1)
271
272 # VIM MODLINE
273 # vim: ai ts=4 sw=4 sts=4 expandtab