]> kaliko git repositories - mpd-sima.git/blob - sima/utils/config.py
f7ec982f61096579f45982d4f9d5bab4dd46ec36
[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 #
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, R_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         'echonest': {
70             'queue_mode': "track", #TODO control values
71             'max_art': 15,
72             'single_album': "false",
73             'track_to_add': 1,
74             'album_to_add': 1,
75             'depth': 1,
76             'priority': 100,
77             },
78         'lastfm': {
79             'queue_mode': "track", #TODO control values
80             'max_art': 10,
81             'single_album': "false",
82             'track_to_add': 1,
83             'album_to_add': 1,
84             'depth': 1,
85             'cache': True,
86             'priority': 100,
87             },
88         'random': {
89             'flavour': "sensible", # in pure, sensible
90             'track_to_add': 1,
91             'priority': 50,
92             },
93         }
94 #
95
96
97 class ConfMan(object):  # CONFIG MANAGER CLASS
98     """
99     Configuration manager.
100     Default configuration is stored in DEFAULT_CONF dictionnary.
101     First init_config() run to get config from file.
102     Then control_conf() is run and retrieve configuration from defaults if not
103     set in conf files.
104     These settings are then updated with command line options with
105     supersedes_config_with_cmd_line_options().
106
107     Order of priority for the origin of an option is then (lowest to highest):
108         * DEFAULT_CONF
109         * Env. Var for MPD host, port and password
110         * configuration file (overrides previous)
111         * command line options (overrides previous)
112     """
113
114     def __init__(self, options=None):
115         self.log = logging.getLogger('sima')
116         # options settings priority:
117         # defauts < env. var. < conf. file < command line
118         self.conf_file = options.get('conf_file')
119         self.config = configparser.ConfigParser(inline_comment_prefixes='#')
120         self.config.read_dict(DEFAULT_CONF)
121         # update DEFAULT_CONF with env. var.
122         self.use_envar()
123         self.startopt = options
124
125         ## INIT CALLS
126         self.init_config()
127         self.supersedes_config_with_cmd_line_options()
128         # Controls files access
129         self.control_facc()
130         # generate dbfile
131         self.config['sima']['db_file'] = join(self.config['sima']['var_dir'], 'sima.db')
132
133     def control_facc(self):
134         """Controls file access.
135         This is relevant only for file provided through the configuration file
136         since files provided on the command line are already checked with
137         argparse.
138         """
139         ok = True
140         for op, ftochk in [('log', self.config['log']['logfile']),
141                            ('pidfile', self.config['daemon']['pidfile']),]:
142             if not ftochk:
143                 continue
144             if isdir(ftochk):
145                 self.log.critical('Need a file not a directory: "{}"'.format(ftochk))
146                 ok = False
147             if not exists(ftochk):
148                 # Is parent directory writable then
149                 filedir = dirname(ftochk)
150                 if not access(filedir, W_OK):
151                     self.log.critical('no write access to "{0}" ({1})'.format(filedir, op))
152                     ok = False
153             else:
154                 if not access(ftochk, W_OK):
155                     self.log.critical('no write access to "{0}" ({1})'.format(ftochk, op))
156                     ok = False
157         if not ok:
158             if exists(self.conf_file):
159                 self.log.warning('Try to check the configuration file: {}'.format(self.conf_file))
160             sys.exit(2)
161
162     def control_mod(self):
163         """
164         Controls conf file permissions.
165         """
166         mode = S_IMODE(stat(self.conf_file)[ST_MODE])
167         self.log.debug('file permission is: %o' % mode)
168         if mode & S_IRWXO or mode & S_IRWXG:
169             self.log.warning('File is readable by "other" and/or' +
170                              ' "group" (actual permission %o octal).' %
171                              mode)
172             self.log.warning('Consider setting permissions' +
173                              ' to 600 octal.')
174
175     def supersedes_config_with_cmd_line_options(self):
176         """Updates defaults settings with command line options"""
177         for sec in self.config.sections():
178             for opt in self.config.options(sec):
179                 if opt in list(self.startopt.keys()):
180                     self.config.set(sec, opt, str(self.startopt.get(opt)))
181
182     def use_envar(self):
183         """Use MPD en.var. to set defaults"""
184         mpd_host, mpd_port, passwd = utils.get_mpd_environ()
185         if mpd_host:
186             self.log.info('Env. variable MPD_HOST set to "%s"' % mpd_host)
187             self.config['MPD'].update(host=mpd_host)
188         if passwd:
189             self.log.info('Env. variable MPD_HOST contains password.')
190             self.config['MPD'].update(password=passwd)
191         if mpd_port:
192             self.log.info('Env. variable MPD_PORT set to "%s".' % mpd_port)
193             self.config['MPD'].update(port=mpd_port)
194
195     def init_config(self):
196         """
197         Use XDG directory standard if exists
198         else use "HOME/(.config|.local/share)/sima/"
199         http://standards.freedesktop.org/basedir-spec/basedir-spec-0.6.html
200         """
201
202         homedir = environ.get('HOME')
203
204         if environ.get('XDG_DATA_HOME'):
205             data_dir = join(environ.get('XDG_DATA_HOME'), DIRNAME)
206         elif homedir and isdir(homedir) and homedir not in ['/']:
207             data_dir = join(homedir, '.local', 'share', DIRNAME)
208         else:
209             self.log.error('Can\'t find a suitable location for data folder (XDG_DATA_HOME)')
210             self.log.error('Please use "--var_dir" to set a proper location')
211             sys.exit(1)
212
213         if not isdir(data_dir):
214             makedirs(data_dir)
215             chmod(data_dir, 0o700)
216
217         if self.startopt.get('conf_file'):
218             # No need to handle conf file location
219             pass
220         elif environ.get('XDG_CONFIG_HOME'):
221             conf_dir = join(environ.get('XDG_CONFIG_HOME'), DIRNAME)
222         elif homedir and isdir(homedir) and homedir not in ['/']:
223             conf_dir = join(homedir, '.config', DIRNAME)
224             # Create conf_dir if necessary
225             if not isdir(conf_dir):
226                 makedirs(conf_dir)
227                 chmod(conf_dir, 0o700)
228             self.conf_file = join(conf_dir, CONF_FILE)
229         else:
230             self.log.critical('Can\'t find a suitable location for config folder (XDG_CONFIG_HOME)')
231             self.log.critical('Please use "--config" to locate the conf file')
232             sys.exit(1)
233
234         ## Sima sqlite DB
235         self.config['sima']['var_dir'] = join(data_dir)
236
237         # If no conf file present, uses defaults
238         if not isfile(self.conf_file):
239             return
240
241         self.log.info('Loading configuration from:  %s' % self.conf_file)
242         self.control_mod()
243
244         try:
245             self.config.read(self.conf_file)
246         except Error as err:
247             self.log.error(err)
248             sys.exit(1)
249
250 # VIM MODLINE
251 # vim: ai ts=4 sw=4 sts=4 expandtab