From b8e180e1f0d63ef8db61085df5a6957cde855183 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Thu, 10 Jan 2013 10:09:26 -0500 Subject: [PATCH] feeds: Follow the the XDG Base Directory Specification This splits config files and data files into different directories (by default), so we no longer need an rss2email subdirectory (we only have one config file and one data file). The default config file is now ~/.config/rss2email.cfg and the default data file is now ~/.local/share/rss2email.json. Signed-off-by: W. Trevor King --- CHANGELOG | 3 +- README | 11 +++---- r2e.1 | 50 ++++++++++++++++++++++++------- r2e.bat | 2 +- rss2email/feeds.py | 75 +++++++++++++++++++++++++++++++++++++--------- 5 files changed, 109 insertions(+), 32 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index eceb25d..b3cb20d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ v3.0 (unreleased) * Changed project email (rss2email@tremily.us) and homepage (http://github.com/wking/rss2email) - * Split static configuration parameters into a ConfigParser-read config file. Only data that depends on the feed state is still pickled. + * Split static configuration parameters into a ConfigParser-read config file (rss2email.cfg). Data that depends on the feed state is recorded using JSON (rss2email.json). + * Use the XDG Base Directory Specification for standardized configuration and data file locations. * Converted the command line interface to argparse, with some restructuring along the way. * Added the r2e.1 man page (based on one from the Debian package). * Added setup.py and a PyPI page for simpler installation (http://pypi.python.org/pypi/rss2email) diff --git a/README b/README index e3de032..e8084b2 100644 --- a/README +++ b/README @@ -86,8 +86,8 @@ Upgrading to a new version Just repeat the installation procedure for the new source package. If your config file and data file were in the old source directory, move them over to the new source directory. If the config and data files -were in another directory (e.g. ``~/.config/rss2email/``), there is no -need to move them. +were in another directory (e.g. ``~/.config`` and ``~/.local/share``), +there is no need to move them. Using rss2email =============== @@ -97,10 +97,11 @@ Create a new feed database to send updates to your email address:: $ r2e new you@yourdomain.com This command will create a configuration file -(``~/.config/rss2email/config`` by default) and a feed database -(``~/.config/rss2email/feeds.dat`` by default). If you'd rather those +(``$XDG_CONFIG_HOME/rss2email.cfs`` by default) and a feed database +(``$XDG_DATA_HOME/rss2email.json`` by default). If you'd rather those files were stored in other locations, use the ``--config`` and -``--data`` options. +``--data`` options. ``XDG_CONFIG_HOME`` defaults to ``$HOME/.config`` +and ``XDG_DATA_HOME`` defaults to ``$HOME/.local/share``. You should edit the default configuration file now to adjust rss2email for your local system. Unless you've installed a local diff --git a/r2e.1 b/r2e.1 index 4cd9baa..879ff2b 100644 --- a/r2e.1 +++ b/r2e.1 @@ -29,12 +29,14 @@ Print the rss2email help and exit. Print the rss2email version and exit. .TP \-c, \-\-config \fI\fR -The program is configured by ~/\&.config/rss2mail/config by -default. Use this option to set a different config file. +The program configuration is read from $XDG_CONFIG_HOME/rss2mail.cfg +by default (see also FILES and ENVIRONMENT VARIABLES below). Use this +option to set a different configuration file. .TP \-d, \-\-data \fI\fR -The program is configured by ~/\&.config/rss2mail/config by -default. Use this option to set a different config file. +Dynamic program data is read from $XDG_DATA_HOME/rss2mail\&.json by +default (see also FILES and ENVIRONMENT VARIABLES below). Use this +option to set a different data file. .TP \-V, \-\-verbose Increment the logging verbosity. @@ -99,9 +101,10 @@ data will be written. If \fI\fR is not given \fBr2e\fR writes the data to stdout. .SH "CONFIGURATION" The program's behavior can be controlled via the -~/.config/rss2email/config file. The file format is similar to a -Microsoft Windows INI file. It is parsed by Python's ConfigParser -class, so see the Python documentation at +$XDG_CONFIG_HOME/rss2email.cfg (see also FILES and ENVIRONMENT +VARIABLES below). The file format is similar to a Microsoft Windows +INI file. It is parsed by Python's ConfigParser class, so see the +Python documentation at http://docs\&.python\&.org/py3k/library/configparser\&.html for format details. .P @@ -132,13 +135,38 @@ friendly-name = False .RE .P .SH FILES +.TP 4 +.B $XDG_CONFIG_HOME/rss2email.cfg +If this file exists, it it read to configure the program. .TP -.B ~/.rss2email/feeds\&.dat +.B $XDG_DATA_HOME/rss2email\&.json The database of feeds. Use \fBr2e\fR to add, remove, or modify feeds, do not edit it directly. -.TP -.B ~/.rss2email/config\&.py -If this file exists, it it read to configure the program. +.SH "ENVIRONMENT VARIABLES" +The environment variables used by \fBr2e\fR are all defined in the XDG +Base Directory Specification, which aims to standardize locations for +user-specific configuration and data files. +.TP 4 +.B XDG_CONFIG_HOME +The preferred directory for configuration files. Defaults to +$HOME/\&.config. +.TP +.B XDG_DATA_HOME +The preferred directory for data files. Defaults to +$HOME/\&.local/share. +.TP +.B XDG_CONFIG_DIRS +A colon ':' separated, preference ordered list of base directories for +configuration files in addition to $XDG_CONFIG_HOME. Defaults to +/etc/xdg. If multiple configuration files are found in this path, +they will all be read by the ConfigParser class (see also +CONFIGURATION above). +.TP +.B XDG_DATA_DIRS +A colon ':' separated, preference ordered list of base directories for +data files. Defaults to /usr/local/share/:/usr/share/. Only the +first matching file is used. +.B .SH AUTHORS rss2email was started by Aaron Swartz, and is currently maintained by Lindsey Smith. For a more complete list of contributors, see the diff --git a/r2e.bat b/r2e.bat index e41fac2..ea2a20b 100755 --- a/r2e.bat +++ b/r2e.bat @@ -1 +1 @@ -@python3 -m rss2email.main -c config -d feeds.dat %1 %2 %3 %4 %5 %6 %7 %8 %9 +@python3 -m rss2email.main -c rss2email.cfg -d rss2email.json %1 %2 %3 %4 %5 %6 %7 %8 %9 diff --git a/rss2email/feeds.py b/rss2email/feeds.py index a61e647..c73c29f 100644 --- a/rss2email/feeds.py +++ b/rss2email/feeds.py @@ -49,6 +49,10 @@ except: pass +# Path to the filesystem root, '/' on POSIX.1 (IEEE Std 1003.1-2008). +ROOT_PATH = _os.path.splitdrive(_sys.executable)[0] or _os.sep + + class Feeds (list): """Utility class for rss2email activity. @@ -61,7 +65,7 @@ class Feeds (list): Setup a temporary directory to load. >>> tmpdir = tempfile.TemporaryDirectory(prefix='rss2email-test-') - >>> configfile = os.path.join(tmpdir.name, 'config') + >>> configfile = os.path.join(tmpdir.name, 'rss2email.cfg') >>> with open(configfile, 'w') as f: ... count = f.write('[DEFAULT]\\n') ... count = f.write('to = a@b.com\\n') @@ -70,7 +74,7 @@ class Feeds (list): ... count = f.write('to = x@y.net\\n') ... count = f.write('[feed.f2]\\n') ... count = f.write('url = http://b.com/rss.atom\\n') - >>> datafile = os.path.join(tmpdir.name, 'feeds.dat') + >>> datafile = os.path.join(tmpdir.name, 'rss2email.json') >>> with codecs.open(datafile, 'w', Feeds.datafile_encoding) as f: ... json.dump({ ... 'version': 1, @@ -124,18 +128,14 @@ class Feeds (list): datafile_version = 1 datafile_encoding = 'utf-8' - def __init__(self, configdir=None, datafile=None, configfiles=None, - config=None): + def __init__(self, configfiles=None, datafile=None, config=None): super(Feeds, self).__init__() - if configdir is None: - configdir = _os.path.expanduser(_os.path.join( - '~', '.config', 'rss2email')) - if datafile is None: - datafile = _os.path.join(configdir, 'feeds.dat') - self.datafile = datafile if configfiles is None: - configfiles = [_os.path.join(configdir, 'config')] + configfiles = self._get_configfiles() self.configfiles = configfiles + if datafile is None: + datafile = self._get_datafile() + self.datafile = datafile if config is None: config = _config.CONFIG self.config = config @@ -185,13 +185,57 @@ class Feeds (list): while self: self.pop(0) + def _get_configfiles(self): + """Get configuration file paths + + Following the XDG Base Directory Specification. + """ + config_home = _os.environ.get( + 'XDG_CONFIG_HOME', + _os.path.expanduser(_os.path.join('~', '.config'))) + config_dirs = [config_home] + config_dirs.extend( + _os.environ.get( + 'XDG_CONFIG_DIRS', + _os.path.join(ROOT_PATH, 'etc', 'xdg'), + ).split(':')) + # reverse because ConfigParser wants most significan last + return list(reversed( + [_os.path.join(config_dir, 'rss2email.cfg') + for config_dir in config_dirs])) + + def _get_datafile(self): + """Get the data file path + + Following the XDG Base Directory Specification. + """ + data_home = _os.environ.get( + 'XDG_DATA_HOME', + _os.path.expanduser(_os.path.join('~', '.local', 'share'))) + data_dirs = [data_home] + data_dirs.extend( + _os.environ.get( + 'XDG_DATA_DIRS', + ':'.join([ + _os.path.join(ROOT_PATH, 'usr', 'local', 'share'), + _os.path.join(ROOT_PATH, 'usr', 'share'), + ]), + ).split(':')) + datafiles = [_os.path.join(data_dir, 'rss2email.json') + for data_dir in data_dirs] + for datafile in datafiles: + if _os.path.isfile(datafile): + return datafile + return datafiles[0] + def load(self, lock=True, require=False): _LOG.debug('load feed configuration from {}'.format(self.configfiles)) if self.configfiles: self.read_configfiles = self.config.read(self.configfiles) else: self.read_configfiles = [] - _LOG.debug('loaded confguration from {}'.format(self.read_configfiles)) + _LOG.debug('loaded configuration from {}'.format( + self.read_configfiles)) self._load_feeds(lock=lock, require=require) def _load_feeds(self, lock, require): @@ -201,6 +245,9 @@ class Feeds (list): raise _error.NoDataFile(feeds=self) _LOG.info('feed data file not found at {}'.format(self.datafile)) _LOG.debug('creating an empty data file') + dirname = _os.path.dirname(self.datafile) + if dirname and not _os.path.isdir(dirname): + _os.makedirs(dirname, mode=0o700, exist_ok=True) with _codecs.open(self.datafile, 'w', self.datafile_encoding) as f: self._save_feed_states(feeds=[], stream=f) try: @@ -284,7 +331,7 @@ class Feeds (list): feed.save_to_config() dirname = _os.path.dirname(self.configfiles[-1]) if dirname and not _os.path.isdir(dirname): - _os.makedirs(dirname) + _os.makedirs(dirname, mode=0o700, exist_ok=True) with open(self.configfiles[-1], 'w') as f: self.config.write(f) self._save_feeds() @@ -293,7 +340,7 @@ class Feeds (list): _LOG.debug('save feed data to {}'.format(self.datafile)) dirname = _os.path.dirname(self.datafile) if dirname and not _os.path.isdir(dirname): - _os.makedirs(dirname) + _os.makedirs(dirname, mode=0o700, exist_ok=True) if UNIX: tmpfile = self.datafile + '.tmp' with _codecs.open(tmpfile, 'w', self.datafile_encoding) as f: -- 2.26.2