# HG changeset patch # User Goffi # Date 1456942694 -3600 # Node ID 28b29381db7558fb008292f71c7c8b6f75303818 # Parent 47108a4f3a70336ebd9c79f7a10ced5eef15ee47 jp (blog/edit): added metadata handling through a .json file (named like content temporary file, but with extension replaced by "_metadata.json"). Modification to this file before the end of edition will be taken into account. diff -r 47108a4f3a70 -r 28b29381db75 frontends/src/jp/cmd_blog.py --- a/frontends/src/jp/cmd_blog.py Tue Mar 01 16:36:16 2016 +0100 +++ b/frontends/src/jp/cmd_blog.py Wed Mar 02 19:18:14 2016 +0100 @@ -38,6 +38,8 @@ } CONF_SYNTAX_EXT = 'syntax_ext_dict' BLOG_TMP_DIR="blog" +# key to remove from metadata tmp file if they exist +KEY_TO_REMOVE_METADATA = ('id','content', 'content_rich', 'content_xhtml', 'comments_node', 'comments_service') URL_REDIRECT_PREFIX = 'url_redirect_' @@ -54,6 +56,12 @@ self.parser.add_argument("--no-comment", action='store_true', help=_(u"disable comments")) def getTmpFile(self, sat_conf, tmp_suff): + """Create a temporary file to received blog item body + + @param sat_conf(ConfigParser.ConfigParser): instance opened on sat configuration + @param tmp_suff (str): suffix to use for the filename + @return (tuple(file, str)): opened (w+b) file object and file path + """ local_dir = config.getConfig(sat_conf, '', 'local_dir', Exception) tmp_dir = os.path.join(local_dir, BLOG_TMP_DIR) if not os.path.exists(tmp_dir): @@ -63,63 +71,109 @@ self.disp(u"Can't create {path} directory: {reason}".format( path=tmp_dir, reason=e), error=True) self.host.quit(1) - try: - return tempfile.mkstemp(suffix=tmp_suff, + fd, path = tempfile.mkstemp(suffix=tmp_suff, prefix=time.strftime('blog_%Y-%m-%d_%H:%M:%S_'), dir=tmp_dir, text=True) + return os.fdopen(fd, 'w+b'), path except OSError as e: self.disp(u"Can't create temporary file: {reason}".format(reason=e), error=True) self.host.quit(1) - def edit(self, sat_conf, tmp_file, tmp_file_obj, item_id=None): + def buildMetadataFile(self, tmp_file_path, mb_data=None): + """Build a metadata file using json + + The file is named after tmp_file_path, with extension replaced by _metadata.json + @param tmp_file_path(str): path to the temporary file which will contain the body + @param mb_data(dict, None): microblog metadata (for existing items) + @return (tuple[dict, str]): merged metadata put originaly in metadata file + and path to temporary metadata file + """ + # we first construct metadata from edited item ones and CLI argumments + mb_data = {} if mb_data is None else mb_data.copy() + for key in KEY_TO_REMOVE_METADATA: + try: + del mb_data[key] + except KeyError: + pass + mb_data['allow_comments'] = C.boolConst(not self.args.no_comment) + if self.args.tag: + common.iter2dict('tag', self.args.tag, mb_data) + if self.args.title is not None: + mb_data['title'] = self.args.title + + # the we create the file and write metadata there, as JSON dict + meta_file_path = os.path.splitext(tmp_file_path)[0] + '_metadata.json' + # XXX: if we port jp one day on Windows, O_BINARY may need to be added here + if os.path.exists(meta_file_path): + self.disp(u"metadata file {} already exists, this should not happen! Cancelling...", error=True) + with os.fdopen(os.open(meta_file_path, os.O_RDWR | os.O_CREAT ,0o600), 'w+b') as f: + # we need to use an intermediate unicode buffer to write to the file unicode without escaping characters + unicode_dump = json.dumps(mb_data, ensure_ascii=False, indent=4, separators=(',', ': '), sort_keys=True) + f.write(unicode_dump.encode('utf-8')) + + return mb_data, meta_file_path + + def edit(self, sat_conf, tmp_file_path, tmp_file_obj, mb_data=None): """Edit the file contening the content using editor, and publish it""" - # we first calculate hash to check for modifications + item_ori_mb_data = mb_data + # we first create metadata file + meta_ori, meta_file_path = self.buildMetadataFile(tmp_file_path, item_ori_mb_data) + + # then we calculate hashes to check for modifications import hashlib tmp_file_obj.seek(0) - ori_hash = hashlib.sha1(tmp_file_obj.read()).digest() + tmp_ori_hash = hashlib.sha1(tmp_file_obj.read()).digest() tmp_file_obj.close() - # the we launch editor + # then we launch editor editor = config.getConfig(sat_conf, 'jp', 'editor') or os.getenv('EDITOR', 'vi') - editor_exit = subprocess.call([editor, tmp_file]) + editor_exit = subprocess.call([editor, tmp_file_path]) # we send the file if edition was a success if editor_exit != 0: self.disp(u"Editor exited with an error code, so temporary file has not be deleted, and blog item is not published.\nTou can find temporary file at {path}".format( - path=tmp_file), error=True) + path=tmp_file_path), error=True) else: - with open(tmp_file, 'rb') as f: - content = f.read() + try: + with open(tmp_file_path, 'rb') as f: + content = f.read() + with open(meta_file_path, 'rb') as f: + mb_data = json.load(f) + except OSError: + self.disp(u"Can write files at {file_path} and/or {meta_path}, have they been deleted? Cancelling edition".format( + file_path=tmp_file_path, meta_path=meta_file_path), error=True) + self.host.quit(1) + except ValueError: + self.disp(u"Can't parse metadata, please check it is correct JSON format. Cancelling edition.\n" + + "You can find tmp file at {file_path} and temporary meta file at {meta_path}.".format( + file_path=tmp_file_path, meta_path=meta_file_path), error=True) + self.host.quit(1) if len(content) == 0: self.disp(u"Content is empty, cancelling the blog edition") # time to re-check the hash - elif ori_hash == hashlib.sha1(content).digest(): + elif (tmp_ori_hash == hashlib.sha1(content).digest() and + meta_ori == mb_data): self.disp(u"The content has not been modified, cancelling the blog edition") else: # we can now send the blog - mb_data = { - 'content_rich': content.decode('utf-8'), - 'allow_comments': C.boolConst(not self.args.no_comment), - } - if item_id: - mb_data['id'] = item_id - if self.args.tag: - common.iter2dict('tag', self.args.tag, mb_data) + mb_data['content_rich'] = content.decode('utf-8') - if self.args.title is not None: - mb_data['title'] = self.args.title + if item_ori_mb_data is not None: + mb_data['id'] = item_ori_mb_data['id'] + try: self.host.bridge.mbSend('', '', mb_data, self.profile) except Exception as e: - self.disp(u"Error while sending your blog, the temporary file has been kept at {path}: {reason}".format( - path=tmp_file, reason=e), error=True) + self.disp(u"Error while sending your blog, the temporary files have been kept at {file_path} and {meta_path}: {reason}".format( + file_path=tmp_file_path, meta_path=meta_file_path, reason=e), error=True) self.host.quit(1) - os.unlink(tmp_file) + os.unlink(tmp_file_path) + os.unlink(meta_file_path) def start(self): # we get current syntax to determine file extension @@ -131,12 +185,12 @@ # we now create a temporary file tmp_suff = '.' + SYNTAX_EXT.get(current_syntax, SYNTAX_EXT['']) - fd, tmp_file = self.getTmpFile(sat_conf, tmp_suff) + tmp_file_obj, tmp_file_path = self.getTmpFile(sat_conf, tmp_suff) item_lower = self.args.item.lower() if item_lower == 'new': self.disp(u'Editing a new blog item', 2) - self.edit(sat_conf, tmp_file, os.fdopen(fd)) + self.edit(sat_conf, tmp_file_path, tmp_file_obj) elif item_lower == 'last': self.disp(u'Editing last published item', 2) try: @@ -148,10 +202,9 @@ content = mb_data['content_xhtml'] if current_syntax != 'XHTML': content = self.host.bridge.syntaxConvert(content, 'XHTML', current_syntax, False, self.profile) - f = os.fdopen(fd, 'w+b') - f.write(content.encode('utf-8')) - f.seek(0) - self.edit(sat_conf, tmp_file, f, mb_data['id']) + tmp_file_obj.write(content.encode('utf-8')) + tmp_file_obj.seek(0) + self.edit(sat_conf, tmp_file_path, tmp_file_obj, mb_data=mb_data) class Import(base.CommandAnswering):