aird 0.4.23.dev11__tar.gz → 0.4.23.dev12__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 (208) hide show
  1. {aird-0.4.23.dev11/aird.egg-info → aird-0.4.23.dev12}/PKG-INFO +1 -1
  2. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/cli/session.py +10 -71
  3. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/config.py +30 -4
  4. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/constants/__init__.py +3 -5
  5. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/core/events.py +2 -0
  6. aird-0.4.23.dev12/aird/core/folder_size.py +51 -0
  7. aird-0.4.23.dev12/aird/core/zip_download.py +116 -0
  8. aird-0.4.23.dev12/aird/email/__init__.py +6 -0
  9. aird-0.4.23.dev12/aird/email/brevo.py +83 -0
  10. aird-0.4.23.dev12/aird/email/resolve.py +36 -0
  11. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/handlers/api_handlers.py +11 -5
  12. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/handlers/file_op_handlers.py +140 -466
  13. aird-0.4.23.dev12/aird/handlers/folder_size_ws_handlers.py +193 -0
  14. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/handlers/share_handlers.py +6 -2
  15. aird-0.4.23.dev12/aird/handlers/transfer_ws_handlers.py +364 -0
  16. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/handlers/view_handlers.py +0 -2
  17. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/main.py +14 -0
  18. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/services/__init__.py +4 -0
  19. aird-0.4.23.dev12/aird/services/email_service.py +97 -0
  20. aird-0.4.23.dev12/aird/services/email_subscriber.py +52 -0
  21. aird-0.4.23.dev12/aird/static/css/app.css +2 -0
  22. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/static/js/browse/app.js +458 -252
  23. aird-0.4.23.dev12/aird/static/js/download-manager.js +309 -0
  24. aird-0.4.23.dev12/aird/static/js/feature-flags-live.js +44 -0
  25. aird-0.4.23.dev12/aird/static/js/file-transfer-ws.js +223 -0
  26. aird-0.4.23.dev12/aird/static/js/folder-size-scan.js +115 -0
  27. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/static/js/share/app.js +46 -12
  28. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/templates/browse.html +33 -54
  29. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/templates/share.html +52 -37
  30. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/templates/shared_list.html +23 -6
  31. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/templates/tagged_files.html +2 -0
  32. {aird-0.4.23.dev11 → aird-0.4.23.dev12/aird.egg-info}/PKG-INFO +1 -1
  33. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird.egg-info/SOURCES.txt +17 -1
  34. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/setup.py +1 -1
  35. aird-0.4.23.dev12/tests/test_email_service.py +76 -0
  36. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/tests/test_file_op_handlers.py +7 -183
  37. aird-0.4.23.dev12/tests/test_folder_size.py +39 -0
  38. aird-0.4.23.dev12/tests/test_transfer_ws_handlers.py +101 -0
  39. aird-0.4.23.dev12/tests/test_zip_download.py +43 -0
  40. aird-0.4.23.dev11/aird/static/css/app.css +0 -2
  41. aird-0.4.23.dev11/aird/static/js/feature-flags-live.js +0 -43
  42. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/LICENSE +0 -0
  43. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/MANIFEST.in +0 -0
  44. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/README.md +0 -0
  45. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/__init__.py +0 -0
  46. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/__main__.py +0 -0
  47. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/app_context.py +0 -0
  48. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/cli/__init__.py +0 -0
  49. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/cli/__main__.py +0 -0
  50. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/cli/authelia.py +0 -0
  51. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/cli/config.py +0 -0
  52. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/cli/main.py +0 -0
  53. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/cloud/__init__.py +0 -0
  54. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/constants/admin.py +0 -0
  55. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/constants/file_ops.py +0 -0
  56. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/constants/input_limits.py +0 -0
  57. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/constants/media.py +0 -0
  58. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/core/__init__.py +0 -0
  59. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/core/file_operations.py +0 -0
  60. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/core/filter_expression.py +0 -0
  61. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/core/input_validation.py +0 -0
  62. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/core/mmap_handler.py +0 -0
  63. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/core/security.py +0 -0
  64. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/core/share_root.py +0 -0
  65. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/core/websocket_manager.py +0 -0
  66. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/database/__init__.py +0 -0
  67. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/database/db.py +0 -0
  68. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/database/feature_flags.py +0 -0
  69. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/database/ldap.py +0 -0
  70. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/db/__init__.py +0 -0
  71. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/db/audit.py +0 -0
  72. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/db/config.py +0 -0
  73. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/db/favorites.py +0 -0
  74. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/db/network_shares.py +0 -0
  75. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/db/policies.py +0 -0
  76. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/db/policy_decisions.py +0 -0
  77. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/db/policy_seeds.py +0 -0
  78. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/db/quota.py +0 -0
  79. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/db/resource_tags.py +0 -0
  80. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/db/schema.py +0 -0
  81. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/db/shares.py +0 -0
  82. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/db/user_attributes.py +0 -0
  83. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/db/users.py +0 -0
  84. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/domain/__init__.py +0 -0
  85. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/domain/contracts.py +0 -0
  86. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/domain/models.py +0 -0
  87. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/handlers/__init__.py +0 -0
  88. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/handlers/abac_handlers.py +0 -0
  89. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/handlers/admin_handlers.py +0 -0
  90. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/handlers/auth_handlers.py +0 -0
  91. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/handlers/base_handler.py +0 -0
  92. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/handlers/constants.py +0 -0
  93. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/handlers/health_handler.py +0 -0
  94. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/handlers/p2p_handlers.py +0 -0
  95. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/network_share_manager.py +0 -0
  96. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/server_runtime.py +0 -0
  97. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/services/audit_service.py +0 -0
  98. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/services/config_service.py +0 -0
  99. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/services/event_subscribers.py +0 -0
  100. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/services/favorites_service.py +0 -0
  101. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/services/network_share_service.py +0 -0
  102. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/services/p2p_service.py +0 -0
  103. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/services/policy_service.py +0 -0
  104. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/services/quota_service.py +0 -0
  105. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/services/share_service.py +0 -0
  106. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/services/tag_service.py +0 -0
  107. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/services/user_service.py +0 -0
  108. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/sql_identifiers.py +0 -0
  109. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/static/favicon.png +0 -0
  110. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/static/favicon.svg +0 -0
  111. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/static/img/logo-icon.png +0 -0
  112. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/static/img/logo-mark.svg +0 -0
  113. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/static/img/logo-text.png +0 -0
  114. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/static/img/logo.png +0 -0
  115. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/static/js/aird-core.js +0 -0
  116. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/static/js/bg-canvas.js +0 -0
  117. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/static/js/common/command-palette.js +0 -0
  118. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/static/js/components/folder-picker.js +0 -0
  119. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/static/js/login-ui.js +0 -0
  120. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/static/js/media-view.js +0 -0
  121. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/static/js/p2p/app.js +0 -0
  122. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/static/js/p2p/mediator.js +0 -0
  123. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/static/js/p2p/qr-adapter.js +0 -0
  124. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/static/js/p2p/signaling-service.js +0 -0
  125. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/static/js/p2p/state-machine.js +0 -0
  126. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/static/js/p2p/transfer-service.js +0 -0
  127. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/static/js/pages/p2p-page.js +0 -0
  128. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/static/js/pages/super-search.js +0 -0
  129. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/static/js/theme.js +0 -0
  130. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/static/js/vendor/pdf.min.js +0 -0
  131. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/static/js/vendor/pdf.worker.min.js +0 -0
  132. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/static/js/vendor/qrcode-browser.js +0 -0
  133. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/templates/_admin_tabs.html +0 -0
  134. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/templates/_app_nav_header.html +0 -0
  135. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/templates/_bg_canvas.html +0 -0
  136. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/templates/_theme_early.html +0 -0
  137. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/templates/_theme_login_corner.html +0 -0
  138. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/templates/admin.html +0 -0
  139. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/templates/admin_audit.html +0 -0
  140. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/templates/admin_ldap.html +0 -0
  141. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/templates/admin_login.html +0 -0
  142. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/templates/admin_network_shares.html +0 -0
  143. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/templates/admin_policies.html +0 -0
  144. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/templates/admin_tags.html +0 -0
  145. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/templates/admin_user_attributes.html +0 -0
  146. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/templates/admin_users.html +0 -0
  147. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/templates/directory.html +0 -0
  148. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/templates/edit.html +0 -0
  149. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/templates/error.html +0 -0
  150. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/templates/file.html +0 -0
  151. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/templates/ldap_config_create.html +0 -0
  152. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/templates/ldap_config_edit.html +0 -0
  153. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/templates/login.html +0 -0
  154. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/templates/media_view.html +0 -0
  155. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/templates/p2p_transfer.html +0 -0
  156. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/templates/profile.html +0 -0
  157. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/templates/super_search.html +0 -0
  158. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/templates/token_verification.html +0 -0
  159. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/templates/user_create.html +0 -0
  160. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/templates/user_edit.html +0 -0
  161. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/utils/__init__.py +0 -0
  162. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird/utils/util.py +0 -0
  163. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird.egg-info/dependency_links.txt +0 -0
  164. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird.egg-info/entry_points.txt +0 -0
  165. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird.egg-info/requires.txt +0 -0
  166. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/aird.egg-info/top_level.txt +0 -0
  167. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/setup.cfg +0 -0
  168. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/tests/__init__.py +0 -0
  169. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/tests/conftest.py +0 -0
  170. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/tests/handler_helpers.py +0 -0
  171. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/tests/test_admin_handlers.py +0 -0
  172. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/tests/test_api_handlers.py +0 -0
  173. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/tests/test_architecture_conformance.py +0 -0
  174. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/tests/test_auth_handlers.py +0 -0
  175. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/tests/test_auth_handlers_extended.py +0 -0
  176. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/tests/test_base_handler.py +0 -0
  177. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/tests/test_base_handler_pep.py +0 -0
  178. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/tests/test_cli.py +0 -0
  179. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/tests/test_cloud.py +0 -0
  180. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/tests/test_config.py +0 -0
  181. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/tests/test_core_file_operations.py +0 -0
  182. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/tests/test_database_db.py +0 -0
  183. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/tests/test_database_feature_flags.py +0 -0
  184. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/tests/test_database_ldap.py +0 -0
  185. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/tests/test_database_shares.py +0 -0
  186. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/tests/test_database_users.py +0 -0
  187. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/tests/test_database_users_hashing.py +0 -0
  188. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/tests/test_db.py +0 -0
  189. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/tests/test_filter_expression.py +0 -0
  190. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/tests/test_main.py +0 -0
  191. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/tests/test_mmap_handler.py +0 -0
  192. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/tests/test_multi_user.py +0 -0
  193. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/tests/test_network_shares.py +0 -0
  194. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/tests/test_p2p_handlers.py +0 -0
  195. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/tests/test_password_hashing.py +0 -0
  196. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/tests/test_policy_service.py +0 -0
  197. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/tests/test_rate_limit.py +0 -0
  198. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/tests/test_security.py +0 -0
  199. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/tests/test_security_comprehensive.py +0 -0
  200. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/tests/test_server_runtime.py +0 -0
  201. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/tests/test_share_handlers.py +0 -0
  202. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/tests/test_share_ownership.py +0 -0
  203. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/tests/test_super_search_handler.py +0 -0
  204. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/tests/test_tag_service.py +0 -0
  205. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/tests/test_util.py +0 -0
  206. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/tests/test_view_handlers.py +0 -0
  207. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/tests/test_websocket_manager.py +0 -0
  208. {aird-0.4.23.dev11 → aird-0.4.23.dev12}/tests/test_wheel_static_assets.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aird
3
- Version: 0.4.23.dev11
3
+ Version: 0.4.23.dev12
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
@@ -6,7 +6,6 @@ import json
6
6
  import logging
7
7
  import os
8
8
  import re
9
- import uuid
10
9
  from concurrent.futures import ThreadPoolExecutor, as_completed
11
10
  from pathlib import Path
12
11
  from typing import Any, Callable, Iterator
@@ -15,12 +14,10 @@ from urllib.parse import quote, urljoin
15
14
  import requests
16
15
 
17
16
  from aird.cli.config import ensure_config_dir, get_authelia_url, get_server_url, session_path
18
- from aird.constants import UPLOAD_CHUNK_SIZE_BYTES
19
17
 
20
18
  logger = logging.getLogger(__name__)
21
19
 
22
20
  XSRF_COOKIE = "_xsrf"
23
- CHUNK_SIZE = UPLOAD_CHUNK_SIZE_BYTES
24
21
 
25
22
 
26
23
  def _remote_url(base_path: str, remote_path: str) -> str:
@@ -280,86 +277,28 @@ class AirdClient:
280
277
  on_progress(path)
281
278
  return count
282
279
 
283
- def _upload_chunk(
284
- self,
285
- upload_id: str,
286
- remote_dir: str,
287
- filename: str,
288
- data: bytes,
289
- offset: int,
290
- total_size: int,
291
- chunk_last: bool,
292
- ) -> None:
293
- params: dict[str, str | int] = {
294
- "upload_id": upload_id,
295
- "upload_dir": remote_dir,
296
- "upload_filename": filename,
297
- "chunk_offset": offset,
298
- "total_size": total_size,
299
- }
300
- if chunk_last:
301
- params["chunk_last"] = 1
280
+ def upload_file(self, local_path: Path, remote_dir: str = "") -> None:
281
+ self.ensure_auth()
282
+ if not local_path.is_file():
283
+ raise FileNotFoundError(local_path)
284
+ remote_dir = remote_dir.strip("/")
285
+ filename = local_path.name
302
286
  r = self.http.post(
303
287
  self._url("/upload"),
304
- params=params,
305
- data=data,
288
+ params={"upload_dir": remote_dir, "upload_filename": filename},
289
+ data=local_path.read_bytes(),
306
290
  headers={
307
291
  **self._xsrf_header(),
308
292
  "Content-Type": "application/octet-stream",
309
293
  },
310
- timeout=300,
294
+ timeout=600,
311
295
  )
312
296
  if r.status_code >= 400:
313
297
  raise AirdAPIError(
314
- f"Upload chunk failed for {filename} (HTTP {r.status_code})",
298
+ f"Upload failed for {filename} (HTTP {r.status_code})",
315
299
  r.status_code,
316
300
  )
317
301
 
318
- def upload_file(self, local_path: Path, remote_dir: str = "") -> None:
319
- self.ensure_auth()
320
- if not local_path.is_file():
321
- raise FileNotFoundError(local_path)
322
- remote_dir = remote_dir.strip("/")
323
- filename = local_path.name
324
- total = local_path.stat().st_size
325
- upload_id = uuid.uuid4().hex
326
-
327
- if total <= CHUNK_SIZE:
328
- r = self.http.post(
329
- self._url("/upload"),
330
- params={"upload_dir": remote_dir, "upload_filename": filename},
331
- data=local_path.read_bytes(),
332
- headers={
333
- **self._xsrf_header(),
334
- "Content-Type": "application/octet-stream",
335
- },
336
- timeout=300,
337
- )
338
- if r.status_code >= 400:
339
- raise AirdAPIError(
340
- f"Upload failed for {filename} (HTTP {r.status_code})",
341
- r.status_code,
342
- )
343
- return
344
-
345
- offset = 0
346
- with local_path.open("rb") as f:
347
- while offset < total:
348
- chunk = f.read(CHUNK_SIZE)
349
- if not chunk:
350
- break
351
- chunk_last = offset + len(chunk) >= total
352
- self._upload_chunk(
353
- upload_id,
354
- remote_dir,
355
- filename,
356
- chunk,
357
- offset,
358
- total,
359
- chunk_last=chunk_last,
360
- )
361
- offset += len(chunk)
362
-
363
302
  def upload_tree(
364
303
  self,
365
304
  local_dir: Path,
@@ -16,9 +16,7 @@ from aird.constants import (
16
16
  ALLOWED_UPLOAD_EXTENSIONS as _ALLOWED_UPLOAD_EXTENSIONS,
17
17
  MMAP_MIN_SIZE as _MMAP_MIN_SIZE,
18
18
  CHUNK_SIZE as _CHUNK_SIZE,
19
- UPLOAD_CHUNK_SIZE_BYTES as _UPLOAD_CHUNK_SIZE_BYTES,
20
19
  UPLOAD_REQUEST_MAX_BODY_SIZE as _UPLOAD_REQUEST_MAX_BODY_SIZE,
21
- UPLOAD_MAX_PARALLEL_CHUNKS as _UPLOAD_MAX_PARALLEL_CHUNKS,
22
20
  )
23
21
 
24
22
  # Module-level variables to hold configuration
@@ -49,9 +47,35 @@ MAX_READABLE_FILE_SIZE = _MAX_READABLE_FILE_SIZE
49
47
  ALLOWED_UPLOAD_EXTENSIONS = _ALLOWED_UPLOAD_EXTENSIONS
50
48
  MMAP_MIN_SIZE = _MMAP_MIN_SIZE
51
49
  CHUNK_SIZE = _CHUNK_SIZE
52
- UPLOAD_CHUNK_SIZE_BYTES = _UPLOAD_CHUNK_SIZE_BYTES
53
50
  UPLOAD_REQUEST_MAX_BODY_SIZE = _UPLOAD_REQUEST_MAX_BODY_SIZE
54
- UPLOAD_MAX_PARALLEL_CHUNKS = _UPLOAD_MAX_PARALLEL_CHUNKS
51
+ BREVO_API_KEY = None
52
+ BREVO_SENDER_EMAIL = None
53
+ BREVO_SENDER_NAME = "Aird"
54
+ PUBLIC_BASE_URL = None
55
+
56
+
57
+ def _apply_brevo_settings(config: dict) -> None:
58
+ global BREVO_API_KEY, BREVO_SENDER_EMAIL, BREVO_SENDER_NAME, PUBLIC_BASE_URL
59
+ brevo = config.get("brevo") if isinstance(config, dict) else {}
60
+ if not isinstance(brevo, dict):
61
+ brevo = {}
62
+ BREVO_API_KEY = (
63
+ os.environ.get("AIRD_BREVO_API_KEY", "").strip() or brevo.get("api_key")
64
+ )
65
+ BREVO_SENDER_EMAIL = (
66
+ os.environ.get("AIRD_BREVO_SENDER_EMAIL", "").strip()
67
+ or brevo.get("sender_email")
68
+ )
69
+ BREVO_SENDER_NAME = (
70
+ os.environ.get("AIRD_BREVO_SENDER_NAME", "").strip()
71
+ or brevo.get("sender_name")
72
+ or "Aird"
73
+ )
74
+ PUBLIC_BASE_URL = (
75
+ os.environ.get("AIRD_PUBLIC_BASE_URL", "").strip()
76
+ or brevo.get("public_base_url")
77
+ or None
78
+ )
55
79
 
56
80
 
57
81
  def _configure_google_drive(cloud_config: dict) -> None:
@@ -172,6 +196,7 @@ def init_config():
172
196
  global LDAP_BASE_DN, LDAP_USER_TEMPLATE, LDAP_FILTER_TEMPLATE, LDAP_ATTRIBUTES
173
197
  global LDAP_ATTRIBUTE_MAP, HOSTNAME, SSL_CERT, SSL_KEY, ADMIN_USERS, FEATURE_FLAGS, CLOUD_MANAGER
174
198
  global MULTI_USER, WORKERS
199
+ global BREVO_API_KEY, BREVO_SENDER_EMAIL, BREVO_SENDER_NAME, PUBLIC_BASE_URL
175
200
 
176
201
  parser = argparse.ArgumentParser(description="Run Aird")
177
202
  parser.add_argument("--config", help="Path to JSON config file")
@@ -251,6 +276,7 @@ def init_config():
251
276
  LDAP_ATTRIBUTE_MAP = ldap_settings["attribute_map"]
252
277
 
253
278
  _apply_feature_flags_from_config(config)
279
+ _apply_brevo_settings(config)
254
280
 
255
281
  MULTI_USER = args.multi_user or config.get("multi_user", False)
256
282
 
@@ -41,6 +41,7 @@ FEATURE_FLAGS = {
41
41
  "storage_quotas": False,
42
42
  "abac_engine": False,
43
43
  "abac_audit_decisions": True,
44
+ "email_notifications": False,
44
45
  }
45
46
 
46
47
  # WebSocket connection configuration
@@ -59,13 +60,10 @@ UPLOAD_CONFIG = {
59
60
  "allow_all_file_types": 0, # 0 = use whitelist below, 1 = allow any extension
60
61
  }
61
62
 
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
- UPLOAD_REQUEST_MAX_BODY_SIZE = UPLOAD_CHUNK_SIZE_BYTES + (1024 * 1024) # chunk + slack
65
- UPLOAD_MAX_PARALLEL_CHUNKS = 3 # fewer concurrent POSTs through reverse proxies
66
-
67
63
  # File operation constants (derived from UPLOAD_CONFIG at startup)
68
64
  MAX_FILE_SIZE = UPLOAD_CONFIG["max_file_size_mb"] * 1024 * 1024
65
+ # HTTP /upload body limit (browser uploads use WebSocket; CLI may POST whole file)
66
+ UPLOAD_REQUEST_MAX_BODY_SIZE = MAX_FILE_SIZE + (1024 * 1024)
69
67
  MAX_READABLE_FILE_SIZE = 50 * 1024 * 1024 # 50 MB
70
68
 
71
69
  # Default line window for /files/... viewer when no ?end_line= is supplied (protects DOM from huge renders)
@@ -46,6 +46,8 @@ class ShareCreatedEvent:
46
46
  creator: str
47
47
  path_count: int
48
48
  created_at: float
49
+ allowed_users: tuple[str, ...] = ()
50
+ modify_users: tuple[str, ...] = ()
49
51
 
50
52
 
51
53
  @dataclass(frozen=True)
@@ -0,0 +1,51 @@
1
+ """Incremental folder size calculation (sum of file sizes)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+
7
+
8
+ # Files processed per batch before yielding back to the event loop.
9
+ FOLDER_SIZE_BATCH_FILES = 250
10
+
11
+
12
+ class FolderSizeWalker:
13
+ """Walk a directory tree in batches without loading all paths at once."""
14
+
15
+ def __init__(self, root_abspath: str) -> None:
16
+ self._walk_gen = os.walk(root_abspath)
17
+ self._current_dir: str | None = None
18
+ self._pending_files: list[str] = []
19
+ self.total_bytes = 0
20
+ self.file_count = 0
21
+ self.done = False
22
+
23
+ def step(self, batch_size: int = FOLDER_SIZE_BATCH_FILES) -> tuple[int, int, bool]:
24
+ """Process up to *batch_size* files. Returns (total_bytes, file_count, done)."""
25
+ if self.done:
26
+ return self.total_bytes, self.file_count, True
27
+
28
+ processed = 0
29
+ while processed < batch_size and not self.done:
30
+ if not self._pending_files:
31
+ try:
32
+ self._current_dir, _dirnames, filenames = next(self._walk_gen)
33
+ self._pending_files = list(filenames)
34
+ except StopIteration:
35
+ self.done = True
36
+ break
37
+
38
+ while self._pending_files and processed < batch_size:
39
+ fname = self._pending_files.pop()
40
+ if not self._current_dir:
41
+ continue
42
+ fpath = os.path.join(self._current_dir, fname)
43
+ try:
44
+ if os.path.isfile(fpath):
45
+ self.total_bytes += os.path.getsize(fpath)
46
+ except OSError:
47
+ pass
48
+ self.file_count += 1
49
+ processed += 1
50
+
51
+ return self.total_bytes, self.file_count, self.done
@@ -0,0 +1,116 @@
1
+ """Build ZIP archives from user file paths."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import os
7
+ import tempfile
8
+ import zipfile
9
+ from typing import Iterable
10
+
11
+ from aird.core.security import is_within_root
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ MAX_ZIP_ENTRIES = 10_000
16
+ MAX_ZIP_UNCOMPRESSED_BYTES = 2 * 1024 * 1024 * 1024 # 2 GiB
17
+
18
+
19
+ class ZipDownloadError(Exception):
20
+ """Raised when a zip cannot be built."""
21
+
22
+ def __init__(self, message: str, status: int = 400):
23
+ super().__init__(message)
24
+ self.status = status
25
+
26
+
27
+ def _normalise_rel_path(path: str) -> str:
28
+ return path.replace("\\", "/").strip().strip("/")
29
+
30
+
31
+ def _safe_arcname(rel_path: str) -> str | None:
32
+ parts = [p for p in _normalise_rel_path(rel_path).split("/") if p and p not in (".", "..")]
33
+ if not parts:
34
+ return None
35
+ return "/".join(parts)
36
+
37
+
38
+ def collect_zip_entries(root_dir: str, paths: Iterable[str]) -> list[tuple[str, str]]:
39
+ """Return (absolute_path, archive_name) pairs for all files under *paths*."""
40
+ root_dir = os.path.realpath(root_dir)
41
+ entries: list[tuple[str, str]] = []
42
+ seen_arc: set[str] = set()
43
+ total_bytes = 0
44
+
45
+ for raw in paths:
46
+ if not isinstance(raw, str):
47
+ raise ZipDownloadError("Invalid path", 400)
48
+ rel = _normalise_rel_path(raw)
49
+ if not rel:
50
+ continue
51
+ abspath = os.path.realpath(os.path.join(root_dir, rel))
52
+ if not is_within_root(abspath, root_dir):
53
+ raise ZipDownloadError("Access denied", 403)
54
+ if not os.path.exists(abspath):
55
+ raise ZipDownloadError(f"Not found: {rel}", 404)
56
+
57
+ def add_file(file_abs: str, arc: str | None) -> None:
58
+ nonlocal total_bytes
59
+ if arc is None:
60
+ return
61
+ if arc in seen_arc:
62
+ return
63
+ try:
64
+ size = os.path.getsize(file_abs)
65
+ except OSError:
66
+ return
67
+ total_bytes += size
68
+ if total_bytes > MAX_ZIP_UNCOMPRESSED_BYTES:
69
+ raise ZipDownloadError("Selection is too large to zip", 413)
70
+ seen_arc.add(arc)
71
+ entries.append((file_abs, arc))
72
+ if len(entries) > MAX_ZIP_ENTRIES:
73
+ raise ZipDownloadError("Too many files in selection", 400)
74
+
75
+ if os.path.isfile(abspath):
76
+ add_file(abspath, _safe_arcname(rel))
77
+ continue
78
+
79
+ if os.path.isdir(abspath):
80
+ prefix = _safe_arcname(rel)
81
+ for dirpath, _dirnames, filenames in os.walk(abspath):
82
+ for fname in filenames:
83
+ full = os.path.join(dirpath, fname)
84
+ if not os.path.isfile(full):
85
+ continue
86
+ file_rel = os.path.relpath(full, root_dir).replace("\\", "/")
87
+ add_file(full, _safe_arcname(file_rel))
88
+ continue
89
+
90
+ raise ZipDownloadError(f"Not a file or folder: {rel}", 400)
91
+
92
+ return entries
93
+
94
+
95
+ def build_zip_file(entries: list[tuple[str, str]]) -> str:
96
+ """Write entries to a temporary store-only zip (no compression, low CPU)."""
97
+ if not entries:
98
+ raise ZipDownloadError("No files to download", 400)
99
+
100
+ fd, zip_path = tempfile.mkstemp(prefix="aird_zip_", suffix=".zip")
101
+ os.close(fd)
102
+ try:
103
+ # ZIP_STORED: pack files without deflate — minimal CPU; larger wire size.
104
+ with zipfile.ZipFile(
105
+ zip_path, "w", compression=zipfile.ZIP_STORED, allowZip64=True
106
+ ) as zf:
107
+ for file_abs, arcname in entries:
108
+ zf.write(file_abs, arcname, compress_type=zipfile.ZIP_STORED)
109
+ except Exception:
110
+ logger.exception("ZIP build failed")
111
+ try:
112
+ os.remove(zip_path)
113
+ except OSError:
114
+ pass
115
+ raise ZipDownloadError("Failed to create zip archive", 500) from None
116
+ return zip_path
@@ -0,0 +1,6 @@
1
+ """Outbound email (Brevo transactional API)."""
2
+
3
+ from aird.email.brevo import BrevoClient
4
+ from aird.email.resolve import resolve_user_email
5
+
6
+ __all__ = ["BrevoClient", "resolve_user_email"]
@@ -0,0 +1,83 @@
1
+ """Brevo (Sendinblue) transactional email client."""
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
+ BREVO_API_URL = "https://api.brevo.com/v3/smtp/email"
13
+
14
+
15
+ class BrevoError(RuntimeError):
16
+ pass
17
+
18
+
19
+ class BrevoClient:
20
+ def __init__(
21
+ self,
22
+ api_key: str | None,
23
+ *,
24
+ sender_email: str | None,
25
+ sender_name: str = "Aird",
26
+ ):
27
+ self.api_key = (api_key or "").strip()
28
+ self.sender_email = (sender_email or "").strip()
29
+ self.sender_name = (sender_name or "Aird").strip() or "Aird"
30
+
31
+ @property
32
+ def configured(self) -> bool:
33
+ return bool(self.api_key and self.sender_email)
34
+
35
+ def send(
36
+ self,
37
+ to_email: str,
38
+ subject: str,
39
+ *,
40
+ html_content: str,
41
+ text_content: str | None = None,
42
+ to_name: str | None = None,
43
+ ) -> bool:
44
+ if not self.configured:
45
+ logger.debug("Brevo not configured; skipping email to %s", to_email)
46
+ return False
47
+ to_email = to_email.strip()
48
+ if not to_email:
49
+ return False
50
+
51
+ payload: dict[str, Any] = {
52
+ "sender": {"email": self.sender_email, "name": self.sender_name},
53
+ "to": [{"email": to_email, "name": (to_name or to_email).strip()}],
54
+ "subject": subject,
55
+ "htmlContent": html_content,
56
+ }
57
+ if text_content:
58
+ payload["textContent"] = text_content
59
+
60
+ try:
61
+ resp = requests.post(
62
+ BREVO_API_URL,
63
+ headers={
64
+ "api-key": self.api_key,
65
+ "Content-Type": "application/json",
66
+ "accept": "application/json",
67
+ },
68
+ json=payload,
69
+ timeout=30,
70
+ )
71
+ except requests.RequestException as exc:
72
+ logger.warning("Brevo request failed for %s: %s", to_email, exc)
73
+ return False
74
+
75
+ if resp.status_code >= 400:
76
+ logger.warning(
77
+ "Brevo API error %s for %s: %s",
78
+ resp.status_code,
79
+ to_email,
80
+ resp.text[:500],
81
+ )
82
+ return False
83
+ return True
@@ -0,0 +1,36 @@
1
+ """Resolve a delivery email address for an Aird username."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ import sqlite3
7
+
8
+ from aird.db.user_attributes import get_user_attributes
9
+
10
+ _EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
11
+
12
+
13
+ def looks_like_email(value: str) -> bool:
14
+ return bool(_EMAIL_RE.match((value or "").strip()))
15
+
16
+
17
+ def resolve_user_email(conn: sqlite3.Connection | None, username: str) -> str | None:
18
+ """
19
+ Return an email for *username*.
20
+
21
+ Order: user_attributes ``email`` → username when it is an email address.
22
+ """
23
+ username = (username or "").strip()
24
+ if not username:
25
+ return None
26
+ if conn is not None:
27
+ try:
28
+ attrs = get_user_attributes(conn, username)
29
+ raw = (attrs.get("email") or attrs.get("mail") or "").strip()
30
+ if looks_like_email(raw):
31
+ return raw
32
+ except Exception:
33
+ pass
34
+ if looks_like_email(username):
35
+ return username
36
+ return None
@@ -63,7 +63,8 @@ from aird.core.mmap_handler import MMapFileHandler
63
63
  class FeatureFlagSocketHandler(
64
64
  ManagedWebSocketMixin, tornado.websocket.WebSocketHandler
65
65
  ):
66
- # Use connection manager with configurable limits for feature flags
66
+ """Legacy WebSocket kept for backward compatibility; new clients use GET /api/features."""
67
+
67
68
  connection_manager = WebSocketConnectionManager(
68
69
  "feature_flags", default_max_connections=50, default_idle_timeout=600
69
70
  )
@@ -76,25 +77,30 @@ class FeatureFlagSocketHandler(
76
77
  if not self.register_connection():
77
78
  return
78
79
 
79
- # Load current feature flags from SQLite and send to client
80
80
  current_flags = self._get_current_feature_flags()
81
81
  self.write_message(json.dumps(current_flags))
82
82
 
83
83
  def check_origin(self, origin):
84
- # Improved origin validation (Priority 2)
85
84
  return is_valid_websocket_origin(self, origin)
86
85
 
87
86
  def _get_current_feature_flags(self):
88
- """Get current feature flags using the consolidated implementation."""
89
87
  return get_current_feature_flags()
90
88
 
91
89
  @classmethod
92
90
  def send_updates(cls):
93
- """Send feature flag updates to all connected clients."""
94
91
  current_flags = get_current_feature_flags()
95
92
  cls.connection_manager.broadcast_message(json.dumps(current_flags))
96
93
 
97
94
 
95
+ class FeatureFlagAPIHandler(BaseHandler):
96
+ """GET /api/features — lightweight JSON endpoint for on-demand flag checks."""
97
+
98
+ @tornado.web.authenticated
99
+ def get(self):
100
+ self.set_header("Content-Type", "application/json")
101
+ self.write(json.dumps(get_current_feature_flags()))
102
+
103
+
98
104
  class FileStreamHandler(ManagedWebSocketMixin, tornado.websocket.WebSocketHandler):
99
105
  # Use connection manager with configurable limits for file streaming
100
106
  connection_manager = WebSocketConnectionManager(