Mercurial > libervia-backend
comparison sat/plugins/plugin_misc_download.py @ 3088:d1464548055a
plugin file download: meta plugin to handle file download:
- first code in backend to use async/await Python syntax \o/
- plugin with file upload
- URL schemes can be registered
- `http` and `https` schemes are handled by default
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 20 Dec 2019 12:28:04 +0100 |
parents | |
children | 9d0df638c8b4 |
comparison
equal
deleted
inserted
replaced
3087:a51f7fce1e2c | 3088:d1464548055a |
---|---|
1 #!/usr/bin/env python3 | |
2 | |
3 # SAT plugin for downloading files | |
4 # Copyright (C) 2009-2019 Jérôme Poisson (goffi@goffi.org) | |
5 | |
6 # This program is free software: you can redistribute it and/or modify | |
7 # it under the terms of the GNU Affero General Public License as published by | |
8 # the Free Software Foundation, either version 3 of the License, or | |
9 # (at your option) any later version. | |
10 | |
11 # This program is distributed in the hope that it will be useful, | |
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
14 # GNU Affero General Public License for more details. | |
15 | |
16 # You should have received a copy of the GNU Affero General Public License | |
17 # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
18 | |
19 from pathlib import Path | |
20 from urllib.parse import urlparse | |
21 import treq | |
22 from twisted.internet import defer | |
23 from twisted.words.protocols.jabber import error as jabber_error | |
24 from sat.core.i18n import _, D_ | |
25 from sat.core.constants import Const as C | |
26 from sat.core.log import getLogger | |
27 from sat.core import exceptions | |
28 from sat.tools import xml_tools | |
29 from sat.tools.common import data_format | |
30 from sat.tools import stream | |
31 | |
32 log = getLogger(__name__) | |
33 | |
34 | |
35 PLUGIN_INFO = { | |
36 C.PI_NAME: "File Download", | |
37 C.PI_IMPORT_NAME: "DOWNLOAD", | |
38 C.PI_TYPE: C.PLUG_TYPE_MISC, | |
39 C.PI_MAIN: "DownloadPlugin", | |
40 C.PI_HANDLER: "no", | |
41 C.PI_DESCRIPTION: _("""File download management"""), | |
42 } | |
43 | |
44 | |
45 class DownloadPlugin(object): | |
46 | |
47 def __init__(self, host): | |
48 log.info(_("plugin Download initialization")) | |
49 self.host = host | |
50 host.bridge.addMethod( | |
51 "fileDownload", | |
52 ".plugin", | |
53 in_sign="ssss", | |
54 out_sign="a{ss}", | |
55 method=self._fileDownload, | |
56 async_=True, | |
57 ) | |
58 host.bridge.addMethod( | |
59 "fileDownloadComplete", | |
60 ".plugin", | |
61 in_sign="ssss", | |
62 out_sign="s", | |
63 method=self._fileDownloadComplete, | |
64 async_=True, | |
65 ) | |
66 self._download_callbacks = {} | |
67 self.registerScheme('http', self.downloadHTTP) | |
68 self.registerScheme('https', self.downloadHTTP) | |
69 | |
70 def _fileDownload(self, uri, dest_path, options_s, profile): | |
71 client = self.host.getClient(profile) | |
72 options = data_format.deserialise(options_s) | |
73 | |
74 return defer.ensureDeferred(self.fileDownload( | |
75 client, uri, Path(dest_path), options | |
76 )) | |
77 | |
78 async def fileDownload(self, client, uri, dest_path, options=None): | |
79 """Send a file using best available method | |
80 | |
81 parameters are the same as for [download] | |
82 @return (dict): action dictionary, with progress id in case of success, else xmlui | |
83 message | |
84 """ | |
85 try: | |
86 progress_id, __ = await self.download(client, uri, dest_path, options) | |
87 except Exception as e: | |
88 if (isinstance(e, jabber_error.StanzaError) | |
89 and e.condition == 'not-acceptable'): | |
90 reason = e.text | |
91 else: | |
92 reason = str(e) | |
93 msg = D_("Can't download file: {reason}").format(reason=reason) | |
94 log.warning(msg) | |
95 return { | |
96 "xmlui": xml_tools.note( | |
97 msg, D_("Can't download file"), C.XMLUI_DATA_LVL_WARNING | |
98 ).toXml() | |
99 } | |
100 else: | |
101 return {"progress": progress_id} | |
102 | |
103 def _fileDownloadComplete(self, uri, dest_path, options_s, profile): | |
104 client = self.host.getClient(profile) | |
105 options = data_format.deserialise(options_s) | |
106 | |
107 d = defer.ensureDeferred(self.fileDownloadComplete( | |
108 client, uri, Path(dest_path), options | |
109 )) | |
110 d.addCallback(lambda path: str(path)) | |
111 return d | |
112 | |
113 async def fileDownloadComplete(self, client, uri, dest_path, options=None): | |
114 """Helper method to fully download a file and return its path | |
115 | |
116 parameters are the same as for [download] | |
117 @return (str): path to the downloaded file | |
118 """ | |
119 __, download_d = await self.download(client, uri, dest_path, options) | |
120 await download_d | |
121 return dest_path | |
122 | |
123 async def download(self, client, uri, dest_path, options=None): | |
124 """Send a file using best available method | |
125 | |
126 @param uri(str): URI to the file to download | |
127 @param dest_path(str, Path): where the file must be downloaded | |
128 @param options(dict, None): options depending on scheme handler | |
129 Some common options: | |
130 - ignore_tls_errors(bool): True to ignore SSL/TLS certificate verification | |
131 used only if HTTPS transport is needed | |
132 @return (tuple[unicode,D(unicode)]): progress_id and a Deferred which fire | |
133 download URL when download is finished | |
134 """ | |
135 if options is None: | |
136 options = {} | |
137 | |
138 dest_path = Path(dest_path) | |
139 uri_parsed = urlparse(uri, 'http') | |
140 try: | |
141 callback = self._download_callbacks[uri_parsed.scheme] | |
142 except KeyError: | |
143 raise exceptions.NotFound(f"Can't find any handler for uri {uri}") | |
144 else: | |
145 return await callback(client, uri_parsed, dest_path, options) | |
146 | |
147 def registerScheme(self, scheme, download_cb): | |
148 """Register an URI scheme handler | |
149 | |
150 @param scheme(unicode): URI scheme this callback is handling | |
151 @param download_cb(callable): callback to download a file | |
152 arguments are: | |
153 - (SatXMPPClient) client | |
154 - (urllib.parse.SplitResult) parsed URI | |
155 - (Path) destination path where the file must be downloaded | |
156 - (dict) options | |
157 must return a tuple with progress_id and a Deferred which fire when download | |
158 is finished | |
159 """ | |
160 if scheme in self._download_callbacks: | |
161 raise exceptions.ConflictError( | |
162 f"A method with scheme {scheme!r} is already registered" | |
163 ) | |
164 self._download_callbacks[scheme] = download_cb | |
165 | |
166 def unregister(self, scheme): | |
167 try: | |
168 del self._download_callbacks[scheme] | |
169 except KeyError: | |
170 raise exceptions.NotFound(f"No callback registered for scheme {scheme!r}") | |
171 | |
172 async def downloadHTTP(self, client, uri_parsed, dest_path, options): | |
173 url = uri_parsed.geturl() | |
174 | |
175 head_data = await treq.head(url) | |
176 try: | |
177 content_length = int(head_data.headers.getRawHeaders('content-length')[0]) | |
178 except (KeyError, TypeError, IndexError): | |
179 content_length = None | |
180 log.debug(f"No content lenght found at {url}") | |
181 file_obj = stream.SatFile( | |
182 self.host, | |
183 client, | |
184 dest_path, | |
185 mode="wb", | |
186 size = content_length, | |
187 ) | |
188 | |
189 progress_id = file_obj.uid | |
190 | |
191 resp = await treq.get(url, unbuffered=True) | |
192 d = treq.collect(resp, file_obj.write) | |
193 d.addBoth(lambda _: file_obj.close()) | |
194 return progress_id, d |