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