comparison sat_frontends/jp/cmd_blog.py @ 2562:26edcf3a30eb

core, setup: huge cleaning: - moved directories from src and frontends/src to sat and sat_frontends, which is the recommanded naming convention - move twisted directory to root - removed all hacks from setup.py, and added missing dependencies, it is now clean - use https URL for website in setup.py - removed "Environment :: X11 Applications :: GTK", as wix is deprecated and removed - renamed sat.sh to sat and fixed its installation - added python_requires to specify Python version needed - replaced glib2reactor which use deprecated code by gtk3reactor sat can now be installed directly from virtualenv without using --system-site-packages anymore \o/
author Goffi <goffi@goffi.org>
date Mon, 02 Apr 2018 19:44:50 +0200
parents frontends/src/jp/cmd_blog.py@dcc77f23e370
children 0b6adc2672d9
comparison
equal deleted inserted replaced
2561:bd30dc3ffe5a 2562:26edcf3a30eb
1 #!/usr/bin/env python2
2 # -*- coding: utf-8 -*-
3
4 # jp: a SàT command line tool
5 # Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org)
6
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
16
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
20
21 import base
22 from sat.core.i18n import _
23 from sat_frontends.jp.constants import Const as C
24 from sat_frontends.jp import common
25 from sat.tools.common.ansi import ANSI as A
26 from sat.tools.common import data_objects
27 from sat.tools.common import uri
28 from sat.tools import config
29 from ConfigParser import NoSectionError, NoOptionError
30 from functools import partial
31 import json
32 import sys
33 import os.path
34 import os
35 import time
36 import tempfile
37 import subprocess
38 import codecs
39 from sat.tools.common import data_format
40
41 __commands__ = ["Blog"]
42
43 SYNTAX_XHTML = u'xhtml'
44 # extensions to use with known syntaxes
45 SYNTAX_EXT = {
46 '': 'txt', # used when the syntax is not found
47 SYNTAX_XHTML: "xhtml",
48 "markdown": "md"
49 }
50
51
52 CONF_SYNTAX_EXT = u'syntax_ext_dict'
53 BLOG_TMP_DIR = u"blog"
54 # key to remove from metadata tmp file if they exist
55 KEY_TO_REMOVE_METADATA = ('id','content', 'content_xhtml', 'comments_node', 'comments_service', 'updated')
56
57 URL_REDIRECT_PREFIX = 'url_redirect_'
58 INOTIFY_INSTALL = '"pip install inotify"'
59 MB_KEYS = (u"id",
60 u"url",
61 u"atom_id",
62 u"updated",
63 u"published",
64 u"language",
65 u"comments", # this key is used for all comments* keys
66 u"tags", # this key is used for all tag* keys
67 u"author",
68 u"author_jid",
69 u"author_email",
70 u"author_jid_verified",
71 u"content",
72 u"content_xhtml",
73 u"title",
74 u"title_xhtml",
75 )
76 OUTPUT_OPT_NO_HEADER = u'no-header'
77
78
79 def guessSyntaxFromPath(host, sat_conf, path):
80 """Return syntax guessed according to filename extension
81
82 @param sat_conf(ConfigParser.ConfigParser): instance opened on sat configuration
83 @param path(str): path to the content file
84 @return(unicode): syntax to use
85 """
86 # we first try to guess syntax with extension
87 ext = os.path.splitext(path)[1][1:] # we get extension without the '.'
88 if ext:
89 for k,v in SYNTAX_EXT.iteritems():
90 if k and ext == v:
91 return k
92
93 # if not found, we use current syntax
94 return host.bridge.getParamA("Syntax", "Composition", "value", host.profile)
95
96
97 class BlogPublishCommon(object):
98 """handle common option for publising commands (Set and Edit)"""
99
100 def add_parser_options(self):
101 self.parser.add_argument("-T", '--title', type=base.unicode_decoder, help=_(u"title of the item"))
102 self.parser.add_argument("-t", '--tag', type=base.unicode_decoder, action='append', help=_(u"tag (category) of your item"))
103 self.parser.add_argument("-C", "--comments", action='store_true', help=_(u"enable comments"))
104 self.parser.add_argument("-S", '--syntax', type=base.unicode_decoder, help=_(u"syntax to use (default: get profile's default syntax)"))
105
106 def setMbDataContent(self, content, mb_data):
107 if self.args.syntax is None:
108 # default syntax has been used
109 mb_data['content_rich'] = content
110 elif self.current_syntax == SYNTAX_XHTML:
111 mb_data['content_xhtml'] = content
112 else:
113 mb_data['content_xhtml'] = self.host.bridge.syntaxConvert(content, self.current_syntax, SYNTAX_XHTML, False, self.profile)
114
115 def setMbDataFromArgs(self, mb_data):
116 """set microblog metadata according to command line options
117
118 if metadata already exist, it will be overwritten
119 """
120 mb_data['allow_comments'] = C.boolConst(self.args.comments)
121 if self.args.tag:
122 data_format.iter2dict('tag', self.args.tag, mb_data, check_conflict=False)
123 if self.args.title is not None:
124 mb_data['title'] = self.args.title
125
126
127 class Set(base.CommandBase, BlogPublishCommon):
128
129 def __init__(self, host):
130 base.CommandBase.__init__(self, host, 'set', use_pubsub=True, pubsub_flags={C.SINGLE_ITEM},
131 help=_(u'publish a new blog item or update an existing one'))
132 BlogPublishCommon.__init__(self)
133 self.need_loop=True
134
135 def add_parser_options(self):
136 BlogPublishCommon.add_parser_options(self)
137
138 def mbSendCb(self):
139 self.disp(u"Item published")
140 self.host.quit(C.EXIT_OK)
141
142 def start(self):
143 self.pubsub_item = self.args.item
144 mb_data = {}
145 self.setMbDataFromArgs(mb_data)
146 content = codecs.getreader('utf-8')(sys.stdin).read()
147 self.setMbDataContent(content, mb_data)
148
149 self.host.bridge.mbSend(
150 self.args.service,
151 self.args.node,
152 mb_data,
153 self.profile,
154 callback=self.exitCb,
155 errback=partial(self.errback,
156 msg=_(u"can't send item: {}"),
157 exit_code=C.EXIT_BRIDGE_ERRBACK))
158
159
160 class Get(base.CommandBase):
161 TEMPLATE = u"blog/articles.html"
162
163 def __init__(self, host):
164 extra_outputs = {'default': self.default_output,
165 'fancy': self.fancy_output}
166 base.CommandBase.__init__(self, host, 'get', use_verbose=True, use_pubsub=True, pubsub_flags={C.MULTI_ITEMS},
167 use_output=C.OUTPUT_COMPLEX, extra_outputs=extra_outputs, help=_(u'get blog item(s)'))
168 self.need_loop=True
169
170 def add_parser_options(self):
171 # TODO: a key(s) argument to select keys to display
172 self.parser.add_argument("-k", "--key", type=base.unicode_decoder, action='append', dest='keys',
173 help=_(u"microblog data key(s) to display (default: depend of verbosity)"))
174 # TODO: add MAM filters
175
176 def template_data_mapping(self, data):
177 return {u'items': data_objects.BlogItems(data)}
178
179 def format_comments(self, item, keys):
180 comments_data = data_format.dict2iterdict(u'comments', item, (u'node', u'service'), pop=True)
181 lines = []
182 for data in comments_data:
183 lines.append(data[u'comments'])
184 for k in (u'node', u'service'):
185 if OUTPUT_OPT_NO_HEADER in self.args.output_opts:
186 header = u''
187 else:
188 header = C.A_HEADER + k + u': ' + A.RESET
189 lines.append(header + data[k])
190 return u'\n'.join(lines)
191
192 def format_tags(self, item, keys):
193 tags = data_format.dict2iter('tag', item, pop=True)
194 return u', '.join(tags)
195
196 def format_updated(self, item, keys):
197 return self.format_time(item['updated'])
198
199 def format_published(self, item, keys):
200 return self.format_time(item['published'])
201
202 def format_url(self, item, keys):
203 return uri.buildXMPPUri(u'pubsub',
204 subtype=u'microblog',
205 path=self.metadata[u'service'],
206 node=self.metadata[u'node'],
207 item=item[u'id'])
208
209 def get_keys(self):
210 """return keys to display according to verbosity or explicit key request"""
211 verbosity = self.args.verbose
212 if self.args.keys:
213 if not set(MB_KEYS).issuperset(self.args.keys):
214 self.disp(u"following keys are invalid: {invalid}.\n"
215 u"Valid keys are: {valid}.".format(
216 invalid = u', '.join(set(self.args.keys).difference(MB_KEYS)),
217 valid = u', '.join(sorted(MB_KEYS))),
218 error=True)
219 self.host.quit(C.EXIT_BAD_ARG)
220 return self.args.keys
221 else:
222 if verbosity == 0:
223 return (u'title', u'content')
224 elif verbosity == 1:
225 return (u"title", u"tags", u"author", u"author_jid", u"author_email", u"author_jid_verified", u"published", u"updated", u"content")
226 else:
227 return MB_KEYS
228
229 def default_output(self, data):
230 """simple key/value output"""
231 items, self.metadata = data
232 keys = self.get_keys()
233
234 # k_cb use format_[key] methods for complex formattings
235 k_cb = {}
236 for k in keys:
237 try:
238 callback = getattr(self, "format_" + k)
239 except AttributeError:
240 pass
241 else:
242 k_cb[k] = callback
243 for idx, item in enumerate(items):
244 for k in keys:
245 if k not in item and k not in k_cb:
246 continue
247 if OUTPUT_OPT_NO_HEADER in self.args.output_opts:
248 header = ''
249 else:
250 header = u"{k_fmt}{key}:{k_fmt_e} {sep}".format(
251 k_fmt = C.A_HEADER,
252 key = k,
253 k_fmt_e = A.RESET,
254 sep = u'\n' if 'content' in k else u'')
255 value = k_cb[k](item, keys) if k in k_cb else item[k]
256 self.disp(header + value)
257 # we want a separation line after each item but the last one
258 if idx < len(items)-1:
259 print(u'')
260
261 def format_time(self, timestamp):
262 """return formatted date for timestamp
263
264 @param timestamp(str,int,float): unix timestamp
265 @return (unicode): formatted date
266 """
267 fmt = u"%d/%m/%Y %H:%M:%S"
268 return time.strftime(fmt, time.localtime(float(timestamp)))
269
270 def fancy_output(self, data):
271 """display blog is a nice to read way
272
273 this output doesn't use keys filter
274 """
275 # thanks to http://stackoverflow.com/a/943921
276 rows, columns = map(int, os.popen('stty size', 'r').read().split())
277 items, metadata = data
278 verbosity = self.args.verbose
279 sep = A.color(A.FG_BLUE, columns * u'▬')
280 if items:
281 print(u'\n' + sep + '\n')
282
283 for idx, item in enumerate(items):
284 title = item.get(u'title')
285 if verbosity > 0:
286 author = item[u'author']
287 published, updated = item[u'published'], item.get('updated')
288 else:
289 author = published = updated = None
290 if verbosity > 1:
291 tags = list(data_format.dict2iter('tag', item, pop=True))
292 else:
293 tags = None
294 content = item.get(u'content')
295
296 if title:
297 print(A.color(A.BOLD, A.FG_CYAN, item[u'title']))
298 meta = []
299 if author:
300 meta.append(A.color(A.FG_YELLOW, author))
301 if published:
302 meta.append(A.color(A.FG_YELLOW, u'on ', self.format_time(published)))
303 if updated != published:
304 meta.append(A.color(A.FG_YELLOW, u'(updated on ', self.format_time(updated), u')'))
305 print(u' '.join(meta))
306 if tags:
307 print(A.color(A.FG_MAGENTA, u', '.join(tags)))
308 if (title or tags) and content:
309 print("")
310 if content:
311 self.disp(content)
312
313 print(u'\n' + sep + '\n')
314
315
316 def mbGetCb(self, mb_result):
317 self.output(mb_result)
318 self.host.quit(C.EXIT_OK)
319
320 def mbGetEb(self, failure_):
321 self.disp(u"can't get blog items: {reason}".format(
322 reason=failure_), error=True)
323 self.host.quit(C.EXIT_BRIDGE_ERRBACK)
324
325 def start(self):
326 self.host.bridge.mbGet(
327 self.args.service,
328 self.args.node,
329 self.args.max,
330 self.args.items,
331 {},
332 self.profile,
333 callback=self.mbGetCb,
334 errback=self.mbGetEb)
335
336
337 class Edit(base.CommandBase, BlogPublishCommon, common.BaseEdit):
338
339 def __init__(self, host):
340 base.CommandBase.__init__(self, host, 'edit', use_pubsub=True, pubsub_flags={C.SINGLE_ITEM},
341 use_draft=True, use_verbose=True, help=_(u'edit an existing or new blog post'))
342 BlogPublishCommon.__init__(self)
343 common.BaseEdit.__init__(self, self.host, BLOG_TMP_DIR, use_metadata=True)
344
345 @property
346 def current_syntax(self):
347 if self._current_syntax is None:
348 self._current_syntax = self.host.bridge.getParamA("Syntax", "Composition", "value", self.profile)
349 return self._current_syntax
350
351 def add_parser_options(self):
352 BlogPublishCommon.add_parser_options(self)
353 self.parser.add_argument("-P", "--preview", action="store_true", help=_(u"launch a blog preview in parallel"))
354
355 def buildMetadataFile(self, content_file_path, mb_data=None):
356 """Build a metadata file using json
357
358 The file is named after content_file_path, with extension replaced by _metadata.json
359 @param content_file_path(str): path to the temporary file which will contain the body
360 @param mb_data(dict, None): microblog metadata (for existing items)
361 @return (tuple[dict, str]): merged metadata put originaly in metadata file
362 and path to temporary metadata file
363 """
364 # we first construct metadata from edited item ones and CLI argumments
365 # or re-use the existing one if it exists
366 meta_file_path = os.path.splitext(content_file_path)[0] + common.METADATA_SUFF
367 if os.path.exists(meta_file_path):
368 self.disp(u"Metadata file already exists, we re-use it")
369 try:
370 with open(meta_file_path, 'rb') as f:
371 mb_data = json.load(f)
372 except (OSError, IOError, ValueError) as e:
373 self.disp(u"Can't read existing metadata file at {path}, aborting: {reason}".format(
374 path=meta_file_path, reason=e), error=True)
375 self.host.quit(1)
376 else:
377 mb_data = {} if mb_data is None else mb_data.copy()
378
379 # in all cases, we want to remove unwanted keys
380 for key in KEY_TO_REMOVE_METADATA:
381 try:
382 del mb_data[key]
383 except KeyError:
384 pass
385 # and override metadata with command-line arguments
386 self.setMbDataFromArgs(mb_data)
387
388 # then we create the file and write metadata there, as JSON dict
389 # XXX: if we port jp one day on Windows, O_BINARY may need to be added here
390 with os.fdopen(os.open(meta_file_path, os.O_RDWR | os.O_CREAT | os.O_TRUNC,0o600), 'w+b') as f:
391 # we need to use an intermediate unicode buffer to write to the file unicode without escaping characters
392 unicode_dump = json.dumps(mb_data, ensure_ascii=False, indent=4, separators=(',', ': '), sort_keys=True)
393 f.write(unicode_dump.encode('utf-8'))
394
395 return mb_data, meta_file_path
396
397 def edit(self, content_file_path, content_file_obj,
398 mb_data=None):
399 """Edit the file contening the content using editor, and publish it"""
400 # we first create metadata file
401 meta_ori, meta_file_path = self.buildMetadataFile(content_file_path, mb_data)
402
403 # do we need a preview ?
404 if self.args.preview:
405 self.disp(u"Preview requested, launching it", 1)
406 # we redirect outputs to /dev/null to avoid console pollution in editor
407 # if user wants to see messages, (s)he can call "blog preview" directly
408 DEVNULL = open(os.devnull, 'wb')
409 subprocess.Popen([sys.argv[0], "blog", "preview", "--inotify", "true", "-p", self.profile, content_file_path], stdout=DEVNULL, stderr=subprocess.STDOUT)
410
411 # we launch editor
412 self.runEditor("blog_editor_args", content_file_path, content_file_obj, meta_file_path=meta_file_path, meta_ori=meta_ori)
413
414 def publish(self, content, mb_data):
415 self.setMbDataContent(content, mb_data)
416
417 if self.pubsub_item is not None:
418 mb_data['id'] = self.pubsub_item
419
420 self.host.bridge.mbSend(self.pubsub_service, self.pubsub_node, mb_data, self.profile)
421 self.disp(u"Blog item published")
422
423
424 def getTmpSuff(self):
425 # we get current syntax to determine file extension
426 return SYNTAX_EXT.get(self.current_syntax, SYNTAX_EXT[''])
427
428 def getItemData(self, service, node, item):
429 items = [item] if item is not None else []
430 mb_data = self.host.bridge.mbGet(service, node, 1, items, {}, self.profile)[0][0]
431 try:
432 content = mb_data['content_xhtml']
433 except KeyError:
434 content = mb_data['content']
435 if content:
436 content = self.host.bridge.syntaxConvert(content, 'text', SYNTAX_XHTML, False, self.profile)
437 if content and self.current_syntax != SYNTAX_XHTML:
438 content = self.host.bridge.syntaxConvert(content, SYNTAX_XHTML, self.current_syntax, False, self.profile)
439 if content and self.current_syntax == SYNTAX_XHTML:
440 try:
441 from lxml import etree
442 except ImportError:
443 self.disp(_(u"You need lxml to edit pretty XHTML"))
444 else:
445 parser = etree.XMLParser(remove_blank_text=True)
446 root = etree.fromstring(content, parser)
447 content = etree.tostring(root, encoding=unicode, pretty_print=True)
448
449 return content, mb_data, mb_data['id']
450
451 def start(self):
452 # if there are user defined extension, we use them
453 SYNTAX_EXT.update(config.getConfig(self.sat_conf, 'jp', CONF_SYNTAX_EXT, {}))
454 self._current_syntax = self.args.syntax
455 if self._current_syntax is not None:
456 try:
457 self._current_syntax = self.args.syntax = self.host.bridge.syntaxGet(self.current_syntax)
458 except Exception as e:
459 if "NotFound" in unicode(e): # FIXME: there is not good way to check bridge errors
460 self.parser.error(_(u"unknown syntax requested ({syntax})").format(syntax=self.args.syntax))
461 else:
462 raise e
463
464 self.pubsub_service, self.pubsub_node, self.pubsub_item, content_file_path, content_file_obj, mb_data = self.getItemPath()
465
466 self.edit(content_file_path, content_file_obj, mb_data=mb_data)
467
468
469 class Preview(base.CommandBase):
470 # TODO: need to be rewritten with template output
471
472 def __init__(self, host):
473 base.CommandBase.__init__(self, host, 'preview', use_verbose=True, help=_(u'preview a blog content'))
474
475 def add_parser_options(self):
476 self.parser.add_argument("--inotify", type=str, choices=('auto', 'true', 'false'), default=u'auto', help=_(u"use inotify to handle preview"))
477 self.parser.add_argument("file", type=base.unicode_decoder, nargs='?', default=u'current', help=_(u"path to the content file"))
478
479 def showPreview(self):
480 # we implement showPreview here so we don't have to import webbrowser and urllib
481 # when preview is not used
482 url = 'file:{}'.format(self.urllib.quote(self.preview_file_path))
483 self.webbrowser.open_new_tab(url)
484
485 def _launchPreviewExt(self, cmd_line, opt_name):
486 url = 'file:{}'.format(self.urllib.quote(self.preview_file_path))
487 args = common.parse_args(self.host, cmd_line, url=url, preview_file=self.preview_file_path)
488 if not args:
489 self.disp(u"Couln't find command in \"{name}\", abording".format(name=opt_name), error=True)
490 self.host.quit(1)
491 subprocess.Popen(args)
492
493 def openPreviewExt(self):
494 self._launchPreviewExt(self.open_cb_cmd, "blog_preview_open_cmd")
495
496 def updatePreviewExt(self):
497 self._launchPreviewExt(self.update_cb_cmd, "blog_preview_update_cmd")
498
499 def updateContent(self):
500 with open(self.content_file_path, 'rb') as f:
501 content = f.read().decode('utf-8-sig')
502 if content and self.syntax != SYNTAX_XHTML:
503 # we use safe=True because we want to have a preview as close as possible to what the
504 # people will see
505 content = self.host.bridge.syntaxConvert(content, self.syntax, SYNTAX_XHTML, True, self.profile)
506
507 xhtml = (u'<html xmlns="http://www.w3.org/1999/xhtml">' +
508 u'<head><meta http-equiv="Content-Type" content="text/html;charset=utf-8" /></head>'+
509 '<body>{}</body>' +
510 u'</html>').format(content)
511
512 with open(self.preview_file_path, 'wb') as f:
513 f.write(xhtml.encode('utf-8'))
514
515 def start(self):
516 import webbrowser
517 import urllib
518 self.webbrowser, self.urllib = webbrowser, urllib
519
520 if self.args.inotify != 'false':
521 try:
522 import inotify.adapters
523 import inotify.constants
524 from inotify.calls import InotifyError
525 except ImportError:
526 if self.args.inotify == 'auto':
527 inotify = None
528 self.disp(u'inotify module not found, deactivating feature. You can install it with {install}'.format(install=INOTIFY_INSTALL))
529 else:
530 self.disp(u"inotify not found, can't activate the feature! Please install it with {install}".format(install=INOTIFY_INSTALL), error=True)
531 self.host.quit(1)
532 else:
533 # we deactivate logging in inotify, which is quite annoying
534 try:
535 inotify.adapters._LOGGER.setLevel(40)
536 except AttributeError:
537 self.disp(u"Logger doesn't exists, inotify may have chanded", error=True)
538 else:
539 inotify=None
540
541 sat_conf = config.parseMainConf()
542 SYNTAX_EXT.update(config.getConfig(sat_conf, 'jp', CONF_SYNTAX_EXT, {}))
543
544 try:
545 self.open_cb_cmd = config.getConfig(sat_conf, 'jp', "blog_preview_open_cmd", Exception)
546 except (NoOptionError, NoSectionError):
547 self.open_cb_cmd = None
548 open_cb = self.showPreview
549 else:
550 open_cb = self.openPreviewExt
551
552 self.update_cb_cmd = config.getConfig(sat_conf, 'jp', "blog_preview_update_cmd", self.open_cb_cmd)
553 if self.update_cb_cmd is None:
554 update_cb = self.showPreview
555 else:
556 update_cb = self.updatePreviewExt
557
558 # which file do we need to edit?
559 if self.args.file == 'current':
560 self.content_file_path = self.getCurrentFile(sat_conf, self.profile)
561 else:
562 self.content_file_path = os.path.abspath(self.args.file)
563
564 self.syntax = self.guessSyntaxFromPath(sat_conf, self.content_file_path)
565
566
567 # at this point the syntax is converted, we can display the preview
568 preview_file = tempfile.NamedTemporaryFile(suffix='.xhtml', delete=False)
569 self.preview_file_path = preview_file.name
570 preview_file.close()
571 self.updateContent()
572
573 if inotify is None:
574 # XXX: we don't delete file automatically because browser need it (and webbrowser.open can return before it is read)
575 self.disp(u'temporary file created at {}\nthis file will NOT BE DELETED AUTOMATICALLY, please delete it yourself when you have finished'.format(self.preview_file_path))
576 open_cb()
577 else:
578 open_cb()
579 i = inotify.adapters.Inotify(block_duration_s=60) # no need for 1 s duraction, inotify drive actions here
580
581 def add_watch():
582 i.add_watch(self.content_file_path, mask=inotify.constants.IN_CLOSE_WRITE |
583 inotify.constants.IN_DELETE_SELF |
584 inotify.constants.IN_MOVE_SELF)
585 add_watch()
586
587 try:
588 for event in i.event_gen():
589 if event is not None:
590 self.disp(u"Content updated", 1)
591 if {"IN_DELETE_SELF", "IN_MOVE_SELF"}.intersection(event[1]):
592 self.disp(u"{} event catched, changing the watch".format(", ".join(event[1])), 2)
593 try:
594 add_watch()
595 except InotifyError:
596 # if the new file is not here yet we can have an error
597 # as a workaround, we do a little rest
598 time.sleep(1)
599 add_watch()
600 self.updateContent()
601 update_cb()
602 except InotifyError:
603 self.disp(u"Can't catch inotify events, as the file been deleted?", error=True)
604 finally:
605 os.unlink(self.preview_file_path)
606 try:
607 i.remove_watch(self.content_file_path)
608 except InotifyError:
609 pass
610
611
612 class Import(base.CommandAnswering):
613 def __init__(self, host):
614 super(Import, self).__init__(host, 'import', use_pubsub=True, use_progress=True, help=_(u'import an external blog'))
615 self.need_loop=True
616
617 def add_parser_options(self):
618 self.parser.add_argument("importer", type=base.unicode_decoder, nargs='?', help=_(u"importer name, nothing to display importers list"))
619 self.parser.add_argument('--host', type=base.unicode_decoder, help=_(u"original blog host"))
620 self.parser.add_argument('--no-images-upload', action='store_true', help=_(u"do *NOT* upload images (default: do upload images)"))
621 self.parser.add_argument('--upload-ignore-host', help=_(u"do not upload images from this host (default: upload all images)"))
622 self.parser.add_argument("--ignore-tls-errors", action="store_true", help=_("ignore invalide TLS certificate for uploads"))
623 self.parser.add_argument('-o', '--option', action='append', nargs=2, default=[], metavar=(u'NAME', u'VALUE'),
624 help=_(u"importer specific options (see importer description)"))
625 self.parser.add_argument("location", type=base.unicode_decoder, nargs='?',
626 help=_(u"importer data location (see importer description), nothing to show importer description"))
627
628 def onProgressStarted(self, metadata):
629 self.disp(_(u'Blog upload started'),2)
630
631 def onProgressFinished(self, metadata):
632 self.disp(_(u'Blog uploaded successfully'),2)
633 redirections = {k[len(URL_REDIRECT_PREFIX):]:v for k,v in metadata.iteritems()
634 if k.startswith(URL_REDIRECT_PREFIX)}
635 if redirections:
636 conf = u'\n'.join([
637 u'url_redirections_profile = {}'.format(self.profile),
638 u"url_redirections_dict = {}".format(
639 # we need to add ' ' before each new line and to double each '%' for ConfigParser
640 u'\n '.join(json.dumps(redirections, indent=1, separators=(',',': ')).replace(u'%', u'%%').split(u'\n'))),
641 ])
642 self.disp(_(u'\nTo redirect old URLs to new ones, put the following lines in your sat.conf file, in [libervia] section:\n\n{conf}'.format(conf=conf)))
643
644 def onProgressError(self, error_msg):
645 self.disp(_(u'Error while uploading blog: {}').format(error_msg),error=True)
646
647 def error(self, failure):
648 self.disp(_("Error while trying to upload a blog: {reason}").format(reason=failure), error=True)
649 self.host.quit(1)
650
651 def start(self):
652 if self.args.location is None:
653 for name in ('option', 'service', 'no_images_upload'):
654 if getattr(self.args, name):
655 self.parser.error(_(u"{name} argument can't be used without location argument").format(name=name))
656 if self.args.importer is None:
657 self.disp(u'\n'.join([u'{}: {}'.format(name, desc) for name, desc in self.host.bridge.blogImportList()]))
658 else:
659 try:
660 short_desc, long_desc = self.host.bridge.blogImportDesc(self.args.importer)
661 except Exception as e:
662 msg = [l for l in unicode(e).split('\n') if l][-1] # we only keep the last line
663 self.disp(msg)
664 self.host.quit(1)
665 else:
666 self.disp(u"{name}: {short_desc}\n\n{long_desc}".format(name=self.args.importer, short_desc=short_desc, long_desc=long_desc))
667 self.host.quit()
668 else:
669 # we have a location, an import is requested
670 options = {key: value for key, value in self.args.option}
671 if self.args.host:
672 options['host'] = self.args.host
673 if self.args.ignore_tls_errors:
674 options['ignore_tls_errors'] = C.BOOL_TRUE
675 if self.args.no_images_upload:
676 options['upload_images'] = C.BOOL_FALSE
677 if self.args.upload_ignore_host:
678 self.parser.error(u"upload-ignore-host option can't be used when no-images-upload is set")
679 elif self.args.upload_ignore_host:
680 options['upload_ignore_host'] = self.args.upload_ignore_host
681 def gotId(id_):
682 self.progress_id = id_
683 self.host.bridge.blogImport(self.args.importer, self.args.location, options, self.args.service, self.args.node, self.profile,
684 callback=gotId, errback=self.error)
685
686
687 class Blog(base.CommandBase):
688 subcommands = (Set, Get, Edit, Preview, Import)
689
690 def __init__(self, host):
691 super(Blog, self).__init__(host, 'blog', use_profile=False, help=_('blog/microblog management'))