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