aird 0.4.23.dev18__tar.gz → 0.4.23.dev22__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.23.dev18/aird.egg-info → aird-0.4.23.dev22}/PKG-INFO +2 -1
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/cli/main.py +100 -60
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/cli/session.py +100 -67
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/config.py +83 -58
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/constants/__init__.py +9 -3
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/core/file_operations.py +58 -41
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/core/folder_size.py +24 -42
- aird-0.4.23.dev22/aird/core/http_range.py +114 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/core/mmap_handler.py +49 -31
- aird-0.4.23.dev22/aird/core/secret_storage.py +72 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/core/security.py +27 -2
- aird-0.4.23.dev22/aird/core/webauthn_config.py +20 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/core/zip_download.py +50 -36
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/database/db.py +7 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/db/network_shares.py +6 -3
- aird-0.4.23.dev22/aird/db/ranged_uploads.py +73 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/db/schema.py +35 -0
- aird-0.4.23.dev22/aird/db/webauthn.py +248 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/handlers/admin_handlers.py +1 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/handlers/api_handlers.py +66 -45
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/handlers/auth_handlers.py +55 -8
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/handlers/base_handler.py +45 -20
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/handlers/constants.py +6 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/handlers/file_op_handlers.py +21 -1
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/handlers/p2p_handlers.py +12 -5
- aird-0.4.23.dev22/aird/handlers/ranged_upload_handlers.py +264 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/handlers/share_handlers.py +41 -25
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/handlers/transfer_ws_handlers.py +6 -3
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/handlers/view_handlers.py +594 -541
- aird-0.4.23.dev22/aird/handlers/webauthn_handlers.py +393 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/main.py +22 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/services/policy_service.py +1 -2
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/static/css/app.css +1 -1
- aird-0.4.23.dev22/aird/static/js/aird-core.js +476 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/static/js/browse/app.js +399 -371
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/static/js/download-manager.js +52 -49
- aird-0.4.23.dev22/aird/static/js/file-transfer-http.js +387 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/static/js/file-transfer-ws.js +349 -260
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/static/js/folder-size-scan.js +7 -4
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/static/js/pages/p2p-page.js +2 -2
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/static/js/share/app.js +69 -48
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/static/js/transfer-tracker.js +70 -35
- aird-0.4.23.dev22/aird/static/js/webauthn.js +259 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/templates/_app_nav_header.html +17 -14
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/templates/_bg_canvas.html +1 -1
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/templates/_theme_early.html +5 -5
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/templates/_theme_login_corner.html +26 -14
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/templates/admin.html +4 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/templates/admin_audit.html +1 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/templates/admin_login.html +2 -1
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/templates/admin_tags.html +2 -2
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/templates/browse.html +38 -12
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/templates/edit.html +2 -1
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/templates/file.html +2 -2
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/templates/login.html +68 -5
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/templates/p2p_transfer.html +2 -1
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/templates/profile.html +70 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/templates/share.html +3 -2
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/templates/tagged_files.html +1 -1
- {aird-0.4.23.dev18 → aird-0.4.23.dev22/aird.egg-info}/PKG-INFO +2 -1
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird.egg-info/SOURCES.txt +11 -1
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird.egg-info/requires.txt +1 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/setup.py +2 -1
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/tests/test_architecture_conformance.py +4 -1
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/tests/test_auth_handlers_extended.py +4 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/tests/test_core_file_operations.py +250 -231
- aird-0.4.23.dev22/tests/test_http_range.py +41 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/tests/test_multi_user.py +2 -1
- aird-0.4.23.dev22/tests/test_secret_storage.py +27 -0
- aird-0.4.23.dev22/tests/test_webauthn_handlers.py +137 -0
- aird-0.4.23.dev18/aird/static/js/aird-core.js +0 -227
- aird-0.4.23.dev18/aird/static/js/file-transfer-http.js +0 -98
- aird-0.4.23.dev18/aird/static/js/file-transfer.js +0 -55
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/LICENSE +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/MANIFEST.in +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/README.md +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/__init__.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/__main__.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/app_context.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/cli/__init__.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/cli/__main__.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/cli/authelia.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/cli/config.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/cloud/__init__.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/constants/admin.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/constants/file_ops.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/constants/input_limits.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/constants/media.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/core/__init__.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/core/events.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/core/filter_expression.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/core/input_validation.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/core/share_root.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/core/websocket_manager.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/database/__init__.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/database/feature_flags.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/database/ldap.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/db/__init__.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/db/audit.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/db/config.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/db/favorites.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/db/policies.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/db/policy_decisions.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/db/policy_seeds.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/db/quota.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/db/resource_tags.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/db/shares.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/db/user_attributes.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/db/users.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/domain/__init__.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/domain/contracts.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/domain/models.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/email/__init__.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/email/brevo.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/email/resolve.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/event_loop.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/handlers/__init__.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/handlers/abac_handlers.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/handlers/health_handler.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/network_share_manager.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/server_runtime.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/services/__init__.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/services/audit_service.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/services/config_service.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/services/email_service.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/services/email_subscriber.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/services/event_subscribers.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/services/favorites_service.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/services/network_share_service.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/services/p2p_service.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/services/quota_service.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/services/share_service.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/services/tag_service.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/services/user_service.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/sql_identifiers.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/static/favicon.png +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/static/favicon.svg +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/static/img/logo-icon.png +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/static/img/logo-mark.svg +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/static/img/logo-text.png +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/static/img/logo.png +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/static/js/bg-canvas.js +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/static/js/common/command-palette.js +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/static/js/components/folder-picker.js +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/static/js/feature-flags-live.js +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/static/js/login-ui.js +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/static/js/media-view.js +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/static/js/p2p/app.js +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/static/js/p2p/mediator.js +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/static/js/p2p/qr-adapter.js +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/static/js/p2p/signaling-service.js +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/static/js/p2p/state-machine.js +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/static/js/p2p/transfer-service.js +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/static/js/pages/super-search.js +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/static/js/theme.js +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/static/js/vendor/pdf.min.js +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/static/js/vendor/pdf.worker.min.js +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/static/js/vendor/qrcode-browser.js +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/templates/_admin_tabs.html +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/templates/admin_ldap.html +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/templates/admin_network_shares.html +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/templates/admin_policies.html +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/templates/admin_user_attributes.html +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/templates/admin_users.html +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/templates/directory.html +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/templates/error.html +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/templates/ldap_config_create.html +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/templates/ldap_config_edit.html +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/templates/media_view.html +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/templates/shared_list.html +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/templates/super_search.html +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/templates/token_verification.html +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/templates/user_create.html +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/templates/user_edit.html +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/utils/__init__.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird/utils/util.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird.egg-info/dependency_links.txt +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird.egg-info/entry_points.txt +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/aird.egg-info/top_level.txt +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/setup.cfg +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/tests/__init__.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/tests/conftest.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/tests/handler_helpers.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/tests/test_admin_handlers.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/tests/test_api_handlers.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/tests/test_auth_handlers.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/tests/test_base_handler.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/tests/test_base_handler_pep.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/tests/test_cli.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/tests/test_cloud.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/tests/test_config.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/tests/test_database_db.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/tests/test_database_feature_flags.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/tests/test_database_ldap.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/tests/test_database_shares.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/tests/test_database_users.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/tests/test_database_users_hashing.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/tests/test_db.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/tests/test_email_service.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/tests/test_file_op_handlers.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/tests/test_filter_expression.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/tests/test_folder_size.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/tests/test_main.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/tests/test_mmap_handler.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/tests/test_network_shares.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/tests/test_p2p_handlers.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/tests/test_password_hashing.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/tests/test_policy_service.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/tests/test_rate_limit.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/tests/test_security.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/tests/test_security_comprehensive.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/tests/test_server_runtime.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/tests/test_share_handlers.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/tests/test_share_ownership.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/tests/test_super_search_handler.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/tests/test_tag_service.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/tests/test_transfer_ws_handlers.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/tests/test_util.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/tests/test_view_handlers.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/tests/test_websocket_manager.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/tests/test_wheel_static_assets.py +0 -0
- {aird-0.4.23.dev18 → aird-0.4.23.dev22}/tests/test_zip_download.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aird
|
|
3
|
-
Version: 0.4.23.
|
|
3
|
+
Version: 0.4.23.dev22
|
|
4
4
|
Summary: Aird - A lightweight web-based file browser, editor, and streamer with real-time capabilities
|
|
5
5
|
Home-page: https://github.com/blinkerbit/aird
|
|
6
6
|
Author: Viswantha Srinivas P
|
|
@@ -23,6 +23,7 @@ Requires-Dist: pysmbserver>=0.1.0; python_version >= "3.13"
|
|
|
23
23
|
Requires-Dist: wsgidav>=4.3.0
|
|
24
24
|
Requires-Dist: cheroot>=10.0.0
|
|
25
25
|
Requires-Dist: pyasn1>=0.6.2
|
|
26
|
+
Requires-Dist: webauthn>=2.0.0
|
|
26
27
|
Dynamic: author
|
|
27
28
|
Dynamic: author-email
|
|
28
29
|
Dynamic: classifier
|
|
@@ -42,27 +42,92 @@ def cmd_config_show(_args: argparse.Namespace) -> int:
|
|
|
42
42
|
cfg = load_config()
|
|
43
43
|
if not cfg:
|
|
44
44
|
print("(no config — use: aird-cli config set server https://host)")
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
45
|
+
else:
|
|
46
|
+
for k, v in sorted(cfg.items()):
|
|
47
|
+
if k == "password":
|
|
48
|
+
print(f"{k}: ***")
|
|
49
|
+
else:
|
|
50
|
+
print(f"{k}: {v}")
|
|
51
51
|
return 0
|
|
52
52
|
|
|
53
53
|
|
|
54
|
-
def
|
|
54
|
+
def _resolve_login_server(args: argparse.Namespace) -> str | None:
|
|
55
55
|
server = get_server_url()
|
|
56
56
|
if not server and args.server:
|
|
57
57
|
cfg = load_config()
|
|
58
58
|
cfg["server"] = args.server.rstrip("/")
|
|
59
59
|
save_config(cfg)
|
|
60
60
|
server = cfg["server"]
|
|
61
|
+
return server
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _try_token_login(client: AirdClient, server: str, args: argparse.Namespace) -> int | None:
|
|
65
|
+
if not args.token:
|
|
66
|
+
return None
|
|
67
|
+
client.set_bearer_token(args.token)
|
|
68
|
+
client.refresh_xsrf()
|
|
69
|
+
client.save()
|
|
70
|
+
try:
|
|
71
|
+
client.check_auth()
|
|
72
|
+
print(f"Logged in to {server} (access token)")
|
|
73
|
+
return 0
|
|
74
|
+
except (AirdAuthError, AirdAPIError) as exc:
|
|
75
|
+
print(f"Token login failed: {exc}", file=sys.stderr)
|
|
76
|
+
return 1
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _run_authelia_login(
|
|
80
|
+
client: AirdClient,
|
|
81
|
+
authelia_url: str,
|
|
82
|
+
username: str,
|
|
83
|
+
password: str,
|
|
84
|
+
totp: str | None,
|
|
85
|
+
server: str,
|
|
86
|
+
) -> int | None:
|
|
87
|
+
try:
|
|
88
|
+
authelia_login(
|
|
89
|
+
client.http,
|
|
90
|
+
authelia_url,
|
|
91
|
+
username,
|
|
92
|
+
password,
|
|
93
|
+
totp=totp,
|
|
94
|
+
target_url=f"{server}/login",
|
|
95
|
+
)
|
|
96
|
+
except AutheliaError as exc:
|
|
97
|
+
if str(exc) != "second_factor_required":
|
|
98
|
+
print(f"Authelia login failed: {exc}", file=sys.stderr)
|
|
99
|
+
return 1
|
|
100
|
+
totp = totp or _prompt_totp()
|
|
101
|
+
try:
|
|
102
|
+
second_factor(client.http, authelia_url, totp)
|
|
103
|
+
except AutheliaError as exc2:
|
|
104
|
+
print(f"Authelia login failed: {exc2}", file=sys.stderr)
|
|
105
|
+
return 1
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _save_login_config(
|
|
110
|
+
username: str, server: str, authelia_url: str | None
|
|
111
|
+
) -> None:
|
|
112
|
+
cfg = load_config()
|
|
113
|
+
cfg["username"] = username
|
|
114
|
+
cfg["server"] = server
|
|
115
|
+
if authelia_url:
|
|
116
|
+
cfg["authelia_url"] = authelia_url
|
|
117
|
+
save_config(cfg)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def cmd_login(args: argparse.Namespace) -> int:
|
|
121
|
+
server = _resolve_login_server(args)
|
|
61
122
|
if not server:
|
|
62
123
|
print("Set server URL first: aird-cli config set server https://your-host", file=sys.stderr)
|
|
63
124
|
return 1
|
|
64
125
|
|
|
65
126
|
client = AirdClient(server)
|
|
127
|
+
token_result = _try_token_login(client, server, args)
|
|
128
|
+
if token_result is not None:
|
|
129
|
+
return token_result
|
|
130
|
+
|
|
66
131
|
authelia_url = get_authelia_url() or args.authelia_url
|
|
67
132
|
|
|
68
133
|
username = args.username or load_config().get("username") or input("Username: ").strip()
|
|
@@ -70,55 +135,22 @@ def cmd_login(args: argparse.Namespace) -> int:
|
|
|
70
135
|
print("Username required", file=sys.stderr)
|
|
71
136
|
return 1
|
|
72
137
|
|
|
73
|
-
if args.token:
|
|
74
|
-
client.set_bearer_token(args.token)
|
|
75
|
-
client.refresh_xsrf()
|
|
76
|
-
client.save()
|
|
77
|
-
try:
|
|
78
|
-
client.check_auth()
|
|
79
|
-
print(f"Logged in to {server} (access token)")
|
|
80
|
-
return 0
|
|
81
|
-
except (AirdAuthError, AirdAPIError) as exc:
|
|
82
|
-
print(f"Token login failed: {exc}", file=sys.stderr)
|
|
83
|
-
return 1
|
|
84
|
-
|
|
85
138
|
password = args.password or _prompt_password()
|
|
86
139
|
totp = args.totp
|
|
87
140
|
|
|
88
141
|
if authelia_url:
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
password,
|
|
95
|
-
totp=totp,
|
|
96
|
-
target_url=f"{server}/login",
|
|
97
|
-
)
|
|
98
|
-
except AutheliaError as exc:
|
|
99
|
-
if str(exc) == "second_factor_required":
|
|
100
|
-
totp = totp or _prompt_totp()
|
|
101
|
-
try:
|
|
102
|
-
second_factor(client.http, authelia_url, totp)
|
|
103
|
-
except AutheliaError as exc2:
|
|
104
|
-
print(f"Authelia login failed: {exc2}", file=sys.stderr)
|
|
105
|
-
return 1
|
|
106
|
-
else:
|
|
107
|
-
print(f"Authelia login failed: {exc}", file=sys.stderr)
|
|
108
|
-
return 1
|
|
142
|
+
authelia_result = _run_authelia_login(
|
|
143
|
+
client, authelia_url, username, password, totp, server
|
|
144
|
+
)
|
|
145
|
+
if authelia_result is not None:
|
|
146
|
+
return authelia_result
|
|
109
147
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
client.login_password("", "", token=aird_token)
|
|
148
|
+
if args.aird_token:
|
|
149
|
+
client.login_password("", "", token=args.aird_token)
|
|
113
150
|
else:
|
|
114
151
|
client.login_password(username, password)
|
|
115
152
|
|
|
116
|
-
|
|
117
|
-
cfg["username"] = username
|
|
118
|
-
cfg["server"] = server
|
|
119
|
-
if authelia_url:
|
|
120
|
-
cfg["authelia_url"] = authelia_url
|
|
121
|
-
save_config(cfg)
|
|
153
|
+
_save_login_config(username, server, authelia_url)
|
|
122
154
|
client.save()
|
|
123
155
|
|
|
124
156
|
try:
|
|
@@ -250,6 +282,24 @@ def cmd_upload(args: argparse.Namespace) -> int:
|
|
|
250
282
|
return 1
|
|
251
283
|
|
|
252
284
|
|
|
285
|
+
def _print_my_shares(mine) -> None:
|
|
286
|
+
print("My shares:")
|
|
287
|
+
items = mine.values() if isinstance(mine, dict) else mine
|
|
288
|
+
for s in items:
|
|
289
|
+
sid = s.get("id", "?")
|
|
290
|
+
paths = s.get("paths") or []
|
|
291
|
+
print(f" {sid}\t{len(paths)} path(s)\t{s.get('created_at', '')}")
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _print_shared_with_me(shared) -> None:
|
|
295
|
+
print("Shared with me:")
|
|
296
|
+
for s in shared:
|
|
297
|
+
sid = s.get("id", "?")
|
|
298
|
+
creator = s.get("creator") or s.get("username") or "?"
|
|
299
|
+
paths = s.get("paths") or []
|
|
300
|
+
print(f" {sid}\tfrom {creator}\t{len(paths)} path(s)")
|
|
301
|
+
|
|
302
|
+
|
|
253
303
|
def cmd_shares_list(_args: argparse.Namespace) -> int:
|
|
254
304
|
try:
|
|
255
305
|
client = AirdClient()
|
|
@@ -260,19 +310,9 @@ def cmd_shares_list(_args: argparse.Namespace) -> int:
|
|
|
260
310
|
print("(no shares)")
|
|
261
311
|
return 0
|
|
262
312
|
if mine:
|
|
263
|
-
|
|
264
|
-
items = mine.values() if isinstance(mine, dict) else mine
|
|
265
|
-
for s in items:
|
|
266
|
-
sid = s.get("id", "?")
|
|
267
|
-
paths = s.get("paths") or []
|
|
268
|
-
print(f" {sid}\t{len(paths)} path(s)\t{s.get('created_at', '')}")
|
|
313
|
+
_print_my_shares(mine)
|
|
269
314
|
if shared:
|
|
270
|
-
|
|
271
|
-
for s in shared:
|
|
272
|
-
sid = s.get("id", "?")
|
|
273
|
-
creator = s.get("creator") or s.get("username") or "?"
|
|
274
|
-
paths = s.get("paths") or []
|
|
275
|
-
print(f" {sid}\tfrom {creator}\t{len(paths)} path(s)")
|
|
315
|
+
_print_shared_with_me(shared)
|
|
276
316
|
return 0
|
|
277
317
|
except (AirdAuthError, AirdAPIError) as exc:
|
|
278
318
|
print(str(exc), file=sys.stderr)
|
|
@@ -91,6 +91,43 @@ def _load_cookies(session: requests.Session) -> bool:
|
|
|
91
91
|
return bool(data.get("cookies") or token)
|
|
92
92
|
|
|
93
93
|
|
|
94
|
+
def _collect_local_upload_files(
|
|
95
|
+
local_dir: Path, remote_dir: str
|
|
96
|
+
) -> list[tuple[Path, str]]:
|
|
97
|
+
files: list[tuple[Path, str]] = []
|
|
98
|
+
|
|
99
|
+
def walk(base: Path, rel_remote: str) -> None:
|
|
100
|
+
for child in sorted(base.iterdir()):
|
|
101
|
+
rel = f"{rel_remote}/{child.name}" if rel_remote else child.name
|
|
102
|
+
if child.is_dir():
|
|
103
|
+
walk(child, rel)
|
|
104
|
+
elif child.is_file():
|
|
105
|
+
remote_parent = (
|
|
106
|
+
f"{remote_dir}/{rel_remote}".strip("/") if rel_remote else remote_dir
|
|
107
|
+
)
|
|
108
|
+
files.append((child, remote_parent.strip("/")))
|
|
109
|
+
|
|
110
|
+
walk(local_dir, "")
|
|
111
|
+
return files
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _run_path_jobs(
|
|
115
|
+
file_paths: list[str],
|
|
116
|
+
job: Callable[[str], str],
|
|
117
|
+
workers: int,
|
|
118
|
+
on_progress: Callable[[str], None] | None,
|
|
119
|
+
) -> int:
|
|
120
|
+
count = 0
|
|
121
|
+
with ThreadPoolExecutor(max_workers=max(1, workers)) as pool:
|
|
122
|
+
futures = [pool.submit(job, p) for p in file_paths]
|
|
123
|
+
for fut in as_completed(futures):
|
|
124
|
+
p = fut.result()
|
|
125
|
+
count += 1
|
|
126
|
+
if on_progress:
|
|
127
|
+
on_progress(p)
|
|
128
|
+
return count
|
|
129
|
+
|
|
130
|
+
|
|
94
131
|
class AirdClient:
|
|
95
132
|
def __init__(self, server_url: str | None = None, *, _reuse: "AirdClient | None" = None):
|
|
96
133
|
if _reuse is not None:
|
|
@@ -309,29 +346,15 @@ class AirdClient:
|
|
|
309
346
|
) -> int:
|
|
310
347
|
if not local_dir.is_dir():
|
|
311
348
|
raise NotADirectoryError(local_dir)
|
|
312
|
-
|
|
313
|
-
files: list[tuple[Path, str]] = []
|
|
314
|
-
|
|
315
|
-
def walk(base: Path, rel_remote: str) -> None:
|
|
316
|
-
for child in sorted(base.iterdir()):
|
|
317
|
-
rel = f"{rel_remote}/{child.name}" if rel_remote else child.name
|
|
318
|
-
if child.is_dir():
|
|
319
|
-
walk(child, rel)
|
|
320
|
-
elif child.is_file():
|
|
321
|
-
remote_parent = (
|
|
322
|
-
f"{remote_dir}/{rel_remote}".strip("/") if rel_remote else remote_dir
|
|
323
|
-
)
|
|
324
|
-
files.append((child, remote_parent.strip("/")))
|
|
325
|
-
|
|
326
|
-
walk(local_dir, "")
|
|
349
|
+
files = _collect_local_upload_files(local_dir.resolve(), remote_dir)
|
|
327
350
|
if not files:
|
|
328
351
|
return 0
|
|
329
352
|
|
|
330
|
-
count = 0
|
|
331
353
|
def _upload_job(lp: Path, rd: str) -> Path:
|
|
332
354
|
self.clone().upload_file(lp, rd)
|
|
333
355
|
return lp
|
|
334
356
|
|
|
357
|
+
count = 0
|
|
335
358
|
with ThreadPoolExecutor(max_workers=max(1, workers)) as pool:
|
|
336
359
|
futures = {pool.submit(_upload_job, lp, rd): (lp, rd) for lp, rd in files}
|
|
337
360
|
for fut in as_completed(futures):
|
|
@@ -360,31 +383,34 @@ class AirdClient:
|
|
|
360
383
|
return s, False
|
|
361
384
|
return None, False
|
|
362
385
|
|
|
386
|
+
def _expand_single_path(self, raw: str, seen: set[str], files: list[str]) -> None:
|
|
387
|
+
p = (raw or "").strip("/")
|
|
388
|
+
if not p:
|
|
389
|
+
return
|
|
390
|
+
try:
|
|
391
|
+
self.list_dir(p)
|
|
392
|
+
for rel, _ in self.iter_tree(p):
|
|
393
|
+
if rel not in seen:
|
|
394
|
+
seen.add(rel)
|
|
395
|
+
files.append(rel)
|
|
396
|
+
except AirdAPIError as exc:
|
|
397
|
+
if exc.status == 404:
|
|
398
|
+
if p not in seen:
|
|
399
|
+
seen.add(p)
|
|
400
|
+
files.append(p)
|
|
401
|
+
else:
|
|
402
|
+
raise
|
|
403
|
+
|
|
363
404
|
def expand_paths_to_files(self, paths: list[str]) -> list[str]:
|
|
364
405
|
"""Expand share path entries (files or folders) to file paths in your tree."""
|
|
365
406
|
files: list[str] = []
|
|
366
407
|
seen: set[str] = set()
|
|
367
408
|
for raw in paths:
|
|
368
|
-
|
|
369
|
-
if not p:
|
|
370
|
-
continue
|
|
371
|
-
try:
|
|
372
|
-
self.list_dir(p)
|
|
373
|
-
for rel, _ in self.iter_tree(p):
|
|
374
|
-
if rel not in seen:
|
|
375
|
-
seen.add(rel)
|
|
376
|
-
files.append(rel)
|
|
377
|
-
except AirdAPIError as exc:
|
|
378
|
-
if exc.status == 404:
|
|
379
|
-
if p not in seen:
|
|
380
|
-
seen.add(p)
|
|
381
|
-
files.append(p)
|
|
382
|
-
else:
|
|
383
|
-
raise
|
|
409
|
+
self._expand_single_path(raw, seen, files)
|
|
384
410
|
return files
|
|
385
411
|
|
|
386
412
|
def verify_share(self, share_id: str, token: str) -> None:
|
|
387
|
-
|
|
413
|
+
self.refresh_xsrf()
|
|
388
414
|
r = self.http.post(
|
|
389
415
|
self._url(f"/shared/{quote(share_id)}/verify"),
|
|
390
416
|
json={"token": token},
|
|
@@ -433,6 +459,42 @@ class AirdClient:
|
|
|
433
459
|
if chunk:
|
|
434
460
|
f.write(chunk)
|
|
435
461
|
|
|
462
|
+
def _own_share_download_job(self, local_dir: Path) -> Callable[[str], str]:
|
|
463
|
+
def _own_job(path: str) -> str:
|
|
464
|
+
dest = local_dir / path
|
|
465
|
+
self.clone().download_file(path, dest)
|
|
466
|
+
return path
|
|
467
|
+
|
|
468
|
+
return _own_job
|
|
469
|
+
|
|
470
|
+
def _shared_download_job(
|
|
471
|
+
self, share_id: str, local_dir: Path
|
|
472
|
+
) -> Callable[[str], str]:
|
|
473
|
+
def _shared_job(path: str) -> str:
|
|
474
|
+
dest = local_dir / path
|
|
475
|
+
self.clone().download_share_file(share_id, path, dest)
|
|
476
|
+
return path
|
|
477
|
+
|
|
478
|
+
return _shared_job
|
|
479
|
+
|
|
480
|
+
def _resolve_own_share_paths(self, share: dict) -> list[str]:
|
|
481
|
+
paths = self.expand_paths_to_files(list(share.get("paths") or []))
|
|
482
|
+
if not paths:
|
|
483
|
+
raise AirdAPIError("No files in share")
|
|
484
|
+
return paths
|
|
485
|
+
|
|
486
|
+
def _resolve_shared_file_paths(
|
|
487
|
+
self, share_id: str, share_token: str | None, share: dict | None
|
|
488
|
+
) -> list[str]:
|
|
489
|
+
if share_token:
|
|
490
|
+
self.verify_share(share_id, share_token)
|
|
491
|
+
file_paths = self.share_file_paths(share_id)
|
|
492
|
+
if not file_paths and share:
|
|
493
|
+
file_paths = list(share.get("paths") or [])
|
|
494
|
+
if not file_paths:
|
|
495
|
+
raise AirdAPIError("No files in share (or access denied — try --token)")
|
|
496
|
+
return file_paths
|
|
497
|
+
|
|
436
498
|
def download_share(
|
|
437
499
|
self,
|
|
438
500
|
share_id: str,
|
|
@@ -445,42 +507,13 @@ class AirdClient:
|
|
|
445
507
|
share, is_mine = self.find_share(share_id)
|
|
446
508
|
|
|
447
509
|
if is_mine and share:
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
raise AirdAPIError("No files in share")
|
|
451
|
-
|
|
452
|
-
def _own_job(path: str) -> str:
|
|
453
|
-
dest = local_dir / path
|
|
454
|
-
self.clone().download_file(path, dest)
|
|
455
|
-
return path
|
|
456
|
-
|
|
457
|
-
jobs = _own_job
|
|
458
|
-
file_paths = paths
|
|
510
|
+
file_paths = self._resolve_own_share_paths(share)
|
|
511
|
+
jobs = self._own_share_download_job(local_dir)
|
|
459
512
|
else:
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
file_paths = self.share_file_paths(share_id)
|
|
463
|
-
if not file_paths and share:
|
|
464
|
-
file_paths = list(share.get("paths") or [])
|
|
465
|
-
if not file_paths:
|
|
466
|
-
raise AirdAPIError("No files in share (or access denied — try --token)")
|
|
467
|
-
|
|
468
|
-
def _shared_job(path: str) -> str:
|
|
469
|
-
dest = local_dir / path
|
|
470
|
-
self.clone().download_share_file(share_id, path, dest)
|
|
471
|
-
return path
|
|
472
|
-
|
|
473
|
-
jobs = _shared_job
|
|
513
|
+
file_paths = self._resolve_shared_file_paths(share_id, share_token, share)
|
|
514
|
+
jobs = self._shared_download_job(share_id, local_dir)
|
|
474
515
|
|
|
475
|
-
|
|
476
|
-
with ThreadPoolExecutor(max_workers=max(1, workers)) as pool:
|
|
477
|
-
futures = [pool.submit(jobs, p) for p in file_paths]
|
|
478
|
-
for fut in as_completed(futures):
|
|
479
|
-
p = fut.result()
|
|
480
|
-
count += 1
|
|
481
|
-
if on_progress:
|
|
482
|
-
on_progress(p)
|
|
483
|
-
return count
|
|
516
|
+
return _run_path_jobs(file_paths, jobs, workers, on_progress)
|
|
484
517
|
|
|
485
518
|
def download_all_shares(
|
|
486
519
|
self,
|
|
@@ -40,7 +40,7 @@ FEATURE_FLAGS = {}
|
|
|
40
40
|
CLOUD_MANAGER = CloudManager()
|
|
41
41
|
WEBSOCKET_CONFIG = {}
|
|
42
42
|
MULTI_USER = False
|
|
43
|
-
WORKERS = None
|
|
43
|
+
WORKERS = None
|
|
44
44
|
DB_CONN = None
|
|
45
45
|
MAX_FILE_SIZE = _MAX_FILE_SIZE
|
|
46
46
|
MAX_READABLE_FILE_SIZE = _MAX_READABLE_FILE_SIZE
|
|
@@ -187,6 +187,82 @@ def _apply_feature_flags_from_config(config: dict) -> None:
|
|
|
187
187
|
FEATURE_FLAGS[feature_name] = bool(feature_value)
|
|
188
188
|
|
|
189
189
|
|
|
190
|
+
def _load_config_dict(args) -> dict:
|
|
191
|
+
global CONFIG_FILE
|
|
192
|
+
if not args.config:
|
|
193
|
+
return {}
|
|
194
|
+
CONFIG_FILE = args.config
|
|
195
|
+
with open(CONFIG_FILE, encoding="utf-8") as f:
|
|
196
|
+
return json.load(f)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _apply_access_tokens(args, config: dict) -> tuple[bool, bool]:
|
|
200
|
+
"""Apply ACCESS_TOKEN and ADMIN_TOKEN; return (access_explicit, admin_explicit)."""
|
|
201
|
+
global ACCESS_TOKEN, ADMIN_TOKEN
|
|
202
|
+
|
|
203
|
+
token_provided_explicitly = bool(
|
|
204
|
+
args.token or config.get("token") or os.environ.get("AIRD_ACCESS_TOKEN")
|
|
205
|
+
)
|
|
206
|
+
admin_token_provided_explicitly = bool(
|
|
207
|
+
args.admin_token or config.get("admin_token")
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
ACCESS_TOKEN = (
|
|
211
|
+
args.token
|
|
212
|
+
or config.get("token")
|
|
213
|
+
or os.environ.get("AIRD_ACCESS_TOKEN")
|
|
214
|
+
or secrets.token_urlsafe(64)
|
|
215
|
+
)
|
|
216
|
+
ADMIN_TOKEN = (
|
|
217
|
+
args.admin_token or config.get("admin_token") or secrets.token_urlsafe(64)
|
|
218
|
+
)
|
|
219
|
+
return token_provided_explicitly, admin_token_provided_explicitly
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _apply_server_settings(args, config: dict) -> None:
|
|
223
|
+
global ROOT_DIR, PORT, MULTI_USER, WORKERS, SSL_CERT, SSL_KEY, ADMIN_USERS, HOSTNAME
|
|
224
|
+
|
|
225
|
+
ROOT_DIR = args.root or config.get("root") or os.getcwd()
|
|
226
|
+
PORT = args.port or config.get("port") or 8000
|
|
227
|
+
MULTI_USER = args.multi_user or config.get("multi_user", False)
|
|
228
|
+
workers_arg = args.workers if args.workers is not None else config.get("workers")
|
|
229
|
+
WORKERS = int(workers_arg) if workers_arg is not None else None
|
|
230
|
+
SSL_CERT = args.ssl_cert or config.get("ssl_cert")
|
|
231
|
+
SSL_KEY = args.ssl_key or config.get("ssl_key")
|
|
232
|
+
ADMIN_USERS = config.get("admin_users", [])
|
|
233
|
+
HOSTNAME = args.hostname or config.get("hostname") or socket.getfqdn()
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _apply_ldap_globals(ldap_settings: dict) -> None:
|
|
237
|
+
global LDAP_ENABLED, LDAP_SERVER, LDAP_BASE_DN, LDAP_USER_TEMPLATE
|
|
238
|
+
global LDAP_FILTER_TEMPLATE, LDAP_ATTRIBUTES, LDAP_ATTRIBUTE_MAP
|
|
239
|
+
|
|
240
|
+
LDAP_ENABLED = ldap_settings["enabled"]
|
|
241
|
+
LDAP_SERVER = ldap_settings["server"]
|
|
242
|
+
LDAP_BASE_DN = ldap_settings["base_dn"]
|
|
243
|
+
LDAP_USER_TEMPLATE = ldap_settings["user_template"]
|
|
244
|
+
LDAP_FILTER_TEMPLATE = ldap_settings["filter_template"]
|
|
245
|
+
LDAP_ATTRIBUTES = ldap_settings["attributes"]
|
|
246
|
+
LDAP_ATTRIBUTE_MAP = ldap_settings["attribute_map"]
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _print_generated_tokens(
|
|
250
|
+
token_provided_explicitly: bool, admin_token_provided_explicitly: bool
|
|
251
|
+
) -> None:
|
|
252
|
+
if not token_provided_explicitly:
|
|
253
|
+
print(f"\n{'='*60}")
|
|
254
|
+
print(f"Access token (generated): {ACCESS_TOKEN}")
|
|
255
|
+
print(f"{'='*60}")
|
|
256
|
+
print("Note: Copy the token above exactly as shown .")
|
|
257
|
+
print("WARNING: Store this token securely. It grants access to your files.")
|
|
258
|
+
print(f"{'='*60}\n")
|
|
259
|
+
if not admin_token_provided_explicitly:
|
|
260
|
+
print(f"\n{'='*60}")
|
|
261
|
+
print(f"Admin token (generated): {ADMIN_TOKEN}")
|
|
262
|
+
print("WARNING: Store this token securely. It grants admin access.")
|
|
263
|
+
print(f"{'='*60}\n")
|
|
264
|
+
|
|
265
|
+
|
|
190
266
|
def init_config():
|
|
191
267
|
"""
|
|
192
268
|
Initializes the application configuration by parsing command-line arguments,
|
|
@@ -236,70 +312,19 @@ def init_config():
|
|
|
236
312
|
)
|
|
237
313
|
args = parser.parse_args()
|
|
238
314
|
|
|
239
|
-
config =
|
|
240
|
-
if args.config:
|
|
241
|
-
CONFIG_FILE = args.config
|
|
242
|
-
with open(CONFIG_FILE) as f:
|
|
243
|
-
config = json.load(f)
|
|
244
|
-
else:
|
|
245
|
-
config = {}
|
|
315
|
+
config = _load_config_dict(args)
|
|
246
316
|
|
|
247
317
|
_configure_cloud_providers(config)
|
|
248
318
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
token_provided_explicitly = bool(
|
|
253
|
-
args.token or config.get("token") or os.environ.get("AIRD_ACCESS_TOKEN")
|
|
254
|
-
)
|
|
255
|
-
admin_token_provided_explicitly = bool(
|
|
256
|
-
args.admin_token or config.get("admin_token")
|
|
257
|
-
)
|
|
258
|
-
|
|
259
|
-
ACCESS_TOKEN = (
|
|
260
|
-
args.token
|
|
261
|
-
or config.get("token")
|
|
262
|
-
or os.environ.get("AIRD_ACCESS_TOKEN")
|
|
263
|
-
or secrets.token_urlsafe(64)
|
|
264
|
-
)
|
|
265
|
-
ADMIN_TOKEN = (
|
|
266
|
-
args.admin_token or config.get("admin_token") or secrets.token_urlsafe(64)
|
|
319
|
+
token_provided_explicitly, admin_token_provided_explicitly = _apply_access_tokens(
|
|
320
|
+
args, config
|
|
267
321
|
)
|
|
322
|
+
_apply_server_settings(args, config)
|
|
268
323
|
|
|
269
324
|
ldap_settings = _parse_ldap_settings(args, config)
|
|
270
|
-
|
|
271
|
-
LDAP_SERVER = ldap_settings["server"]
|
|
272
|
-
LDAP_BASE_DN = ldap_settings["base_dn"]
|
|
273
|
-
LDAP_USER_TEMPLATE = ldap_settings["user_template"]
|
|
274
|
-
LDAP_FILTER_TEMPLATE = ldap_settings["filter_template"]
|
|
275
|
-
LDAP_ATTRIBUTES = ldap_settings["attributes"]
|
|
276
|
-
LDAP_ATTRIBUTE_MAP = ldap_settings["attribute_map"]
|
|
325
|
+
_apply_ldap_globals(ldap_settings)
|
|
277
326
|
|
|
278
327
|
_apply_feature_flags_from_config(config)
|
|
279
328
|
_apply_brevo_settings(config)
|
|
280
329
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
workers_arg = args.workers if args.workers is not None else config.get("workers")
|
|
284
|
-
WORKERS = int(workers_arg) if workers_arg is not None else None
|
|
285
|
-
|
|
286
|
-
SSL_CERT = args.ssl_cert or config.get("ssl_cert")
|
|
287
|
-
SSL_KEY = args.ssl_key or config.get("ssl_key")
|
|
288
|
-
|
|
289
|
-
ADMIN_USERS = config.get("admin_users", [])
|
|
290
|
-
|
|
291
|
-
HOSTNAME = args.hostname or config.get("hostname") or socket.getfqdn()
|
|
292
|
-
|
|
293
|
-
# Print tokens when they were not explicitly provided (masked for security)
|
|
294
|
-
if not token_provided_explicitly:
|
|
295
|
-
print(f"\n{'='*60}")
|
|
296
|
-
print(f"Access token (generated): {ACCESS_TOKEN}")
|
|
297
|
-
print(f"{'='*60}")
|
|
298
|
-
print("Note: Copy the token above exactly as shown .")
|
|
299
|
-
print("WARNING: Store this token securely. It grants access to your files.")
|
|
300
|
-
print(f"{'='*60}\n")
|
|
301
|
-
if not admin_token_provided_explicitly:
|
|
302
|
-
print(f"\n{'='*60}")
|
|
303
|
-
print(f"Admin token (generated): {ADMIN_TOKEN}")
|
|
304
|
-
print("WARNING: Store this token securely. It grants admin access.")
|
|
305
|
-
print(f"{'='*60}\n")
|
|
330
|
+
_print_generated_tokens(token_provided_explicitly, admin_token_provided_explicitly)
|
|
@@ -43,6 +43,7 @@ FEATURE_FLAGS = {
|
|
|
43
43
|
"abac_engine": False,
|
|
44
44
|
"abac_audit_decisions": True,
|
|
45
45
|
"email_notifications": False,
|
|
46
|
+
"webauthn": False,
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
# WebSocket connection configuration
|
|
@@ -63,8 +64,13 @@ UPLOAD_CONFIG = {
|
|
|
63
64
|
|
|
64
65
|
# File operation constants (derived from UPLOAD_CONFIG at startup)
|
|
65
66
|
MAX_FILE_SIZE = UPLOAD_CONFIG["max_file_size_mb"] * 1024 * 1024
|
|
66
|
-
# HTTP /upload body limit (browser uploads
|
|
67
|
+
# HTTP /upload body limit (browser small uploads + CLI)
|
|
67
68
|
UPLOAD_REQUEST_MAX_BODY_SIZE = MAX_FILE_SIZE + (1024 * 1024)
|
|
69
|
+
# Files below this use single POST /upload; at or above use Content-Range API
|
|
70
|
+
LARGE_FILE_THRESHOLD_BYTES = 500 * 1024 * 1024
|
|
71
|
+
RANGE_CHUNK_BYTES = 16 * 1024 * 1024
|
|
72
|
+
# Max JSON WebSocket control message size (search, stream commands, P2P signaling)
|
|
73
|
+
WS_JSON_MESSAGE_MAX_BYTES = 64 * 1024
|
|
68
74
|
MAX_READABLE_FILE_SIZE = 50 * 1024 * 1024 # 50 MB
|
|
69
75
|
|
|
70
76
|
# Default line window for /files/... viewer when no ?end_line= is supplied (protects DOM from huge renders)
|
|
@@ -101,8 +107,8 @@ UPLOAD_ALLOWED_EXTENSIONS = set(ALLOWED_UPLOAD_EXTENSIONS)
|
|
|
101
107
|
# Mmap constants
|
|
102
108
|
MMAP_MIN_SIZE = 1 * 1024 * 1024 # 1 MB
|
|
103
109
|
CHUNK_SIZE = 64 * 1024 # 64 KB
|
|
104
|
-
# WebSocket
|
|
105
|
-
WS_TRANSFER_FRAME_BYTES =
|
|
110
|
+
# Legacy WebSocket frame size (optional fallback; HTTP is primary for transfers)
|
|
111
|
+
WS_TRANSFER_FRAME_BYTES = 2 * 1024 * 1024
|
|
106
112
|
|
|
107
113
|
# Network share manager (set at startup)
|
|
108
114
|
NETWORK_SHARE_MANAGER = None
|