feeds: Follow the the XDG Base Directory Specification
authorW. Trevor King <wking@tremily.us>
Thu, 10 Jan 2013 15:09:26 +0000 (10:09 -0500)
committerW. Trevor King <wking@tremily.us>
Thu, 10 Jan 2013 15:54:57 +0000 (10:54 -0500)
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 <wking@tremily.us>
CHANGELOG
README
r2e.1
r2e.bat
rss2email/feeds.py

index eceb25ddfbee6dd2ea5aaa42b567e259f3377d3c..b3cb20dedc354367e89d820b56fdca81875b0979 100644 (file)
--- 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 e3de032b1534c0ef458a2470737d7923d44fb9c8..e8084b204495c555055d2f24ee9f6a80cbeeb200 100644 (file)
--- 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 4cd9baade6f342a9b7443882c22be75aff3e7967..879ff2bb798967f18f999618e41ee2ba682707bd 100644 (file)
--- 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<path>\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<path>\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<path>\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 e41fac2d45e4cd1bcc1a16ceb076f85b9a2cd391..ea2a20bb27583d553f075777b0caaee5add3a804 100755 (executable)
--- 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
index a61e6474b40105822b05992b2d73a0f92c7fccff..c73c29f499c9984d6a93eccff9ee378e9b4ecd44 100644 (file)
@@ -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: