diff frontends/src/jp/cmd_blog.py @ 1921:b111f6589da4

jp (blog): drafts are now put in a sub dir per profile + added a security which keep the last 10 drafts before removing them (all profiles included).
author Goffi <goffi@goffi.org>
date Tue, 22 Mar 2016 22:46:05 +0100
parents d3354c80bd1f
children e28c6c4dffa5
line wrap: on
line diff
--- a/frontends/src/jp/cmd_blog.py	Tue Mar 22 22:46:04 2016 +0100
+++ b/frontends/src/jp/cmd_blog.py	Tue Mar 22 22:46:05 2016 +0100
@@ -33,6 +33,7 @@
 import shlex
 import glob
 from sat.tools.common import data_format
+from sat.tools.common import regex
 
 __commands__ = ["Blog"]
 
@@ -62,29 +63,76 @@
 
 URL_REDIRECT_PREFIX = 'url_redirect_'
 INOTIFY_INSTALL = '"pip install inotify"'
+SECURE_UNLINK_MAX = 10 * 2 # we double value has there are 2 files per draft (content and metadata)
+SECURE_UNLINK_DIR = ".backup"
 
 
 class BlogCommon(object):
-    def getTmpDir(self, sat_conf):
+
+    def __init__(self, host):
+        self.host = host
+
+    def getTmpDir(self, sat_conf, sub_dir=None):
         """Return directory used to store temporary files
 
         @param sat_conf(ConfigParser.ConfigParser): instance opened on sat configuration
+        @param sub_dir(str): sub directory where data need to be put
+            profile can be used here, or special directory name
+            sub_dir will be escaped to be usable in path (use regex.pathUnescape to find
+            initial str)
         @return (str): path to the dir
         """
         local_dir = config.getConfig(sat_conf, '', 'local_dir', Exception)
-        return os.path.join(local_dir, BLOG_TMP_DIR)
+        path = [local_dir, BLOG_TMP_DIR]
+        if sub_dir is not None:
+            path.append(regex.pathEscape(sub_dir))
+        return os.path.join(*path)
 
-    def getCurrentFile(self, sat_conf):
+    def getCurrentFile(self, sat_conf, profile):
+        """Get most recently edited file
+
+        @param sat_conf(ConfigParser.ConfigParser): instance opened on sat configuration
+        @param profile(unicode): profile linked to the blog draft
+        @return(str): full path of current file
+        """
         # we guess the blog item currently edited by choosing
         # the most recent file corresponding to temp file pattern
         # in tmp_dir, excluding metadata files
-        tmp_dir = self.getTmpDir(sat_conf)
+        tmp_dir = self.getTmpDir(sat_conf, profile.encode('utf-8'))
         available = [path for path in glob.glob(os.path.join(tmp_dir, 'blog_*')) if not path.endswith(METADATA_SUFF)]
         if not available:
             self.disp(u"Counldn't find any content draft in {path}".format(path=tmp_dir), error=True)
             self.host.quit(1)
         return max(available, key=lambda path: os.stat(path).st_mtime)
 
+    def secureUnlink(self, sat_conf, path):
+        """Unlink given path after keeping it for a while
+
+        This method is used to prevent accidental deletion of a blog draft
+        If there are more file in SECURE_UNLINK_DIR than SECURE_UNLINK_MAX,
+        older file are deleted
+        @param sat_conf(ConfigParser.ConfigParser): instance opened on sat configuration
+        @param path(str): file to unlink
+        """
+        if not os.path.isfile(path):
+            raise OSError(u"path must link to a regular file")
+        backup_dir = self.getTmpDir(sat_conf, SECURE_UNLINK_DIR)
+        if not os.path.exists(backup_dir):
+            os.makedirs(backup_dir)
+        filename = os.path.basename(path)
+        backup_path = os.path.join(backup_dir, filename)
+        # we move file to backup dir
+        self.host.disp(u"Backuping file {src} to {dst}".format(
+            src=path.decode('utf-8'), dst=backup_path.decode('utf-8')), 1)
+        os.rename(path, backup_path)
+        # and if we exceeded the limit, we remove older file
+        backup_files = [os.path.join(backup_dir, f) for f in os.listdir(backup_dir)]
+        if len(backup_files) > SECURE_UNLINK_MAX:
+            backup_files.sort(key=lambda path: os.stat(path).st_mtime)
+            for path in backup_files[:len(backup_files)  - SECURE_UNLINK_MAX]:
+                self.host.disp(u"Purging backup file {}".format(path.decode('utf-8')), 2)
+                os.unlink(path)
+
     def guessSyntaxFromPath(self, sat_conf, path):
         """Return syntax guessed according to filename extension
 
@@ -121,7 +169,8 @@
 class Edit(base.CommandBase, BlogCommon):
 
     def __init__(self, host):
-        super(Edit, self).__init__(host, 'edit', use_verbose=True, help=_(u'edit an existing or new blog post'))
+        base.CommandBase.__init__(self, host, 'edit', use_verbose=True, help=_(u'edit an existing or new blog post'))
+        BlogCommon.__init__(self, self.host)
 
     def add_parser_options(self):
         self.parser.add_argument("item", type=base.unicode_decoder, nargs='?', default=u'new', help=_(u"URL of the item to edit, or keyword"))
@@ -137,7 +186,7 @@
         @param tmp_suff (str): suffix to use for the filename
         @return (tuple(file, str)): opened (w+b) file object and file path
         """
-        tmp_dir = self.getTmpDir(sat_conf)
+        tmp_dir = self.getTmpDir(sat_conf, self.profile.encode('utf-8'))
         if not os.path.exists(tmp_dir):
             try:
                 os.makedirs(tmp_dir)
@@ -283,8 +332,8 @@
                 else:
                     self.disp(u"Blog item published")
 
-            os.unlink(content_file_path)
-            os.unlink(meta_file_path)
+            self.secureUnlink(sat_conf, content_file_path)
+            self.secureUnlink(sat_conf, meta_file_path)
 
     def start(self):
         item_lower = self.args.item.lower()
@@ -319,7 +368,7 @@
             mb_data = None
             if item_lower == 'current':
                 # use wants to continue current draft
-                content_file_path = self.getCurrentFile(sat_conf)
+                content_file_path = self.getCurrentFile(sat_conf, self.profile)
                 self.disp(u'Continuing edition of current draft', 2)
             else:
                 # for now we taxe the item as a file path
@@ -334,7 +383,8 @@
 class Preview(base.CommandBase, BlogCommon):
 
     def __init__(self, host):
-        super(Preview, self).__init__(host, 'preview', use_verbose=True, help=_(u'preview a blog content'))
+        base.CommandBase.__init__(self, host, 'preview', use_verbose=True, help=_(u'preview a blog content'))
+        BlogCommon.__init__(self, self.host)
 
     def add_parser_options(self):
         self.parser.add_argument("--inotify", type=str, choices=('auto', 'true', 'false'), default=u'auto', help=_(u"use inotify to handle preview"))
@@ -421,7 +471,7 @@
 
         # which file do we need to edit?
         if self.args.file == 'current':
-            self.content_file_path = self.getCurrentFile(sat_conf)
+            self.content_file_path = self.getCurrentFile(sat_conf, self.profile)
         else:
             self.content_file_path = os.path.abspath(self.args.file)