aird 0.4.22__tar.gz → 0.4.23.dev2__tar.gz
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.
- {aird-0.4.22 → aird-0.4.23.dev2}/PKG-INFO +1 -1
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/config.py +6 -2
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/constants/__init__.py +4 -2
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/constants/file_ops.py +5 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/handlers/admin_handlers.py +1 -3
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/handlers/file_op_handlers.py +328 -17
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/handlers/share_handlers.py +21 -5
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/handlers/view_handlers.py +2 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/main.py +6 -6
- aird-0.4.23.dev2/aird/static/css/app.css +2 -0
- aird-0.4.23.dev2/aird/static/favicon.png +0 -0
- aird-0.4.23.dev2/aird/static/favicon.svg +12 -0
- aird-0.4.23.dev2/aird/static/img/logo-icon.png +0 -0
- aird-0.4.23.dev2/aird/static/img/logo-mark.svg +15 -0
- aird-0.4.23.dev2/aird/static/img/logo-text.png +0 -0
- aird-0.4.23.dev2/aird/static/img/logo.png +0 -0
- aird-0.4.23.dev2/aird/static/js/aird-core.js +227 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/static/js/bg-canvas.js +1 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/static/js/browse/app.js +641 -319
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/static/js/common/command-palette.js +11 -3
- aird-0.4.23.dev2/aird/static/js/components/folder-picker.js +163 -0
- aird-0.4.23.dev2/aird/static/js/feature-flags-live.js +43 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/static/js/login-ui.js +6 -4
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/static/js/pages/p2p-page.js +22 -18
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/static/js/pages/super-search.js +2 -1
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/static/js/share/app.js +162 -169
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/static/js/theme.js +34 -1
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/templates/_app_nav_header.html +4 -3
- aird-0.4.23.dev2/aird/templates/_theme_early.html +29 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/templates/admin.html +6 -5
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/templates/admin_audit.html +12 -16
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/templates/admin_ldap.html +4 -3
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/templates/admin_login.html +9 -3
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/templates/admin_network_shares.html +4 -3
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/templates/admin_policies.html +7 -5
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/templates/admin_tags.html +9 -6
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/templates/admin_user_attributes.html +6 -4
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/templates/admin_users.html +4 -3
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/templates/browse.html +170 -152
- aird-0.4.23.dev2/aird/templates/directory.html +46 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/templates/edit.html +4 -3
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/templates/error.html +4 -3
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/templates/file.html +15 -11
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/templates/ldap_config_create.html +3 -2
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/templates/ldap_config_edit.html +3 -2
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/templates/login.html +9 -3
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/templates/p2p_transfer.html +4 -4
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/templates/profile.html +4 -3
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/templates/share.html +10 -10
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/templates/shared_list.html +110 -6
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/templates/super_search.html +8 -7
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/templates/tagged_files.html +5 -4
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/templates/token_verification.html +4 -3
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/templates/user_create.html +3 -2
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/templates/user_edit.html +3 -2
- {aird-0.4.22 → aird-0.4.23.dev2}/aird.egg-info/PKG-INFO +1 -1
- {aird-0.4.22 → aird-0.4.23.dev2}/aird.egg-info/SOURCES.txt +9 -3
- {aird-0.4.22 → aird-0.4.23.dev2}/setup.py +4 -1
- {aird-0.4.22 → aird-0.4.23.dev2}/tests/test_admin_handlers.py +3 -3
- {aird-0.4.22 → aird-0.4.23.dev2}/tests/test_file_op_handlers.py +155 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/tests/test_main.py +2 -2
- {aird-0.4.22 → aird-0.4.23.dev2}/tests/test_share_handlers.py +59 -0
- aird-0.4.22/aird/static/css/app.css +0 -2
- aird-0.4.22/aird/static/js/aird-core.js +0 -107
- aird-0.4.22/aird/static/js/common/ui-utils.js +0 -40
- aird-0.4.22/aird/static/js/vendor/marked.umd.min.js +0 -60
- aird-0.4.22/aird/static/js/vendor/purify.min.js +0 -3
- aird-0.4.22/aird/templates/directory.html +0 -33
- {aird-0.4.22 → aird-0.4.23.dev2}/LICENSE +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/README.md +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/__init__.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/__main__.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/app_context.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/cloud/__init__.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/constants/admin.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/constants/input_limits.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/constants/media.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/core/__init__.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/core/events.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/core/file_operations.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/core/filter_expression.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/core/input_validation.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/core/mmap_handler.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/core/security.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/core/share_root.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/core/websocket_manager.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/database/__init__.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/database/db.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/database/feature_flags.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/database/ldap.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/db/__init__.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/db/audit.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/db/config.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/db/favorites.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/db/network_shares.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/db/policies.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/db/policy_decisions.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/db/policy_seeds.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/db/quota.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/db/resource_tags.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/db/schema.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/db/shares.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/db/user_attributes.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/db/users.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/domain/__init__.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/domain/contracts.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/domain/models.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/handlers/__init__.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/handlers/abac_handlers.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/handlers/api_handlers.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/handlers/auth_handlers.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/handlers/base_handler.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/handlers/constants.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/handlers/health_handler.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/handlers/p2p_handlers.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/network_share_manager.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/services/__init__.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/services/audit_service.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/services/config_service.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/services/event_subscribers.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/services/favorites_service.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/services/network_share_service.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/services/p2p_service.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/services/policy_service.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/services/quota_service.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/services/share_service.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/services/tag_service.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/services/user_service.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/sql_identifiers.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/static/js/p2p/app.js +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/static/js/p2p/mediator.js +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/static/js/p2p/qr-adapter.js +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/static/js/p2p/signaling-service.js +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/static/js/p2p/state-machine.js +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/static/js/p2p/transfer-service.js +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/static/js/vendor/pdf.min.js +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/static/js/vendor/pdf.worker.min.js +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/static/js/vendor/qrcode-browser.js +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/templates/_admin_tabs.html +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/templates/_bg_canvas.html +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/templates/_theme_login_corner.html +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/utils/__init__.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird/utils/util.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird.egg-info/dependency_links.txt +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird.egg-info/entry_points.txt +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird.egg-info/requires.txt +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/aird.egg-info/top_level.txt +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/setup.cfg +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/tests/__init__.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/tests/conftest.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/tests/handler_helpers.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/tests/test_api_handlers.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/tests/test_architecture_conformance.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/tests/test_auth_handlers.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/tests/test_auth_handlers_extended.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/tests/test_base_handler.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/tests/test_base_handler_pep.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/tests/test_cloud.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/tests/test_config.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/tests/test_core_file_operations.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/tests/test_database_db.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/tests/test_database_feature_flags.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/tests/test_database_ldap.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/tests/test_database_shares.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/tests/test_database_users.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/tests/test_database_users_hashing.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/tests/test_db.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/tests/test_filter_expression.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/tests/test_mmap_handler.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/tests/test_multi_user.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/tests/test_network_shares.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/tests/test_p2p_handlers.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/tests/test_password_hashing.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/tests/test_policy_service.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/tests/test_rate_limit.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/tests/test_security.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/tests/test_security_comprehensive.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/tests/test_super_search_handler.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/tests/test_tag_service.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/tests/test_util.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/tests/test_view_handlers.py +0 -0
- {aird-0.4.22 → aird-0.4.23.dev2}/tests/test_websocket_manager.py +0 -0
|
@@ -16,7 +16,9 @@ from aird.constants import (
|
|
|
16
16
|
ALLOWED_UPLOAD_EXTENSIONS as _ALLOWED_UPLOAD_EXTENSIONS,
|
|
17
17
|
MMAP_MIN_SIZE as _MMAP_MIN_SIZE,
|
|
18
18
|
CHUNK_SIZE as _CHUNK_SIZE,
|
|
19
|
-
|
|
19
|
+
UPLOAD_CHUNK_SIZE_BYTES as _UPLOAD_CHUNK_SIZE_BYTES,
|
|
20
|
+
UPLOAD_REQUEST_MAX_BODY_SIZE as _UPLOAD_REQUEST_MAX_BODY_SIZE,
|
|
21
|
+
UPLOAD_MAX_PARALLEL_CHUNKS as _UPLOAD_MAX_PARALLEL_CHUNKS,
|
|
20
22
|
)
|
|
21
23
|
|
|
22
24
|
# Module-level variables to hold configuration
|
|
@@ -46,7 +48,9 @@ MAX_READABLE_FILE_SIZE = _MAX_READABLE_FILE_SIZE
|
|
|
46
48
|
ALLOWED_UPLOAD_EXTENSIONS = _ALLOWED_UPLOAD_EXTENSIONS
|
|
47
49
|
MMAP_MIN_SIZE = _MMAP_MIN_SIZE
|
|
48
50
|
CHUNK_SIZE = _CHUNK_SIZE
|
|
49
|
-
|
|
51
|
+
UPLOAD_CHUNK_SIZE_BYTES = _UPLOAD_CHUNK_SIZE_BYTES
|
|
52
|
+
UPLOAD_REQUEST_MAX_BODY_SIZE = _UPLOAD_REQUEST_MAX_BODY_SIZE
|
|
53
|
+
UPLOAD_MAX_PARALLEL_CHUNKS = _UPLOAD_MAX_PARALLEL_CHUNKS
|
|
50
54
|
|
|
51
55
|
|
|
52
56
|
def _configure_google_drive(cloud_config: dict) -> None:
|
|
@@ -59,8 +59,10 @@ UPLOAD_CONFIG = {
|
|
|
59
59
|
"allow_all_file_types": 0, # 0 = use whitelist below, 1 = allow any extension
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
#
|
|
63
|
-
|
|
62
|
+
# Per-request body limit (Cloudflare-compatible chunked uploads; admin sets total file cap separately)
|
|
63
|
+
UPLOAD_CHUNK_SIZE_BYTES = 90 * 1024 * 1024 # 90 MiB per HTTP request
|
|
64
|
+
UPLOAD_REQUEST_MAX_BODY_SIZE = UPLOAD_CHUNK_SIZE_BYTES + (1024 * 1024) # chunk + slack
|
|
65
|
+
UPLOAD_MAX_PARALLEL_CHUNKS = 5 # max concurrent chunk requests from the browser
|
|
64
66
|
|
|
65
67
|
# File operation constants (derived from UPLOAD_CONFIG at startup)
|
|
66
68
|
MAX_FILE_SIZE = UPLOAD_CONFIG["max_file_size_mb"] * 1024 * 1024
|
|
@@ -35,6 +35,11 @@ UNSUPPORTED_FILE_TYPE = (
|
|
|
35
35
|
)
|
|
36
36
|
UPLOAD_SAVE_FAILED = "Failed to save upload. Please try again."
|
|
37
37
|
UPLOAD_SUCCESSFUL = "Upload successful"
|
|
38
|
+
MISSING_UPLOAD_CHUNK_HEADERS = "Missing chunked upload headers"
|
|
39
|
+
INVALID_UPLOAD_CHUNK_HEADERS = "Invalid chunked upload headers"
|
|
40
|
+
UPLOAD_CHUNK_OUT_OF_ORDER = "Upload chunk out of order"
|
|
41
|
+
UPLOAD_SESSION_NOT_FOUND = "Upload session not found or expired"
|
|
42
|
+
UPLOAD_CHUNK_RECEIVED = '{"status":"chunk_received"}'
|
|
38
43
|
|
|
39
44
|
# CreateFolderHandler
|
|
40
45
|
FOLDER_CREATE_DISABLED = (
|
|
@@ -202,9 +202,7 @@ class AdminHandler(BaseHandler):
|
|
|
202
202
|
|
|
203
203
|
# Update upload configuration
|
|
204
204
|
try:
|
|
205
|
-
max_file_size_mb = max(
|
|
206
|
-
1, min(10240, int(self.get_argument("max_file_size_mb", "512")))
|
|
207
|
-
)
|
|
205
|
+
max_file_size_mb = max(1, int(self.get_argument("max_file_size_mb", "512")))
|
|
208
206
|
UPLOAD_CONFIG["max_file_size_mb"] = max_file_size_mb
|
|
209
207
|
constants_module.MAX_FILE_SIZE = max_file_size_mb * 1024 * 1024
|
|
210
208
|
UPLOAD_CONFIG["allow_all_file_types"] = (
|
|
@@ -5,6 +5,9 @@ import tempfile
|
|
|
5
5
|
import json
|
|
6
6
|
import logging
|
|
7
7
|
import pathlib
|
|
8
|
+
import hashlib
|
|
9
|
+
import re
|
|
10
|
+
import secrets
|
|
8
11
|
from collections import deque
|
|
9
12
|
from urllib.parse import unquote
|
|
10
13
|
import asyncio
|
|
@@ -81,6 +84,11 @@ from aird.constants.file_ops import (
|
|
|
81
84
|
UPLOAD_SAVE_FAILED,
|
|
82
85
|
UPLOAD_SUCCESSFUL,
|
|
83
86
|
MISSING_UPLOAD_FILENAME_HEADER,
|
|
87
|
+
MISSING_UPLOAD_CHUNK_HEADERS,
|
|
88
|
+
INVALID_UPLOAD_CHUNK_HEADERS,
|
|
89
|
+
UPLOAD_CHUNK_OUT_OF_ORDER,
|
|
90
|
+
UPLOAD_SESSION_NOT_FOUND,
|
|
91
|
+
UPLOAD_CHUNK_RECEIVED,
|
|
84
92
|
)
|
|
85
93
|
from aird.config import (
|
|
86
94
|
ALLOWED_UPLOAD_EXTENSIONS,
|
|
@@ -125,6 +133,182 @@ def _validate_upload_destination(upload_dir, filename, root_dir):
|
|
|
125
133
|
return (final_path_abs, None)
|
|
126
134
|
|
|
127
135
|
|
|
136
|
+
_UPLOAD_ID_RE = re.compile(r"^[a-zA-Z0-9\-]{1,64}$")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _upload_parts_dir() -> str:
|
|
140
|
+
path = os.path.join(tempfile.gettempdir(), "aird_upload_parts")
|
|
141
|
+
os.makedirs(path, exist_ok=True)
|
|
142
|
+
return path
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _upload_session_dir(username: str, upload_id: str) -> str:
|
|
146
|
+
user_hash = hashlib.sha256(username.encode("utf-8")).hexdigest()[:16]
|
|
147
|
+
return os.path.join(_upload_parts_dir(), f"{user_hash}_{upload_id}")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _chunk_file_path(session_dir: str, offset: int) -> str:
|
|
151
|
+
return os.path.join(session_dir, f"{offset}.part")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _iter_expected_chunks(total_size: int) -> list[tuple[int, int]]:
|
|
155
|
+
"""Return (byte_offset, chunk_length) for each upload part."""
|
|
156
|
+
chunk_size = constants_module.UPLOAD_CHUNK_SIZE_BYTES
|
|
157
|
+
parts: list[tuple[int, int]] = []
|
|
158
|
+
offset = 0
|
|
159
|
+
while offset < total_size:
|
|
160
|
+
length = min(chunk_size, total_size - offset)
|
|
161
|
+
parts.append((offset, length))
|
|
162
|
+
offset += length
|
|
163
|
+
return parts
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _upload_chunks_complete(session_dir: str, total_size: int) -> bool:
|
|
167
|
+
for offset, expected_len in _iter_expected_chunks(total_size):
|
|
168
|
+
path = _chunk_file_path(session_dir, offset)
|
|
169
|
+
if not os.path.isfile(path):
|
|
170
|
+
return False
|
|
171
|
+
try:
|
|
172
|
+
if os.path.getsize(path) != expected_len:
|
|
173
|
+
return False
|
|
174
|
+
except OSError:
|
|
175
|
+
return False
|
|
176
|
+
return True
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _stitch_upload_session(session_dir: str, dest_path: str, total_size: int) -> None:
|
|
180
|
+
"""Concatenate per-offset chunk files into one file."""
|
|
181
|
+
with open(dest_path, "wb") as out:
|
|
182
|
+
for offset, _length in _iter_expected_chunks(total_size):
|
|
183
|
+
part_path = _chunk_file_path(session_dir, offset)
|
|
184
|
+
with open(part_path, "rb") as part_file:
|
|
185
|
+
shutil.copyfileobj(part_file, out)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _stitch_lock_path(session_dir: str) -> str:
|
|
189
|
+
return os.path.join(session_dir, ".stitching")
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _try_acquire_stitch_lock(session_dir: str) -> bool:
|
|
193
|
+
try:
|
|
194
|
+
fd = os.open(_stitch_lock_path(session_dir), os.O_CREAT | os.O_EXCL | os.O_WRONLY)
|
|
195
|
+
os.close(fd)
|
|
196
|
+
return True
|
|
197
|
+
except FileExistsError:
|
|
198
|
+
return False
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _release_stitch_lock(session_dir: str) -> None:
|
|
202
|
+
try:
|
|
203
|
+
os.remove(_stitch_lock_path(session_dir))
|
|
204
|
+
except OSError:
|
|
205
|
+
logging.debug("stitch lock remove failed", exc_info=True)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _remove_upload_session(session_dir: str) -> None:
|
|
209
|
+
if not session_dir or not os.path.isdir(session_dir):
|
|
210
|
+
return
|
|
211
|
+
try:
|
|
212
|
+
shutil.rmtree(session_dir)
|
|
213
|
+
except OSError:
|
|
214
|
+
logging.debug("upload session remove failed", exc_info=True)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _sanitize_upload_id(upload_id: str | None) -> str | None:
|
|
218
|
+
if not upload_id or not _UPLOAD_ID_RE.fullmatch(upload_id):
|
|
219
|
+
return None
|
|
220
|
+
return upload_id
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _parse_positive_int(value: str | None) -> int | None:
|
|
224
|
+
if value is None or value == "":
|
|
225
|
+
return None
|
|
226
|
+
try:
|
|
227
|
+
parsed = int(value)
|
|
228
|
+
except ValueError:
|
|
229
|
+
return None
|
|
230
|
+
if parsed < 0:
|
|
231
|
+
return None
|
|
232
|
+
return parsed
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _query_arg(query_args: dict, name: str) -> str | None:
|
|
236
|
+
"""First value for a Tornado query argument name."""
|
|
237
|
+
if not query_args:
|
|
238
|
+
return None
|
|
239
|
+
raw_list = query_args.get(name)
|
|
240
|
+
if raw_list is None:
|
|
241
|
+
raw_list = query_args.get(name.encode("utf-8"))
|
|
242
|
+
if not raw_list:
|
|
243
|
+
return None
|
|
244
|
+
raw = raw_list[0]
|
|
245
|
+
if isinstance(raw, bytes):
|
|
246
|
+
return raw.decode("utf-8", errors="replace")
|
|
247
|
+
return str(raw)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _chunk_field(
|
|
251
|
+
headers, query_args: dict, header_name: str, query_name: str
|
|
252
|
+
) -> str | None:
|
|
253
|
+
"""Read chunked-upload metadata from header with query-string fallback (Cloudflare-safe)."""
|
|
254
|
+
value = headers.get(header_name)
|
|
255
|
+
if value:
|
|
256
|
+
return value
|
|
257
|
+
return _query_arg(query_args, query_name)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _parse_chunk_headers(
|
|
261
|
+
headers, query_args: dict | None = None,
|
|
262
|
+
) -> tuple[dict | None, str | None]:
|
|
263
|
+
"""Return (chunk_info, error_message). chunk_info keys: upload_id, offset, total_size, is_last."""
|
|
264
|
+
query_args = query_args or {}
|
|
265
|
+
upload_id = _sanitize_upload_id(
|
|
266
|
+
_chunk_field(headers, query_args, "X-Upload-Id", "upload_id")
|
|
267
|
+
)
|
|
268
|
+
offset = _parse_positive_int(
|
|
269
|
+
_chunk_field(headers, query_args, "X-Chunk-Offset", "chunk_offset")
|
|
270
|
+
)
|
|
271
|
+
total_size = _parse_positive_int(
|
|
272
|
+
_chunk_field(headers, query_args, "X-Upload-Total-Size", "total_size")
|
|
273
|
+
)
|
|
274
|
+
last_raw = (
|
|
275
|
+
_chunk_field(headers, query_args, "X-Chunk-Last", "chunk_last") or ""
|
|
276
|
+
).strip().lower()
|
|
277
|
+
def _has_query(name: str) -> bool:
|
|
278
|
+
return name in query_args or name.encode("utf-8") in query_args
|
|
279
|
+
|
|
280
|
+
has_chunk_request = any(
|
|
281
|
+
[
|
|
282
|
+
headers.get("X-Upload-Id"),
|
|
283
|
+
headers.get("X-Chunk-Offset"),
|
|
284
|
+
headers.get("X-Upload-Total-Size"),
|
|
285
|
+
headers.get("X-Chunk-Last"),
|
|
286
|
+
_has_query("upload_id"),
|
|
287
|
+
_has_query("chunk_offset"),
|
|
288
|
+
_has_query("total_size"),
|
|
289
|
+
_has_query("chunk_last"),
|
|
290
|
+
]
|
|
291
|
+
)
|
|
292
|
+
if not has_chunk_request:
|
|
293
|
+
return (None, None)
|
|
294
|
+
if not upload_id or offset is None or total_size is None or not last_raw:
|
|
295
|
+
return (None, MISSING_UPLOAD_CHUNK_HEADERS)
|
|
296
|
+
if total_size == 0:
|
|
297
|
+
return (None, INVALID_UPLOAD_CHUNK_HEADERS)
|
|
298
|
+
is_last = last_raw in ("1", "true", "yes")
|
|
299
|
+
if offset >= total_size and not is_last:
|
|
300
|
+
return (None, INVALID_UPLOAD_CHUNK_HEADERS)
|
|
301
|
+
return (
|
|
302
|
+
{
|
|
303
|
+
"upload_id": upload_id,
|
|
304
|
+
"offset": offset,
|
|
305
|
+
"total_size": total_size,
|
|
306
|
+
"is_last": is_last,
|
|
307
|
+
},
|
|
308
|
+
None,
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
|
|
128
312
|
# ---------------------------------------------------------------------------
|
|
129
313
|
# Helpers for bulk actions (reduce cognitive complexity)
|
|
130
314
|
# ---------------------------------------------------------------------------
|
|
@@ -275,6 +459,22 @@ def _process_bulk_action(
|
|
|
275
459
|
|
|
276
460
|
@tornado.web.stream_request_body
|
|
277
461
|
class UploadHandler(BaseHandler):
|
|
462
|
+
def check_xsrf_cookie(self) -> None:
|
|
463
|
+
"""Streamed uploads send raw body; accept X-XSRFToken header or ?_xsrf= query param."""
|
|
464
|
+
cookie_token = self.get_cookie("_xsrf")
|
|
465
|
+
if not cookie_token:
|
|
466
|
+
raise tornado.web.HTTPError(403, "'_xsrf' cookie missing")
|
|
467
|
+
provided = self.request.headers.get("X-XSRFToken")
|
|
468
|
+
if not provided:
|
|
469
|
+
provided = _query_arg(self.request.arguments, "_xsrf")
|
|
470
|
+
if not provided or not secrets.compare_digest(provided, cookie_token):
|
|
471
|
+
raise tornado.web.HTTPError(403, "XSRF validation failed")
|
|
472
|
+
|
|
473
|
+
def _request_body_limit(self) -> int:
|
|
474
|
+
if getattr(self, "_chunk_mode", False):
|
|
475
|
+
return constants_module.UPLOAD_CHUNK_SIZE_BYTES
|
|
476
|
+
return constants_module.MAX_FILE_SIZE
|
|
477
|
+
|
|
278
478
|
async def prepare(self):
|
|
279
479
|
# Defaults for safety
|
|
280
480
|
self._reject: bool = False
|
|
@@ -287,6 +487,10 @@ class UploadHandler(BaseHandler):
|
|
|
287
487
|
self._moved: bool = False
|
|
288
488
|
self._bytes_received: int = 0
|
|
289
489
|
self._too_large: bool = False
|
|
490
|
+
self._chunk_mode: bool = False
|
|
491
|
+
self._keep_session: bool = False
|
|
492
|
+
self._chunk_info: dict | None = None
|
|
493
|
+
self._session_dir: str | None = None
|
|
290
494
|
|
|
291
495
|
# Feature flag check (using SQLite-backed flags)
|
|
292
496
|
# Deferred to post() for clear response, but avoid heavy work if disabled
|
|
@@ -295,9 +499,17 @@ class UploadHandler(BaseHandler):
|
|
|
295
499
|
self._reject_reason = FILE_UPLOAD_DISABLED
|
|
296
500
|
return
|
|
297
501
|
|
|
298
|
-
#
|
|
299
|
-
raw_dir =
|
|
300
|
-
|
|
502
|
+
# Headers with query fallback (proxies such as Cloudflare may strip custom X-* headers)
|
|
503
|
+
raw_dir = (
|
|
504
|
+
self.request.headers.get("X-Upload-Dir")
|
|
505
|
+
or _query_arg(self.request.arguments, "upload_dir")
|
|
506
|
+
or ""
|
|
507
|
+
)
|
|
508
|
+
raw_filename = (
|
|
509
|
+
self.request.headers.get("X-Upload-Filename")
|
|
510
|
+
or _query_arg(self.request.arguments, "upload_filename")
|
|
511
|
+
or ""
|
|
512
|
+
)
|
|
301
513
|
self.upload_dir = unquote(raw_dir)
|
|
302
514
|
self.filename = unquote(raw_filename)
|
|
303
515
|
|
|
@@ -307,23 +519,52 @@ class UploadHandler(BaseHandler):
|
|
|
307
519
|
self._reject_reason = MISSING_UPLOAD_FILENAME_HEADER
|
|
308
520
|
return
|
|
309
521
|
|
|
310
|
-
|
|
522
|
+
chunk_info, chunk_err = _parse_chunk_headers(
|
|
523
|
+
self.request.headers, self.request.arguments
|
|
524
|
+
)
|
|
525
|
+
if chunk_err:
|
|
526
|
+
self._reject = True
|
|
527
|
+
self._reject_reason = chunk_err
|
|
528
|
+
return
|
|
529
|
+
if chunk_info:
|
|
530
|
+
if not self.get_current_user():
|
|
531
|
+
self._reject = True
|
|
532
|
+
self._reject_reason = ACCESS_DENIED
|
|
533
|
+
return
|
|
534
|
+
if chunk_info["total_size"] > constants_module.MAX_FILE_SIZE:
|
|
535
|
+
self._reject = True
|
|
536
|
+
self._reject_reason = FILE_TOO_LARGE
|
|
537
|
+
return
|
|
538
|
+
self._chunk_mode = True
|
|
539
|
+
self._chunk_info = chunk_info
|
|
540
|
+
username = self.get_display_username()
|
|
541
|
+
self._session_dir = _upload_session_dir(username, chunk_info["upload_id"])
|
|
542
|
+
offset = chunk_info["offset"]
|
|
543
|
+
if offset == 0 and os.path.isdir(self._session_dir):
|
|
544
|
+
_remove_upload_session(self._session_dir)
|
|
545
|
+
os.makedirs(self._session_dir, exist_ok=True)
|
|
546
|
+
self._temp_path = _chunk_file_path(self._session_dir, offset)
|
|
547
|
+
self._aiofile = await aiofiles.open(self._temp_path, "wb")
|
|
548
|
+
return
|
|
549
|
+
|
|
550
|
+
# Single-request upload (legacy / small files)
|
|
311
551
|
fd, self._temp_path = tempfile.mkstemp(prefix="aird_upload_")
|
|
312
|
-
# Close the low-level fd; we'll use aiofiles on the path
|
|
313
552
|
os.close(fd)
|
|
314
553
|
self._aiofile = await aiofiles.open(self._temp_path, "wb")
|
|
315
554
|
|
|
316
555
|
def data_received(self, chunk: bytes) -> None:
|
|
317
556
|
if self._reject:
|
|
318
557
|
return
|
|
319
|
-
# Track size to enforce limit at the end
|
|
320
558
|
self._bytes_received += len(chunk)
|
|
321
|
-
if self._bytes_received >
|
|
559
|
+
if self._bytes_received > self._request_body_limit():
|
|
322
560
|
self._too_large = True
|
|
323
|
-
# We still accept the stream but won't persist it
|
|
324
561
|
return
|
|
562
|
+
if self._chunk_mode and self._chunk_info:
|
|
563
|
+
end_offset = self._chunk_info["offset"] + self._bytes_received
|
|
564
|
+
if end_offset > self._chunk_info["total_size"]:
|
|
565
|
+
self._too_large = True
|
|
566
|
+
return
|
|
325
567
|
|
|
326
|
-
# Queue the chunk and ensure a writer task is draining
|
|
327
568
|
self._buffer.append(chunk)
|
|
328
569
|
if not self._writing:
|
|
329
570
|
self._writing = True
|
|
@@ -351,7 +592,7 @@ class UploadHandler(BaseHandler):
|
|
|
351
592
|
except Exception:
|
|
352
593
|
logging.debug("upload aiofile close failed", exc_info=True)
|
|
353
594
|
|
|
354
|
-
def _check_quota_exceeded(self) -> bool:
|
|
595
|
+
def _check_quota_exceeded(self, upload_bytes: int) -> bool:
|
|
355
596
|
"""Return True and set error response if storage quota would be exceeded."""
|
|
356
597
|
if not is_feature_enabled("storage_quotas", False):
|
|
357
598
|
return False
|
|
@@ -359,13 +600,47 @@ class UploadHandler(BaseHandler):
|
|
|
359
600
|
quota = self.get_service("quota_service").get_quota(self.db_conn, username)
|
|
360
601
|
if (
|
|
361
602
|
quota["quota_bytes"] is not None
|
|
362
|
-
and quota["used_bytes"] +
|
|
603
|
+
and quota["used_bytes"] + upload_bytes > quota["quota_bytes"]
|
|
363
604
|
):
|
|
364
605
|
self.set_status(413)
|
|
365
606
|
self.write("Storage quota exceeded")
|
|
366
607
|
return True
|
|
367
608
|
return False
|
|
368
609
|
|
|
610
|
+
def _remove_upload_session_dir(self) -> None:
|
|
611
|
+
if getattr(self, "_session_dir", None):
|
|
612
|
+
_remove_upload_session(self._session_dir)
|
|
613
|
+
|
|
614
|
+
async def _finalize_chunked_upload(self) -> bool:
|
|
615
|
+
"""Stitch chunk files when complete. Return True if upload finished."""
|
|
616
|
+
session_dir = self._session_dir
|
|
617
|
+
total_size = self._chunk_info["total_size"]
|
|
618
|
+
if not _upload_chunks_complete(session_dir, total_size):
|
|
619
|
+
self._keep_session = True
|
|
620
|
+
self.set_status(200)
|
|
621
|
+
self.write(UPLOAD_CHUNK_RECEIVED)
|
|
622
|
+
return False
|
|
623
|
+
if not _try_acquire_stitch_lock(session_dir):
|
|
624
|
+
self._keep_session = True
|
|
625
|
+
self.set_status(200)
|
|
626
|
+
self.write(UPLOAD_CHUNK_RECEIVED)
|
|
627
|
+
return False
|
|
628
|
+
stitched_path = os.path.join(session_dir, "assembled.part")
|
|
629
|
+
try:
|
|
630
|
+
await asyncio.to_thread(
|
|
631
|
+
_stitch_upload_session, session_dir, stitched_path, total_size
|
|
632
|
+
)
|
|
633
|
+
except Exception:
|
|
634
|
+
logging.exception("Upload stitch failed")
|
|
635
|
+
self.set_status(500)
|
|
636
|
+
self.write(UPLOAD_SAVE_FAILED)
|
|
637
|
+
_remove_upload_session(session_dir)
|
|
638
|
+
return False
|
|
639
|
+
finally:
|
|
640
|
+
_release_stitch_lock(session_dir)
|
|
641
|
+
self._temp_path = stitched_path
|
|
642
|
+
return True
|
|
643
|
+
|
|
369
644
|
@tornado.web.authenticated
|
|
370
645
|
@require_action("file.write")
|
|
371
646
|
@require_modify_access()
|
|
@@ -383,15 +658,45 @@ class UploadHandler(BaseHandler):
|
|
|
383
658
|
|
|
384
659
|
await self._finalize_stream()
|
|
385
660
|
|
|
386
|
-
# Enforce size limit
|
|
387
661
|
if self._too_large:
|
|
388
662
|
limit_mb = constants_module.UPLOAD_CONFIG.get("max_file_size_mb", 512)
|
|
389
663
|
self.set_status(413)
|
|
390
664
|
self.write(FILE_TOO_LARGE_TEMPLATE.format(limit_mb=limit_mb))
|
|
665
|
+
if self._chunk_mode:
|
|
666
|
+
self._remove_upload_session_dir()
|
|
391
667
|
return
|
|
392
668
|
|
|
393
|
-
|
|
394
|
-
|
|
669
|
+
if self._chunk_mode and self._chunk_info:
|
|
670
|
+
chunk_end = self._chunk_info["offset"] + self._bytes_received
|
|
671
|
+
if chunk_end > self._chunk_info["total_size"]:
|
|
672
|
+
self.set_status(400)
|
|
673
|
+
self.write(INVALID_UPLOAD_CHUNK_HEADERS)
|
|
674
|
+
self._remove_upload_session_dir()
|
|
675
|
+
return
|
|
676
|
+
try:
|
|
677
|
+
chunk_size = os.path.getsize(self._temp_path)
|
|
678
|
+
except OSError:
|
|
679
|
+
chunk_size = -1
|
|
680
|
+
if chunk_size != self._bytes_received:
|
|
681
|
+
self.set_status(400)
|
|
682
|
+
self.write(UPLOAD_CHUNK_OUT_OF_ORDER)
|
|
683
|
+
self._remove_upload_session_dir()
|
|
684
|
+
return
|
|
685
|
+
finished = await self._finalize_chunked_upload()
|
|
686
|
+
if not finished:
|
|
687
|
+
return
|
|
688
|
+
upload_bytes = self._chunk_info["total_size"]
|
|
689
|
+
else:
|
|
690
|
+
if self._bytes_received > constants_module.MAX_FILE_SIZE:
|
|
691
|
+
limit_mb = constants_module.UPLOAD_CONFIG.get("max_file_size_mb", 512)
|
|
692
|
+
self.set_status(413)
|
|
693
|
+
self.write(FILE_TOO_LARGE_TEMPLATE.format(limit_mb=limit_mb))
|
|
694
|
+
return
|
|
695
|
+
upload_bytes = self._bytes_received
|
|
696
|
+
|
|
697
|
+
if self._check_quota_exceeded(upload_bytes):
|
|
698
|
+
if self._chunk_mode:
|
|
699
|
+
self._remove_upload_session_dir()
|
|
395
700
|
return
|
|
396
701
|
|
|
397
702
|
final_path_abs, upload_err = _validate_upload_destination(
|
|
@@ -400,6 +705,8 @@ class UploadHandler(BaseHandler):
|
|
|
400
705
|
if upload_err is not None:
|
|
401
706
|
self.set_status(upload_err[0])
|
|
402
707
|
self.write(upload_err[1])
|
|
708
|
+
if self._chunk_mode:
|
|
709
|
+
self._remove_upload_session_dir()
|
|
403
710
|
return
|
|
404
711
|
|
|
405
712
|
os.makedirs(os.path.dirname(final_path_abs), exist_ok=True)
|
|
@@ -407,16 +714,19 @@ class UploadHandler(BaseHandler):
|
|
|
407
714
|
try:
|
|
408
715
|
shutil.move(self._temp_path, final_path_abs)
|
|
409
716
|
self._moved = True
|
|
717
|
+
if self._chunk_mode and self._session_dir:
|
|
718
|
+
_remove_upload_session(self._session_dir)
|
|
410
719
|
except Exception:
|
|
411
720
|
logging.exception("Upload save failed")
|
|
412
721
|
self.set_status(500)
|
|
413
722
|
self.write(UPLOAD_SAVE_FAILED)
|
|
723
|
+
if self._chunk_mode:
|
|
724
|
+
self._remove_upload_session_dir()
|
|
414
725
|
return
|
|
415
726
|
|
|
416
|
-
# Update used bytes
|
|
417
727
|
if is_feature_enabled("storage_quotas", False):
|
|
418
728
|
self.get_service("quota_service").update_used_bytes(
|
|
419
|
-
self.db_conn, self.get_display_username(),
|
|
729
|
+
self.db_conn, self.get_display_username(), upload_bytes
|
|
420
730
|
)
|
|
421
731
|
|
|
422
732
|
self.get_service("audit_service").log(
|
|
@@ -430,8 +740,9 @@ class UploadHandler(BaseHandler):
|
|
|
430
740
|
self.write(UPLOAD_SUCCESSFUL)
|
|
431
741
|
|
|
432
742
|
def on_finish(self) -> None:
|
|
433
|
-
# Clean up temp file on failures
|
|
434
743
|
try:
|
|
744
|
+
if getattr(self, "_keep_session", False) or getattr(self, "_chunk_mode", False):
|
|
745
|
+
return
|
|
435
746
|
if getattr(self, "_temp_path", None) and not getattr(self, "_moved", False):
|
|
436
747
|
if os.path.exists(self._temp_path):
|
|
437
748
|
try:
|
|
@@ -76,10 +76,15 @@ def _add_local_path(ap, path_str, share_type, valid_paths, dynamic_folders):
|
|
|
76
76
|
else:
|
|
77
77
|
try:
|
|
78
78
|
all_files = get_all_files_recursive(ap, path_str)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
79
|
+
if all_files:
|
|
80
|
+
valid_paths.extend(all_files)
|
|
81
|
+
logging.debug(
|
|
82
|
+
"Added %s files from directory: %s", len(all_files), path_str
|
|
83
|
+
)
|
|
84
|
+
else:
|
|
85
|
+
# Keep empty directories so static shares can be created for them.
|
|
86
|
+
valid_paths.append(path_str)
|
|
87
|
+
logging.debug("Added empty directory: %s", path_str)
|
|
83
88
|
except Exception:
|
|
84
89
|
logging.exception("Error scanning directory %s", path_str)
|
|
85
90
|
|
|
@@ -304,7 +309,18 @@ def _get_share_file_list(share, db_conn=None):
|
|
|
304
309
|
)
|
|
305
310
|
return filter_files_by_patterns(dynamic_files, allow_list, avoid_list)
|
|
306
311
|
|
|
307
|
-
|
|
312
|
+
static_paths = []
|
|
313
|
+
for rel_path in share.get("paths") or []:
|
|
314
|
+
try:
|
|
315
|
+
full_path = os.path.abspath(os.path.join(root, rel_path))
|
|
316
|
+
if os.path.isdir(full_path):
|
|
317
|
+
continue
|
|
318
|
+
except Exception:
|
|
319
|
+
logging.debug(
|
|
320
|
+
"Skipping static share path check for %r", rel_path, exc_info=True
|
|
321
|
+
)
|
|
322
|
+
static_paths.append(rel_path)
|
|
323
|
+
return filter_files_by_patterns(static_paths, allow_list, avoid_list)
|
|
308
324
|
|
|
309
325
|
|
|
310
326
|
def _is_path_in_share(share, path, db_conn=None):
|
|
@@ -196,6 +196,8 @@ class MainHandler(BaseHandler):
|
|
|
196
196
|
get_file_icon=get_file_icon,
|
|
197
197
|
features=flags_for_template,
|
|
198
198
|
max_file_size=constants_module.MAX_FILE_SIZE,
|
|
199
|
+
upload_chunk_size=constants_module.UPLOAD_CHUNK_SIZE_BYTES,
|
|
200
|
+
upload_max_parallel=constants_module.UPLOAD_MAX_PARALLEL_CHUNKS,
|
|
199
201
|
user_favorites=user_favorites,
|
|
200
202
|
file_tags_map=file_tags_map,
|
|
201
203
|
)
|
|
@@ -154,8 +154,8 @@ def make_app(
|
|
|
154
154
|
settings.setdefault("static_url_prefix", "/static/")
|
|
155
155
|
# Limit request size to avoid Tornado rejecting large uploads with
|
|
156
156
|
# "Content-Length too long" before our handler can respond.
|
|
157
|
-
settings.setdefault("max_body_size", constants.
|
|
158
|
-
settings.setdefault("max_buffer_size", constants.
|
|
157
|
+
settings.setdefault("max_body_size", constants.UPLOAD_REQUEST_MAX_BODY_SIZE)
|
|
158
|
+
settings.setdefault("max_buffer_size", constants.UPLOAD_REQUEST_MAX_BODY_SIZE)
|
|
159
159
|
|
|
160
160
|
if ldap_enabled:
|
|
161
161
|
settings["ldap_server"] = ldap_server
|
|
@@ -495,8 +495,8 @@ def _start_server(app, ssl_options, port: int, hostname: str) -> None:
|
|
|
495
495
|
app.listen(
|
|
496
496
|
port,
|
|
497
497
|
ssl_options=ssl_options,
|
|
498
|
-
max_body_size=constants.
|
|
499
|
-
max_buffer_size=constants.
|
|
498
|
+
max_body_size=constants.UPLOAD_REQUEST_MAX_BODY_SIZE,
|
|
499
|
+
max_buffer_size=constants.UPLOAD_REQUEST_MAX_BODY_SIZE,
|
|
500
500
|
)
|
|
501
501
|
logger.info(
|
|
502
502
|
f"Serving HTTPS on 0.0.0.0 port {port} ({proto}://0.0.0.0:{port}/) ..."
|
|
@@ -505,8 +505,8 @@ def _start_server(app, ssl_options, port: int, hostname: str) -> None:
|
|
|
505
505
|
proto = "http"
|
|
506
506
|
app.listen(
|
|
507
507
|
port,
|
|
508
|
-
max_body_size=constants.
|
|
509
|
-
max_buffer_size=constants.
|
|
508
|
+
max_body_size=constants.UPLOAD_REQUEST_MAX_BODY_SIZE,
|
|
509
|
+
max_buffer_size=constants.UPLOAD_REQUEST_MAX_BODY_SIZE,
|
|
510
510
|
)
|
|
511
511
|
logger.info(
|
|
512
512
|
f"Serving HTTP on 0.0.0.0 port {port} ({proto}://0.0.0.0:{port}/) ..."
|