onionshare_cli 2.6.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. onionshare_cli/__init__.py +538 -0
  2. onionshare_cli/censorship.py +311 -0
  3. onionshare_cli/common.py +624 -0
  4. onionshare_cli/meek.py +199 -0
  5. onionshare_cli/mode_settings.py +150 -0
  6. onionshare_cli/onion.py +964 -0
  7. onionshare_cli/onionshare.py +91 -0
  8. onionshare_cli/resources/static/css/style.css +432 -0
  9. onionshare_cli/resources/static/img/ajax.gif +0 -0
  10. onionshare_cli/resources/static/img/favicon.ico +0 -0
  11. onionshare_cli/resources/static/img/logo.png +0 -0
  12. onionshare_cli/resources/static/img/logo_large.png +0 -0
  13. onionshare_cli/resources/static/img/web_file.png +0 -0
  14. onionshare_cli/resources/static/img/web_folder.png +0 -0
  15. onionshare_cli/resources/static/js/chat.js +188 -0
  16. onionshare_cli/resources/static/js/jquery-3.5.1.min.js +2 -0
  17. onionshare_cli/resources/static/js/receive.js +141 -0
  18. onionshare_cli/resources/static/js/send.js +79 -0
  19. onionshare_cli/resources/static/js/socket.io.js +9 -0
  20. onionshare_cli/resources/static/js/socket.io.min.js +7 -0
  21. onionshare_cli/resources/templates/403.html +21 -0
  22. onionshare_cli/resources/templates/404.html +21 -0
  23. onionshare_cli/resources/templates/405.html +21 -0
  24. onionshare_cli/resources/templates/500.html +21 -0
  25. onionshare_cli/resources/templates/chat.html +47 -0
  26. onionshare_cli/resources/templates/denied.html +14 -0
  27. onionshare_cli/resources/templates/listing.html +60 -0
  28. onionshare_cli/resources/templates/receive.html +66 -0
  29. onionshare_cli/resources/templates/send.html +70 -0
  30. onionshare_cli/resources/templates/thankyou.html +27 -0
  31. onionshare_cli/resources/torrc_template +12 -0
  32. onionshare_cli/resources/torrc_template-meek_lite_azure +2 -0
  33. onionshare_cli/resources/torrc_template-obfs4 +12 -0
  34. onionshare_cli/resources/torrc_template-snowflake +3 -0
  35. onionshare_cli/resources/version.txt +1 -0
  36. onionshare_cli/resources/wordlist.txt +7776 -0
  37. onionshare_cli/settings.py +209 -0
  38. onionshare_cli/web/__init__.py +21 -0
  39. onionshare_cli/web/chat_mode.py +190 -0
  40. onionshare_cli/web/receive_mode.py +630 -0
  41. onionshare_cli/web/send_base_mode.py +360 -0
  42. onionshare_cli/web/share_mode.py +599 -0
  43. onionshare_cli/web/web.py +409 -0
  44. onionshare_cli/web/website_mode.py +122 -0
  45. onionshare_cli-2.6.1.dist-info/METADATA +44 -0
  46. onionshare_cli-2.6.1.dist-info/RECORD +48 -0
  47. onionshare_cli-2.6.1.dist-info/WHEEL +4 -0
  48. onionshare_cli-2.6.1.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,630 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ OnionShare | https://onionshare.org/
4
+
5
+ Copyright (C) 2014-2022 Micah Lee, et al. <micah@micahflee.com>
6
+
7
+ This program is free software: you can redistribute it and/or modify
8
+ it under the terms of the GNU 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 General Public License for more details.
16
+
17
+ You should have received a copy of the GNU General Public License
18
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
19
+ """
20
+
21
+ import os
22
+ import tempfile
23
+ import json
24
+ import requests
25
+ from datetime import datetime
26
+ from flask import Request, request, render_template, make_response, flash, redirect
27
+ from werkzeug.utils import secure_filename
28
+
29
+ # Receive mode uses a special flask requests object, ReceiveModeRequest, in
30
+ # order to keep track of upload progress. Here's what happens when someone
31
+ # uploads files:
32
+ # - new ReceiveModeRequest object is created
33
+ # - ReceiveModeRequest.__init__
34
+ # - creates a directory based on the timestamp
35
+ # - creates empty self.progress = dict, which will map uploaded files to their upload progress
36
+ # - ReceiveModeRequest._get_file_stream
37
+ # - called for each file that gets upload
38
+ # - the first time, send REQUEST_STARTED to GUI, and append to self.web.receive_mode.uploads_in_progress
39
+ # - updates self.progress[self.filename] for the current file
40
+ # - uses custom ReceiveModeFile to save file to disk
41
+ # - ReceiveModeRequest.file_write_func called on each write
42
+ # - Display progress in CLI, and send REQUEST_PROGRESS to GUI
43
+ # - ReceiveModeRequest.file_close_func called when each file closes
44
+ # - self.progress[filename]["complete"] = True
45
+ # - ReceiveModeRequest.close
46
+ # - send either REQUEST_UPLOAD_CANCELED or REQUEST_UPLOAD_FINISHED to GUI
47
+ # - remove from self.web.receive_mode.uploads_in_progress
48
+
49
+
50
+ class ReceiveModeWeb:
51
+ """
52
+ All of the web logic for receive mode
53
+ """
54
+
55
+ def __init__(self, common, web):
56
+ self.common = common
57
+ self.common.log("ReceiveModeWeb", "__init__")
58
+
59
+ self.web = web
60
+
61
+ self.can_upload = True
62
+ self.uploads_in_progress = []
63
+
64
+ # This tracks the history id
65
+ self.cur_history_id = 0
66
+
67
+ # Whether or not we can send REQUEST_INDIVIDUAL_FILE_STARTED
68
+ # and maybe other events when requests come in to this mode
69
+ self.supports_file_requests = True
70
+
71
+ self.define_routes()
72
+
73
+ def define_routes(self):
74
+ """
75
+ The web app routes for receiving files
76
+ """
77
+
78
+ @self.web.app.route("/", methods=["GET"], provide_automatic_options=False)
79
+ def index():
80
+ history_id = self.cur_history_id
81
+ self.cur_history_id += 1
82
+ self.web.add_request(
83
+ self.web.REQUEST_INDIVIDUAL_FILE_STARTED,
84
+ request.path,
85
+ {"id": history_id, "status_code": 200},
86
+ )
87
+
88
+ self.web.add_request(self.web.REQUEST_LOAD, request.path)
89
+ return render_template(
90
+ "receive.html",
91
+ static_url_path=self.web.static_url_path,
92
+ disable_text=self.web.settings.get("receive", "disable_text"),
93
+ disable_files=self.web.settings.get("receive", "disable_files"),
94
+ title=self.web.settings.get("general", "title")
95
+ )
96
+
97
+ @self.web.app.route("/upload", methods=["POST"], provide_automatic_options=False)
98
+ def upload(ajax=False):
99
+ """
100
+ Handle the upload files POST request, though at this point, the files have
101
+ already been uploaded and saved to their correct locations.
102
+ """
103
+ message_received = request.includes_message
104
+
105
+ files_received = 0
106
+ if not self.web.settings.get("receive", "disable_files"):
107
+ files = request.files.getlist("file[]")
108
+
109
+ filenames = []
110
+ for f in files:
111
+ if f.filename != "":
112
+ filename = secure_filename(f.filename)
113
+ filenames.append(filename)
114
+ local_path = os.path.join(request.receive_mode_dir, filename)
115
+ basename = os.path.basename(local_path)
116
+
117
+ # Tell the GUI the receive mode directory for this file
118
+ self.web.add_request(
119
+ self.web.REQUEST_UPLOAD_SET_DIR,
120
+ request.path,
121
+ {
122
+ "id": request.history_id,
123
+ "filename": basename,
124
+ "dir": request.receive_mode_dir,
125
+ },
126
+ )
127
+
128
+ self.common.log(
129
+ "ReceiveModeWeb",
130
+ "define_routes",
131
+ f"/upload, uploaded {f.filename}, saving to {local_path}",
132
+ )
133
+ print(f"Received: {local_path}")
134
+
135
+ files_received = len(filenames)
136
+
137
+ # Send webhook if configured
138
+ if (
139
+ self.web.settings.get("receive", "webhook_url") is not None
140
+ and not request.upload_error
141
+ and (message_received or files_received)
142
+ ):
143
+ msg = ""
144
+ if files_received > 0:
145
+ if files_received == 1:
146
+ msg += "1 file"
147
+ else:
148
+ msg += f"{files_received} files"
149
+ if message_received:
150
+ if msg == "":
151
+ msg = "A text message"
152
+ else:
153
+ msg += " and a text message"
154
+ self.send_webhook_notification(f"{msg} submitted to OnionShare")
155
+
156
+ if request.upload_error:
157
+ self.common.log(
158
+ "ReceiveModeWeb",
159
+ "define_routes",
160
+ "/upload, there was an upload error",
161
+ )
162
+
163
+ self.web.add_request(
164
+ self.web.REQUEST_ERROR_DATA_DIR_CANNOT_CREATE,
165
+ request.path,
166
+ {"receive_mode_dir": request.receive_mode_dir},
167
+ )
168
+ print(
169
+ f"Could not create OnionShare data folder: {request.receive_mode_dir}"
170
+ )
171
+
172
+ msg = "Error uploading, please inform the OnionShare user"
173
+ if ajax:
174
+ return json.dumps({"error_flashes": [msg]})
175
+ else:
176
+ flash(msg, "error")
177
+ return redirect("/")
178
+
179
+ if ajax:
180
+ info_flashes = []
181
+
182
+ if files_received > 0:
183
+ files_msg = ""
184
+ for filename in filenames:
185
+ files_msg += f"{filename}, "
186
+ files_msg = files_msg.rstrip(", ")
187
+
188
+ if message_received:
189
+ if files_received > 0:
190
+ msg = f"Message submitted, uploaded {files_msg}"
191
+ else:
192
+ msg = "Message submitted"
193
+ else:
194
+ if files_received > 0:
195
+ msg = f"Uploaded {files_msg}"
196
+ else:
197
+ msg = "Nothing submitted"
198
+
199
+ if ajax:
200
+ info_flashes.append(msg)
201
+ else:
202
+ flash(msg, "info")
203
+
204
+ if self.can_upload:
205
+ if ajax:
206
+ return json.dumps({"info_flashes": info_flashes})
207
+ else:
208
+ return redirect("/")
209
+ else:
210
+ if ajax:
211
+ return json.dumps(
212
+ {
213
+ "new_body": render_template(
214
+ "thankyou.html",
215
+ static_url_path=self.web.static_url_path,
216
+ title=self.web.settings.get("general", "title"),
217
+ )
218
+ }
219
+ )
220
+ else:
221
+ # It was the last upload and the timer ran out
222
+ return make_response(
223
+ render_template("thankyou.html"),
224
+ static_url_path=self.web.static_url_path,
225
+ title=self.web.settings.get("general", "title"),
226
+ )
227
+
228
+ @self.web.app.route("/upload-ajax", methods=["POST"], provide_automatic_options=False)
229
+ def upload_ajax_public():
230
+ if not self.can_upload:
231
+ return self.web.error403()
232
+ return upload(ajax=True)
233
+
234
+ def send_webhook_notification(self, data):
235
+ self.common.log("ReceiveModeWeb", "send_webhook_notification", data)
236
+ try:
237
+ requests.post(
238
+ self.web.settings.get("receive", "webhook_url"),
239
+ data=data,
240
+ timeout=5,
241
+ proxies=self.web.proxies,
242
+ )
243
+ except Exception as e:
244
+ print(f"Webhook notification failed: {e}")
245
+
246
+
247
+ class ReceiveModeWSGIMiddleware(object):
248
+ """
249
+ Custom WSGI middleware in order to attach the Web object to environ, so
250
+ ReceiveModeRequest can access it.
251
+ """
252
+
253
+ def __init__(self, app, web):
254
+ self.app = app
255
+ self.web = web
256
+
257
+ def __call__(self, environ, start_response):
258
+ environ["web"] = self.web
259
+ environ["stop_q"] = self.web.stop_q
260
+ return self.app(environ, start_response)
261
+
262
+
263
+ class ReceiveModeFile(object):
264
+ """
265
+ A custom file object that tells ReceiveModeRequest every time data gets
266
+ written to it, in order to track the progress of uploads. It starts out with
267
+ a .part file extension, and when it's complete it removes that extension.
268
+ """
269
+
270
+ def __init__(self, request, filename, write_func, close_func):
271
+ self.onionshare_request = request
272
+ self.onionshare_filename = filename
273
+ self.onionshare_write_func = write_func
274
+ self.onionshare_close_func = close_func
275
+
276
+ self.filename = os.path.join(self.onionshare_request.receive_mode_dir, filename)
277
+ self.filename_in_progress = f"{self.filename}.part"
278
+
279
+ # Open the file
280
+ self.upload_error = False
281
+ try:
282
+ self.f = open(self.filename_in_progress, "wb+")
283
+ except Exception:
284
+ # This will only happen if someone is messing with the data dir while
285
+ # OnionShare is running, but if it does make sure to throw an error
286
+ self.upload_error = True
287
+ self.f = tempfile.TemporaryFile("wb+")
288
+
289
+ # Make all the file-like methods and attributes actually access the
290
+ # TemporaryFile, except for write
291
+ attrs = [
292
+ "closed",
293
+ "detach",
294
+ "fileno",
295
+ "flush",
296
+ "isatty",
297
+ "mode",
298
+ "name",
299
+ "peek",
300
+ "raw",
301
+ "read",
302
+ "read1",
303
+ "readable",
304
+ "readinto",
305
+ "readinto1",
306
+ "readline",
307
+ "readlines",
308
+ "seek",
309
+ "seekable",
310
+ "tell",
311
+ "truncate",
312
+ "writable",
313
+ "writelines",
314
+ ]
315
+ for attr in attrs:
316
+ setattr(self, attr, getattr(self.f, attr))
317
+
318
+ def write(self, b):
319
+ """
320
+ Custom write method that calls out to onionshare_write_func
321
+ """
322
+ if self.upload_error or (not self.onionshare_request.stop_q.empty()):
323
+ self.close()
324
+ self.onionshare_request.close()
325
+ return
326
+
327
+ try:
328
+ bytes_written = self.f.write(b)
329
+ self.onionshare_write_func(self.onionshare_filename, bytes_written)
330
+
331
+ except Exception:
332
+ self.upload_error = True
333
+
334
+ def close(self):
335
+ """
336
+ Custom close method that calls out to onionshare_close_func
337
+ """
338
+ try:
339
+ self.f.close()
340
+
341
+ if not self.upload_error:
342
+ # Rename the in progress file to the final filename
343
+ os.rename(self.filename_in_progress, self.filename)
344
+
345
+ except Exception:
346
+ self.upload_error = True
347
+
348
+ self.onionshare_close_func(self.onionshare_filename, self.upload_error)
349
+
350
+
351
+ class ReceiveModeRequest(Request):
352
+ """
353
+ A custom flask Request object that keeps track of how much data has been
354
+ uploaded for each file, for receive mode.
355
+ """
356
+
357
+ def __init__(self, environ, populate_request=True, shallow=False):
358
+ super(ReceiveModeRequest, self).__init__(environ, populate_request, shallow)
359
+ self.web = environ["web"]
360
+ self.stop_q = environ["stop_q"]
361
+ self.filename = None
362
+
363
+ # Prevent running the close() method more than once
364
+ self.closed = False
365
+
366
+ # Is this a valid upload request?
367
+ self.upload_request = False
368
+ if self.method == "POST":
369
+ if self.path == "/upload" or self.path == "/upload-ajax":
370
+ self.upload_request = True
371
+
372
+ if self.upload_request:
373
+ self.web.common.log("ReceiveModeRequest", "__init__")
374
+
375
+ # No errors yet
376
+ self.upload_error = False
377
+
378
+ # Figure out what files should be saved
379
+ now = datetime.now()
380
+ date_dir = now.strftime("%Y-%m-%d")
381
+ time_dir = now.strftime("%H%M%S%f")
382
+ self.receive_mode_dir = os.path.join(
383
+ self.web.settings.get("receive", "data_dir"), date_dir, time_dir
384
+ )
385
+
386
+ # Create that directory, which shouldn't exist yet
387
+ try:
388
+ os.makedirs(self.receive_mode_dir, 0o700, exist_ok=False)
389
+ except OSError:
390
+ # If this directory already exists, maybe someone else is uploading files at
391
+ # the same second, so use a different name in that case
392
+ if os.path.exists(self.receive_mode_dir):
393
+ # Keep going until we find a directory name that's available
394
+ i = 1
395
+ while True:
396
+ new_receive_mode_dir = f"{self.receive_mode_dir}-{i}"
397
+ try:
398
+ os.makedirs(new_receive_mode_dir, 0o700, exist_ok=False)
399
+ self.receive_mode_dir = new_receive_mode_dir
400
+ break
401
+ except OSError:
402
+ pass
403
+ i += 1
404
+ # Failsafe
405
+ if i == 100:
406
+ self.web.common.log(
407
+ "ReceiveModeRequest",
408
+ "__init__",
409
+ "Error finding available receive mode directory",
410
+ )
411
+ self.upload_error = True
412
+ break
413
+ except PermissionError:
414
+ self.web.add_request(
415
+ self.web.REQUEST_ERROR_DATA_DIR_CANNOT_CREATE,
416
+ request.path,
417
+ {"receive_mode_dir": self.receive_mode_dir},
418
+ )
419
+ print(
420
+ f"Could not create OnionShare data folder: {self.receive_mode_dir}"
421
+ )
422
+ self.web.common.log(
423
+ "ReceiveModeRequest",
424
+ "__init__",
425
+ "Permission denied creating receive mode directory",
426
+ )
427
+ self.upload_error = True
428
+
429
+ # Figure out the message filename, in case there is a message
430
+ self.message_filename = f"{self.receive_mode_dir}-message.txt"
431
+
432
+ # If there's an error so far, finish early
433
+ if self.upload_error:
434
+ return
435
+
436
+ # A dictionary that maps filenames to the bytes uploaded so far
437
+ self.progress = {}
438
+
439
+ # Prevent new uploads if we've said so (timer expired)
440
+ if self.web.receive_mode.can_upload:
441
+
442
+ # Create an history_id, attach it to the request
443
+ self.history_id = self.web.receive_mode.cur_history_id
444
+ self.web.receive_mode.cur_history_id += 1
445
+
446
+ # Figure out the content length
447
+ try:
448
+ self.content_length = int(self.headers["Content-Length"])
449
+ except Exception:
450
+ self.content_length = 0
451
+
452
+ date_str = datetime.now().strftime("%b %d, %I:%M%p")
453
+ size_str = self.web.common.human_readable_filesize(self.content_length)
454
+ print(f"{date_str}: Upload of total size {size_str} is starting")
455
+
456
+ # Don't tell the GUI that a request has started until we start receiving files
457
+ self.told_gui_about_request = False
458
+
459
+ self.previous_file = None
460
+
461
+ # Is there a text message?
462
+ self.includes_message = False
463
+ if not self.web.settings.get("receive", "disable_text"):
464
+ text_message = self.form.get("text")
465
+ if text_message:
466
+ if text_message.strip() != "":
467
+ self.includes_message = True
468
+
469
+ with open(self.message_filename, "w") as f:
470
+ f.write(text_message)
471
+
472
+ self.web.common.log(
473
+ "ReceiveModeRequest",
474
+ "__init__",
475
+ f"saved message to {self.message_filename}",
476
+ )
477
+ print(f"Received: {self.message_filename}")
478
+
479
+ # Tell the GUI about the message
480
+ self.tell_gui_request_started()
481
+ self.web.common.log(
482
+ "ReceiveModeRequest",
483
+ "__init__",
484
+ "sending REQUEST_UPLOAD_INCLUDES_MESSAGE to GUI",
485
+ )
486
+ self.web.add_request(
487
+ self.web.REQUEST_UPLOAD_INCLUDES_MESSAGE,
488
+ self.path,
489
+ {
490
+ "id": self.history_id,
491
+ "filename": self.message_filename,
492
+ },
493
+ )
494
+
495
+ def tell_gui_request_started(self):
496
+ # Tell the GUI about the request
497
+ if not self.told_gui_about_request:
498
+ self.web.common.log(
499
+ "ReceiveModeRequest",
500
+ "tell_gui_request_started",
501
+ "sending REQUEST_STARTED to GUI",
502
+ )
503
+ self.web.add_request(
504
+ self.web.REQUEST_STARTED,
505
+ self.path,
506
+ {
507
+ "id": self.history_id,
508
+ "content_length": self.content_length,
509
+ },
510
+ )
511
+ self.web.receive_mode.uploads_in_progress.append(self.history_id)
512
+
513
+ self.told_gui_about_request = True
514
+
515
+ def _get_file_stream(
516
+ self, total_content_length, content_type, filename=None, content_length=None
517
+ ):
518
+ """
519
+ This gets called for each file that gets uploaded, and returns an file-like
520
+ writable stream.
521
+ """
522
+ if self.upload_request:
523
+ self.tell_gui_request_started()
524
+
525
+ self.filename = secure_filename(filename)
526
+
527
+ self.progress[self.filename] = {"uploaded_bytes": 0, "complete": False}
528
+
529
+ f = ReceiveModeFile(
530
+ self, self.filename, self.file_write_func, self.file_close_func
531
+ )
532
+ if f.upload_error:
533
+ self.web.common.log(
534
+ "ReceiveModeRequest", "_get_file_stream", "Error creating file"
535
+ )
536
+ self.upload_error = True
537
+ return f
538
+
539
+ def close(self):
540
+ """
541
+ Closing the request.
542
+ """
543
+ super(ReceiveModeRequest, self).close()
544
+
545
+ # Prevent calling this method more than once per request
546
+ if self.closed:
547
+ return
548
+ self.closed = True
549
+
550
+ if self.upload_request:
551
+ self.web.common.log("ReceiveModeRequest", "close")
552
+
553
+ if self.told_gui_about_request:
554
+ history_id = self.history_id
555
+
556
+ if not self.web.stop_q.empty() or (
557
+ self.filename in self.progress
558
+ and not self.progress[self.filename]["complete"]
559
+ ):
560
+ # Inform the GUI that the upload has canceled
561
+ self.web.common.log(
562
+ "ReceiveModeRequest",
563
+ "close",
564
+ "sending REQUEST_UPLOAD_CANCELED to GUI",
565
+ )
566
+ self.web.add_request(
567
+ self.web.REQUEST_UPLOAD_CANCELED,
568
+ self.path,
569
+ {"id": history_id},
570
+ )
571
+ else:
572
+ # Inform the GUI that the upload has finished
573
+ self.web.common.log(
574
+ "ReceiveModeRequest",
575
+ "close",
576
+ "sending REQUEST_UPLOAD_FINISHED to GUI",
577
+ )
578
+ self.web.add_request(
579
+ self.web.REQUEST_UPLOAD_FINISHED,
580
+ self.path,
581
+ {"id": history_id},
582
+ )
583
+ self.web.receive_mode.uploads_in_progress.remove(history_id)
584
+
585
+ # If no files were written to self.receive_mode_dir, delete it
586
+ try:
587
+ if len(os.listdir(self.receive_mode_dir)) == 0:
588
+ os.rmdir(self.receive_mode_dir)
589
+ except Exception:
590
+ pass
591
+
592
+ def file_write_func(self, filename, length):
593
+ """
594
+ This function gets called when a specific file is written to.
595
+ """
596
+ if self.closed:
597
+ return
598
+
599
+ if self.upload_request:
600
+ self.progress[filename]["uploaded_bytes"] += length
601
+
602
+ if self.previous_file != filename:
603
+ self.previous_file = filename
604
+
605
+ size_str = self.web.common.human_readable_filesize(
606
+ self.progress[filename]["uploaded_bytes"]
607
+ )
608
+
609
+ if self.web.common.verbose:
610
+ print(f"=> {size_str} {filename}")
611
+ else:
612
+ print(f"\r=> {size_str} {filename} ", end="")
613
+
614
+ # Update the GUI on the upload progress
615
+ if self.told_gui_about_request:
616
+ self.web.add_request(
617
+ self.web.REQUEST_PROGRESS,
618
+ self.path,
619
+ {"id": self.history_id, "progress": self.progress},
620
+ )
621
+
622
+ def file_close_func(self, filename, upload_error=False):
623
+ """
624
+ This function gets called when a specific file is closed.
625
+ """
626
+ self.progress[filename]["complete"] = True
627
+
628
+ # If the file tells us there was an upload error, let the request know as well
629
+ if upload_error:
630
+ self.upload_error = True