aird 0.4.23.dev12__tar.gz → 0.4.23.dev17__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 (211) hide show
  1. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/MANIFEST.in +1 -0
  2. {aird-0.4.23.dev12/aird.egg-info → aird-0.4.23.dev17}/PKG-INFO +1 -1
  3. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/constants/__init__.py +42 -0
  4. aird-0.4.23.dev17/aird/core/folder_size.py +101 -0
  5. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/core/mmap_handler.py +67 -26
  6. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/handlers/api_handlers.py +53 -0
  7. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/handlers/base_handler.py +1 -1
  8. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/handlers/transfer_ws_handlers.py +33 -15
  9. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/handlers/view_handlers.py +16 -3
  10. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/main.py +2 -2
  11. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/static/css/app.css +1 -1
  12. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/static/js/browse/app.js +69 -228
  13. aird-0.4.23.dev17/aird/static/js/download-manager.js +151 -0
  14. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/static/js/file-transfer-ws.js +40 -3
  15. aird-0.4.23.dev17/aird/static/js/folder-size-scan.js +188 -0
  16. aird-0.4.23.dev17/aird/static/js/transfer-tracker.js +315 -0
  17. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/templates/_app_nav_header.html +32 -3
  18. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/templates/_bg_canvas.html +1 -1
  19. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/templates/admin.html +1 -1
  20. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/templates/admin_audit.html +4 -4
  21. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/templates/admin_ldap.html +3 -3
  22. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/templates/admin_login.html +4 -4
  23. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/templates/admin_network_shares.html +3 -3
  24. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/templates/admin_policies.html +4 -4
  25. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/templates/admin_tags.html +4 -4
  26. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/templates/admin_user_attributes.html +4 -4
  27. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/templates/admin_users.html +3 -3
  28. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/templates/browse.html +14 -12
  29. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/templates/directory.html +3 -3
  30. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/templates/edit.html +4 -4
  31. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/templates/error.html +3 -3
  32. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/templates/file.html +4 -4
  33. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/templates/ldap_config_create.html +97 -97
  34. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/templates/ldap_config_edit.html +99 -99
  35. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/templates/login.html +4 -4
  36. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/templates/media_view.html +3 -3
  37. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/templates/p2p_transfer.html +11 -11
  38. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/templates/profile.html +3 -3
  39. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/templates/share.html +2 -2
  40. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/templates/shared_list.html +3 -7
  41. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/templates/super_search.html +3 -3
  42. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/templates/tagged_files.html +32 -10
  43. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/templates/token_verification.html +3 -3
  44. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/templates/user_create.html +75 -75
  45. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/templates/user_edit.html +116 -116
  46. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/utils/util.py +26 -5
  47. {aird-0.4.23.dev12 → aird-0.4.23.dev17/aird.egg-info}/PKG-INFO +1 -1
  48. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird.egg-info/SOURCES.txt +1 -1
  49. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/setup.py +1 -1
  50. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/tests/test_folder_size.py +7 -1
  51. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/tests/test_mmap_handler.py +42 -4
  52. aird-0.4.23.dev17/tests/test_wheel_static_assets.py +112 -0
  53. aird-0.4.23.dev12/aird/core/folder_size.py +0 -51
  54. aird-0.4.23.dev12/aird/handlers/folder_size_ws_handlers.py +0 -193
  55. aird-0.4.23.dev12/aird/static/js/download-manager.js +0 -309
  56. aird-0.4.23.dev12/aird/static/js/folder-size-scan.js +0 -115
  57. aird-0.4.23.dev12/tests/test_wheel_static_assets.py +0 -48
  58. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/LICENSE +0 -0
  59. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/README.md +0 -0
  60. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/__init__.py +0 -0
  61. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/__main__.py +0 -0
  62. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/app_context.py +0 -0
  63. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/cli/__init__.py +0 -0
  64. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/cli/__main__.py +0 -0
  65. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/cli/authelia.py +0 -0
  66. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/cli/config.py +0 -0
  67. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/cli/main.py +0 -0
  68. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/cli/session.py +0 -0
  69. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/cloud/__init__.py +0 -0
  70. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/config.py +0 -0
  71. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/constants/admin.py +0 -0
  72. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/constants/file_ops.py +0 -0
  73. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/constants/input_limits.py +0 -0
  74. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/constants/media.py +0 -0
  75. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/core/__init__.py +0 -0
  76. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/core/events.py +0 -0
  77. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/core/file_operations.py +0 -0
  78. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/core/filter_expression.py +0 -0
  79. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/core/input_validation.py +0 -0
  80. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/core/security.py +0 -0
  81. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/core/share_root.py +0 -0
  82. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/core/websocket_manager.py +0 -0
  83. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/core/zip_download.py +0 -0
  84. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/database/__init__.py +0 -0
  85. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/database/db.py +0 -0
  86. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/database/feature_flags.py +0 -0
  87. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/database/ldap.py +0 -0
  88. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/db/__init__.py +0 -0
  89. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/db/audit.py +0 -0
  90. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/db/config.py +0 -0
  91. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/db/favorites.py +0 -0
  92. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/db/network_shares.py +0 -0
  93. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/db/policies.py +0 -0
  94. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/db/policy_decisions.py +0 -0
  95. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/db/policy_seeds.py +0 -0
  96. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/db/quota.py +0 -0
  97. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/db/resource_tags.py +0 -0
  98. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/db/schema.py +0 -0
  99. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/db/shares.py +0 -0
  100. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/db/user_attributes.py +0 -0
  101. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/db/users.py +0 -0
  102. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/domain/__init__.py +0 -0
  103. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/domain/contracts.py +0 -0
  104. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/domain/models.py +0 -0
  105. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/email/__init__.py +0 -0
  106. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/email/brevo.py +0 -0
  107. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/email/resolve.py +0 -0
  108. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/handlers/__init__.py +0 -0
  109. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/handlers/abac_handlers.py +0 -0
  110. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/handlers/admin_handlers.py +0 -0
  111. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/handlers/auth_handlers.py +0 -0
  112. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/handlers/constants.py +0 -0
  113. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/handlers/file_op_handlers.py +0 -0
  114. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/handlers/health_handler.py +0 -0
  115. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/handlers/p2p_handlers.py +0 -0
  116. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/handlers/share_handlers.py +0 -0
  117. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/network_share_manager.py +0 -0
  118. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/server_runtime.py +0 -0
  119. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/services/__init__.py +0 -0
  120. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/services/audit_service.py +0 -0
  121. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/services/config_service.py +0 -0
  122. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/services/email_service.py +0 -0
  123. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/services/email_subscriber.py +0 -0
  124. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/services/event_subscribers.py +0 -0
  125. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/services/favorites_service.py +0 -0
  126. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/services/network_share_service.py +0 -0
  127. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/services/p2p_service.py +0 -0
  128. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/services/policy_service.py +0 -0
  129. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/services/quota_service.py +0 -0
  130. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/services/share_service.py +0 -0
  131. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/services/tag_service.py +0 -0
  132. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/services/user_service.py +0 -0
  133. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/sql_identifiers.py +0 -0
  134. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/static/favicon.png +0 -0
  135. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/static/favicon.svg +0 -0
  136. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/static/img/logo-icon.png +0 -0
  137. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/static/img/logo-mark.svg +0 -0
  138. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/static/img/logo-text.png +0 -0
  139. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/static/img/logo.png +0 -0
  140. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/static/js/aird-core.js +0 -0
  141. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/static/js/bg-canvas.js +0 -0
  142. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/static/js/common/command-palette.js +0 -0
  143. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/static/js/components/folder-picker.js +0 -0
  144. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/static/js/feature-flags-live.js +0 -0
  145. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/static/js/login-ui.js +0 -0
  146. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/static/js/media-view.js +0 -0
  147. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/static/js/p2p/app.js +0 -0
  148. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/static/js/p2p/mediator.js +0 -0
  149. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/static/js/p2p/qr-adapter.js +0 -0
  150. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/static/js/p2p/signaling-service.js +0 -0
  151. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/static/js/p2p/state-machine.js +0 -0
  152. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/static/js/p2p/transfer-service.js +0 -0
  153. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/static/js/pages/p2p-page.js +0 -0
  154. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/static/js/pages/super-search.js +0 -0
  155. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/static/js/share/app.js +0 -0
  156. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/static/js/theme.js +0 -0
  157. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/static/js/vendor/pdf.min.js +0 -0
  158. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/static/js/vendor/pdf.worker.min.js +0 -0
  159. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/static/js/vendor/qrcode-browser.js +0 -0
  160. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/templates/_admin_tabs.html +0 -0
  161. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/templates/_theme_early.html +0 -0
  162. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/templates/_theme_login_corner.html +0 -0
  163. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird/utils/__init__.py +0 -0
  164. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird.egg-info/dependency_links.txt +0 -0
  165. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird.egg-info/entry_points.txt +0 -0
  166. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird.egg-info/requires.txt +0 -0
  167. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/aird.egg-info/top_level.txt +0 -0
  168. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/setup.cfg +0 -0
  169. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/tests/__init__.py +0 -0
  170. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/tests/conftest.py +0 -0
  171. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/tests/handler_helpers.py +0 -0
  172. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/tests/test_admin_handlers.py +0 -0
  173. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/tests/test_api_handlers.py +0 -0
  174. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/tests/test_architecture_conformance.py +0 -0
  175. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/tests/test_auth_handlers.py +0 -0
  176. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/tests/test_auth_handlers_extended.py +0 -0
  177. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/tests/test_base_handler.py +0 -0
  178. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/tests/test_base_handler_pep.py +0 -0
  179. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/tests/test_cli.py +0 -0
  180. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/tests/test_cloud.py +0 -0
  181. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/tests/test_config.py +0 -0
  182. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/tests/test_core_file_operations.py +0 -0
  183. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/tests/test_database_db.py +0 -0
  184. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/tests/test_database_feature_flags.py +0 -0
  185. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/tests/test_database_ldap.py +0 -0
  186. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/tests/test_database_shares.py +0 -0
  187. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/tests/test_database_users.py +0 -0
  188. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/tests/test_database_users_hashing.py +0 -0
  189. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/tests/test_db.py +0 -0
  190. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/tests/test_email_service.py +0 -0
  191. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/tests/test_file_op_handlers.py +0 -0
  192. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/tests/test_filter_expression.py +0 -0
  193. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/tests/test_main.py +0 -0
  194. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/tests/test_multi_user.py +0 -0
  195. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/tests/test_network_shares.py +0 -0
  196. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/tests/test_p2p_handlers.py +0 -0
  197. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/tests/test_password_hashing.py +0 -0
  198. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/tests/test_policy_service.py +0 -0
  199. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/tests/test_rate_limit.py +0 -0
  200. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/tests/test_security.py +0 -0
  201. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/tests/test_security_comprehensive.py +0 -0
  202. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/tests/test_server_runtime.py +0 -0
  203. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/tests/test_share_handlers.py +0 -0
  204. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/tests/test_share_ownership.py +0 -0
  205. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/tests/test_super_search_handler.py +0 -0
  206. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/tests/test_tag_service.py +0 -0
  207. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/tests/test_transfer_ws_handlers.py +0 -0
  208. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/tests/test_util.py +0 -0
  209. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/tests/test_view_handlers.py +0 -0
  210. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/tests/test_websocket_manager.py +0 -0
  211. {aird-0.4.23.dev12 → aird-0.4.23.dev17}/tests/test_zip_download.py +0 -0
@@ -4,3 +4,4 @@ recursive-include aird/static/js *.js
4
4
  recursive-include aird/static/img *
5
5
  include aird/static/favicon.png
6
6
  include aird/static/favicon.ico
7
+ include aird/static/favicon.svg
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aird
3
- Version: 0.4.23.dev12
3
+ Version: 0.4.23.dev17
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
@@ -2,6 +2,7 @@
2
2
 
3
3
  import os
4
4
  import sys
5
+ from pathlib import Path
5
6
 
6
7
  sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
7
8
  from aird.cloud import CloudManager
@@ -100,6 +101,8 @@ UPLOAD_ALLOWED_EXTENSIONS = set(ALLOWED_UPLOAD_EXTENSIONS)
100
101
  # Mmap constants
101
102
  MMAP_MIN_SIZE = 1 * 1024 * 1024 # 1 MB
102
103
  CHUNK_SIZE = 64 * 1024 # 64 KB
104
+ # WebSocket binary frame size (stay under Cloudflare ~1 MiB message limit)
105
+ WS_TRANSFER_FRAME_BYTES = 768 * 1024
103
106
 
104
107
  # Network share manager (set at startup)
105
108
  NETWORK_SHARE_MANAGER = None
@@ -141,3 +144,42 @@ def _read_app_version() -> str:
141
144
 
142
145
 
143
146
  APP_VERSION = _read_app_version()
147
+
148
+ _UI_PACKAGE_SUFFIXES = frozenset(
149
+ {".html", ".css", ".js", ".png", ".ico", ".svg", ".jpg", ".jpeg", ".webp", ".gif"}
150
+ )
151
+ _PKG_ROOT = Path(__file__).resolve().parent.parent
152
+ _static_version_cache: tuple[float, str] | None = None
153
+ _STATIC_VERSION_TTL = 2.0
154
+
155
+
156
+ def _ui_fingerprint() -> str:
157
+ """Hex fingerprint from latest mtime among shipped UI package-data files."""
158
+ latest = 0
159
+ for sub in ("static", "templates"):
160
+ base = _PKG_ROOT / sub
161
+ if not base.is_dir():
162
+ continue
163
+ for path in base.rglob("*"):
164
+ if not path.is_file():
165
+ continue
166
+ if path.suffix.lower() not in _UI_PACKAGE_SUFFIXES:
167
+ continue
168
+ try:
169
+ latest = max(latest, path.stat().st_mtime_ns)
170
+ except OSError:
171
+ pass
172
+ return format(latest, "x")
173
+
174
+
175
+ def get_static_version() -> str:
176
+ """Cache-bust query value: package version plus UI file fingerprint."""
177
+ import time
178
+
179
+ global _static_version_cache
180
+ now = time.monotonic()
181
+ if _static_version_cache and now - _static_version_cache[0] < _STATIC_VERSION_TTL:
182
+ return _static_version_cache[1]
183
+ version = f"{APP_VERSION}-{_ui_fingerprint()}"
184
+ _static_version_cache = (now, version)
185
+ return version
@@ -0,0 +1,101 @@
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
52
+
53
+
54
+ def norm_rel_path(rel_path: str) -> str:
55
+ return rel_path.replace("\\", "/").strip().strip("/")
56
+
57
+
58
+ def resolve_folder_abspath(user_root: str, rel_path: str) -> str | None:
59
+ """Return absolute folder path or None if invalid / not a directory."""
60
+ from aird.core.security import is_within_root
61
+
62
+ rel = norm_rel_path(rel_path)
63
+ if not rel or ".." in rel.split("/"):
64
+ return None
65
+ abs_path = os.path.abspath(os.path.join(user_root, rel))
66
+ if not is_within_root(abs_path, user_root) or not os.path.isdir(abs_path):
67
+ return None
68
+ return abs_path
69
+
70
+
71
+ def compute_folder_size(root_abspath: str) -> tuple[int, int]:
72
+ """Sum file sizes under *root_abspath*. Returns (total_bytes, file_count)."""
73
+ walker = FolderSizeWalker(root_abspath)
74
+ while not walker.done:
75
+ walker.step(FOLDER_SIZE_BATCH_FILES)
76
+ return walker.total_bytes, walker.file_count
77
+
78
+
79
+ def norm_rel_path(rel_path: str) -> str:
80
+ return rel_path.replace("\\", "/").strip().strip("/")
81
+
82
+
83
+ def resolve_folder_abspath(user_root: str, rel_path: str) -> str | None:
84
+ """Return absolute folder path or None if invalid / not a directory."""
85
+ from aird.core.security import is_within_root
86
+
87
+ rel = norm_rel_path(rel_path)
88
+ if not rel or ".." in rel.split("/"):
89
+ return None
90
+ abs_path = os.path.abspath(os.path.join(user_root, rel))
91
+ if not is_within_root(abs_path, user_root) or not os.path.isdir(abs_path):
92
+ return None
93
+ return abs_path
94
+
95
+
96
+ def compute_folder_size(root_abspath: str) -> tuple[int, int]:
97
+ """Sum file sizes under *root_abspath*. Returns (total_bytes, file_count)."""
98
+ walker = FolderSizeWalker(root_abspath)
99
+ while not walker.done:
100
+ walker.step(FOLDER_SIZE_BATCH_FILES)
101
+ return walker.total_bytes, walker.file_count
@@ -25,20 +25,50 @@ def _read_chunks_sync(
25
25
  return chunks
26
26
 
27
27
 
28
- def _read_chunks_mmap(
29
- file_path: str, start: int, end: int | None, file_size: int, chunk_size: int
30
- ) -> list[bytes]:
31
- """Read file chunks using mmap."""
32
- chunks = []
33
- with open(file_path, "rb") as f:
34
- with mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) as mm:
35
- actual_end = min(end or file_size - 1, file_size - 1)
36
- current = start
37
- while current <= actual_end:
38
- chunk_end = min(current + chunk_size, actual_end + 1)
39
- chunks.append(mm[current:chunk_end])
40
- current = chunk_end
41
- return chunks
28
+ class _SyncChunkReader:
29
+ """Read one chunk at a time from disk (mmap or plain file)."""
30
+
31
+ def __init__(
32
+ self,
33
+ file_path: str,
34
+ start: int,
35
+ end: int | None,
36
+ file_size: int,
37
+ chunk_size: int,
38
+ use_mmap: bool,
39
+ ) -> None:
40
+ self._file_path = file_path
41
+ self._pos = start
42
+ self._end = min(end if end is not None else file_size - 1, file_size - 1)
43
+ self._chunk_size = chunk_size
44
+ self._use_mmap = use_mmap
45
+ self._file = None
46
+ self._mm = None
47
+
48
+ def open(self) -> None:
49
+ self._file = open(self._file_path, "rb")
50
+ if self._use_mmap:
51
+ self._mm = mmap.mmap(self._file.fileno(), 0, access=mmap.ACCESS_READ)
52
+
53
+ def read_next(self) -> bytes | None:
54
+ if self._pos > self._end:
55
+ return None
56
+ chunk_end = min(self._pos + self._chunk_size, self._end + 1)
57
+ if self._mm is not None:
58
+ chunk = self._mm[self._pos : chunk_end]
59
+ else:
60
+ self._file.seek(self._pos)
61
+ chunk = self._file.read(chunk_end - self._pos)
62
+ self._pos = chunk_end
63
+ return chunk if chunk else None
64
+
65
+ def close(self) -> None:
66
+ if self._mm is not None:
67
+ self._mm.close()
68
+ self._mm = None
69
+ if self._file is not None:
70
+ self._file.close()
71
+ self._file = None
42
72
 
43
73
 
44
74
  class MMapFileHandler:
@@ -53,12 +83,11 @@ class MMapFileHandler:
53
83
  async def serve_file_chunk(
54
84
  file_path: str, start: int = 0, end: int = None, chunk_size: int = CHUNK_SIZE
55
85
  ):
56
- """Serve file chunks using mmap for efficient memory usage"""
86
+ """Serve file chunks; only one chunk is held in memory at a time."""
57
87
  try:
58
88
  file_size = await asyncio.to_thread(os.path.getsize, file_path)
59
89
 
60
90
  if not MMapFileHandler.should_use_mmap(file_size):
61
- # Use async file API for small files
62
91
  remaining = (end - start + 1) if end is not None else file_size - start
63
92
  async with aiofiles.open(file_path, "rb") as f:
64
93
  await f.seek(start)
@@ -70,21 +99,33 @@ class MMapFileHandler:
70
99
  remaining -= len(chunk)
71
100
  return
72
101
 
73
- # Use mmap for large files (run in thread pool - mmap requires sync fd)
74
- chunks = await asyncio.to_thread(
75
- _read_chunks_mmap, file_path, start, end, file_size, chunk_size
102
+ reader = _SyncChunkReader(
103
+ file_path, start, end, file_size, chunk_size, use_mmap=True
76
104
  )
77
- for chunk in chunks:
78
- yield chunk
105
+ try:
106
+ await asyncio.to_thread(reader.open)
107
+ while True:
108
+ chunk = await asyncio.to_thread(reader.read_next)
109
+ if not chunk:
110
+ break
111
+ yield chunk
112
+ finally:
113
+ await asyncio.to_thread(reader.close)
79
114
 
80
115
  except (OSError, ValueError):
81
- # Fallback to traditional method on mmap errors (run in thread pool)
82
116
  file_size = await asyncio.to_thread(os.path.getsize, file_path)
83
- chunks = await asyncio.to_thread(
84
- _read_chunks_sync, file_path, start, end, file_size, chunk_size
117
+ reader = _SyncChunkReader(
118
+ file_path, start, end, file_size, chunk_size, use_mmap=False
85
119
  )
86
- for chunk in chunks:
87
- yield chunk
120
+ try:
121
+ await asyncio.to_thread(reader.open)
122
+ while True:
123
+ chunk = await asyncio.to_thread(reader.read_next)
124
+ if not chunk:
125
+ break
126
+ yield chunk
127
+ finally:
128
+ await asyncio.to_thread(reader.close)
88
129
 
89
130
  @staticmethod
90
131
  def find_line_offsets(file_path: str, max_lines: int = None) -> list[int]:
@@ -336,6 +336,59 @@ class FileStreamHandler(ManagedWebSocketMixin, tornado.websocket.WebSocketHandle
336
336
  return is_valid_websocket_origin(self, origin)
337
337
 
338
338
 
339
+ class FolderSizeAPIHandler(BaseHandler):
340
+ """GET /api/folder-size?path= — recursive folder byte total (on demand)."""
341
+
342
+ @tornado.web.authenticated
343
+ async def get(self):
344
+ from aird.core.folder_size import (
345
+ compute_folder_size,
346
+ norm_rel_path,
347
+ resolve_folder_abspath,
348
+ )
349
+ from aird.utils.util import format_size
350
+
351
+ path = self.get_argument("path", "").strip()
352
+ norm_path = norm_rel_path(path)
353
+ decision = self.check_access("file.list", resource_path=norm_path)
354
+ if decision is not None and decision.is_deny:
355
+ self.set_status(403)
356
+ self.set_header("Content-Type", "application/json")
357
+ self.write(json.dumps({"error": decision.reason or "Access denied"}))
358
+ return
359
+
360
+ user_root = get_user_root(self)
361
+ abs_path = resolve_folder_abspath(user_root, path)
362
+ if not abs_path:
363
+ self.set_status(404)
364
+ self.set_header("Content-Type", "application/json")
365
+ self.write(json.dumps({"error": "Folder not found"}))
366
+ return
367
+
368
+ try:
369
+ total_bytes, file_count = await asyncio.to_thread(
370
+ compute_folder_size, abs_path
371
+ )
372
+ except Exception:
373
+ logging.exception("Folder size calculation failed for %s", norm_path)
374
+ self.set_status(500)
375
+ self.set_header("Content-Type", "application/json")
376
+ self.write(json.dumps({"error": "Folder size calculation failed"}))
377
+ return
378
+
379
+ self.set_header("Content-Type", "application/json")
380
+ self.write(
381
+ json.dumps(
382
+ {
383
+ "path": norm_path,
384
+ "bytes": total_bytes,
385
+ "files": file_count,
386
+ "size_str": format_size(total_bytes),
387
+ }
388
+ )
389
+ )
390
+
391
+
339
392
  class FileListAPIHandler(BaseHandler):
340
393
  @tornado.web.authenticated
341
394
  @require_action("file.list", resource_arg="path")
@@ -897,7 +897,7 @@ class BaseHandler(tornado.web.RequestHandler):
897
897
  namespace.setdefault("nav_title", "")
898
898
  namespace.setdefault("show_admin_link", False)
899
899
  namespace.setdefault("ldap_enabled", self.settings.get("ldap_server") is not None)
900
- namespace.setdefault("static_version", constants_module.APP_VERSION)
900
+ namespace.setdefault("static_version", constants_module.get_static_version())
901
901
  return namespace
902
902
 
903
903
  def get_current_user(self):
@@ -21,6 +21,7 @@ from aird.constants.file_ops import (
21
21
  FILE_UPLOAD_DISABLED,
22
22
  UPLOAD_SAVE_FAILED,
23
23
  )
24
+ from aird.constants import WS_TRANSFER_FRAME_BYTES
24
25
  from aird.core.mmap_handler import MMapFileHandler
25
26
  from aird.core.security import is_valid_websocket_origin, is_within_root
26
27
  from aird.handlers.base_handler import (
@@ -108,7 +109,8 @@ class FileTransferWebSocketHandler(
108
109
  super().__init__(application, request, **kwargs)
109
110
  self._upload: dict | None = None
110
111
  self._upload_buffer: deque[bytes] = deque()
111
- self._upload_writing = False
112
+ self._upload_buffer_event = asyncio.Event()
113
+ self._upload_writer_done = False
112
114
  self._upload_writer_task: asyncio.Task | None = None
113
115
  self._cancelled = False
114
116
  self._download_task: asyncio.Task | None = None
@@ -199,6 +201,9 @@ class FileTransferWebSocketHandler(
199
201
  "temp_path": temp_path,
200
202
  "aiofile": aiofile,
201
203
  }
204
+ self._upload_writer_done = False
205
+ self._upload_buffer_event.clear()
206
+ self._upload_writer_task = asyncio.create_task(self._upload_writer_loop())
202
207
  await self._send_json({"type": "upload_started", "total_size": total_size})
203
208
 
204
209
  async def _handle_upload_binary(self, data: bytes) -> None:
@@ -212,31 +217,40 @@ class FileTransferWebSocketHandler(
212
217
  return
213
218
 
214
219
  self._upload_buffer.append(data)
215
- if not self._upload_writing:
216
- self._upload_writing = True
217
- self._upload_writer_task = asyncio.create_task(self._drain_upload_buffer())
220
+ self._upload_buffer_event.set()
218
221
 
219
- async def _drain_upload_buffer(self) -> None:
222
+ async def _upload_writer_loop(self) -> None:
223
+ """Drain the upload buffer continuously until signalled to stop."""
220
224
  try:
221
- while self._upload_buffer and self._upload:
222
- chunk = self._upload_buffer.popleft()
223
- await self._upload["aiofile"].write(chunk)
224
- if self._upload:
225
- await self._upload["aiofile"].flush()
226
- finally:
227
- self._upload_writing = False
225
+ while True:
226
+ await self._upload_buffer_event.wait()
227
+ self._upload_buffer_event.clear()
228
+ while self._upload_buffer and self._upload:
229
+ chunk = self._upload_buffer.popleft()
230
+ await self._upload["aiofile"].write(chunk)
231
+ if self._upload:
232
+ await self._upload["aiofile"].flush()
233
+ if self._upload_writer_done:
234
+ return
235
+ except asyncio.CancelledError:
236
+ pass
237
+ except Exception:
238
+ logger.debug("WS upload writer loop failed", exc_info=True)
228
239
 
229
240
  async def _finalize_upload_writer(self) -> None:
241
+ self._upload_writer_done = True
242
+ self._upload_buffer_event.set()
230
243
  if self._upload_writer_task is not None:
231
244
  try:
232
245
  await self._upload_writer_task
233
246
  except Exception:
234
- logging.debug("WS upload writer await failed", exc_info=True)
247
+ logger.debug("WS upload writer await failed", exc_info=True)
248
+ self._upload_writer_task = None
235
249
  if self._upload and self._upload.get("aiofile"):
236
250
  try:
237
251
  await self._upload["aiofile"].close()
238
252
  except Exception:
239
- logging.debug("WS upload file close failed", exc_info=True)
253
+ logger.debug("WS upload file close failed", exc_info=True)
240
254
  self._upload["aiofile"] = None
241
255
 
242
256
  async def _abort_upload(self, message: str | None = None) -> None:
@@ -339,12 +353,16 @@ class FileTransferWebSocketHandler(
339
353
  "filename": filename,
340
354
  "content_type": content_type,
341
355
  "size": file_size,
356
+ "chunk_size": WS_TRANSFER_FRAME_BYTES,
342
357
  }
343
358
  )
344
- async for chunk in MMapFileHandler.serve_file_chunk(abs_path):
359
+ async for chunk in MMapFileHandler.serve_file_chunk(
360
+ abs_path, chunk_size=WS_TRANSFER_FRAME_BYTES
361
+ ):
345
362
  if self._cancelled:
346
363
  return
347
364
  await self.write_message(chunk, binary=True)
365
+ await asyncio.sleep(0)
348
366
  await self._send_json({"type": "download_end"})
349
367
  except asyncio.CancelledError:
350
368
  raise
@@ -506,18 +506,31 @@ class TaggedFilesHandler(BaseHandler):
506
506
  patterns = [r["glob_pattern"] for r in rules if r.get("tag") == tag_name]
507
507
  all_tags = sorted({r["tag"] for r in rules if r.get("tag")})
508
508
 
509
- files: list[str] = []
509
+ entries: list[dict] = []
510
+ file_count = 0
511
+ folder_count = 0
510
512
  if patterns:
511
513
  root = constants_module.ROOT_DIR
512
- files = get_files_by_tag_patterns(patterns, root)
514
+ for path in get_files_by_tag_patterns(patterns, root):
515
+ is_dir = path.endswith("/")
516
+ if is_dir:
517
+ folder_count += 1
518
+ name = path.rstrip("/")
519
+ else:
520
+ file_count += 1
521
+ name = path
522
+ entries.append({"path": path, "name": name, "is_dir": is_dir})
513
523
 
514
524
  self.render(
515
525
  "tagged_files.html",
516
526
  tag_name=tag_name,
517
527
  patterns=patterns,
518
- files=files,
528
+ entries=entries,
529
+ file_count=file_count,
530
+ folder_count=folder_count,
519
531
  all_tags=all_tags,
520
532
  user=self.current_user,
533
+ get_file_icon=get_file_icon,
521
534
  )
522
535
 
523
536
 
@@ -89,6 +89,7 @@ from aird.handlers.api_handlers import (
89
89
  FeatureFlagAPIHandler,
90
90
  FeatureFlagSocketHandler,
91
91
  FileListAPIHandler,
92
+ FolderSizeAPIHandler,
92
93
  FileStreamHandler,
93
94
  ShareDetailsAPIHandler,
94
95
  ShareDetailsByIdAPIHandler,
@@ -105,7 +106,6 @@ from aird.handlers.auth_handlers import (
105
106
  MandatoryPasswordHandler,
106
107
  ProfileHandler,
107
108
  )
108
- from aird.handlers.folder_size_ws_handlers import FolderSizeWebSocketHandler
109
109
  from aird.handlers.transfer_ws_handlers import FileTransferWebSocketHandler
110
110
  from aird.handlers.file_op_handlers import (
111
111
  CloudUploadHandler,
@@ -237,7 +237,7 @@ def make_app(
237
237
  (r"/ws/policy-decisions", PolicyDecisionsWebSocket),
238
238
  (r"/stream/(.*)", FileStreamHandler),
239
239
  (r"/ws/file-transfer", FileTransferWebSocketHandler),
240
- (r"/ws/folder-sizes", FolderSizeWebSocketHandler),
240
+ (r"/api/folder-size", FolderSizeAPIHandler),
241
241
  (r"/features", FeatureFlagSocketHandler),
242
242
  (r"/api/features", FeatureFlagAPIHandler),
243
243
  (r"/upload", UploadHandler),