Mercurial > libervia-backend
comparison sat_frontends/jp/cmd_file.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_file.py@842bd1594077 |
children | c0401a72cbb4 |
comparison
equal
deleted
inserted
replaced
2561:bd30dc3ffe5a | 2562:26edcf3a30eb |
---|---|
1 #!/usr/bin/env python2 | |
2 # -*- coding: utf-8 -*- | |
3 | |
4 # jp: a SAT 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 import sys | |
23 import os | |
24 import os.path | |
25 import tarfile | |
26 from sat.core.i18n import _ | |
27 from sat_frontends.jp.constants import Const as C | |
28 from sat_frontends.jp import common | |
29 from sat_frontends.tools import jid | |
30 from sat.tools.common.ansi import ANSI as A | |
31 import tempfile | |
32 import xml.etree.ElementTree as ET # FIXME: used temporarily to manage XMLUI | |
33 from functools import partial | |
34 import json | |
35 | |
36 __commands__ = ["File"] | |
37 | |
38 | |
39 class Send(base.CommandBase): | |
40 def __init__(self, host): | |
41 super(Send, self).__init__(host, 'send', use_progress=True, use_verbose=True, help=_('send a file to a contact')) | |
42 self.need_loop=True | |
43 | |
44 def add_parser_options(self): | |
45 self.parser.add_argument("files", type=str, nargs='+', metavar='file', help=_(u"a list of file")) | |
46 self.parser.add_argument("jid", type=base.unicode_decoder, help=_(u"the destination jid")) | |
47 self.parser.add_argument("-b", "--bz2", action="store_true", help=_(u"make a bzip2 tarball")) | |
48 self.parser.add_argument("-d", "--path", type=base.unicode_decoder, help=(u"path to the directory where the file must be stored")) | |
49 self.parser.add_argument("-N", "--namespace", type=base.unicode_decoder, help=(u"namespace of the file")) | |
50 self.parser.add_argument("-n", "--name", type=base.unicode_decoder, default=u'', help=(u"name to use (DEFAULT: use source file name)")) | |
51 | |
52 def start(self): | |
53 """Send files to jabber contact""" | |
54 self.send_files() | |
55 | |
56 def onProgressStarted(self, metadata): | |
57 self.disp(_(u'File copy started'),2) | |
58 | |
59 def onProgressFinished(self, metadata): | |
60 self.disp(_(u'File sent successfully'),2) | |
61 | |
62 def onProgressError(self, error_msg): | |
63 if error_msg == C.PROGRESS_ERROR_DECLINED: | |
64 self.disp(_(u'The file has been refused by your contact')) | |
65 else: | |
66 self.disp(_(u'Error while sending file: {}').format(error_msg),error=True) | |
67 | |
68 def gotId(self, data, file_): | |
69 """Called when a progress id has been received | |
70 | |
71 @param pid(unicode): progress id | |
72 @param file_(str): file path | |
73 """ | |
74 #FIXME: this show progress only for last progress_id | |
75 self.disp(_(u"File request sent to {jid}".format(jid=self.full_dest_jid)), 1) | |
76 try: | |
77 self.progress_id = data['progress'] | |
78 except KeyError: | |
79 # TODO: if 'xmlui' key is present, manage xmlui message display | |
80 self.disp(_(u"Can't send file to {jid}".format(jid=self.full_dest_jid)), error=True) | |
81 self.host.quit(2) | |
82 | |
83 def error(self, failure): | |
84 self.disp(_("Error while trying to send a file: {reason}").format(reason=failure), error=True) | |
85 self.host.quit(1) | |
86 | |
87 def send_files(self): | |
88 for file_ in self.args.files: | |
89 if not os.path.exists(file_): | |
90 self.disp(_(u"file [{}] doesn't exist !").format(file_), error=True) | |
91 self.host.quit(1) | |
92 if not self.args.bz2 and os.path.isdir(file_): | |
93 self.disp(_(u"[{}] is a dir ! Please send files inside or use compression").format(file_)) | |
94 self.host.quit(1) | |
95 | |
96 self.full_dest_jid = self.host.get_full_jid(self.args.jid) | |
97 extra = {} | |
98 if self.args.path: | |
99 extra[u'path'] = self.args.path | |
100 if self.args.namespace: | |
101 extra[u'namespace'] = self.args.namespace | |
102 | |
103 if self.args.bz2: | |
104 with tempfile.NamedTemporaryFile('wb', delete=False) as buf: | |
105 self.host.addOnQuitCallback(os.unlink, buf.name) | |
106 self.disp(_(u"bz2 is an experimental option, use with caution")) | |
107 #FIXME: check free space | |
108 self.disp(_(u"Starting compression, please wait...")) | |
109 sys.stdout.flush() | |
110 bz2 = tarfile.open(mode="w:bz2", fileobj=buf) | |
111 archive_name = u'{}.tar.bz2'.format(os.path.basename(self.args.files[0]) or u'compressed_files') | |
112 for file_ in self.args.files: | |
113 self.disp(_(u"Adding {}").format(file_), 1) | |
114 bz2.add(file_) | |
115 bz2.close() | |
116 self.disp(_(u"Done !"), 1) | |
117 | |
118 self.host.bridge.fileSend(self.full_dest_jid, buf.name, self.args.name or archive_name, '', extra, self.profile, | |
119 callback=lambda pid, file_=buf.name: self.gotId(pid, file_), errback=self.error) | |
120 else: | |
121 for file_ in self.args.files: | |
122 path = os.path.abspath(file_) | |
123 self.host.bridge.fileSend(self.full_dest_jid, path, self.args.name, '', extra, self.profile, | |
124 callback=lambda pid, file_=file_: self.gotId(pid, file_), errback=self.error) | |
125 | |
126 | |
127 class Request(base.CommandBase): | |
128 | |
129 def __init__(self, host): | |
130 super(Request, self).__init__(host, 'request', use_progress=True, use_verbose=True, help=_('request a file from a contact')) | |
131 self.need_loop=True | |
132 | |
133 @property | |
134 def filename(self): | |
135 return self.args.name or self.args.hash or u"output" | |
136 | |
137 def add_parser_options(self): | |
138 self.parser.add_argument("jid", type=base.unicode_decoder, help=_(u"the destination jid")) | |
139 self.parser.add_argument("-D", "--dest", type=base.unicode_decoder, help=_(u"destination path where the file will be saved (default: [current_dir]/[name|hash])")) | |
140 self.parser.add_argument("-n", "--name", type=base.unicode_decoder, default=u'', help=_(u"name of the file")) | |
141 self.parser.add_argument("-H", "--hash", type=base.unicode_decoder, default=u'', help=_(u"hash of the file")) | |
142 self.parser.add_argument("-a", "--hash-algo", type=base.unicode_decoder, default=u'sha-256', help=_(u"hash algorithm use for --hash (default: sha-256)")) | |
143 self.parser.add_argument("-d", "--path", type=base.unicode_decoder, help=(u"path to the directory containing the file")) | |
144 self.parser.add_argument("-N", "--namespace", type=base.unicode_decoder, help=(u"namespace of the file")) | |
145 self.parser.add_argument("-f", "--force", action='store_true', help=_(u"overwrite existing file without confirmation")) | |
146 | |
147 def onProgressStarted(self, metadata): | |
148 self.disp(_(u'File copy started'),2) | |
149 | |
150 def onProgressFinished(self, metadata): | |
151 self.disp(_(u'File received successfully'),2) | |
152 | |
153 def onProgressError(self, error_msg): | |
154 if error_msg == C.PROGRESS_ERROR_DECLINED: | |
155 self.disp(_(u'The file request has been refused')) | |
156 else: | |
157 self.disp(_(u'Error while requesting file: {}').format(error_msg), error=True) | |
158 | |
159 def gotId(self, progress_id): | |
160 """Called when a progress id has been received | |
161 | |
162 @param progress_id(unicode): progress id | |
163 """ | |
164 self.progress_id = progress_id | |
165 | |
166 def error(self, failure): | |
167 self.disp(_("Error while trying to send a file: {reason}").format(reason=failure), error=True) | |
168 self.host.quit(1) | |
169 | |
170 def start(self): | |
171 if not self.args.name and not self.args.hash: | |
172 self.parser.error(_(u'at least one of --name or --hash must be provided')) | |
173 # extra = dict(self.args.extra) | |
174 if self.args.dest: | |
175 path = os.path.abspath(os.path.expanduser(self.args.dest)) | |
176 if os.path.isdir(path): | |
177 path = os.path.join(path, self.filename) | |
178 else: | |
179 path = os.path.abspath(self.filename) | |
180 | |
181 if os.path.exists(path) and not self.args.force: | |
182 message = _(u'File {path} already exists! Do you want to overwrite?').format(path=path) | |
183 confirm = raw_input(u"{} (y/N) ".format(message).encode('utf-8')) | |
184 if confirm not in (u"y", u"Y"): | |
185 self.disp(_(u"file request cancelled")) | |
186 self.host.quit(2) | |
187 | |
188 self.full_dest_jid = self.host.get_full_jid(self.args.jid) | |
189 extra = {} | |
190 if self.args.path: | |
191 extra[u'path'] = self.args.path | |
192 if self.args.namespace: | |
193 extra[u'namespace'] = self.args.namespace | |
194 self.host.bridge.fileJingleRequest(self.full_dest_jid, | |
195 path, | |
196 self.args.name, | |
197 self.args.hash, | |
198 self.args.hash_algo if self.args.hash else u'', | |
199 extra, | |
200 self.profile, | |
201 callback=self.gotId, | |
202 errback=partial(self.errback, | |
203 msg=_(u"can't request file: {}"), | |
204 exit_code=C.EXIT_BRIDGE_ERRBACK)) | |
205 | |
206 | |
207 class Receive(base.CommandAnswering): | |
208 | |
209 def __init__(self, host): | |
210 super(Receive, self).__init__(host, 'receive', use_progress=True, use_verbose=True, help=_('wait for a file to be sent by a contact')) | |
211 self._overwrite_refused = False # True when one overwrite as already been refused | |
212 self.action_callbacks = {C.META_TYPE_FILE: self.onFileAction, | |
213 C.META_TYPE_OVERWRITE: self.onOverwriteAction} | |
214 | |
215 def onProgressStarted(self, metadata): | |
216 self.disp(_(u'File copy started'),2) | |
217 | |
218 def onProgressFinished(self, metadata): | |
219 self.disp(_(u'File received successfully'),2) | |
220 if metadata.get('hash_verified', False): | |
221 try: | |
222 self.disp(_(u'hash checked: {algo}:{checksum}').format( | |
223 algo=metadata['hash_algo'], | |
224 checksum=metadata['hash']), | |
225 1) | |
226 except KeyError: | |
227 self.disp(_(u'hash is checked but hash value is missing', 1), error=True) | |
228 else: | |
229 self.disp(_(u"hash can't be verified"), 1) | |
230 | |
231 def onProgressError(self, error_msg): | |
232 self.disp(_(u'Error while receiving file: {}').format(error_msg),error=True) | |
233 | |
234 def getXmluiId(self, action_data): | |
235 # FIXME: we temporarily use ElementTree, but a real XMLUI managing module | |
236 # should be available in the futur | |
237 # TODO: XMLUI module | |
238 try: | |
239 xml_ui = action_data['xmlui'] | |
240 except KeyError: | |
241 self.disp(_(u"Action has no XMLUI"), 1) | |
242 else: | |
243 ui = ET.fromstring(xml_ui.encode('utf-8')) | |
244 xmlui_id = ui.get('submit') | |
245 if not xmlui_id: | |
246 self.disp(_(u"Invalid XMLUI received"), error=True) | |
247 return xmlui_id | |
248 | |
249 def onFileAction(self, action_data, action_id, security_limit, profile): | |
250 xmlui_id = self.getXmluiId(action_data) | |
251 if xmlui_id is None: | |
252 return self.host.quitFromSignal(1) | |
253 try: | |
254 from_jid = jid.JID(action_data['meta_from_jid']) | |
255 except KeyError: | |
256 self.disp(_(u"Ignoring action without from_jid data"), 1) | |
257 return | |
258 try: | |
259 progress_id = action_data['meta_progress_id'] | |
260 except KeyError: | |
261 self.disp(_(u"ignoring action without progress id"), 1) | |
262 return | |
263 | |
264 if not self.bare_jids or from_jid.bare in self.bare_jids: | |
265 if self._overwrite_refused: | |
266 self.disp(_(u"File refused because overwrite is needed"), error=True) | |
267 self.host.bridge.launchAction(xmlui_id, {'cancelled': C.BOOL_TRUE}, profile_key=profile) | |
268 return self.host.quitFromSignal(2) | |
269 self.progress_id = progress_id | |
270 xmlui_data = {'path': self.path} | |
271 self.host.bridge.launchAction(xmlui_id, xmlui_data, profile_key=profile) | |
272 | |
273 def onOverwriteAction(self, action_data, action_id, security_limit, profile): | |
274 xmlui_id = self.getXmluiId(action_data) | |
275 if xmlui_id is None: | |
276 return self.host.quitFromSignal(1) | |
277 try: | |
278 progress_id = action_data['meta_progress_id'] | |
279 except KeyError: | |
280 self.disp(_(u"ignoring action without progress id"), 1) | |
281 return | |
282 self.disp(_(u"Overwriting needed"), 1) | |
283 | |
284 if progress_id == self.progress_id: | |
285 if self.args.force: | |
286 self.disp(_(u"Overwrite accepted"), 2) | |
287 else: | |
288 self.disp(_(u"Refused to overwrite"), 2) | |
289 self._overwrite_refused = True | |
290 | |
291 xmlui_data = {'answer': C.boolConst(self.args.force)} | |
292 self.host.bridge.launchAction(xmlui_id, xmlui_data, profile_key=profile) | |
293 | |
294 def add_parser_options(self): | |
295 self.parser.add_argument("jids", type=base.unicode_decoder, nargs="*", help=_(u'jids accepted (accept everything if none is specified)')) | |
296 self.parser.add_argument("-m", "--multiple", action="store_true", help=_(u"accept multiple files (you'll have to stop manually)")) | |
297 self.parser.add_argument("-f", "--force", action="store_true", help=_(u"force overwritting of existing files (/!\\ name is choosed by sender)")) | |
298 self.parser.add_argument("--path", default='.', metavar='DIR', help=_(u"destination path (default: working directory)")) | |
299 | |
300 def start(self): | |
301 self.bare_jids = [jid.JID(jid_).bare for jid_ in self.args.jids] | |
302 self.path = os.path.abspath(self.args.path) | |
303 if not os.path.isdir(self.path): | |
304 self.disp(_(u"Given path is not a directory !", error=True)) | |
305 self.host.quit(2) | |
306 if self.args.multiple: | |
307 self.host.quit_on_progress_end = False | |
308 self.disp(_(u"waiting for incoming file request"),2) | |
309 | |
310 | |
311 class Upload(base.CommandBase): | |
312 | |
313 def __init__(self, host): | |
314 super(Upload, self).__init__(host, 'upload', use_progress=True, use_verbose=True, help=_('upload a file')) | |
315 self.need_loop=True | |
316 | |
317 def add_parser_options(self): | |
318 self.parser.add_argument("file", type=str, help=_("file to upload")) | |
319 self.parser.add_argument("jid", type=base.unicode_decoder, nargs='?', help=_("jid of upload component (nothing to autodetect)")) | |
320 self.parser.add_argument("--ignore-tls-errors", action="store_true", help=_("ignore invalide TLS certificate")) | |
321 | |
322 def onProgressStarted(self, metadata): | |
323 self.disp(_(u'File upload started'),2) | |
324 | |
325 def onProgressFinished(self, metadata): | |
326 self.disp(_(u'File uploaded successfully'),2) | |
327 try: | |
328 url = metadata['url'] | |
329 except KeyError: | |
330 self.disp(u'download URL not found in metadata') | |
331 else: | |
332 self.disp(_(u'URL to retrieve the file:'),1) | |
333 # XXX: url is display alone on a line to make parsing easier | |
334 self.disp(url) | |
335 | |
336 def onProgressError(self, error_msg): | |
337 self.disp(_(u'Error while uploading file: {}').format(error_msg),error=True) | |
338 | |
339 def gotId(self, data, file_): | |
340 """Called when a progress id has been received | |
341 | |
342 @param pid(unicode): progress id | |
343 @param file_(str): file path | |
344 """ | |
345 try: | |
346 self.progress_id = data['progress'] | |
347 except KeyError: | |
348 # TODO: if 'xmlui' key is present, manage xmlui message display | |
349 self.disp(_(u"Can't upload file"), error=True) | |
350 self.host.quit(2) | |
351 | |
352 def error(self, failure): | |
353 self.disp(_("Error while trying to upload a file: {reason}").format(reason=failure), error=True) | |
354 self.host.quit(1) | |
355 | |
356 def start(self): | |
357 file_ = self.args.file | |
358 if not os.path.exists(file_): | |
359 self.disp(_(u"file [{}] doesn't exist !").format(file_), error=True) | |
360 self.host.quit(1) | |
361 if os.path.isdir(file_): | |
362 self.disp(_(u"[{}] is a dir! Can't upload a dir").format(file_)) | |
363 self.host.quit(1) | |
364 | |
365 self.full_dest_jid = self.host.get_full_jid(self.args.jid) if self.args.jid is not None else '' | |
366 options = {} | |
367 if self.args.ignore_tls_errors: | |
368 options['ignore_tls_errors'] = C.BOOL_TRUE | |
369 | |
370 path = os.path.abspath(file_) | |
371 self.host.bridge.fileUpload(path, '', self.full_dest_jid, options, self.profile, callback=lambda pid, file_=file_: self.gotId(pid, file_), errback=self.error) | |
372 | |
373 | |
374 class ShareList(base.CommandBase): | |
375 | |
376 def __init__(self, host): | |
377 extra_outputs = {'default': self.default_output} | |
378 super(ShareList, self).__init__(host, 'list', use_output=C.OUTPUT_LIST_DICT, extra_outputs=extra_outputs, help=_(u'retrieve files shared by an entity'), use_verbose=True) | |
379 self.need_loop=True | |
380 | |
381 def add_parser_options(self): | |
382 self.parser.add_argument("-d", "--path", default=u'', help=_(u"path to the directory containing the files")) | |
383 self.parser.add_argument("jid", type=base.unicode_decoder, nargs='?', help=_("jid of sharing entity (nothing to check our own jid)")) | |
384 | |
385 def file_gen(self, files_data): | |
386 for file_data in files_data: | |
387 yield file_data[u'name'] | |
388 yield file_data.get(u'size', '') | |
389 yield file_data.get(u'hash','') | |
390 | |
391 def _name_filter(self, name, row): | |
392 if row.type == C.FILE_TYPE_DIRECTORY: | |
393 return A.color(C.A_DIRECTORY, name) | |
394 elif row.type == C.FILE_TYPE_FILE: | |
395 return A.color(C.A_FILE, name) | |
396 else: | |
397 self.disp(_(u'unknown file type: {type}').format(type=row.type), error=True) | |
398 return name | |
399 | |
400 def _size_filter(self, size, row): | |
401 if not size: | |
402 return u'' | |
403 size = int(size) | |
404 # cf. https://stackoverflow.com/a/1094933 (thanks) | |
405 suffix = u'o' | |
406 for unit in [u'', u'Ki', u'Mi', u'Gi', u'Ti', u'Pi', u'Ei', u'Zi']: | |
407 if abs(size) < 1024.0: | |
408 return A.color(A.BOLD, u"{:.2f}".format(size), unit, suffix) | |
409 size /= 1024.0 | |
410 | |
411 return A.color(A.BOLD, u"{:.2f}".format(size), u'Yi', suffix) | |
412 | |
413 def default_output(self, files_data): | |
414 """display files a way similar to ls""" | |
415 files_data.sort(key=lambda d: d['name'].lower()) | |
416 show_header = False | |
417 if self.verbosity == 0: | |
418 headers = (u'name', u'type') | |
419 elif self.verbosity == 1: | |
420 headers = (u'name', u'type', u'size') | |
421 elif self.verbosity > 1: | |
422 show_header = True | |
423 headers = (u'name', u'type', u'size', u'hash') | |
424 table = common.Table.fromDict(self.host, | |
425 files_data, | |
426 headers, | |
427 filters={u'name': self._name_filter, | |
428 u'size': self._size_filter}, | |
429 defaults={u'size': u'', | |
430 u'hash': u''}, | |
431 ) | |
432 table.display_blank(show_header=show_header, hide_cols=['type']) | |
433 | |
434 def _FISListCb(self, files_data): | |
435 self.output(files_data) | |
436 self.host.quit() | |
437 | |
438 def start(self): | |
439 self.host.bridge.FISList( | |
440 self.args.jid, | |
441 self.args.path, | |
442 {}, | |
443 self.profile, | |
444 callback=self._FISListCb, | |
445 errback=partial(self.errback, | |
446 msg=_(u"can't retrieve shared files: {}"), | |
447 exit_code=C.EXIT_BRIDGE_ERRBACK)) | |
448 | |
449 | |
450 class SharePath(base.CommandBase): | |
451 | |
452 def __init__(self, host): | |
453 super(SharePath, self).__init__(host, 'path', help=_(u'share a file or directory'), use_verbose=True) | |
454 self.need_loop=True | |
455 | |
456 def add_parser_options(self): | |
457 self.parser.add_argument("-n", "--name", type=base.unicode_decoder, default=u'', help=_(u"virtual name to use (default: use directory/file name)")) | |
458 perm_group = self.parser.add_mutually_exclusive_group() | |
459 perm_group.add_argument("-j", "--jid", type=base.unicode_decoder, action='append', dest="jids", default=[], help=_(u"jid of contacts allowed to retrieve the files")) | |
460 perm_group.add_argument("--public", action='store_true', help=_(u"share publicly the file(s) (/!\\ *everybody* will be able to access them)")) | |
461 self.parser.add_argument("path", type=base.unicode_decoder, help=_(u"path to a file or directory to share")) | |
462 | |
463 | |
464 def _FISSharePathCb(self, name): | |
465 self.disp(_(u'{path} shared under the name "{name}"').format( | |
466 path = self.path, | |
467 name = name)) | |
468 self.host.quit() | |
469 | |
470 def start(self): | |
471 self.path = os.path.abspath(self.args.path) | |
472 if self.args.public: | |
473 access = {u'read': {u'type': u'public'}} | |
474 else: | |
475 jids = self.args.jids | |
476 if jids: | |
477 access = {u'read': {u'type': 'whitelist', | |
478 u'jids': jids}} | |
479 else: | |
480 access = {} | |
481 self.host.bridge.FISSharePath( | |
482 self.args.name, | |
483 self.path, | |
484 json.dumps(access, ensure_ascii=False), | |
485 self.profile, | |
486 callback=self._FISSharePathCb, | |
487 errback=partial(self.errback, | |
488 msg=_(u"can't share path: {}"), | |
489 exit_code=C.EXIT_BRIDGE_ERRBACK)) | |
490 | |
491 | |
492 class Share(base.CommandBase): | |
493 subcommands = (ShareList, SharePath) | |
494 | |
495 def __init__(self, host): | |
496 super(Share, self).__init__(host, 'share', use_profile=False, help=_(u'files sharing management')) | |
497 | |
498 | |
499 class File(base.CommandBase): | |
500 subcommands = (Send, Request, Receive, Upload, Share) | |
501 | |
502 def __init__(self, host): | |
503 super(File, self).__init__(host, 'file', use_profile=False, help=_(u'files sending/receiving/management')) |