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).

This commit is contained in:
Goffi 2016-03-22 22:46:05 +01:00
commit 2fd6fa663c

View file

@ -33,6 +33,7 @@ import subprocess
import shlex
import glob
from sat.tools.common import data_format
from sat.tools.common import regex
__commands__ = ["Blog"]
@ -62,29 +63,76 @@ KEY_TO_REMOVE_METADATA = ('id','content', 'content_xhtml', 'comments_node', 'com
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 BlogCommon(object):
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 @@ class Edit(base.CommandBase, BlogCommon):
@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 @@ class Edit(base.CommandBase, BlogCommon):
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 @@ class Edit(base.CommandBase, BlogCommon):
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 Edit(base.CommandBase, BlogCommon):
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 @@ class Preview(base.CommandBase, BlogCommon):
# 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)