transferwee.py - transferwee - Download/upload file via wetransfer.com
(HTM) git clone https://github.com/iamleot/transferwee
(DIR) Log
(DIR) Files
(DIR) Refs
(DIR) README
---
transferwee.py (12768B)
---
1 #!/usr/bin/env python3
2
3 #
4 # Copyright (c) 2018-2019 Leonardo Taccari
5 # All rights reserved.
6 #
7 # Redistribution and use in source and binary forms, with or without
8 # modification, are permitted provided that the following conditions
9 # are met:
10 #
11 # 1. Redistributions of source code must retain the above copyright
12 # notice, this list of conditions and the following disclaimer.
13 # 2. Redistributions in binary form must reproduce the above copyright
14 # notice, this list of conditions and the following disclaimer in the
15 # documentation and/or other materials provided with the distribution.
16 #
17 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
19 # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
20 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS
21 # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22 # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23 # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24 # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25 # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 # POSSIBILITY OF SUCH DAMAGE.
28 #
29
30
31 """
32 Download/upload files via wetransfer.com
33
34 transferwee is a script/module to download/upload files via wetransfer.com.
35
36 It exposes `download' and `upload' subcommands, respectively used to download
37 files from a `we.tl' or `wetransfer.com/downloads' URLs and upload files that
38 will be shared via emails or link.
39 """
40
41 from sys import exit
42 from typing import List
43 import os.path
44 import re
45 import urllib.parse
46 import zlib
47
48 import requests
49
50
51 WETRANSFER_API_URL = 'https://wetransfer.com/api/v4/transfers'
52 WETRANSFER_DOWNLOAD_URL = WETRANSFER_API_URL + '/{transfer_id}/download'
53 WETRANSFER_UPLOAD_EMAIL_URL = WETRANSFER_API_URL + '/email'
54 WETRANSFER_UPLOAD_LINK_URL = WETRANSFER_API_URL + '/link'
55 WETRANSFER_FILES_URL = WETRANSFER_API_URL + '/{transfer_id}/files'
56 WETRANSFER_PART_PUT_URL = WETRANSFER_FILES_URL + '/{file_id}/part-put-url'
57 WETRANSFER_FINALIZE_MPP_URL = WETRANSFER_FILES_URL + '/{file_id}/finalize-mpp'
58 WETRANSFER_FINALIZE_URL = WETRANSFER_API_URL + '/{transfer_id}/finalize'
59
60 WETRANSFER_DEFAULT_CHUNK_SIZE = 5242880
61
62
63 def download_url(url: str) -> str:
64 """Given a wetransfer.com download URL download return the downloadable URL.
65
66 The URL should be of the form `https://we.tl/' or
67 `https://wetransfer.com/downloads/'. If it is a short URL (i.e. `we.tl')
68 the redirect is followed in order to retrieve the corresponding
69 `wetransfer.com/downloads/' URL.
70
71 The following type of URLs are supported:
72 - `https://we.tl/<short_url_id>`:
73 received via link upload, via email to the sender and printed by
74 `upload` action
75 - `https://wetransfer.com/<transfer_id>/<security_hash>`:
76 directly not shared in any ways but the short URLs actually redirect to
77 them
78 - `https://wetransfer.com/<transfer_id>/<recipient_id>/<security_hash>`:
79 received via email by recipients when the files are shared via email
80 upload
81
82 Return the download URL (AKA `direct_link') as a str or None if the URL
83 could not be parsed.
84 """
85 # Follow the redirect if we have a short URL
86 if url.startswith('https://we.tl/'):
87 r = requests.head(url, allow_redirects=True)
88 url = r.url
89
90 recipient_id = None
91 params = url.replace('https://wetransfer.com/downloads/', '').split('/')
92
93 if len(params) == 2:
94 transfer_id, security_hash = params
95 elif len(params) == 3:
96 transfer_id, recipient_id, security_hash = params
97 else:
98 return None
99
100 j = {
101 "security_hash": security_hash,
102 }
103 if recipient_id:
104 j["recipient_id"] = recipient_id
105 request_data = _prepare_request_data()
106 r = requests.post(WETRANSFER_DOWNLOAD_URL.format(transfer_id=transfer_id),
107 json=j,
108 cookies=request_data['cookies'],
109 headers=request_data['headers'])
110
111 j = r.json()
112 return j.get('direct_link')
113
114
115 def _file_unquote(file: str) -> str:
116 """Given a URL encoded file unquote it.
117
118 All occurences of `\', `/' and `../' will be ignored to avoid possible
119 directory traversals.
120 """
121 return urllib.parse.unquote(file).replace('../', '').replace('/', '').replace('\\', '')
122
123
124 def download(url: str) -> None:
125 """Given a `we.tl/' or `wetransfer.com/downloads/' download it.
126
127 First a direct link is retrieved (via download_url()), the filename will
128 be extracted to it and it will be fetched and stored on the current
129 working directory.
130 """
131 dl_url = download_url(url)
132 file = _file_unquote(urllib.parse.urlparse(dl_url).path.split('/')[-1])
133
134 r = requests.get(dl_url, stream=True)
135 with open(file, 'wb') as f:
136 for chunk in r.iter_content(chunk_size=1024):
137 f.write(chunk)
138
139
140 def _file_name_and_size(file: str) -> dict:
141 """Given a file, prepare the "name" and "size" dictionary.
142
143 Return a dictionary with "name" and "size" keys.
144 """
145 filename = os.path.basename(file)
146 filesize = os.path.getsize(file)
147
148 return {
149 "name": filename,
150 "size": filesize
151 }
152
153
154 def _prepare_request_data() -> dict:
155 """Prepare a wetransfer.com request.
156
157 Return a dictionary with "cookies" and "headers" keys, needed for most
158 wetransfer requests.
159 """
160
161 r = requests.get('https://wetransfer.com/')
162 m = re.search('name="csrf-token" content="([^"]+)"', r.text)
163
164 return {
165 'headers': { 'X-CSRF-Token': m.group(1), },
166 'cookies': r.cookies,
167 }
168
169
170 def _prepare_email_upload(filenames: List[str], message: str,
171 sender: str, recipients: List[str],
172 request_data: dict) -> str:
173 """Given a list of filenames, message a sender and recipients prepare for
174 the email upload.
175
176 Return the parsed JSON response.
177 """
178 j = {
179 "files": [_file_name_and_size(f) for f in filenames],
180 "from": sender,
181 "message": message,
182 "recipients": recipients,
183 "ui_language": "en",
184 }
185
186 r = requests.post(WETRANSFER_UPLOAD_EMAIL_URL, json=j,
187 cookies=request_data['cookies'],
188 headers=request_data['headers'])
189 return r.json()
190
191
192 def _prepare_link_upload(filenames: List[str], message: str,
193 request_data: dict) -> str:
194 """Given a list of filenames and a message prepare for the link upload.
195
196 Return the parsed JSON response.
197 """
198 j = {
199 "files": [_file_name_and_size(f) for f in filenames],
200 "message": message,
201 "ui_language": "en",
202 }
203
204 r = requests.post(WETRANSFER_UPLOAD_LINK_URL, json=j,
205 cookies=request_data['cookies'],
206 headers=request_data['headers'])
207 return r.json()
208
209
210 def _prepare_file_upload(transfer_id: str, file: str,
211 request_data: dict) -> str:
212 """Given a transfer_id and file prepare it for the upload.
213
214 Return the parsed JSON response.
215 """
216 j = _file_name_and_size(file)
217 r = requests.post(WETRANSFER_FILES_URL.format(transfer_id=transfer_id),
218 json=j,
219 cookies=request_data['cookies'],
220 headers=request_data['headers'])
221 return r.json()
222
223
224 def _upload_chunks(transfer_id: str, file_id: str, file: str,
225 request_data: dict,
226 default_chunk_size: int = WETRANSFER_DEFAULT_CHUNK_SIZE) -> str:
227 """Given a transfer_id, file_id and file upload it.
228
229 Return the parsed JSON response.
230 """
231 f = open(file, 'rb')
232
233 chunk_number = 0
234 while True:
235 chunk = f.read(default_chunk_size)
236 chunk_size = len(chunk)
237 if chunk_size == 0:
238 break
239 chunk_number += 1
240
241 j = {
242 "chunk_crc": zlib.crc32(chunk),
243 "chunk_number": chunk_number,
244 "chunk_size": chunk_size,
245 "retries": 0
246 }
247
248 r = requests.post(
249 WETRANSFER_PART_PUT_URL.format(transfer_id=transfer_id,
250 file_id=file_id),
251 json=j,
252 cookies=request_data['cookies'],
253 headers=request_data['headers'])
254 url = r.json().get('url')
255 r = requests.options(url,
256 headers={
257 'Origin': 'https://wetransfer.com',
258 'Access-Control-Request-Method': 'PUT',
259 })
260 r = requests.put(url, data=chunk)
261
262 j = {
263 'chunk_count': chunk_number
264 }
265 r = requests.put(
266 WETRANSFER_FINALIZE_MPP_URL.format(transfer_id=transfer_id,
267 file_id=file_id),
268 json=j,
269 cookies=request_data['cookies'],
270 headers=request_data['headers'])
271
272 return r.json()
273
274
275 def _finalize_upload(transfer_id: str, request_data: dict) -> str:
276 """Given a transfer_id finalize the upload.
277
278 Return the parsed JSON response.
279 """
280 r = requests.put(WETRANSFER_FINALIZE_URL.format(transfer_id=transfer_id),
281 cookies=request_data['cookies'],
282 headers=request_data['headers'])
283
284 return r.json()
285
286
287 def upload(files: List[str], message: str = '', sender: str = None,
288 recipients: List[str] = []) -> str:
289 """Given a list of files upload them and return the corresponding URL.
290
291 Also accepts optional parameters:
292 - `message': message used as a description of the transfer
293 - `sender': email address used to receive an ACK if the upload is
294 successfull. For every download by the recipients an email
295 will be also sent
296 - `recipients': list of email addresses of recipients. When the upload
297 succeed every recipients will receive an email with a link
298
299 If both sender and recipient parameters are passed the email upload will be
300 used. Otherwise, the link upload will be used.
301
302 Return the short URL of the transfer on success.
303 """
304
305 # Check that all files exists
306 for f in files:
307 if not os.path.exists(f):
308 raise FileNotFoundError(f)
309
310 # Check that there are no duplicates filenames
311 # (despite possible different dirname())
312 filenames = [os.path.basename(f) for f in files]
313 if len(files) != len(set(filenames)):
314 raise FileExistsError('Duplicate filenames')
315
316 transfer_id = None
317 rd = _prepare_request_data()
318 if sender and recipients:
319 # email upload
320 transfer_id = \
321 _prepare_email_upload(files, message, sender, recipients, rd)['id']
322 else:
323 # link upload
324 transfer_id = _prepare_link_upload(files, message, rd)['id']
325
326 for f in files:
327 file_id = _prepare_file_upload(transfer_id, f, rd)['id']
328 _upload_chunks(transfer_id, file_id, f, rd)
329
330 return _finalize_upload(transfer_id, rd)['shortened_url']
331
332
333 if __name__ == '__main__':
334 import argparse
335
336 ap = argparse.ArgumentParser(
337 prog='transferwee',
338 description='Download/upload files via wetransfer.com'
339 )
340 sp = ap.add_subparsers(dest='action', help='action')
341
342 # download subcommand
343 dp = sp.add_parser('download', help='download files')
344 dp.add_argument('-g', action='store_true',
345 help='only print the direct link (without downloading it)')
346 dp.add_argument('url', nargs='+', type=str, metavar='url',
347 help='URL (we.tl/... or wetransfer.com/downloads/...)')
348
349 # upload subcommand
350 up = sp.add_parser('upload', help='upload files')
351 up.add_argument('-m', type=str, default='', metavar='message',
352 help='message description for the transfer')
353 up.add_argument('-f', type=str, metavar='from', help='sender email')
354 up.add_argument('-t', nargs='+', type=str, metavar='to',
355 help='recipient emails')
356 up.add_argument('files', nargs='+', type=str, metavar='file',
357 help='files to upload')
358
359 args = ap.parse_args()
360
361 if args.action == 'download':
362 if args.g:
363 for u in args.url:
364 print(download_url(u))
365 else:
366 for u in args.url:
367 download(u)
368 exit(0)
369
370 if args.action == 'upload':
371 print(upload(args.files, args.m, args.f, args.t))
372 exit(0)
373
374 # No action selected, print help message
375 ap.print_help()
376 exit(1)