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