Mercurial > libervia-backend
comparison frontends/src/jp/common.py @ 2273:5f0dbf42aa9c
jp (blog, common): various fixes in common and blog:
- parse_args has been moved to common
- cat_dir is converted to str on BaseEdit init, so it can be use to make str path for files manipulation
- fixed use of EDITOR_ARGS_MAGIC when use_metadata is False
- fixed unlink of metadata files when use_metadata is False
author | Goffi <goffi@goffi.org> |
---|---|
date | Tue, 27 Jun 2017 19:38:22 +0200 |
parents | 07caa12be945 |
children | 5cd45a79775b |
comparison
equal
deleted
inserted
replaced
2272:b5befe7722d3 | 2273:5f0dbf42aa9c |
---|---|
27 import os.path | 27 import os.path |
28 import time | 28 import time |
29 import tempfile | 29 import tempfile |
30 import subprocess | 30 import subprocess |
31 import glob | 31 import glob |
32 import shlex | |
32 | 33 |
33 # defaut arguments used for some known editors (editing with metadata) | 34 # defaut arguments used for some known editors (editing with metadata) |
34 VIM_SPLIT_ARGS = "-c 'vsplit|wincmd w|next|wincmd w'" | 35 VIM_SPLIT_ARGS = "-c 'vsplit|wincmd w|next|wincmd w'" |
35 EMACS_SPLIT_ARGS = '--eval "(split-window-horizontally)"' | 36 EMACS_SPLIT_ARGS = '--eval "(split-window-horizontally)"' |
36 EDITOR_ARGS_MAGIC = { | 37 EDITOR_ARGS_MAGIC = { |
48 | 49 |
49 def getTmpDir(sat_conf, cat_dir, sub_dir=None): | 50 def getTmpDir(sat_conf, cat_dir, sub_dir=None): |
50 """Return directory used to store temporary files | 51 """Return directory used to store temporary files |
51 | 52 |
52 @param sat_conf(ConfigParser.ConfigParser): instance opened on sat configuration | 53 @param sat_conf(ConfigParser.ConfigParser): instance opened on sat configuration |
53 @param cat_dir(str): directory of the category (e.g. "blog") | 54 @param cat_dir(unicode): directory of the category (e.g. "blog") |
54 @param sub_dir(str): sub directory where data need to be put | 55 @param sub_dir(str): sub directory where data need to be put |
55 profile can be used here, or special directory name | 56 profile can be used here, or special directory name |
56 sub_dir will be escaped to be usable in path (use regex.pathUnescape to find | 57 sub_dir will be escaped to be usable in path (use regex.pathUnescape to find |
57 initial str) | 58 initial str) |
58 @return (str): path to the dir | 59 @return (str): path to the dir |
59 """ | 60 """ |
60 local_dir = config.getConfig(sat_conf, '', 'local_dir', Exception) | 61 local_dir = config.getConfig(sat_conf, '', 'local_dir', Exception) |
61 path = [local_dir, cat_dir] | 62 path = [local_dir.encode('utf-8'), cat_dir.encode('utf-8')] |
62 if sub_dir is not None: | 63 if sub_dir is not None: |
63 path.append(regex.pathEscape(sub_dir)) | 64 path.append(regex.pathEscape(sub_dir)) |
64 return os.path.join(*path) | 65 return os.path.join(*path) |
65 | 66 |
66 | 67 |
68 def parse_args(host, cmd_line, **format_kw): | |
69 """Parse command arguments | |
70 | |
71 @param cmd_line(unicode): command line as found in sat.conf | |
72 @param format_kw: keywords used for formating | |
73 @return (list(unicode)): list of arguments to pass to subprocess function | |
74 """ | |
75 try: | |
76 # we split the arguments and add the known fields | |
77 # we split arguments first to avoid escaping issues in file names | |
78 return [a.format(**format_kw) for a in shlex.split(cmd_line)] | |
79 except ValueError as e: | |
80 host.disp(u"Couldn't parse editor cmd [{cmd}]: {reason}".format(cmd=cmd_line, reason=e)) | |
81 return [] | |
82 | |
83 | |
67 class BaseEdit(object): | 84 class BaseEdit(object): |
68 u"""base class for editing commands | 85 u"""base class for editing commands |
69 | 86 |
70 This class allows to edit file for PubSub or something else. | 87 This class allows to edit file for PubSub or something else. |
71 It works with temporary files in SàT local_dir, in a "cat_dir" subdir | 88 It works with temporary files in SàT local_dir, in a "cat_dir" subdir |
72 """ | 89 """ |
73 | 90 |
74 def __init__(self, host, cat_dir, use_metadata=True): | 91 def __init__(self, host, cat_dir, use_metadata=False): |
75 """ | 92 """ |
76 @param sat_conf(ConfigParser.ConfigParser): instance opened on sat configuration | 93 @param sat_conf(ConfigParser.ConfigParser): instance opened on sat configuration |
77 @param cat_dir(unicode): directory to use for drafts | 94 @param cat_dir(unicode): directory to use for drafts |
78 this will be a sub-directory of SàT's local_dir | 95 this will be a sub-directory of SàT's local_dir |
79 @param use_metadata(bool): True is edition need a second file for metadata | 96 @param use_metadata(bool): True is edition need a second file for metadata |
80 most of signature change with use_metadata with an additional metadata argument. | 97 most of signature change with use_metadata with an additional metadata argument. |
81 This is done to raise error if a command needs metadata but forget the flag, and vice versa | 98 This is done to raise error if a command needs metadata but forget the flag, and vice versa |
82 """ | 99 """ |
83 self.host = host | 100 self.host = host |
84 self.sat_conf = config.parseMainConf() | 101 self.sat_conf = config.parseMainConf() |
85 self.cat_dir = cat_dir.encode('utf-8') | 102 self.cat_dir_str = cat_dir.encode('utf-8') |
86 self.use_metadata = use_metadata | 103 self.use_metadata = use_metadata |
87 | 104 |
88 def add_parser_options(self): | 105 def add_parser_options(self): |
89 self.parser.add_argument("--force-item", action='store_true', help=_(u"don't use magic and take item argument as an actual item")) | 106 self.parser.add_argument("--force-item", action='store_true', help=_(u"don't use magic and take item argument as an actual item")) |
90 | 107 |
96 older file are deleted | 113 older file are deleted |
97 @param path(str): file to unlink | 114 @param path(str): file to unlink |
98 """ | 115 """ |
99 if not os.path.isfile(path): | 116 if not os.path.isfile(path): |
100 raise OSError(u"path must link to a regular file") | 117 raise OSError(u"path must link to a regular file") |
101 if not path.startswith(getTmpDir(self.sat_conf, self.cat_dir)): | 118 if not path.startswith(getTmpDir(self.sat_conf, self.cat_dir_str)): |
102 self.disp(u"File {} is not in SàT temporary hierarchy, we do not remove it".format(path.decode('utf-8')), 2) | 119 self.disp(u"File {} is not in SàT temporary hierarchy, we do not remove it".format(path.decode('utf-8')), 2) |
103 return | 120 return |
104 # we have 2 files per draft with use_metadata, so we double max | 121 # we have 2 files per draft with use_metadata, so we double max |
105 unlink_max = SECURE_UNLINK_MAX * 2 if self.use_metadata else SECURE_UNLINK_MAX | 122 unlink_max = SECURE_UNLINK_MAX * 2 if self.use_metadata else SECURE_UNLINK_MAX |
106 backup_dir = getTmpDir(self.sat_conf, self.cat_dir, SECURE_UNLINK_DIR) | 123 backup_dir = getTmpDir(self.sat_conf, self.cat_dir_str, SECURE_UNLINK_DIR) |
107 if not os.path.exists(backup_dir): | 124 if not os.path.exists(backup_dir): |
108 os.makedirs(backup_dir) | 125 os.makedirs(backup_dir) |
109 filename = os.path.basename(path) | 126 filename = os.path.basename(path) |
110 backup_path = os.path.join(backup_dir, filename) | 127 backup_path = os.path.join(backup_dir, filename) |
111 # we move file to backup dir | 128 # we move file to backup dir |
148 try: | 165 try: |
149 # is there custom arguments in sat.conf ? | 166 # is there custom arguments in sat.conf ? |
150 editor_args = config.getConfig(self.sat_conf, 'jp', editor_args_opt, Exception) | 167 editor_args = config.getConfig(self.sat_conf, 'jp', editor_args_opt, Exception) |
151 except (NoOptionError, NoSectionError): | 168 except (NoOptionError, NoSectionError): |
152 # no, we check if we know the editor and have special arguments | 169 # no, we check if we know the editor and have special arguments |
153 editor_args = EDITOR_ARGS_MAGIC.get(os.path.basename(editor), '') | 170 if self.use_metadata: |
171 editor_args = EDITOR_ARGS_MAGIC.get(os.path.basename(editor), '') | |
172 else: | |
173 editor_args = '' | |
154 parse_kwargs = {'content_file': content_file_path} | 174 parse_kwargs = {'content_file': content_file_path} |
155 if self.use_metadata: | 175 if self.use_metadata: |
156 parse_kwargs['metadata_file'] = meta_file_path | 176 parse_kwargs['metadata_file'] = meta_file_path |
157 args = self.parse_args(editor_args, **parse_kwargs) | 177 args = parse_args(self.host, editor_args, **parse_kwargs) |
158 if not args: | 178 if not args: |
159 args = [content_file_path] | 179 args = [content_file_path] |
160 | 180 |
161 # actual editing | 181 # actual editing |
162 editor_exit = subprocess.call([editor] + args) | 182 editor_exit = subprocess.call([editor] + args) |
196 "temporary file path:\t{content_path}\nmetadata file path:\t{meta_path}".format( | 216 "temporary file path:\t{content_path}\nmetadata file path:\t{meta_path}".format( |
197 content_path=content_file_path, meta_path=meta_file_path), error=True) | 217 content_path=content_file_path, meta_path=meta_file_path), error=True) |
198 self.host.quit() | 218 self.host.quit() |
199 | 219 |
200 if len(content) == 0: | 220 if len(content) == 0: |
201 self.disp(u"Content is empty, cancelling the blog edition") | 221 self.disp(u"Content is empty, cancelling the edition") |
202 if not content_file_path.startswith(getTmpDir(self.sat_conf, self.cat_dir)): | 222 if not content_file_path.startswith(getTmpDir(self.sat_conf, self.cat_dir_str)): |
203 self.disp(u"File are not in SàT temporary hierarchy, we do not remove them", 2) | 223 self.disp(u"File are not in SàT temporary hierarchy, we do not remove them", 2) |
204 self.host.quit() | 224 self.host.quit() |
205 self.disp(u"Deletion of {}".format(content_file_path.decode('utf-8')), 2) | 225 self.disp(u"Deletion of {}".format(content_file_path.decode('utf-8')), 2) |
206 os.unlink(content_file_path) | 226 os.unlink(content_file_path) |
207 self.disp(u"Deletion of {}".format(meta_file_path.decode('utf-8')), 2) | 227 if self.use_metadata: |
208 os.unlink(meta_file_path) | 228 self.disp(u"Deletion of {}".format(meta_file_path.decode('utf-8')), 2) |
229 os.unlink(meta_file_path) | |
209 self.host.quit() | 230 self.host.quit() |
210 | 231 |
211 # time to re-check the hash | 232 # time to re-check the hash |
212 elif (tmp_ori_hash == hashlib.sha1(content).digest() and | 233 elif (tmp_ori_hash == hashlib.sha1(content).digest() and |
213 (not self.use_metadata or meta_ori == metadata)): | 234 (not self.use_metadata or meta_ori == metadata)): |
214 self.disp(u"The content has not been modified, cancelling the blog edition") | 235 self.disp(u"The content has not been modified, cancelling the edition") |
215 self.host.quit() | 236 self.host.quit() |
216 | 237 |
217 else: | 238 else: |
218 # we can now send the item | 239 # we can now send the item |
219 content = content.decode('utf-8-sig') # we use utf-8-sig to avoid BOM | 240 content = content.decode('utf-8-sig') # we use utf-8-sig to avoid BOM |
230 self.disp(u"Error while sending your item, the temporary file has been kept at {content_path}: {reason}".format( | 251 self.disp(u"Error while sending your item, the temporary file has been kept at {content_path}: {reason}".format( |
231 content_path=content_file_path, reason=e), error=True) | 252 content_path=content_file_path, reason=e), error=True) |
232 self.host.quit(1) | 253 self.host.quit(1) |
233 | 254 |
234 self.secureUnlink(content_file_path) | 255 self.secureUnlink(content_file_path) |
235 self.secureUnlink(meta_file_path) | 256 if self.use_metadata: |
257 self.secureUnlink(meta_file_path) | |
236 | 258 |
237 def publish(self, content): | 259 def publish(self, content): |
238 # if metadata is needed, publish will be called with it last argument | 260 # if metadata is needed, publish will be called with it last argument |
239 raise NotImplementedError | 261 raise NotImplementedError |
240 | 262 |
242 """Create a temporary file | 264 """Create a temporary file |
243 | 265 |
244 @param suff (str): suffix to use for the filename | 266 @param suff (str): suffix to use for the filename |
245 @return (tuple(file, str)): opened (w+b) file object and file path | 267 @return (tuple(file, str)): opened (w+b) file object and file path |
246 """ | 268 """ |
247 tmp_dir = getTmpDir(self.sat_conf, self.cat_dir, self.profile.encode('utf-8')) | 269 cat_dir_str = self.cat_dir_str |
270 tmp_dir = getTmpDir(self.sat_conf, self.cat_dir_str, self.profile.encode('utf-8')) | |
248 if not os.path.exists(tmp_dir): | 271 if not os.path.exists(tmp_dir): |
249 try: | 272 try: |
250 os.makedirs(tmp_dir) | 273 os.makedirs(tmp_dir) |
251 except OSError as e: | 274 except OSError as e: |
252 self.disp(u"Can't create {path} directory: {reason}".format( | 275 self.disp(u"Can't create {path} directory: {reason}".format( |
253 path=tmp_dir, reason=e), error=True) | 276 path=tmp_dir, reason=e), error=True) |
254 self.host.quit(1) | 277 self.host.quit(1) |
255 try: | 278 try: |
256 fd, path = tempfile.mkstemp(suffix=suff, | 279 fd, path = tempfile.mkstemp(suffix=suff.encode('utf-8'), |
257 prefix=time.strftime(self.cat_dir.encode('utf-8') + '_%Y-%m-%d_%H:%M:%S_'), | 280 prefix=time.strftime(cat_dir_str + '_%Y-%m-%d_%H:%M:%S_'), |
258 dir=tmp_dir, text=True) | 281 dir=tmp_dir, text=True) |
259 return os.fdopen(fd, 'w+b'), path | 282 return os.fdopen(fd, 'w+b'), path |
260 except OSError as e: | 283 except OSError as e: |
261 self.disp(u"Can't create temporary file: {reason}".format(reason=e), error=True) | 284 self.disp(u"Can't create temporary file: {reason}".format(reason=e), error=True) |
262 self.host.quit(1) | 285 self.host.quit(1) |
265 """Get most recently edited file | 288 """Get most recently edited file |
266 | 289 |
267 @param profile(unicode): profile linked to the draft | 290 @param profile(unicode): profile linked to the draft |
268 @return(str): full path of current file | 291 @return(str): full path of current file |
269 """ | 292 """ |
270 # we guess the blog item currently edited by choosing | 293 # we guess the item currently edited by choosing |
271 # the most recent file corresponding to temp file pattern | 294 # the most recent file corresponding to temp file pattern |
272 # in tmp_dir, excluding metadata files | 295 # in tmp_dir, excluding metadata files |
273 cat_dir_str = self.cat_dir.encode('utf-8') | 296 cat_dir_str = self.cat_dir_str |
274 tmp_dir = getTmpDir(self.sat_conf, cat_dir_str, profile.encode('utf-8')) | 297 tmp_dir = getTmpDir(self.sat_conf, self.cat_dir_str, profile.encode('utf-8')) |
275 available = [path for path in glob.glob(os.path.join(tmp_dir, cat_dir_str + '_*')) if not path.endswith(METADATA_SUFF)] | 298 available = [path for path in glob.glob(os.path.join(tmp_dir, cat_dir_str + '_*')) if not path.endswith(METADATA_SUFF)] |
276 if not available: | 299 if not available: |
277 self.disp(u"Could not find any content draft in {path}".format(path=tmp_dir), error=True) | 300 self.disp(u"Could not find any content draft in {path}".format(path=tmp_dir), error=True) |
278 self.host.quit(1) | 301 self.host.quit(1) |
279 return max(available, key=lambda path: os.stat(path).st_mtime) | 302 return max(available, key=lambda path: os.stat(path).st_mtime) |
282 """return formatted content and metadata (or not if use_metadata is false)""" | 305 """return formatted content and metadata (or not if use_metadata is false)""" |
283 raise NotImplementedError | 306 raise NotImplementedError |
284 | 307 |
285 def getTmpSuff(self): | 308 def getTmpSuff(self): |
286 """return suffix used for content file""" | 309 """return suffix used for content file""" |
287 return 'xml' | 310 return u'xml' |
288 | 311 |
289 def getItemPath(self, item): | 312 def getItemPath(self, item): |
290 """retrieve item path (i.e. service and node) from item argument | 313 """retrieve item path (i.e. service and node) from item argument |
291 | 314 |
292 This method is obviously only useful for edition of PubSub based features | 315 This method is obviously only useful for edition of PubSub based features |