aird 0.4.23.dev3__tar.gz → 0.4.23.dev11__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.dev11/MANIFEST.in +6 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/PKG-INFO +1 -1
- aird-0.4.23.dev11/aird/cli/__init__.py +3 -0
- aird-0.4.23.dev11/aird/cli/__main__.py +5 -0
- aird-0.4.23.dev11/aird/cli/authelia.py +86 -0
- aird-0.4.23.dev11/aird/cli/config.py +94 -0
- aird-0.4.23.dev11/aird/cli/main.py +410 -0
- aird-0.4.23.dev11/aird/cli/session.py +571 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/constants/media.py +17 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/handlers/base_handler.py +3 -1
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/handlers/file_op_handlers.py +142 -24
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/handlers/p2p_handlers.py +59 -14
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/handlers/view_handlers.py +39 -2
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/services/p2p_service.py +1 -1
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/static/css/app.css +1 -1
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/static/js/browse/app.js +21 -12
- aird-0.4.23.dev11/aird/static/js/media-view.js +94 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/static/js/pages/p2p-page.js +16 -6
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/static/js/share/app.js +4 -2
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/_app_nav_header.html +3 -3
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/_theme_login_corner.html +14 -1
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/browse.html +4 -7
- aird-0.4.23.dev11/aird/templates/media_view.html +108 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/p2p_transfer.html +15 -3
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/share.html +3 -3
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/tagged_files.html +4 -4
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/utils/util.py +22 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird.egg-info/PKG-INFO +1 -1
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird.egg-info/SOURCES.txt +12 -1
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird.egg-info/entry_points.txt +1 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/setup.py +4 -1
- aird-0.4.23.dev11/tests/test_cli.py +51 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_file_op_handlers.py +21 -4
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_p2p_handlers.py +29 -9
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_util.py +16 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_view_handlers.py +60 -0
- aird-0.4.23.dev11/tests/test_wheel_static_assets.py +48 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/LICENSE +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/README.md +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/__init__.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/__main__.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/app_context.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/cloud/__init__.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/config.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/constants/__init__.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/constants/admin.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/constants/file_ops.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/constants/input_limits.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/core/__init__.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/core/events.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/core/file_operations.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/core/filter_expression.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/core/input_validation.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/core/mmap_handler.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/core/security.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/core/share_root.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/core/websocket_manager.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/database/__init__.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/database/db.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/database/feature_flags.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/database/ldap.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/db/__init__.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/db/audit.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/db/config.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/db/favorites.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/db/network_shares.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/db/policies.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/db/policy_decisions.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/db/policy_seeds.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/db/quota.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/db/resource_tags.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/db/schema.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/db/shares.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/db/user_attributes.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/db/users.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/domain/__init__.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/domain/contracts.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/domain/models.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/handlers/__init__.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/handlers/abac_handlers.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/handlers/admin_handlers.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/handlers/api_handlers.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/handlers/auth_handlers.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/handlers/constants.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/handlers/health_handler.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/handlers/share_handlers.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/main.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/network_share_manager.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/server_runtime.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/services/__init__.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/services/audit_service.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/services/config_service.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/services/event_subscribers.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/services/favorites_service.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/services/network_share_service.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/services/policy_service.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/services/quota_service.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/services/share_service.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/services/tag_service.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/services/user_service.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/sql_identifiers.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/static/favicon.png +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/static/favicon.svg +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/static/img/logo-icon.png +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/static/img/logo-mark.svg +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/static/img/logo-text.png +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/static/img/logo.png +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/static/js/aird-core.js +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/static/js/bg-canvas.js +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/static/js/common/command-palette.js +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/static/js/components/folder-picker.js +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/static/js/feature-flags-live.js +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/static/js/login-ui.js +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/static/js/p2p/app.js +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/static/js/p2p/mediator.js +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/static/js/p2p/qr-adapter.js +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/static/js/p2p/signaling-service.js +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/static/js/p2p/state-machine.js +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/static/js/p2p/transfer-service.js +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/static/js/pages/super-search.js +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/static/js/theme.js +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/static/js/vendor/pdf.min.js +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/static/js/vendor/pdf.worker.min.js +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/static/js/vendor/qrcode-browser.js +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/_admin_tabs.html +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/_bg_canvas.html +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/_theme_early.html +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/admin.html +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/admin_audit.html +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/admin_ldap.html +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/admin_login.html +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/admin_network_shares.html +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/admin_policies.html +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/admin_tags.html +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/admin_user_attributes.html +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/admin_users.html +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/directory.html +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/edit.html +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/error.html +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/file.html +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/ldap_config_create.html +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/ldap_config_edit.html +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/login.html +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/profile.html +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/shared_list.html +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/super_search.html +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/token_verification.html +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/user_create.html +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/user_edit.html +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/utils/__init__.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird.egg-info/dependency_links.txt +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird.egg-info/requires.txt +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird.egg-info/top_level.txt +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/setup.cfg +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/__init__.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/conftest.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/handler_helpers.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_admin_handlers.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_api_handlers.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_architecture_conformance.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_auth_handlers.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_auth_handlers_extended.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_base_handler.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_base_handler_pep.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_cloud.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_config.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_core_file_operations.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_database_db.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_database_feature_flags.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_database_ldap.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_database_shares.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_database_users.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_database_users_hashing.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_db.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_filter_expression.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_main.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_mmap_handler.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_multi_user.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_network_shares.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_password_hashing.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_policy_service.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_rate_limit.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_security.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_security_comprehensive.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_server_runtime.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_share_handlers.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_share_ownership.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_super_search_handler.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_tag_service.py +0 -0
- {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_websocket_manager.py +0 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Authelia first/second factor login for CLI sessions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AutheliaError(RuntimeError):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _needs_second_factor(payload: dict[str, Any]) -> bool:
|
|
18
|
+
if payload.get("status") == "OK" and not payload.get("data"):
|
|
19
|
+
return False
|
|
20
|
+
data = payload.get("data") or {}
|
|
21
|
+
if data.get("methods"):
|
|
22
|
+
return True
|
|
23
|
+
if payload.get("status") in ("OK", "200"):
|
|
24
|
+
return bool(data.get("devices") or data.get("authentication_level") == 1)
|
|
25
|
+
return payload.get("status") in ("Unauthorized", "401")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def second_factor(session: requests.Session, authelia_base: str, totp: str) -> None:
|
|
29
|
+
base = authelia_base.rstrip("/")
|
|
30
|
+
token = totp.strip()
|
|
31
|
+
if not token:
|
|
32
|
+
raise AutheliaError("second_factor_required")
|
|
33
|
+
r = session.post(
|
|
34
|
+
f"{base}/api/secondfactor",
|
|
35
|
+
json={"token": token, "method": "totp"},
|
|
36
|
+
timeout=60,
|
|
37
|
+
)
|
|
38
|
+
if r.status_code == 401:
|
|
39
|
+
raise AutheliaError("Invalid one-time code (TOTP)")
|
|
40
|
+
if r.status_code >= 400:
|
|
41
|
+
raise AutheliaError(f"Authelia second factor failed (HTTP {r.status_code})")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def login(
|
|
45
|
+
session: requests.Session,
|
|
46
|
+
authelia_base: str,
|
|
47
|
+
username: str,
|
|
48
|
+
password: str,
|
|
49
|
+
*,
|
|
50
|
+
totp: str | None = None,
|
|
51
|
+
target_url: str | None = None,
|
|
52
|
+
) -> None:
|
|
53
|
+
"""
|
|
54
|
+
Complete Authelia login on *session* (sets Authelia cookies).
|
|
55
|
+
|
|
56
|
+
Raises AutheliaError on failure. Prompts are handled by the caller; pass *totp*
|
|
57
|
+
when second factor is required.
|
|
58
|
+
"""
|
|
59
|
+
base = authelia_base.rstrip("/")
|
|
60
|
+
body: dict[str, Any] = {
|
|
61
|
+
"username": username,
|
|
62
|
+
"password": password,
|
|
63
|
+
"keepMeLoggedIn": False,
|
|
64
|
+
}
|
|
65
|
+
if target_url:
|
|
66
|
+
body["targetURL"] = target_url
|
|
67
|
+
body["requestMethod"] = "GET"
|
|
68
|
+
|
|
69
|
+
r = session.post(f"{base}/api/firstfactor", json=body, timeout=60)
|
|
70
|
+
if r.status_code == 401:
|
|
71
|
+
raise AutheliaError("Authelia rejected username or password")
|
|
72
|
+
if r.status_code >= 400:
|
|
73
|
+
raise AutheliaError(f"Authelia first factor failed (HTTP {r.status_code})")
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
payload = r.json()
|
|
77
|
+
except ValueError as exc:
|
|
78
|
+
raise AutheliaError("Authelia returned a non-JSON response") from exc
|
|
79
|
+
|
|
80
|
+
if not _needs_second_factor(payload):
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
if totp:
|
|
84
|
+
second_factor(session, base, totp)
|
|
85
|
+
return
|
|
86
|
+
raise AutheliaError("second_factor_required")
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""CLI configuration and session file paths."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
CONFIG_DIR_ENV = "AIRD_CLI_CONFIG_DIR"
|
|
12
|
+
DEFAULT_DIR_NAME = "aird"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def config_dir() -> Path:
|
|
16
|
+
override = os.environ.get(CONFIG_DIR_ENV, "").strip()
|
|
17
|
+
if override:
|
|
18
|
+
return Path(override).expanduser()
|
|
19
|
+
if sys.platform == "win32":
|
|
20
|
+
base = os.environ.get("APPDATA") or str(Path.home())
|
|
21
|
+
return Path(base) / DEFAULT_DIR_NAME
|
|
22
|
+
xdg = os.environ.get("XDG_CONFIG_HOME")
|
|
23
|
+
if xdg:
|
|
24
|
+
return Path(xdg) / DEFAULT_DIR_NAME
|
|
25
|
+
return Path.home() / ".config" / DEFAULT_DIR_NAME
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def config_path() -> Path:
|
|
29
|
+
return config_dir() / "config.json"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def session_path() -> Path:
|
|
33
|
+
return config_dir() / "session.json"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def ensure_config_dir() -> Path:
|
|
37
|
+
d = config_dir()
|
|
38
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
return d
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def load_config() -> dict[str, Any]:
|
|
43
|
+
path = config_path()
|
|
44
|
+
if not path.is_file():
|
|
45
|
+
return {}
|
|
46
|
+
try:
|
|
47
|
+
with path.open(encoding="utf-8") as f:
|
|
48
|
+
data = json.load(f)
|
|
49
|
+
return data if isinstance(data, dict) else {}
|
|
50
|
+
except (OSError, json.JSONDecodeError):
|
|
51
|
+
return {}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def save_config(data: dict[str, Any]) -> None:
|
|
55
|
+
ensure_config_dir()
|
|
56
|
+
with config_path().open("w", encoding="utf-8") as f:
|
|
57
|
+
json.dump(data, f, indent=2)
|
|
58
|
+
f.write("\n")
|
|
59
|
+
try:
|
|
60
|
+
os.chmod(config_path(), 0o600)
|
|
61
|
+
except OSError:
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def get_server_url(cfg: dict[str, Any] | None = None) -> str | None:
|
|
66
|
+
cfg = cfg if cfg is not None else load_config()
|
|
67
|
+
url = (
|
|
68
|
+
os.environ.get("AIRD_SERVER", "").strip()
|
|
69
|
+
or str(cfg.get("server") or "").strip()
|
|
70
|
+
)
|
|
71
|
+
return url.rstrip("/") if url else None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def get_authelia_url(cfg: dict[str, Any] | None = None) -> str | None:
|
|
75
|
+
cfg = cfg if cfg is not None else load_config()
|
|
76
|
+
url = (
|
|
77
|
+
os.environ.get("AIRD_AUTHELIA_URL", "").strip()
|
|
78
|
+
or str(cfg.get("authelia_url") or "").strip()
|
|
79
|
+
)
|
|
80
|
+
return url.rstrip("/") if url else None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def get_parallel_jobs(cfg: dict[str, Any] | None = None, default: int = 2) -> int:
|
|
84
|
+
cfg = cfg if cfg is not None else load_config()
|
|
85
|
+
raw = (
|
|
86
|
+
os.environ.get("AIRD_PARALLEL_JOBS", "").strip()
|
|
87
|
+
or cfg.get("parallel_uploads")
|
|
88
|
+
or cfg.get("parallel_downloads")
|
|
89
|
+
)
|
|
90
|
+
try:
|
|
91
|
+
n = int(raw)
|
|
92
|
+
return max(1, n) if n > 0 else default
|
|
93
|
+
except (TypeError, ValueError):
|
|
94
|
+
return default
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
"""aird-cli entry point."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import getpass
|
|
7
|
+
import logging
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from aird.cli import __version__
|
|
12
|
+
from aird.cli.authelia import AutheliaError, login as authelia_login, second_factor
|
|
13
|
+
from aird.cli.config import (
|
|
14
|
+
get_authelia_url,
|
|
15
|
+
get_parallel_jobs,
|
|
16
|
+
get_server_url,
|
|
17
|
+
load_config,
|
|
18
|
+
save_config,
|
|
19
|
+
)
|
|
20
|
+
from aird.cli.session import AirdAPIError, AirdAuthError, AirdClient
|
|
21
|
+
|
|
22
|
+
log = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _prompt_password(prompt: str = "Password: ") -> str:
|
|
26
|
+
return getpass.getpass(prompt)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _prompt_totp() -> str:
|
|
30
|
+
return input("One-time code (TOTP): ").strip()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def cmd_config_set(args: argparse.Namespace) -> int:
|
|
34
|
+
cfg = load_config()
|
|
35
|
+
cfg[args.key] = args.value
|
|
36
|
+
save_config(cfg)
|
|
37
|
+
print(f"Set {args.key} = {args.value}")
|
|
38
|
+
return 0
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def cmd_config_show(_args: argparse.Namespace) -> int:
|
|
42
|
+
cfg = load_config()
|
|
43
|
+
if not cfg:
|
|
44
|
+
print("(no config — use: aird-cli config set server https://host)")
|
|
45
|
+
return 0
|
|
46
|
+
for k, v in sorted(cfg.items()):
|
|
47
|
+
if k == "password":
|
|
48
|
+
print(f"{k}: ***")
|
|
49
|
+
else:
|
|
50
|
+
print(f"{k}: {v}")
|
|
51
|
+
return 0
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def cmd_login(args: argparse.Namespace) -> int:
|
|
55
|
+
server = get_server_url()
|
|
56
|
+
if not server and args.server:
|
|
57
|
+
cfg = load_config()
|
|
58
|
+
cfg["server"] = args.server.rstrip("/")
|
|
59
|
+
save_config(cfg)
|
|
60
|
+
server = cfg["server"]
|
|
61
|
+
if not server:
|
|
62
|
+
print("Set server URL first: aird-cli config set server https://your-host", file=sys.stderr)
|
|
63
|
+
return 1
|
|
64
|
+
|
|
65
|
+
client = AirdClient(server)
|
|
66
|
+
authelia_url = get_authelia_url() or args.authelia_url
|
|
67
|
+
|
|
68
|
+
username = args.username or load_config().get("username") or input("Username: ").strip()
|
|
69
|
+
if not username:
|
|
70
|
+
print("Username required", file=sys.stderr)
|
|
71
|
+
return 1
|
|
72
|
+
|
|
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
|
+
password = args.password or _prompt_password()
|
|
86
|
+
totp = args.totp
|
|
87
|
+
|
|
88
|
+
if authelia_url:
|
|
89
|
+
try:
|
|
90
|
+
authelia_login(
|
|
91
|
+
client.http,
|
|
92
|
+
authelia_url,
|
|
93
|
+
username,
|
|
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
|
|
109
|
+
|
|
110
|
+
aird_token = args.aird_token
|
|
111
|
+
if aird_token:
|
|
112
|
+
client.login_password("", "", token=aird_token)
|
|
113
|
+
else:
|
|
114
|
+
client.login_password(username, password)
|
|
115
|
+
|
|
116
|
+
cfg = load_config()
|
|
117
|
+
cfg["username"] = username
|
|
118
|
+
cfg["server"] = server
|
|
119
|
+
if authelia_url:
|
|
120
|
+
cfg["authelia_url"] = authelia_url
|
|
121
|
+
save_config(cfg)
|
|
122
|
+
client.save()
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
client.check_auth()
|
|
126
|
+
except (AirdAuthError, AirdAPIError) as exc:
|
|
127
|
+
print(f"Login failed: {exc}", file=sys.stderr)
|
|
128
|
+
return 1
|
|
129
|
+
|
|
130
|
+
print(f"Logged in to {server} as {username}")
|
|
131
|
+
return 0
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def cmd_logout(_args: argparse.Namespace) -> int:
|
|
135
|
+
try:
|
|
136
|
+
client = AirdClient()
|
|
137
|
+
client.clear_session()
|
|
138
|
+
except AirdAuthError:
|
|
139
|
+
pass
|
|
140
|
+
print("Session cleared")
|
|
141
|
+
return 0
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _default_jobs(args: argparse.Namespace) -> int:
|
|
145
|
+
jobs = getattr(args, "jobs", None)
|
|
146
|
+
return jobs if jobs is not None else get_parallel_jobs()
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def cmd_whoami(_args: argparse.Namespace) -> int:
|
|
150
|
+
try:
|
|
151
|
+
client = AirdClient()
|
|
152
|
+
client.check_auth()
|
|
153
|
+
username = client.http.cookies.get("user") or load_config().get("username") or "?"
|
|
154
|
+
print(f"OK — {username} @ {client.server}")
|
|
155
|
+
return 0
|
|
156
|
+
except AirdAuthError as exc:
|
|
157
|
+
print(str(exc), file=sys.stderr)
|
|
158
|
+
return 1
|
|
159
|
+
except AirdAPIError as exc:
|
|
160
|
+
print(str(exc), file=sys.stderr)
|
|
161
|
+
return 1
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def cmd_ls(args: argparse.Namespace) -> int:
|
|
165
|
+
try:
|
|
166
|
+
client = AirdClient()
|
|
167
|
+
entries = client.list_dir(args.path or "")
|
|
168
|
+
if not entries:
|
|
169
|
+
print("(empty)")
|
|
170
|
+
return 0
|
|
171
|
+
for e in entries:
|
|
172
|
+
name = e.get("name", "?")
|
|
173
|
+
if e.get("is_dir"):
|
|
174
|
+
print(f"{name}/")
|
|
175
|
+
else:
|
|
176
|
+
size = e.get("size_bytes") or e.get("size") or 0
|
|
177
|
+
print(f"{name}\t{size}")
|
|
178
|
+
return 0
|
|
179
|
+
except (AirdAuthError, AirdAPIError) as exc:
|
|
180
|
+
print(str(exc), file=sys.stderr)
|
|
181
|
+
return 1
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def cmd_download(args: argparse.Namespace) -> int:
|
|
185
|
+
try:
|
|
186
|
+
client = AirdClient()
|
|
187
|
+
remote = (args.remote or "").strip("/")
|
|
188
|
+
local = Path(args.output or ".").resolve()
|
|
189
|
+
|
|
190
|
+
if args.recursive:
|
|
191
|
+
base_name = Path(remote).name if remote else "files"
|
|
192
|
+
dest = (local / base_name) if local.is_dir() else local
|
|
193
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
194
|
+
|
|
195
|
+
def on_progress(p: str) -> None:
|
|
196
|
+
if args.verbose:
|
|
197
|
+
print(p)
|
|
198
|
+
|
|
199
|
+
n = client.download_tree(
|
|
200
|
+
remote,
|
|
201
|
+
dest,
|
|
202
|
+
workers=_default_jobs(args),
|
|
203
|
+
on_progress=on_progress if args.verbose else None,
|
|
204
|
+
)
|
|
205
|
+
print(f"Downloaded {n} file(s) to {dest}")
|
|
206
|
+
return 0
|
|
207
|
+
|
|
208
|
+
if not remote:
|
|
209
|
+
print("Remote path required", file=sys.stderr)
|
|
210
|
+
return 1
|
|
211
|
+
dest = local
|
|
212
|
+
if dest.is_dir():
|
|
213
|
+
dest = dest / Path(remote).name
|
|
214
|
+
client.download_file(remote, dest)
|
|
215
|
+
print(f"Saved {dest}")
|
|
216
|
+
return 0
|
|
217
|
+
except (AirdAuthError, AirdAPIError, OSError) as exc:
|
|
218
|
+
print(str(exc), file=sys.stderr)
|
|
219
|
+
return 1
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def cmd_upload(args: argparse.Namespace) -> int:
|
|
223
|
+
try:
|
|
224
|
+
client = AirdClient()
|
|
225
|
+
local = Path(args.local).resolve()
|
|
226
|
+
remote_dir = (args.remote or "").strip("/")
|
|
227
|
+
|
|
228
|
+
def on_progress(p: str) -> None:
|
|
229
|
+
if args.verbose:
|
|
230
|
+
print(p)
|
|
231
|
+
|
|
232
|
+
if local.is_dir():
|
|
233
|
+
n = client.upload_tree(
|
|
234
|
+
local,
|
|
235
|
+
remote_dir,
|
|
236
|
+
workers=_default_jobs(args),
|
|
237
|
+
on_progress=on_progress if args.verbose else None,
|
|
238
|
+
)
|
|
239
|
+
print(f"Uploaded {n} file(s) to /{remote_dir or ''}")
|
|
240
|
+
elif local.is_file():
|
|
241
|
+
client.upload_file(local, remote_dir)
|
|
242
|
+
print(f"Uploaded {local.name} to /{remote_dir or ''}")
|
|
243
|
+
n = 1
|
|
244
|
+
else:
|
|
245
|
+
print(f"Not found: {local}", file=sys.stderr)
|
|
246
|
+
return 1
|
|
247
|
+
return 0
|
|
248
|
+
except (AirdAuthError, AirdAPIError, OSError) as exc:
|
|
249
|
+
print(str(exc), file=sys.stderr)
|
|
250
|
+
return 1
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def cmd_shares_list(_args: argparse.Namespace) -> int:
|
|
254
|
+
try:
|
|
255
|
+
client = AirdClient()
|
|
256
|
+
data = client.list_shares()
|
|
257
|
+
mine = data.get("shares") or []
|
|
258
|
+
shared = data.get("shared_with_me") or []
|
|
259
|
+
if not mine and not shared:
|
|
260
|
+
print("(no shares)")
|
|
261
|
+
return 0
|
|
262
|
+
if mine:
|
|
263
|
+
print("My shares:")
|
|
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', '')}")
|
|
269
|
+
if shared:
|
|
270
|
+
print("Shared with me:")
|
|
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)")
|
|
276
|
+
return 0
|
|
277
|
+
except (AirdAuthError, AirdAPIError) as exc:
|
|
278
|
+
print(str(exc), file=sys.stderr)
|
|
279
|
+
return 1
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def cmd_shares_download(args: argparse.Namespace) -> int:
|
|
283
|
+
try:
|
|
284
|
+
client = AirdClient()
|
|
285
|
+
dest = Path(args.output or ".").resolve() / args.share_id
|
|
286
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
287
|
+
token = args.token
|
|
288
|
+
|
|
289
|
+
def on_progress(p: str) -> None:
|
|
290
|
+
if args.verbose:
|
|
291
|
+
print(p)
|
|
292
|
+
|
|
293
|
+
n = client.download_share(
|
|
294
|
+
args.share_id,
|
|
295
|
+
dest,
|
|
296
|
+
share_token=token,
|
|
297
|
+
workers=_default_jobs(args),
|
|
298
|
+
on_progress=on_progress if args.verbose else None,
|
|
299
|
+
)
|
|
300
|
+
print(f"Downloaded {n} file(s) from share {args.share_id} to {dest}")
|
|
301
|
+
return 0
|
|
302
|
+
except (AirdAuthError, AirdAPIError, OSError) as exc:
|
|
303
|
+
print(str(exc), file=sys.stderr)
|
|
304
|
+
return 1
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def cmd_shares_download_all(args: argparse.Namespace) -> int:
|
|
308
|
+
try:
|
|
309
|
+
client = AirdClient()
|
|
310
|
+
dest = Path(args.output or ".").resolve()
|
|
311
|
+
|
|
312
|
+
def on_progress(p: str) -> None:
|
|
313
|
+
if args.verbose:
|
|
314
|
+
print(p)
|
|
315
|
+
|
|
316
|
+
n = client.download_all_shares(
|
|
317
|
+
dest,
|
|
318
|
+
workers=_default_jobs(args),
|
|
319
|
+
on_progress=on_progress if args.verbose else None,
|
|
320
|
+
)
|
|
321
|
+
print(f"Downloaded {n} file(s) from all shares to {dest}")
|
|
322
|
+
return 0
|
|
323
|
+
except (AirdAuthError, AirdAPIError, OSError) as exc:
|
|
324
|
+
print(str(exc), file=sys.stderr)
|
|
325
|
+
return 1
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
329
|
+
p = argparse.ArgumentParser(
|
|
330
|
+
prog="aird-cli",
|
|
331
|
+
description="Aird CLI — upload, download, and shares (session saved after login)",
|
|
332
|
+
)
|
|
333
|
+
p.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
334
|
+
p.add_argument("-v", "--verbose", action="store_true", help="Progress per file")
|
|
335
|
+
sub = p.add_subparsers(dest="command", required=True)
|
|
336
|
+
|
|
337
|
+
cfg = sub.add_parser("config", help="Manage CLI config")
|
|
338
|
+
cfg_sub = cfg.add_subparsers(dest="config_cmd", required=True)
|
|
339
|
+
cs = cfg_sub.add_parser("set", help="Set config key")
|
|
340
|
+
cs.add_argument(
|
|
341
|
+
"key",
|
|
342
|
+
choices=["server", "authelia_url", "username", "parallel_uploads", "parallel_downloads"],
|
|
343
|
+
)
|
|
344
|
+
cs.add_argument("value")
|
|
345
|
+
cs.set_defaults(func=cmd_config_set)
|
|
346
|
+
cfg_sub.add_parser("show", help="Show config").set_defaults(func=cmd_config_show)
|
|
347
|
+
|
|
348
|
+
login = sub.add_parser("login", help="Authenticate (Authelia + Aird); session persisted")
|
|
349
|
+
login.add_argument("--server", help="Aird base URL")
|
|
350
|
+
login.add_argument("--authelia-url", dest="authelia_url", help="Authelia portal URL")
|
|
351
|
+
login.add_argument("-u", "--username")
|
|
352
|
+
login.add_argument("-p", "--password", help="Password (avoid on shell history)")
|
|
353
|
+
login.add_argument("--totp", help="Authelia TOTP code")
|
|
354
|
+
login.add_argument(
|
|
355
|
+
"--token",
|
|
356
|
+
help="Aird ACCESS_TOKEN (Bearer); skips username/password",
|
|
357
|
+
)
|
|
358
|
+
login.add_argument(
|
|
359
|
+
"--aird-token",
|
|
360
|
+
dest="aird_token",
|
|
361
|
+
help="Aird login form token field (not Bearer)",
|
|
362
|
+
)
|
|
363
|
+
login.set_defaults(func=cmd_login)
|
|
364
|
+
|
|
365
|
+
sub.add_parser("logout", help="Clear saved session").set_defaults(func=cmd_logout)
|
|
366
|
+
sub.add_parser("whoami", help="Check current session").set_defaults(func=cmd_whoami)
|
|
367
|
+
|
|
368
|
+
ls = sub.add_parser("ls", help="List remote directory")
|
|
369
|
+
ls.add_argument("path", nargs="?", default="", help="Remote path under your files root")
|
|
370
|
+
ls.set_defaults(func=cmd_ls)
|
|
371
|
+
|
|
372
|
+
dl = sub.add_parser("download", help="Download file or folder (no zip)")
|
|
373
|
+
dl.add_argument("remote", help="Remote path")
|
|
374
|
+
dl.add_argument("-o", "--output", help="Local file or directory")
|
|
375
|
+
dl.add_argument("-r", "--recursive", action="store_true", help="Download folder tree")
|
|
376
|
+
dl.add_argument("-j", "--jobs", type=int, default=None, help="Parallel downloads (default from config)")
|
|
377
|
+
dl.set_defaults(func=cmd_download)
|
|
378
|
+
|
|
379
|
+
up = sub.add_parser("upload", help="Upload file or folder")
|
|
380
|
+
up.add_argument("local", help="Local file or directory")
|
|
381
|
+
up.add_argument("remote", nargs="?", default="", help="Remote destination directory")
|
|
382
|
+
up.add_argument("-j", "--jobs", type=int, default=None, help="Parallel file uploads (default from config)")
|
|
383
|
+
up.set_defaults(func=cmd_upload)
|
|
384
|
+
|
|
385
|
+
shares = sub.add_parser("shares", help="Share operations")
|
|
386
|
+
sh_sub = shares.add_subparsers(dest="shares_cmd", required=True)
|
|
387
|
+
sh_sub.add_parser("list", help="List shares").set_defaults(func=cmd_shares_list)
|
|
388
|
+
sh_dl = sh_sub.add_parser("download", help="Download all files in a share")
|
|
389
|
+
sh_dl.add_argument("share_id")
|
|
390
|
+
sh_dl.add_argument("-o", "--output", help="Local parent directory")
|
|
391
|
+
sh_dl.add_argument("--token", help="Share access token if required")
|
|
392
|
+
sh_dl.add_argument("-j", "--jobs", type=int, default=None)
|
|
393
|
+
sh_dl.set_defaults(func=cmd_shares_download)
|
|
394
|
+
sh_all = sh_sub.add_parser("download-all", help="Download every share you can access")
|
|
395
|
+
sh_all.add_argument("-o", "--output", help="Local parent directory")
|
|
396
|
+
sh_all.add_argument("-j", "--jobs", type=int, default=None)
|
|
397
|
+
sh_all.set_defaults(func=cmd_shares_download_all)
|
|
398
|
+
|
|
399
|
+
return p
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def main(argv: list[str] | None = None) -> int:
|
|
403
|
+
logging.basicConfig(level=logging.WARNING)
|
|
404
|
+
parser = build_parser()
|
|
405
|
+
args = parser.parse_args(argv)
|
|
406
|
+
return int(args.func(args))
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
if __name__ == "__main__":
|
|
410
|
+
sys.exit(main())
|