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.
Files changed (190) hide show
  1. aird-0.4.23.dev11/MANIFEST.in +6 -0
  2. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/PKG-INFO +1 -1
  3. aird-0.4.23.dev11/aird/cli/__init__.py +3 -0
  4. aird-0.4.23.dev11/aird/cli/__main__.py +5 -0
  5. aird-0.4.23.dev11/aird/cli/authelia.py +86 -0
  6. aird-0.4.23.dev11/aird/cli/config.py +94 -0
  7. aird-0.4.23.dev11/aird/cli/main.py +410 -0
  8. aird-0.4.23.dev11/aird/cli/session.py +571 -0
  9. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/constants/media.py +17 -0
  10. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/handlers/base_handler.py +3 -1
  11. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/handlers/file_op_handlers.py +142 -24
  12. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/handlers/p2p_handlers.py +59 -14
  13. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/handlers/view_handlers.py +39 -2
  14. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/services/p2p_service.py +1 -1
  15. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/static/css/app.css +1 -1
  16. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/static/js/browse/app.js +21 -12
  17. aird-0.4.23.dev11/aird/static/js/media-view.js +94 -0
  18. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/static/js/pages/p2p-page.js +16 -6
  19. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/static/js/share/app.js +4 -2
  20. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/_app_nav_header.html +3 -3
  21. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/_theme_login_corner.html +14 -1
  22. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/browse.html +4 -7
  23. aird-0.4.23.dev11/aird/templates/media_view.html +108 -0
  24. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/p2p_transfer.html +15 -3
  25. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/share.html +3 -3
  26. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/tagged_files.html +4 -4
  27. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/utils/util.py +22 -0
  28. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird.egg-info/PKG-INFO +1 -1
  29. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird.egg-info/SOURCES.txt +12 -1
  30. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird.egg-info/entry_points.txt +1 -0
  31. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/setup.py +4 -1
  32. aird-0.4.23.dev11/tests/test_cli.py +51 -0
  33. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_file_op_handlers.py +21 -4
  34. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_p2p_handlers.py +29 -9
  35. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_util.py +16 -0
  36. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_view_handlers.py +60 -0
  37. aird-0.4.23.dev11/tests/test_wheel_static_assets.py +48 -0
  38. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/LICENSE +0 -0
  39. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/README.md +0 -0
  40. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/__init__.py +0 -0
  41. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/__main__.py +0 -0
  42. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/app_context.py +0 -0
  43. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/cloud/__init__.py +0 -0
  44. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/config.py +0 -0
  45. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/constants/__init__.py +0 -0
  46. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/constants/admin.py +0 -0
  47. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/constants/file_ops.py +0 -0
  48. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/constants/input_limits.py +0 -0
  49. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/core/__init__.py +0 -0
  50. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/core/events.py +0 -0
  51. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/core/file_operations.py +0 -0
  52. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/core/filter_expression.py +0 -0
  53. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/core/input_validation.py +0 -0
  54. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/core/mmap_handler.py +0 -0
  55. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/core/security.py +0 -0
  56. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/core/share_root.py +0 -0
  57. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/core/websocket_manager.py +0 -0
  58. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/database/__init__.py +0 -0
  59. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/database/db.py +0 -0
  60. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/database/feature_flags.py +0 -0
  61. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/database/ldap.py +0 -0
  62. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/db/__init__.py +0 -0
  63. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/db/audit.py +0 -0
  64. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/db/config.py +0 -0
  65. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/db/favorites.py +0 -0
  66. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/db/network_shares.py +0 -0
  67. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/db/policies.py +0 -0
  68. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/db/policy_decisions.py +0 -0
  69. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/db/policy_seeds.py +0 -0
  70. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/db/quota.py +0 -0
  71. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/db/resource_tags.py +0 -0
  72. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/db/schema.py +0 -0
  73. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/db/shares.py +0 -0
  74. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/db/user_attributes.py +0 -0
  75. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/db/users.py +0 -0
  76. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/domain/__init__.py +0 -0
  77. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/domain/contracts.py +0 -0
  78. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/domain/models.py +0 -0
  79. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/handlers/__init__.py +0 -0
  80. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/handlers/abac_handlers.py +0 -0
  81. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/handlers/admin_handlers.py +0 -0
  82. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/handlers/api_handlers.py +0 -0
  83. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/handlers/auth_handlers.py +0 -0
  84. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/handlers/constants.py +0 -0
  85. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/handlers/health_handler.py +0 -0
  86. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/handlers/share_handlers.py +0 -0
  87. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/main.py +0 -0
  88. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/network_share_manager.py +0 -0
  89. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/server_runtime.py +0 -0
  90. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/services/__init__.py +0 -0
  91. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/services/audit_service.py +0 -0
  92. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/services/config_service.py +0 -0
  93. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/services/event_subscribers.py +0 -0
  94. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/services/favorites_service.py +0 -0
  95. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/services/network_share_service.py +0 -0
  96. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/services/policy_service.py +0 -0
  97. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/services/quota_service.py +0 -0
  98. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/services/share_service.py +0 -0
  99. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/services/tag_service.py +0 -0
  100. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/services/user_service.py +0 -0
  101. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/sql_identifiers.py +0 -0
  102. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/static/favicon.png +0 -0
  103. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/static/favicon.svg +0 -0
  104. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/static/img/logo-icon.png +0 -0
  105. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/static/img/logo-mark.svg +0 -0
  106. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/static/img/logo-text.png +0 -0
  107. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/static/img/logo.png +0 -0
  108. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/static/js/aird-core.js +0 -0
  109. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/static/js/bg-canvas.js +0 -0
  110. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/static/js/common/command-palette.js +0 -0
  111. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/static/js/components/folder-picker.js +0 -0
  112. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/static/js/feature-flags-live.js +0 -0
  113. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/static/js/login-ui.js +0 -0
  114. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/static/js/p2p/app.js +0 -0
  115. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/static/js/p2p/mediator.js +0 -0
  116. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/static/js/p2p/qr-adapter.js +0 -0
  117. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/static/js/p2p/signaling-service.js +0 -0
  118. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/static/js/p2p/state-machine.js +0 -0
  119. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/static/js/p2p/transfer-service.js +0 -0
  120. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/static/js/pages/super-search.js +0 -0
  121. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/static/js/theme.js +0 -0
  122. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/static/js/vendor/pdf.min.js +0 -0
  123. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/static/js/vendor/pdf.worker.min.js +0 -0
  124. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/static/js/vendor/qrcode-browser.js +0 -0
  125. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/_admin_tabs.html +0 -0
  126. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/_bg_canvas.html +0 -0
  127. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/_theme_early.html +0 -0
  128. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/admin.html +0 -0
  129. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/admin_audit.html +0 -0
  130. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/admin_ldap.html +0 -0
  131. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/admin_login.html +0 -0
  132. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/admin_network_shares.html +0 -0
  133. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/admin_policies.html +0 -0
  134. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/admin_tags.html +0 -0
  135. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/admin_user_attributes.html +0 -0
  136. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/admin_users.html +0 -0
  137. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/directory.html +0 -0
  138. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/edit.html +0 -0
  139. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/error.html +0 -0
  140. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/file.html +0 -0
  141. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/ldap_config_create.html +0 -0
  142. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/ldap_config_edit.html +0 -0
  143. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/login.html +0 -0
  144. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/profile.html +0 -0
  145. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/shared_list.html +0 -0
  146. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/super_search.html +0 -0
  147. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/token_verification.html +0 -0
  148. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/user_create.html +0 -0
  149. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/templates/user_edit.html +0 -0
  150. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird/utils/__init__.py +0 -0
  151. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird.egg-info/dependency_links.txt +0 -0
  152. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird.egg-info/requires.txt +0 -0
  153. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/aird.egg-info/top_level.txt +0 -0
  154. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/setup.cfg +0 -0
  155. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/__init__.py +0 -0
  156. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/conftest.py +0 -0
  157. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/handler_helpers.py +0 -0
  158. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_admin_handlers.py +0 -0
  159. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_api_handlers.py +0 -0
  160. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_architecture_conformance.py +0 -0
  161. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_auth_handlers.py +0 -0
  162. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_auth_handlers_extended.py +0 -0
  163. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_base_handler.py +0 -0
  164. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_base_handler_pep.py +0 -0
  165. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_cloud.py +0 -0
  166. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_config.py +0 -0
  167. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_core_file_operations.py +0 -0
  168. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_database_db.py +0 -0
  169. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_database_feature_flags.py +0 -0
  170. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_database_ldap.py +0 -0
  171. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_database_shares.py +0 -0
  172. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_database_users.py +0 -0
  173. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_database_users_hashing.py +0 -0
  174. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_db.py +0 -0
  175. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_filter_expression.py +0 -0
  176. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_main.py +0 -0
  177. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_mmap_handler.py +0 -0
  178. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_multi_user.py +0 -0
  179. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_network_shares.py +0 -0
  180. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_password_hashing.py +0 -0
  181. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_policy_service.py +0 -0
  182. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_rate_limit.py +0 -0
  183. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_security.py +0 -0
  184. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_security_comprehensive.py +0 -0
  185. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_server_runtime.py +0 -0
  186. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_share_handlers.py +0 -0
  187. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_share_ownership.py +0 -0
  188. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_super_search_handler.py +0 -0
  189. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_tag_service.py +0 -0
  190. {aird-0.4.23.dev3 → aird-0.4.23.dev11}/tests/test_websocket_manager.py +0 -0
@@ -0,0 +1,6 @@
1
+ recursive-include aird/templates *.html
2
+ recursive-include aird/static/css *.css
3
+ recursive-include aird/static/js *.js
4
+ recursive-include aird/static/img *
5
+ include aird/static/favicon.png
6
+ include aird/static/favicon.ico
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aird
3
- Version: 0.4.23.dev3
3
+ Version: 0.4.23.dev11
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
@@ -0,0 +1,3 @@
1
+ """Aird command-line client for upload, download, and shares."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,5 @@
1
+ import sys
2
+
3
+ from aird.cli.main import main
4
+
5
+ sys.exit(main())
@@ -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())