]> kaliko git repositories - mpd-sima.git/blob - sima/utils/config.py
Honor MPC password/host format on command line option 'host'
[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             'repeat_disable_queue': "true",
57             'single_disable_queue': "true",
58             },
59         'daemon':{
60             'daemon': False,
61             'pidfile': "",
62             },
63         'log': {
64             'verbosity': "info",
65             'logfile': "",
66             },
67         'crop': {
68             'consume': 10,
69             'priority': 0,
70             },
71         'lastfm': {
72             'queue_mode': "track", #TODO control values
73             'max_art': 10,
74             'single_album': "false",
75             'track_to_add': 1,
76             'album_to_add': 1,
77             'depth': 1,
78             'cache': True,
79             'priority': 100,
80             },
81         'random': {
82             'flavour': "sensible", # in pure, sensible
83             'track_to_add': 1,
84             'priority': 50,
85             },
86         }
87 #
88
89
90 class ConfMan(object):  # CONFIG MANAGER CLASS
91     """
92     Configuration manager.
93     Default configuration is stored in DEFAULT_CONF dictionnary.
94     First init_config() run to get config from file.
95     Then control_conf() is run and retrieve configuration from defaults if not
96     set in conf files.
97     These settings are then updated with command line options with
98     supersedes_config_with_cmd_line_options().
99
100     Order of priority for the origin of an option is then (lowest to highest):
101         * DEFAULT_CONF
102         * Env. Var for MPD host, port and password
103         * configuration file (overrides previous)
104         * command line options (overrides previous)
105     """
106
107     def __init__(self, options=None):
108         self.log = logging.getLogger('sima')
109         # options settings priority:
110         # defauts < env. var. < conf. file < command line
111         self.conf_file = options.get('conf_file')
112         self.config = configparser.ConfigParser(inline_comment_prefixes='#')
113         self.config.read_dict(DEFAULT_CONF)
114         # update DEFAULT_CONF with env. var.
115         self.use_envar()
116         self.startopt = options
117
118         ## INIT CALLS
119         self.init_config()
120         self.supersedes_config_with_cmd_line_options()
121         # Controls files access
122         self.control_facc()
123         # set dbfile
124         self.config['sima']['db_file'] = join(self.config['sima']['var_dir'], 'sima.db')
125
126         # Create directories
127         data_dir = self.config['sima']['var_dir']
128         if not isdir(data_dir):
129             self.log.trace('Creating "{}"'.format(data_dir))
130             makedirs(data_dir)
131             chmod(data_dir, 0o700)
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 [('logfile', self.config.get('log', 'logfile')),
141                            ('pidfile', self.config.get('daemon', 'pidfile')),]:
142             if not ftochk:
143                 continue
144             if isdir(ftochk):
145                 self.log.critical('Need a file not a directory: "%s"', 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 "%s" (%s)', filedir, op)
152                     ok = False
153             else:
154                 if not access(ftochk, W_OK):
155                     self.log.critical('no write access to "%s" (%s)', 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: %s', 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         # honor MPD_HOST format as in mpc(1)  for command line option --host
182         if self.startopt.get('host'):
183             if '@' in self.startopt.get('host'):
184                 print(self.startopt.get('host').split('@'))
185                 passwd, host = self.startopt.get('host').split('@')
186                 self.config.set('MPD', 'password', passwd)
187                 self.config.set('MPD', 'host', host)
188
189     def use_envar(self):
190         """Use MPD en.var. to set defaults"""
191         mpd_host, mpd_port, passwd = utils.get_mpd_environ()
192         if mpd_host:
193             self.log.info('Env. variable MPD_HOST set to "%s"', mpd_host)
194             self.config['MPD'].update(host=mpd_host)
195         if passwd:
196             self.log.info('Env. variable MPD_HOST contains password.')
197             self.config['MPD'].update(password=passwd)
198         if mpd_port:
199             self.log.info('Env. variable MPD_PORT set to "%s".', mpd_port)
200             self.config['MPD'].update(port=mpd_port)
201
202     def init_config(self):
203         """
204         Use XDG directory standard if exists
205         else use "HOME/(.config|.local/share)/sima/"
206         http://standards.freedesktop.org/basedir-spec/basedir-spec-0.6.html
207         """
208
209         homedir = environ.get('HOME')
210
211         if environ.get('XDG_DATA_HOME'):
212             data_dir = join(environ.get('XDG_DATA_HOME'), DIRNAME)
213         elif homedir and isdir(homedir) and homedir not in ['/']:
214             data_dir = join(homedir, '.local', 'share', DIRNAME)
215         else:
216             self.log.critical('Can\'t find a suitable location for data folder (XDG_DATA_HOME)')
217             self.log.critical('Please use "--var-dir" to set a proper location')
218             sys.exit(1)
219
220         if self.startopt.get('conf_file'):
221             # No need to handle conf file location
222             pass
223         elif environ.get('XDG_CONFIG_HOME'):
224             conf_dir = join(environ.get('XDG_CONFIG_HOME'), DIRNAME)
225         elif homedir and isdir(homedir) and homedir not in ['/']:
226             conf_dir = join(homedir, '.config', DIRNAME)
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