Mercurial > libervia-backend
comparison 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 |
comparison
equal
deleted
inserted
replaced
1920:03526c8abeb0 | 1921:b111f6589da4 |
---|---|
31 import tempfile | 31 import tempfile |
32 import subprocess | 32 import subprocess |
33 import shlex | 33 import shlex |
34 import glob | 34 import glob |
35 from sat.tools.common import data_format | 35 from sat.tools.common import data_format |
36 from sat.tools.common import regex | |
36 | 37 |
37 __commands__ = ["Blog"] | 38 __commands__ = ["Blog"] |
38 | 39 |
39 # extensions to use with known syntaxes | 40 # extensions to use with known syntaxes |
40 SYNTAX_EXT = { | 41 SYNTAX_EXT = { |
60 # key to remove from metadata tmp file if they exist | 61 # key to remove from metadata tmp file if they exist |
61 KEY_TO_REMOVE_METADATA = ('id','content', 'content_xhtml', 'comments_node', 'comments_service', 'updated') | 62 KEY_TO_REMOVE_METADATA = ('id','content', 'content_xhtml', 'comments_node', 'comments_service', 'updated') |
62 | 63 |
63 URL_REDIRECT_PREFIX = 'url_redirect_' | 64 URL_REDIRECT_PREFIX = 'url_redirect_' |
64 INOTIFY_INSTALL = '"pip install inotify"' | 65 INOTIFY_INSTALL = '"pip install inotify"' |
66 SECURE_UNLINK_MAX = 10 * 2 # we double value has there are 2 files per draft (content and metadata) | |
67 SECURE_UNLINK_DIR = ".backup" | |
65 | 68 |
66 | 69 |
67 class BlogCommon(object): | 70 class BlogCommon(object): |
68 def getTmpDir(self, sat_conf): | 71 |
72 def __init__(self, host): | |
73 self.host = host | |
74 | |
75 def getTmpDir(self, sat_conf, sub_dir=None): | |
69 """Return directory used to store temporary files | 76 """Return directory used to store temporary files |
70 | 77 |
71 @param sat_conf(ConfigParser.ConfigParser): instance opened on sat configuration | 78 @param sat_conf(ConfigParser.ConfigParser): instance opened on sat configuration |
79 @param sub_dir(str): sub directory where data need to be put | |
80 profile can be used here, or special directory name | |
81 sub_dir will be escaped to be usable in path (use regex.pathUnescape to find | |
82 initial str) | |
72 @return (str): path to the dir | 83 @return (str): path to the dir |
73 """ | 84 """ |
74 local_dir = config.getConfig(sat_conf, '', 'local_dir', Exception) | 85 local_dir = config.getConfig(sat_conf, '', 'local_dir', Exception) |
75 return os.path.join(local_dir, BLOG_TMP_DIR) | 86 path = [local_dir, BLOG_TMP_DIR] |
76 | 87 if sub_dir is not None: |
77 def getCurrentFile(self, sat_conf): | 88 path.append(regex.pathEscape(sub_dir)) |
89 return os.path.join(*path) | |
90 | |
91 def getCurrentFile(self, sat_conf, profile): | |
92 """Get most recently edited file | |
93 | |
94 @param sat_conf(ConfigParser.ConfigParser): instance opened on sat configuration | |
95 @param profile(unicode): profile linked to the blog draft | |
96 @return(str): full path of current file | |
97 """ | |
78 # we guess the blog item currently edited by choosing | 98 # we guess the blog item currently edited by choosing |
79 # the most recent file corresponding to temp file pattern | 99 # the most recent file corresponding to temp file pattern |
80 # in tmp_dir, excluding metadata files | 100 # in tmp_dir, excluding metadata files |
81 tmp_dir = self.getTmpDir(sat_conf) | 101 tmp_dir = self.getTmpDir(sat_conf, profile.encode('utf-8')) |
82 available = [path for path in glob.glob(os.path.join(tmp_dir, 'blog_*')) if not path.endswith(METADATA_SUFF)] | 102 available = [path for path in glob.glob(os.path.join(tmp_dir, 'blog_*')) if not path.endswith(METADATA_SUFF)] |
83 if not available: | 103 if not available: |
84 self.disp(u"Counldn't find any content draft in {path}".format(path=tmp_dir), error=True) | 104 self.disp(u"Counldn't find any content draft in {path}".format(path=tmp_dir), error=True) |
85 self.host.quit(1) | 105 self.host.quit(1) |
86 return max(available, key=lambda path: os.stat(path).st_mtime) | 106 return max(available, key=lambda path: os.stat(path).st_mtime) |
107 | |
108 def secureUnlink(self, sat_conf, path): | |
109 """Unlink given path after keeping it for a while | |
110 | |
111 This method is used to prevent accidental deletion of a blog draft | |
112 If there are more file in SECURE_UNLINK_DIR than SECURE_UNLINK_MAX, | |
113 older file are deleted | |
114 @param sat_conf(ConfigParser.ConfigParser): instance opened on sat configuration | |
115 @param path(str): file to unlink | |
116 """ | |
117 if not os.path.isfile(path): | |
118 raise OSError(u"path must link to a regular file") | |
119 backup_dir = self.getTmpDir(sat_conf, SECURE_UNLINK_DIR) | |
120 if not os.path.exists(backup_dir): | |
121 os.makedirs(backup_dir) | |
122 filename = os.path.basename(path) | |
123 backup_path = os.path.join(backup_dir, filename) | |
124 # we move file to backup dir | |
125 self.host.disp(u"Backuping file {src} to {dst}".format( | |
126 src=path.decode('utf-8'), dst=backup_path.decode('utf-8')), 1) | |
127 os.rename(path, backup_path) | |
128 # and if we exceeded the limit, we remove older file | |
129 backup_files = [os.path.join(backup_dir, f) for f in os.listdir(backup_dir)] | |
130 if len(backup_files) > SECURE_UNLINK_MAX: | |
131 backup_files.sort(key=lambda path: os.stat(path).st_mtime) | |
132 for path in backup_files[:len(backup_files) - SECURE_UNLINK_MAX]: | |
133 self.host.disp(u"Purging backup file {}".format(path.decode('utf-8')), 2) | |
134 os.unlink(path) | |
87 | 135 |
88 def guessSyntaxFromPath(self, sat_conf, path): | 136 def guessSyntaxFromPath(self, sat_conf, path): |
89 """Return syntax guessed according to filename extension | 137 """Return syntax guessed according to filename extension |
90 | 138 |
91 @param sat_conf(ConfigParser.ConfigParser): instance opened on sat configuration | 139 @param sat_conf(ConfigParser.ConfigParser): instance opened on sat configuration |
119 | 167 |
120 | 168 |
121 class Edit(base.CommandBase, BlogCommon): | 169 class Edit(base.CommandBase, BlogCommon): |
122 | 170 |
123 def __init__(self, host): | 171 def __init__(self, host): |
124 super(Edit, self).__init__(host, 'edit', use_verbose=True, help=_(u'edit an existing or new blog post')) | 172 base.CommandBase.__init__(self, host, 'edit', use_verbose=True, help=_(u'edit an existing or new blog post')) |
173 BlogCommon.__init__(self, self.host) | |
125 | 174 |
126 def add_parser_options(self): | 175 def add_parser_options(self): |
127 self.parser.add_argument("item", type=base.unicode_decoder, nargs='?', default=u'new', help=_(u"URL of the item to edit, or keyword")) | 176 self.parser.add_argument("item", type=base.unicode_decoder, nargs='?', default=u'new', help=_(u"URL of the item to edit, or keyword")) |
128 self.parser.add_argument("-P", "--preview", action="store_true", help=_(u"launch a blog preview in parallel")) | 177 self.parser.add_argument("-P", "--preview", action="store_true", help=_(u"launch a blog preview in parallel")) |
129 self.parser.add_argument("-T", '--title', type=base.unicode_decoder, help=_(u"title of the item")) | 178 self.parser.add_argument("-T", '--title', type=base.unicode_decoder, help=_(u"title of the item")) |
135 | 184 |
136 @param sat_conf(ConfigParser.ConfigParser): instance opened on sat configuration | 185 @param sat_conf(ConfigParser.ConfigParser): instance opened on sat configuration |
137 @param tmp_suff (str): suffix to use for the filename | 186 @param tmp_suff (str): suffix to use for the filename |
138 @return (tuple(file, str)): opened (w+b) file object and file path | 187 @return (tuple(file, str)): opened (w+b) file object and file path |
139 """ | 188 """ |
140 tmp_dir = self.getTmpDir(sat_conf) | 189 tmp_dir = self.getTmpDir(sat_conf, self.profile.encode('utf-8')) |
141 if not os.path.exists(tmp_dir): | 190 if not os.path.exists(tmp_dir): |
142 try: | 191 try: |
143 os.makedirs(tmp_dir) | 192 os.makedirs(tmp_dir) |
144 except OSError as e: | 193 except OSError as e: |
145 self.disp(u"Can't create {path} directory: {reason}".format( | 194 self.disp(u"Can't create {path} directory: {reason}".format( |
281 content_path=content_file_path, meta_path=meta_file_path, reason=e), error=True) | 330 content_path=content_file_path, meta_path=meta_file_path, reason=e), error=True) |
282 self.host.quit(1) | 331 self.host.quit(1) |
283 else: | 332 else: |
284 self.disp(u"Blog item published") | 333 self.disp(u"Blog item published") |
285 | 334 |
286 os.unlink(content_file_path) | 335 self.secureUnlink(sat_conf, content_file_path) |
287 os.unlink(meta_file_path) | 336 self.secureUnlink(sat_conf, meta_file_path) |
288 | 337 |
289 def start(self): | 338 def start(self): |
290 item_lower = self.args.item.lower() | 339 item_lower = self.args.item.lower() |
291 sat_conf = config.parseMainConf() | 340 sat_conf = config.parseMainConf() |
292 # if there are user defined extension, we use them | 341 # if there are user defined extension, we use them |
317 content_file_obj.seek(0) | 366 content_file_obj.seek(0) |
318 else: | 367 else: |
319 mb_data = None | 368 mb_data = None |
320 if item_lower == 'current': | 369 if item_lower == 'current': |
321 # use wants to continue current draft | 370 # use wants to continue current draft |
322 content_file_path = self.getCurrentFile(sat_conf) | 371 content_file_path = self.getCurrentFile(sat_conf, self.profile) |
323 self.disp(u'Continuing edition of current draft', 2) | 372 self.disp(u'Continuing edition of current draft', 2) |
324 else: | 373 else: |
325 # for now we taxe the item as a file path | 374 # for now we taxe the item as a file path |
326 content_file_path = os.path.expanduser(self.args.item) | 375 content_file_path = os.path.expanduser(self.args.item) |
327 content_file_obj = open(content_file_path, 'r+b') | 376 content_file_obj = open(content_file_path, 'r+b') |
332 | 381 |
333 | 382 |
334 class Preview(base.CommandBase, BlogCommon): | 383 class Preview(base.CommandBase, BlogCommon): |
335 | 384 |
336 def __init__(self, host): | 385 def __init__(self, host): |
337 super(Preview, self).__init__(host, 'preview', use_verbose=True, help=_(u'preview a blog content')) | 386 base.CommandBase.__init__(self, host, 'preview', use_verbose=True, help=_(u'preview a blog content')) |
387 BlogCommon.__init__(self, self.host) | |
338 | 388 |
339 def add_parser_options(self): | 389 def add_parser_options(self): |
340 self.parser.add_argument("--inotify", type=str, choices=('auto', 'true', 'false'), default=u'auto', help=_(u"use inotify to handle preview")) | 390 self.parser.add_argument("--inotify", type=str, choices=('auto', 'true', 'false'), default=u'auto', help=_(u"use inotify to handle preview")) |
341 self.parser.add_argument("file", type=base.unicode_decoder, nargs='?', default=u'current', help=_(u"path to the content file")) | 391 self.parser.add_argument("file", type=base.unicode_decoder, nargs='?', default=u'current', help=_(u"path to the content file")) |
342 | 392 |
419 else: | 469 else: |
420 update_cb = self.updatePreviewExt | 470 update_cb = self.updatePreviewExt |
421 | 471 |
422 # which file do we need to edit? | 472 # which file do we need to edit? |
423 if self.args.file == 'current': | 473 if self.args.file == 'current': |
424 self.content_file_path = self.getCurrentFile(sat_conf) | 474 self.content_file_path = self.getCurrentFile(sat_conf, self.profile) |
425 else: | 475 else: |
426 self.content_file_path = os.path.abspath(self.args.file) | 476 self.content_file_path = os.path.abspath(self.args.file) |
427 | 477 |
428 self.syntax = self.guessSyntaxFromPath(sat_conf, self.content_file_path) | 478 self.syntax = self.guessSyntaxFromPath(sat_conf, self.content_file_path) |
429 | 479 |