aird 0.4.23.dev2__tar.gz → 0.4.23.dev3__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.dev2 → aird-0.4.23.dev3}/PKG-INFO +5 -1
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/README.md +4 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/config.py +11 -1
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/constants/__init__.py +28 -3
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/db/shares.py +17 -5
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/handlers/api_handlers.py +27 -8
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/handlers/base_handler.py +18 -5
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/handlers/file_op_handlers.py +12 -1
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/handlers/share_handlers.py +50 -27
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/main.py +77 -48
- aird-0.4.23.dev3/aird/server_runtime.py +87 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/static/css/app.css +1 -1
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/static/js/browse/app.js +137 -23
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/static/js/share/app.js +32 -9
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/templates/_theme_early.html +1 -1
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/templates/admin.html +1 -1
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/templates/admin_audit.html +1 -1
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/templates/admin_ldap.html +1 -1
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/templates/admin_login.html +1 -1
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/templates/admin_network_shares.html +1 -1
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/templates/admin_policies.html +1 -1
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/templates/admin_tags.html +1 -1
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/templates/admin_user_attributes.html +1 -1
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/templates/admin_users.html +1 -1
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/templates/browse.html +2 -2
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/templates/directory.html +1 -1
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/templates/edit.html +1 -1
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/templates/error.html +1 -1
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/templates/file.html +1 -1
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/templates/ldap_config_create.html +1 -1
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/templates/ldap_config_edit.html +1 -1
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/templates/login.html +1 -1
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/templates/p2p_transfer.html +1 -1
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/templates/profile.html +1 -1
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/templates/share.html +1 -1
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/templates/shared_list.html +3 -8
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/templates/super_search.html +1 -1
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/templates/tagged_files.html +1 -1
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/templates/token_verification.html +1 -1
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/templates/user_create.html +1 -1
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/templates/user_edit.html +1 -1
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird.egg-info/PKG-INFO +5 -1
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird.egg-info/SOURCES.txt +3 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/setup.py +1 -1
- aird-0.4.23.dev3/tests/test_server_runtime.py +33 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/tests/test_share_handlers.py +33 -1
- aird-0.4.23.dev3/tests/test_share_ownership.py +91 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/LICENSE +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/__init__.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/__main__.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/app_context.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/cloud/__init__.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/constants/admin.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/constants/file_ops.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/constants/input_limits.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/constants/media.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/core/__init__.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/core/events.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/core/file_operations.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/core/filter_expression.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/core/input_validation.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/core/mmap_handler.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/core/security.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/core/share_root.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/core/websocket_manager.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/database/__init__.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/database/db.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/database/feature_flags.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/database/ldap.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/db/__init__.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/db/audit.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/db/config.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/db/favorites.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/db/network_shares.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/db/policies.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/db/policy_decisions.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/db/policy_seeds.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/db/quota.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/db/resource_tags.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/db/schema.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/db/user_attributes.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/db/users.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/domain/__init__.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/domain/contracts.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/domain/models.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/handlers/__init__.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/handlers/abac_handlers.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/handlers/admin_handlers.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/handlers/auth_handlers.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/handlers/constants.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/handlers/health_handler.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/handlers/p2p_handlers.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/handlers/view_handlers.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/network_share_manager.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/services/__init__.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/services/audit_service.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/services/config_service.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/services/event_subscribers.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/services/favorites_service.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/services/network_share_service.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/services/p2p_service.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/services/policy_service.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/services/quota_service.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/services/share_service.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/services/tag_service.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/services/user_service.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/sql_identifiers.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/static/favicon.png +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/static/favicon.svg +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/static/img/logo-icon.png +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/static/img/logo-mark.svg +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/static/img/logo-text.png +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/static/img/logo.png +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/static/js/aird-core.js +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/static/js/bg-canvas.js +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/static/js/common/command-palette.js +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/static/js/components/folder-picker.js +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/static/js/feature-flags-live.js +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/static/js/login-ui.js +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/static/js/p2p/app.js +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/static/js/p2p/mediator.js +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/static/js/p2p/qr-adapter.js +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/static/js/p2p/signaling-service.js +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/static/js/p2p/state-machine.js +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/static/js/p2p/transfer-service.js +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/static/js/pages/p2p-page.js +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/static/js/pages/super-search.js +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/static/js/theme.js +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/static/js/vendor/pdf.min.js +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/static/js/vendor/pdf.worker.min.js +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/static/js/vendor/qrcode-browser.js +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/templates/_admin_tabs.html +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/templates/_app_nav_header.html +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/templates/_bg_canvas.html +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/templates/_theme_login_corner.html +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/utils/__init__.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird/utils/util.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird.egg-info/dependency_links.txt +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird.egg-info/entry_points.txt +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird.egg-info/requires.txt +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/aird.egg-info/top_level.txt +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/setup.cfg +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/tests/__init__.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/tests/conftest.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/tests/handler_helpers.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/tests/test_admin_handlers.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/tests/test_api_handlers.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/tests/test_architecture_conformance.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/tests/test_auth_handlers.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/tests/test_auth_handlers_extended.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/tests/test_base_handler.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/tests/test_base_handler_pep.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/tests/test_cloud.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/tests/test_config.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/tests/test_core_file_operations.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/tests/test_database_db.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/tests/test_database_feature_flags.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/tests/test_database_ldap.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/tests/test_database_shares.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/tests/test_database_users.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/tests/test_database_users_hashing.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/tests/test_db.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/tests/test_file_op_handlers.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/tests/test_filter_expression.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/tests/test_main.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/tests/test_mmap_handler.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/tests/test_multi_user.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/tests/test_network_shares.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/tests/test_p2p_handlers.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/tests/test_password_hashing.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/tests/test_policy_service.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/tests/test_rate_limit.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/tests/test_security.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/tests/test_security_comprehensive.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/tests/test_super_search_handler.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/tests/test_tag_service.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/tests/test_util.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/tests/test_view_handlers.py +0 -0
- {aird-0.4.23.dev2 → aird-0.4.23.dev3}/tests/test_websocket_manager.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.dev3
|
|
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
|
|
@@ -36,6 +36,10 @@ Dynamic: summary
|
|
|
36
36
|
|
|
37
37
|
# Aird - Modern Web-Based File Management Platform
|
|
38
38
|
|
|
39
|
+
<p align="center">
|
|
40
|
+
<img src="aird/static/img/logo.png" alt="Aird" width="280">
|
|
41
|
+
</p>
|
|
42
|
+
|
|
39
43
|

|
|
40
44
|
|
|
41
45
|
🚀 **A lightweight, fast, and secure web-based file browser, editor, and sharing platform built with Python and Tornado.**
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Aird - Modern Web-Based File Management Platform
|
|
2
2
|
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="aird/static/img/logo.png" alt="Aird" width="280">
|
|
5
|
+
</p>
|
|
6
|
+
|
|
3
7
|

|
|
4
8
|
|
|
5
9
|
🚀 **A lightweight, fast, and secure web-based file browser, editor, and sharing platform built with Python and Tornado.**
|
|
@@ -42,6 +42,7 @@ FEATURE_FLAGS = {}
|
|
|
42
42
|
CLOUD_MANAGER = CloudManager()
|
|
43
43
|
WEBSOCKET_CONFIG = {}
|
|
44
44
|
MULTI_USER = False
|
|
45
|
+
WORKERS = None # None = auto (1.25 * threads_per_core * physical_cores)
|
|
45
46
|
DB_CONN = None
|
|
46
47
|
MAX_FILE_SIZE = _MAX_FILE_SIZE
|
|
47
48
|
MAX_READABLE_FILE_SIZE = _MAX_READABLE_FILE_SIZE
|
|
@@ -170,7 +171,7 @@ def init_config():
|
|
|
170
171
|
global CONFIG_FILE, ROOT_DIR, PORT, ACCESS_TOKEN, ADMIN_TOKEN, LDAP_ENABLED, LDAP_SERVER
|
|
171
172
|
global LDAP_BASE_DN, LDAP_USER_TEMPLATE, LDAP_FILTER_TEMPLATE, LDAP_ATTRIBUTES
|
|
172
173
|
global LDAP_ATTRIBUTE_MAP, HOSTNAME, SSL_CERT, SSL_KEY, ADMIN_USERS, FEATURE_FLAGS, CLOUD_MANAGER
|
|
173
|
-
global MULTI_USER
|
|
174
|
+
global MULTI_USER, WORKERS
|
|
174
175
|
|
|
175
176
|
parser = argparse.ArgumentParser(description="Run Aird")
|
|
176
177
|
parser.add_argument("--config", help="Path to JSON config file")
|
|
@@ -202,6 +203,12 @@ def init_config():
|
|
|
202
203
|
action="store_true",
|
|
203
204
|
help="Enable multi-user mode (each user gets a private home directory)",
|
|
204
205
|
)
|
|
206
|
+
parser.add_argument(
|
|
207
|
+
"--workers",
|
|
208
|
+
type=int,
|
|
209
|
+
default=None,
|
|
210
|
+
help="HTTP worker processes (default: ceil(1.25 * threads_per_core * physical_cores); 1 on Windows)",
|
|
211
|
+
)
|
|
205
212
|
args = parser.parse_args()
|
|
206
213
|
|
|
207
214
|
config = {}
|
|
@@ -247,6 +254,9 @@ def init_config():
|
|
|
247
254
|
|
|
248
255
|
MULTI_USER = args.multi_user or config.get("multi_user", False)
|
|
249
256
|
|
|
257
|
+
workers_arg = args.workers if args.workers is not None else config.get("workers")
|
|
258
|
+
WORKERS = int(workers_arg) if workers_arg is not None else None
|
|
259
|
+
|
|
250
260
|
SSL_CERT = args.ssl_cert or config.get("ssl_cert")
|
|
251
261
|
SSL_KEY = args.ssl_key or config.get("ssl_key")
|
|
252
262
|
|
|
@@ -59,10 +59,10 @@ UPLOAD_CONFIG = {
|
|
|
59
59
|
"allow_all_file_types": 0, # 0 = use whitelist below, 1 = allow any extension
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
# Per-request body limit (Cloudflare
|
|
63
|
-
UPLOAD_CHUNK_SIZE_BYTES =
|
|
62
|
+
# Per-request body limit (Cloudflare: keep each POST under ~100 MB and ~100s proxy timeout)
|
|
63
|
+
UPLOAD_CHUNK_SIZE_BYTES = 50 * 1024 * 1024 # 50 MiB per HTTP request
|
|
64
64
|
UPLOAD_REQUEST_MAX_BODY_SIZE = UPLOAD_CHUNK_SIZE_BYTES + (1024 * 1024) # chunk + slack
|
|
65
|
-
UPLOAD_MAX_PARALLEL_CHUNKS =
|
|
65
|
+
UPLOAD_MAX_PARALLEL_CHUNKS = 3 # fewer concurrent POSTs through reverse proxies
|
|
66
66
|
|
|
67
67
|
# File operation constants (derived from UPLOAD_CONFIG at startup)
|
|
68
68
|
MAX_FILE_SIZE = UPLOAD_CONFIG["max_file_size_mb"] * 1024 * 1024
|
|
@@ -118,3 +118,28 @@ CORPORATE_IP_CIDRS: list[str] = [
|
|
|
118
118
|
for c in os.environ.get("AIRD_CORPORATE_IP_CIDRS", "").split(",")
|
|
119
119
|
if c.strip()
|
|
120
120
|
]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _read_app_version() -> str:
|
|
124
|
+
"""Package version for cache-busting static assets in templates (?v=…)."""
|
|
125
|
+
try:
|
|
126
|
+
from importlib.metadata import version
|
|
127
|
+
|
|
128
|
+
return version("aird")
|
|
129
|
+
except Exception:
|
|
130
|
+
pass
|
|
131
|
+
try:
|
|
132
|
+
import re
|
|
133
|
+
from pathlib import Path
|
|
134
|
+
|
|
135
|
+
setup_py = Path(__file__).resolve().parents[2] / "setup.py"
|
|
136
|
+
text = setup_py.read_text(encoding="utf-8")
|
|
137
|
+
match = re.search(r'version\s*=\s*["\']([^"\']+)["\']', text)
|
|
138
|
+
if match:
|
|
139
|
+
return match.group(1)
|
|
140
|
+
except Exception:
|
|
141
|
+
pass
|
|
142
|
+
return "dev"
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
APP_VERSION = _read_app_version()
|
|
@@ -316,9 +316,14 @@ def list_shares_accessible_to_user(conn: sqlite3.Connection, username: str) -> l
|
|
|
316
316
|
result = []
|
|
317
317
|
for row in cursor:
|
|
318
318
|
share = _row_to_share_dict(row, col_names)
|
|
319
|
+
if login_matches_share_creator_field(share.get("created_by"), username):
|
|
320
|
+
continue
|
|
319
321
|
allowed = share.get("allowed_users")
|
|
320
|
-
|
|
321
|
-
if
|
|
322
|
+
modify_users = share.get("modify_users") or []
|
|
323
|
+
if username in modify_users:
|
|
324
|
+
result.append(share)
|
|
325
|
+
continue
|
|
326
|
+
if allowed is None or username in allowed:
|
|
322
327
|
result.append(share)
|
|
323
328
|
return result
|
|
324
329
|
except Exception as e:
|
|
@@ -375,9 +380,16 @@ def _share_covers_dynamic_path(share: dict, rel_path: str, root_dir: str) -> boo
|
|
|
375
380
|
try:
|
|
376
381
|
full_folder_path = os.path.abspath(os.path.join(root_dir, folder_path))
|
|
377
382
|
full_file_path = os.path.abspath(os.path.join(root_dir, rel_path))
|
|
378
|
-
if (
|
|
379
|
-
|
|
380
|
-
|
|
383
|
+
if not is_within_root(full_file_path, root_dir):
|
|
384
|
+
continue
|
|
385
|
+
if os.path.isdir(full_folder_path) and is_within_root(
|
|
386
|
+
full_file_path, full_folder_path
|
|
387
|
+
):
|
|
388
|
+
if filter_files_by_patterns([rel_path], allow_list, avoid_list):
|
|
389
|
+
return True
|
|
390
|
+
elif (
|
|
391
|
+
os.path.isfile(full_folder_path)
|
|
392
|
+
and folder_path.replace("\\", "/") == rel_path.replace("\\", "/")
|
|
381
393
|
and filter_files_by_patterns([rel_path], allow_list, avoid_list)
|
|
382
394
|
):
|
|
383
395
|
return True
|
|
@@ -927,12 +927,14 @@ class ShareDetailsByIdAPIHandler(BaseHandler):
|
|
|
927
927
|
self.write_json_error(404, "Share not found")
|
|
928
928
|
return None
|
|
929
929
|
|
|
930
|
-
if not self.
|
|
930
|
+
if not self.can_edit_share_paths(share):
|
|
931
931
|
self.write_json_error(403, "Access denied")
|
|
932
932
|
return None
|
|
933
933
|
|
|
934
934
|
allowed_users = share.get("allowed_users")
|
|
935
935
|
modify_users = share.get("modify_users")
|
|
936
|
+
show_secret = self.can_manage_share_secrets(share)
|
|
937
|
+
token = share.get("secret_token")
|
|
936
938
|
share_info = {
|
|
937
939
|
"id": share["id"],
|
|
938
940
|
"created": share.get("created", ""),
|
|
@@ -940,8 +942,8 @@ class ShareDetailsByIdAPIHandler(BaseHandler):
|
|
|
940
942
|
"modify_users": modify_users if modify_users is not None else [],
|
|
941
943
|
"url": f"/shared/{share['id']}",
|
|
942
944
|
"paths": share.get("paths", []),
|
|
943
|
-
"has_token":
|
|
944
|
-
"secret_token":
|
|
945
|
+
"has_token": token is not None,
|
|
946
|
+
"secret_token": token if show_secret else None,
|
|
945
947
|
"share_type": share.get("share_type", "static"),
|
|
946
948
|
"tag_name": share.get("tag_name"),
|
|
947
949
|
"allow_list": share.get("allow_list", []),
|
|
@@ -950,6 +952,10 @@ class ShareDetailsByIdAPIHandler(BaseHandler):
|
|
|
950
952
|
"download_count": share_service.get_download_count(
|
|
951
953
|
db_conn, share_id
|
|
952
954
|
),
|
|
955
|
+
"is_owner": self.is_share_owner(share),
|
|
956
|
+
"can_revoke": self.is_share_owner(share),
|
|
957
|
+
"can_manage": self.is_share_owner(share),
|
|
958
|
+
"can_edit_paths": self.can_edit_share_paths(share),
|
|
953
959
|
}
|
|
954
960
|
|
|
955
961
|
return {"share": share_info}
|
|
@@ -970,12 +976,14 @@ def _classify_share_for_user(
|
|
|
970
976
|
allowed_list = allowed_raw if isinstance(allowed_raw, list) else []
|
|
971
977
|
mod_list = share.get("modify_users") or []
|
|
972
978
|
|
|
973
|
-
if
|
|
974
|
-
|
|
975
|
-
|
|
979
|
+
if creator and current_user and login_matches_share_creator_field(
|
|
980
|
+
creator, current_user
|
|
981
|
+
):
|
|
976
982
|
return True, False
|
|
977
983
|
if not creator and is_admin:
|
|
978
984
|
return True, False
|
|
985
|
+
if current_user and current_user in mod_list:
|
|
986
|
+
return False, True
|
|
979
987
|
if not creator and current_user and (
|
|
980
988
|
allowed_raw is None
|
|
981
989
|
or (isinstance(allowed_raw, list) and current_user in allowed_list)
|
|
@@ -986,6 +994,16 @@ def _classify_share_for_user(
|
|
|
986
994
|
return False, False
|
|
987
995
|
|
|
988
996
|
|
|
997
|
+
def _attach_share_capabilities(handler: BaseHandler, share: dict) -> dict:
|
|
998
|
+
"""Add ownership / editor flags for the share management UI."""
|
|
999
|
+
out = dict(share)
|
|
1000
|
+
out["is_owner"] = handler.is_share_owner(share)
|
|
1001
|
+
out["can_revoke"] = handler.is_share_owner(share)
|
|
1002
|
+
out["can_manage"] = handler.is_share_owner(share)
|
|
1003
|
+
out["can_edit_paths"] = handler.can_edit_share_paths(share)
|
|
1004
|
+
return out
|
|
1005
|
+
|
|
1006
|
+
|
|
989
1007
|
class ShareListAPIHandler(BaseHandler):
|
|
990
1008
|
@tornado.web.authenticated
|
|
991
1009
|
def get(self):
|
|
@@ -1010,9 +1028,10 @@ class ShareListAPIHandler(BaseHandler):
|
|
|
1010
1028
|
for sid, share in all_shares.items():
|
|
1011
1029
|
is_mine, is_shared = _classify_share_for_user(share, current_user, is_admin)
|
|
1012
1030
|
if is_mine:
|
|
1013
|
-
my_shares[sid] = share
|
|
1031
|
+
my_shares[sid] = _attach_share_capabilities(self, share)
|
|
1014
1032
|
elif is_shared:
|
|
1015
|
-
|
|
1033
|
+
redacted = _redact_share_secret_token(share)
|
|
1034
|
+
shared_with_me.append(_attach_share_capabilities(self, redacted))
|
|
1016
1035
|
|
|
1017
1036
|
self.write({"shares": my_shares, "shared_with_me": shared_with_me})
|
|
1018
1037
|
|
|
@@ -895,6 +895,7 @@ class BaseHandler(tornado.web.RequestHandler):
|
|
|
895
895
|
namespace.setdefault("nav_title", "")
|
|
896
896
|
namespace.setdefault("show_admin_link", False)
|
|
897
897
|
namespace.setdefault("ldap_enabled", self.settings.get("ldap_server") is not None)
|
|
898
|
+
namespace.setdefault("static_version", constants_module.APP_VERSION)
|
|
898
899
|
return namespace
|
|
899
900
|
|
|
900
901
|
def get_current_user(self):
|
|
@@ -954,15 +955,27 @@ class BaseHandler(tornado.web.RequestHandler):
|
|
|
954
955
|
return _display_username_from_dict(user)
|
|
955
956
|
return _display_username_from_legacy(user, self)
|
|
956
957
|
|
|
957
|
-
def
|
|
958
|
-
"""True if current user
|
|
958
|
+
def is_share_owner(self, share: dict) -> bool:
|
|
959
|
+
"""True if the current user created the share (or is admin for legacy rows)."""
|
|
959
960
|
if self.is_admin_user():
|
|
960
961
|
return True
|
|
961
962
|
u = get_username_string_for_db(self)
|
|
962
963
|
if not u:
|
|
963
964
|
return False
|
|
964
965
|
creator = (share.get("created_by") or "").strip()
|
|
965
|
-
if
|
|
966
|
+
if not creator:
|
|
967
|
+
return False
|
|
968
|
+
return login_matches_share_creator_field(creator, u)
|
|
969
|
+
|
|
970
|
+
def can_edit_share_paths(self, share: dict) -> bool:
|
|
971
|
+
"""True if the user may add/remove files on an existing share."""
|
|
972
|
+
if self.is_share_owner(share):
|
|
966
973
|
return True
|
|
967
|
-
|
|
968
|
-
|
|
974
|
+
u = get_username_string_for_db(self)
|
|
975
|
+
if not u:
|
|
976
|
+
return False
|
|
977
|
+
return u in (share.get("modify_users") or [])
|
|
978
|
+
|
|
979
|
+
def can_manage_share_secrets(self, share: dict) -> bool:
|
|
980
|
+
"""True for share owners: tokens, ACL, revoke, and full settings."""
|
|
981
|
+
return self.is_share_owner(share)
|
|
@@ -540,8 +540,19 @@ class UploadHandler(BaseHandler):
|
|
|
540
540
|
username = self.get_display_username()
|
|
541
541
|
self._session_dir = _upload_session_dir(username, chunk_info["upload_id"])
|
|
542
542
|
offset = chunk_info["offset"]
|
|
543
|
+
# Only reset session on first chunk when no other parts exist (avoid wiping
|
|
544
|
+
# in-flight parallel chunks if chunk 0 is retried by the browser or proxy).
|
|
543
545
|
if offset == 0 and os.path.isdir(self._session_dir):
|
|
544
|
-
|
|
546
|
+
try:
|
|
547
|
+
other_parts = [
|
|
548
|
+
n
|
|
549
|
+
for n in os.listdir(self._session_dir)
|
|
550
|
+
if n.endswith(".part") and n != "0.part"
|
|
551
|
+
]
|
|
552
|
+
except OSError:
|
|
553
|
+
other_parts = []
|
|
554
|
+
if not other_parts:
|
|
555
|
+
_remove_upload_session(self._session_dir)
|
|
545
556
|
os.makedirs(self._session_dir, exist_ok=True)
|
|
546
557
|
self._temp_path = _chunk_file_path(self._session_dir, offset)
|
|
547
558
|
self._aiofile = await aiofiles.open(self._temp_path, "wb")
|
|
@@ -283,6 +283,32 @@ def _is_user_allowed_for_modify(share, get_secure_cookie):
|
|
|
283
283
|
return (True, None)
|
|
284
284
|
|
|
285
285
|
|
|
286
|
+
def _collect_files_for_share_paths(root: str, paths: list, *, include_missing_files: bool) -> list[str]:
|
|
287
|
+
"""Resolve share path entries to relative file paths under *root*."""
|
|
288
|
+
collected: list[str] = []
|
|
289
|
+
for rel_path in paths or []:
|
|
290
|
+
try:
|
|
291
|
+
full_path = os.path.abspath(os.path.join(root, rel_path))
|
|
292
|
+
if not is_within_root(full_path, root):
|
|
293
|
+
continue
|
|
294
|
+
if os.path.isdir(full_path):
|
|
295
|
+
for sub in get_all_files_recursive(full_path, rel_path):
|
|
296
|
+
collected.append(str(sub).replace("\\", "/"))
|
|
297
|
+
elif os.path.isfile(full_path):
|
|
298
|
+
collected.append(str(rel_path).replace("\\", "/"))
|
|
299
|
+
elif include_missing_files:
|
|
300
|
+
collected.append(str(rel_path).replace("\\", "/"))
|
|
301
|
+
except Exception:
|
|
302
|
+
logging.debug("Skipping share path %r", rel_path, exc_info=True)
|
|
303
|
+
seen: set[str] = set()
|
|
304
|
+
unique: list[str] = []
|
|
305
|
+
for path in collected:
|
|
306
|
+
if path not in seen:
|
|
307
|
+
seen.add(path)
|
|
308
|
+
unique.append(path)
|
|
309
|
+
return unique
|
|
310
|
+
|
|
311
|
+
|
|
286
312
|
def _get_share_file_list(share, db_conn=None):
|
|
287
313
|
"""Return list of file paths for the share (dynamic, static, or tag-based)."""
|
|
288
314
|
share_type = share.get("share_type", "static")
|
|
@@ -295,32 +321,13 @@ def _get_share_file_list(share, db_conn=None):
|
|
|
295
321
|
db_conn, share.get("tag_name"), root, allow_list, avoid_list
|
|
296
322
|
)
|
|
297
323
|
|
|
324
|
+
paths = share.get("paths") or []
|
|
298
325
|
if share_type == "dynamic":
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
try:
|
|
302
|
-
full_path = os.path.abspath(os.path.join(root, folder_path))
|
|
303
|
-
if os.path.isdir(full_path) and is_within_root(full_path, root):
|
|
304
|
-
all_files = get_all_files_recursive(full_path, folder_path)
|
|
305
|
-
dynamic_files.extend(all_files)
|
|
306
|
-
except Exception:
|
|
307
|
-
logging.debug(
|
|
308
|
-
"Skipping dynamic share folder %r", folder_path, exc_info=True
|
|
309
|
-
)
|
|
310
|
-
return filter_files_by_patterns(dynamic_files, allow_list, avoid_list)
|
|
326
|
+
resolved = _collect_files_for_share_paths(root, paths, include_missing_files=False)
|
|
327
|
+
return filter_files_by_patterns(resolved, allow_list, avoid_list)
|
|
311
328
|
|
|
312
|
-
|
|
313
|
-
|
|
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)
|
|
329
|
+
resolved = _collect_files_for_share_paths(root, paths, include_missing_files=True)
|
|
330
|
+
return filter_files_by_patterns(resolved, allow_list, avoid_list)
|
|
324
331
|
|
|
325
332
|
|
|
326
333
|
def _is_path_in_share(share, path, db_conn=None):
|
|
@@ -557,6 +564,21 @@ def _execute_share_update(handler, db_conn, share_id, share_data, data):
|
|
|
557
564
|
)
|
|
558
565
|
|
|
559
566
|
|
|
567
|
+
_EDITOR_SHARE_UPDATE_KEYS = frozenset({"share_id", "paths", "remove_files"})
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def _share_update_payload_for_user(handler, share_data: dict, data: dict) -> dict | None:
|
|
571
|
+
"""Return update body allowed for this user, or None if forbidden."""
|
|
572
|
+
if handler.can_manage_share_secrets(share_data):
|
|
573
|
+
return data
|
|
574
|
+
if not handler.can_edit_share_paths(share_data):
|
|
575
|
+
return None
|
|
576
|
+
filtered = {k: v for k, v in data.items() if k in _EDITOR_SHARE_UPDATE_KEYS}
|
|
577
|
+
if not any(k in filtered for k in ("paths", "remove_files")):
|
|
578
|
+
return None
|
|
579
|
+
return filtered
|
|
580
|
+
|
|
581
|
+
|
|
560
582
|
def _apply_metadata_updates(data, share_data, update_fields):
|
|
561
583
|
"""Apply users/token/filters/expiry metadata to update_fields."""
|
|
562
584
|
if "allowed_users" in data:
|
|
@@ -718,9 +740,9 @@ class ShareRevokeHandler(XSRFTokenMixin, BaseHandler):
|
|
|
718
740
|
self.set_status(404)
|
|
719
741
|
self.write({"error": "Share not found"})
|
|
720
742
|
return
|
|
721
|
-
if not self.
|
|
743
|
+
if not self.is_share_owner(share):
|
|
722
744
|
self.set_status(403)
|
|
723
|
-
self.write({"error": "
|
|
745
|
+
self.write({"error": "Only the share owner can revoke this share"})
|
|
724
746
|
return
|
|
725
747
|
self.get_service("share_service").delete_share(self.db_conn, sid)
|
|
726
748
|
self.get_service("audit_service").log(
|
|
@@ -759,7 +781,8 @@ class ShareUpdateHandler(XSRFTokenMixin, BaseHandler):
|
|
|
759
781
|
self.set_status(err_status)
|
|
760
782
|
self.write(err_body)
|
|
761
783
|
return None, None, []
|
|
762
|
-
|
|
784
|
+
data = _share_update_payload_for_user(self, share_data, data)
|
|
785
|
+
if data is None:
|
|
763
786
|
self.set_status(403)
|
|
764
787
|
self.write({"error": "Access denied"})
|
|
765
788
|
return None, None, []
|
|
@@ -5,10 +5,15 @@ import socket
|
|
|
5
5
|
import sqlite3
|
|
6
6
|
import ssl
|
|
7
7
|
|
|
8
|
+
import tornado.httpserver
|
|
8
9
|
import tornado.ioloop
|
|
10
|
+
import tornado.netutil
|
|
11
|
+
import tornado.process
|
|
9
12
|
import tornado.web
|
|
10
13
|
import logging.handlers
|
|
11
14
|
|
|
15
|
+
from aird.server_runtime import describe_worker_layout, resolve_worker_count
|
|
16
|
+
|
|
12
17
|
|
|
13
18
|
import aird.constants as constants
|
|
14
19
|
import aird.config as config
|
|
@@ -486,42 +491,87 @@ def _build_app_context() -> AppContext:
|
|
|
486
491
|
)
|
|
487
492
|
|
|
488
493
|
|
|
489
|
-
def
|
|
494
|
+
def _build_application():
|
|
495
|
+
"""Create the Tornado app (call after _init_database in each process)."""
|
|
496
|
+
cookie_secret = os.environ.get("AIRD_COOKIE_SECRET") or secrets.token_urlsafe(64)
|
|
497
|
+
settings = {
|
|
498
|
+
"cookie_secret": cookie_secret,
|
|
499
|
+
"xsrf_cookies": True,
|
|
500
|
+
"login_url": "/login",
|
|
501
|
+
"admin_login_url": "/admin/login",
|
|
502
|
+
"cloud_manager": constants.CLOUD_MANAGER,
|
|
503
|
+
}
|
|
504
|
+
settings["app_context"] = _build_app_context()
|
|
505
|
+
return make_app(
|
|
506
|
+
settings,
|
|
507
|
+
config.LDAP_ENABLED,
|
|
508
|
+
config.LDAP_SERVER,
|
|
509
|
+
config.LDAP_BASE_DN,
|
|
510
|
+
config.LDAP_USER_TEMPLATE,
|
|
511
|
+
config.LDAP_FILTER_TEMPLATE,
|
|
512
|
+
config.LDAP_ATTRIBUTES,
|
|
513
|
+
config.LDAP_ATTRIBUTE_MAP,
|
|
514
|
+
config.ADMIN_USERS,
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def _run_http_server(app, ssl_options, sockets) -> None:
|
|
519
|
+
server = tornado.httpserver.HTTPServer(
|
|
520
|
+
app,
|
|
521
|
+
ssl_options=ssl_options,
|
|
522
|
+
max_body_size=constants.UPLOAD_REQUEST_MAX_BODY_SIZE,
|
|
523
|
+
max_buffer_size=constants.UPLOAD_REQUEST_MAX_BODY_SIZE,
|
|
524
|
+
)
|
|
525
|
+
server.add_sockets(sockets)
|
|
526
|
+
if tornado.process.task_id() in (0, None):
|
|
527
|
+
tornado.ioloop.IOLoop.current().call_later(
|
|
528
|
+
3600, _run_cleanup_expired_shares
|
|
529
|
+
)
|
|
530
|
+
tornado.ioloop.IOLoop.current().start()
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def _start_server(ssl_options, port: int, hostname: str, worker_count: int) -> None:
|
|
490
534
|
_MAX_PORT_RETRIES = 3
|
|
535
|
+
proto = "https" if ssl_options else "http"
|
|
491
536
|
for attempt in range(_MAX_PORT_RETRIES):
|
|
492
537
|
try:
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
app.listen(
|
|
496
|
-
port,
|
|
497
|
-
ssl_options=ssl_options,
|
|
498
|
-
max_body_size=constants.UPLOAD_REQUEST_MAX_BODY_SIZE,
|
|
499
|
-
max_buffer_size=constants.UPLOAD_REQUEST_MAX_BODY_SIZE,
|
|
500
|
-
)
|
|
538
|
+
sockets = tornado.netutil.bind_sockets(port, address="")
|
|
539
|
+
if worker_count <= 1:
|
|
501
540
|
logger.info(
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
else:
|
|
505
|
-
proto = "http"
|
|
506
|
-
app.listen(
|
|
541
|
+
"Serving %s on 0.0.0.0 port %d (single process) ...",
|
|
542
|
+
proto.upper(),
|
|
507
543
|
port,
|
|
508
|
-
max_body_size=constants.UPLOAD_REQUEST_MAX_BODY_SIZE,
|
|
509
|
-
max_buffer_size=constants.UPLOAD_REQUEST_MAX_BODY_SIZE,
|
|
510
|
-
)
|
|
511
|
-
logger.info(
|
|
512
|
-
f"Serving HTTP on 0.0.0.0 port {port} ({proto}://0.0.0.0:{port}/) ..."
|
|
513
544
|
)
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
545
|
+
_init_database()
|
|
546
|
+
app = _build_application()
|
|
547
|
+
_print_server_urls(port, hostname, proto)
|
|
548
|
+
_run_http_server(app, ssl_options, sockets)
|
|
549
|
+
return
|
|
550
|
+
|
|
551
|
+
logger.info(
|
|
552
|
+
"Serving %s on 0.0.0.0 port %d (%s) ...",
|
|
553
|
+
proto.upper(),
|
|
554
|
+
port,
|
|
555
|
+
describe_worker_layout(worker_count),
|
|
517
556
|
)
|
|
518
|
-
|
|
557
|
+
logger.warning(
|
|
558
|
+
"Multiple workers: in-memory WebSocket/P2P state is per process; "
|
|
559
|
+
"use sticky sessions at the load balancer if needed."
|
|
560
|
+
)
|
|
561
|
+
tornado.process.fork_processes(worker_count)
|
|
562
|
+
_init_database()
|
|
563
|
+
app = _build_application()
|
|
564
|
+
if tornado.process.task_id() == 0:
|
|
565
|
+
_print_server_urls(port, hostname, proto)
|
|
566
|
+
_run_http_server(app, ssl_options, sockets)
|
|
519
567
|
return
|
|
520
568
|
except OSError:
|
|
521
569
|
logger.exception("Failed to bind on port %d", port)
|
|
522
570
|
if attempt < _MAX_PORT_RETRIES - 1:
|
|
523
571
|
port += 1
|
|
524
|
-
logger.warning(
|
|
572
|
+
logger.warning(
|
|
573
|
+
"Retrying on port %d (%d/%d)", port, attempt + 2, _MAX_PORT_RETRIES
|
|
574
|
+
)
|
|
525
575
|
else:
|
|
526
576
|
logger.error(
|
|
527
577
|
"Could not bind after %d attempts. Set a different --port and retry.",
|
|
@@ -570,34 +620,12 @@ def main():
|
|
|
570
620
|
else:
|
|
571
621
|
logger.info("Single-user mode — all users share root: %s", constants.ROOT_DIR)
|
|
572
622
|
|
|
573
|
-
cookie_secret = os.environ.get("AIRD_COOKIE_SECRET") or secrets.token_urlsafe(64)
|
|
574
623
|
if not os.environ.get("AIRD_COOKIE_SECRET"):
|
|
624
|
+
os.environ["AIRD_COOKIE_SECRET"] = secrets.token_urlsafe(64)
|
|
575
625
|
logger.warning(
|
|
576
626
|
"cookie_secret is randomly generated; sessions will be invalidated on restart. "
|
|
577
627
|
"Set the AIRD_COOKIE_SECRET environment variable for persistent sessions."
|
|
578
628
|
)
|
|
579
|
-
settings = {
|
|
580
|
-
"cookie_secret": cookie_secret,
|
|
581
|
-
"xsrf_cookies": True,
|
|
582
|
-
"login_url": "/login",
|
|
583
|
-
"admin_login_url": "/admin/login",
|
|
584
|
-
"cloud_manager": constants.CLOUD_MANAGER,
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
_init_database()
|
|
588
|
-
settings["app_context"] = _build_app_context()
|
|
589
|
-
|
|
590
|
-
app = make_app(
|
|
591
|
-
settings,
|
|
592
|
-
config.LDAP_ENABLED,
|
|
593
|
-
config.LDAP_SERVER,
|
|
594
|
-
config.LDAP_BASE_DN,
|
|
595
|
-
config.LDAP_USER_TEMPLATE,
|
|
596
|
-
config.LDAP_FILTER_TEMPLATE,
|
|
597
|
-
config.LDAP_ATTRIBUTES,
|
|
598
|
-
config.LDAP_ATTRIBUTE_MAP,
|
|
599
|
-
config.ADMIN_USERS,
|
|
600
|
-
)
|
|
601
629
|
|
|
602
630
|
ssl_options = None
|
|
603
631
|
if config.SSL_CERT and config.SSL_KEY:
|
|
@@ -606,7 +634,8 @@ def main():
|
|
|
606
634
|
ssl_context.load_cert_chain(config.SSL_CERT, config.SSL_KEY)
|
|
607
635
|
ssl_options = ssl_context
|
|
608
636
|
|
|
609
|
-
|
|
637
|
+
worker_count = resolve_worker_count(config.WORKERS)
|
|
638
|
+
_start_server(ssl_options, config.PORT, config.HOSTNAME, worker_count)
|
|
610
639
|
|
|
611
640
|
|
|
612
641
|
if __name__ == "__main__":
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""HTTP server process count and Tornado prefork helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import math
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def detect_threads_per_core() -> float:
|
|
14
|
+
"""Logical CPUs per physical core (hyperthreading); default 2."""
|
|
15
|
+
raw = os.environ.get("AIRD_THREADS_PER_CORE", "").strip()
|
|
16
|
+
if raw:
|
|
17
|
+
try:
|
|
18
|
+
value = float(raw)
|
|
19
|
+
if value > 0:
|
|
20
|
+
return value
|
|
21
|
+
except ValueError:
|
|
22
|
+
logger.warning("Invalid AIRD_THREADS_PER_CORE=%r; using 2", raw)
|
|
23
|
+
return 2.0
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def detect_physical_cpu_count() -> int:
|
|
27
|
+
"""Best-effort physical core count; falls back to logical / threads_per_core."""
|
|
28
|
+
logical = os.cpu_count() or 1
|
|
29
|
+
threads_per_core = detect_threads_per_core()
|
|
30
|
+
if sys.platform == "linux":
|
|
31
|
+
try:
|
|
32
|
+
ids: set[int] = set()
|
|
33
|
+
with open("/proc/cpuinfo", encoding="utf-8", errors="replace") as cpuinfo:
|
|
34
|
+
for line in cpuinfo:
|
|
35
|
+
if line.lower().startswith("core id") or line.lower().startswith(
|
|
36
|
+
"cpu cores"
|
|
37
|
+
):
|
|
38
|
+
_, _, val = line.partition(":")
|
|
39
|
+
ids.add(int(val.strip()))
|
|
40
|
+
if ids:
|
|
41
|
+
return max(1, len(ids))
|
|
42
|
+
except OSError:
|
|
43
|
+
pass
|
|
44
|
+
return max(1, round(logical / threads_per_core))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def compute_default_worker_count() -> int:
|
|
48
|
+
"""
|
|
49
|
+
Process count = ceil(1.25 * threads_per_core * physical_cores).
|
|
50
|
+
|
|
51
|
+
Example: 4 physical cores, 2 threads/core -> ceil(1.25 * 2 * 4) = 10 workers.
|
|
52
|
+
"""
|
|
53
|
+
threads_per_core = detect_threads_per_core()
|
|
54
|
+
physical = detect_physical_cpu_count()
|
|
55
|
+
return max(1, math.ceil(1.25 * threads_per_core * physical))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def resolve_worker_count(configured: int | None = None) -> int:
|
|
59
|
+
"""CLI/config > env AIRD_WORKERS > computed default; 1 on Windows."""
|
|
60
|
+
if sys.platform == "win32":
|
|
61
|
+
if configured and configured > 1:
|
|
62
|
+
logger.warning(
|
|
63
|
+
"Multiprocess serving is not supported on Windows; using 1 worker"
|
|
64
|
+
)
|
|
65
|
+
return 1
|
|
66
|
+
if configured is not None and configured > 0:
|
|
67
|
+
return configured
|
|
68
|
+
env_workers = os.environ.get("AIRD_WORKERS", "").strip()
|
|
69
|
+
if env_workers:
|
|
70
|
+
try:
|
|
71
|
+
parsed = int(env_workers)
|
|
72
|
+
if parsed > 0:
|
|
73
|
+
return parsed
|
|
74
|
+
except ValueError:
|
|
75
|
+
logger.warning("Invalid AIRD_WORKERS=%r; using default formula", env_workers)
|
|
76
|
+
return compute_default_worker_count()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def describe_worker_layout(worker_count: int) -> str:
|
|
80
|
+
logical = os.cpu_count() or 1
|
|
81
|
+
tpc = detect_threads_per_core()
|
|
82
|
+
physical = detect_physical_cpu_count()
|
|
83
|
+
return (
|
|
84
|
+
f"workers={worker_count} "
|
|
85
|
+
f"(logical_cpus={logical}, physical_cpus={physical}, "
|
|
86
|
+
f"threads_per_core={tpc:g}, formula=ceil(1.25*tpc*physical))"
|
|
87
|
+
)
|