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