aird 0.4.23.dev12__tar.gz → 0.4.23.dev18__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 (214) hide show
  1. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/MANIFEST.in +1 -0
  2. {aird-0.4.23.dev12/aird.egg-info → aird-0.4.23.dev18}/PKG-INFO +2 -1
  3. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/constants/__init__.py +43 -1
  4. aird-0.4.23.dev18/aird/core/folder_size.py +101 -0
  5. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/core/mmap_handler.py +67 -26
  6. aird-0.4.23.dev18/aird/event_loop.py +34 -0
  7. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/handlers/api_handlers.py +53 -0
  8. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/handlers/base_handler.py +1 -1
  9. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/handlers/file_op_handlers.py +1 -1
  10. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/handlers/transfer_ws_handlers.py +33 -15
  11. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/handlers/view_handlers.py +16 -3
  12. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/main.py +6 -2
  13. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/static/css/app.css +1 -1
  14. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/static/js/browse/app.js +72 -232
  15. aird-0.4.23.dev18/aird/static/js/download-manager.js +151 -0
  16. aird-0.4.23.dev18/aird/static/js/file-transfer-http.js +98 -0
  17. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/static/js/file-transfer-ws.js +40 -3
  18. aird-0.4.23.dev18/aird/static/js/file-transfer.js +55 -0
  19. aird-0.4.23.dev18/aird/static/js/folder-size-scan.js +188 -0
  20. aird-0.4.23.dev18/aird/static/js/transfer-tracker.js +315 -0
  21. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/templates/_app_nav_header.html +32 -3
  22. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/templates/_bg_canvas.html +1 -1
  23. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/templates/admin.html +1 -1
  24. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/templates/admin_audit.html +4 -4
  25. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/templates/admin_ldap.html +3 -3
  26. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/templates/admin_login.html +4 -4
  27. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/templates/admin_network_shares.html +3 -3
  28. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/templates/admin_policies.html +4 -4
  29. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/templates/admin_tags.html +4 -4
  30. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/templates/admin_user_attributes.html +4 -4
  31. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/templates/admin_users.html +3 -3
  32. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/templates/browse.html +16 -12
  33. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/templates/directory.html +3 -3
  34. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/templates/edit.html +4 -4
  35. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/templates/error.html +3 -3
  36. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/templates/file.html +4 -4
  37. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/templates/ldap_config_create.html +97 -97
  38. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/templates/ldap_config_edit.html +99 -99
  39. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/templates/login.html +4 -4
  40. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/templates/media_view.html +3 -3
  41. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/templates/p2p_transfer.html +11 -11
  42. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/templates/profile.html +3 -3
  43. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/templates/share.html +2 -2
  44. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/templates/shared_list.html +3 -7
  45. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/templates/super_search.html +3 -3
  46. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/templates/tagged_files.html +32 -10
  47. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/templates/token_verification.html +3 -3
  48. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/templates/user_create.html +75 -75
  49. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/templates/user_edit.html +116 -116
  50. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/utils/util.py +26 -5
  51. {aird-0.4.23.dev12 → aird-0.4.23.dev18/aird.egg-info}/PKG-INFO +2 -1
  52. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird.egg-info/SOURCES.txt +4 -1
  53. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird.egg-info/requires.txt +3 -0
  54. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/setup.py +2 -1
  55. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/tests/test_folder_size.py +7 -1
  56. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/tests/test_mmap_handler.py +42 -4
  57. aird-0.4.23.dev18/tests/test_wheel_static_assets.py +112 -0
  58. aird-0.4.23.dev12/aird/core/folder_size.py +0 -51
  59. aird-0.4.23.dev12/aird/handlers/folder_size_ws_handlers.py +0 -193
  60. aird-0.4.23.dev12/aird/static/js/download-manager.js +0 -309
  61. aird-0.4.23.dev12/aird/static/js/folder-size-scan.js +0 -115
  62. aird-0.4.23.dev12/tests/test_wheel_static_assets.py +0 -48
  63. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/LICENSE +0 -0
  64. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/README.md +0 -0
  65. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/__init__.py +0 -0
  66. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/__main__.py +0 -0
  67. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/app_context.py +0 -0
  68. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/cli/__init__.py +0 -0
  69. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/cli/__main__.py +0 -0
  70. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/cli/authelia.py +0 -0
  71. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/cli/config.py +0 -0
  72. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/cli/main.py +0 -0
  73. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/cli/session.py +0 -0
  74. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/cloud/__init__.py +0 -0
  75. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/config.py +0 -0
  76. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/constants/admin.py +0 -0
  77. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/constants/file_ops.py +0 -0
  78. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/constants/input_limits.py +0 -0
  79. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/constants/media.py +0 -0
  80. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/core/__init__.py +0 -0
  81. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/core/events.py +0 -0
  82. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/core/file_operations.py +0 -0
  83. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/core/filter_expression.py +0 -0
  84. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/core/input_validation.py +0 -0
  85. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/core/security.py +0 -0
  86. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/core/share_root.py +0 -0
  87. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/core/websocket_manager.py +0 -0
  88. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/core/zip_download.py +0 -0
  89. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/database/__init__.py +0 -0
  90. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/database/db.py +0 -0
  91. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/database/feature_flags.py +0 -0
  92. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/database/ldap.py +0 -0
  93. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/db/__init__.py +0 -0
  94. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/db/audit.py +0 -0
  95. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/db/config.py +0 -0
  96. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/db/favorites.py +0 -0
  97. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/db/network_shares.py +0 -0
  98. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/db/policies.py +0 -0
  99. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/db/policy_decisions.py +0 -0
  100. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/db/policy_seeds.py +0 -0
  101. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/db/quota.py +0 -0
  102. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/db/resource_tags.py +0 -0
  103. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/db/schema.py +0 -0
  104. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/db/shares.py +0 -0
  105. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/db/user_attributes.py +0 -0
  106. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/db/users.py +0 -0
  107. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/domain/__init__.py +0 -0
  108. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/domain/contracts.py +0 -0
  109. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/domain/models.py +0 -0
  110. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/email/__init__.py +0 -0
  111. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/email/brevo.py +0 -0
  112. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/email/resolve.py +0 -0
  113. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/handlers/__init__.py +0 -0
  114. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/handlers/abac_handlers.py +0 -0
  115. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/handlers/admin_handlers.py +0 -0
  116. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/handlers/auth_handlers.py +0 -0
  117. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/handlers/constants.py +0 -0
  118. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/handlers/health_handler.py +0 -0
  119. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/handlers/p2p_handlers.py +0 -0
  120. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/handlers/share_handlers.py +0 -0
  121. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/network_share_manager.py +0 -0
  122. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/server_runtime.py +0 -0
  123. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/services/__init__.py +0 -0
  124. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/services/audit_service.py +0 -0
  125. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/services/config_service.py +0 -0
  126. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/services/email_service.py +0 -0
  127. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/services/email_subscriber.py +0 -0
  128. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/services/event_subscribers.py +0 -0
  129. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/services/favorites_service.py +0 -0
  130. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/services/network_share_service.py +0 -0
  131. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/services/p2p_service.py +0 -0
  132. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/services/policy_service.py +0 -0
  133. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/services/quota_service.py +0 -0
  134. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/services/share_service.py +0 -0
  135. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/services/tag_service.py +0 -0
  136. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/services/user_service.py +0 -0
  137. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/sql_identifiers.py +0 -0
  138. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/static/favicon.png +0 -0
  139. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/static/favicon.svg +0 -0
  140. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/static/img/logo-icon.png +0 -0
  141. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/static/img/logo-mark.svg +0 -0
  142. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/static/img/logo-text.png +0 -0
  143. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/static/img/logo.png +0 -0
  144. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/static/js/aird-core.js +0 -0
  145. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/static/js/bg-canvas.js +0 -0
  146. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/static/js/common/command-palette.js +0 -0
  147. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/static/js/components/folder-picker.js +0 -0
  148. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/static/js/feature-flags-live.js +0 -0
  149. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/static/js/login-ui.js +0 -0
  150. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/static/js/media-view.js +0 -0
  151. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/static/js/p2p/app.js +0 -0
  152. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/static/js/p2p/mediator.js +0 -0
  153. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/static/js/p2p/qr-adapter.js +0 -0
  154. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/static/js/p2p/signaling-service.js +0 -0
  155. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/static/js/p2p/state-machine.js +0 -0
  156. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/static/js/p2p/transfer-service.js +0 -0
  157. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/static/js/pages/p2p-page.js +0 -0
  158. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/static/js/pages/super-search.js +0 -0
  159. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/static/js/share/app.js +0 -0
  160. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/static/js/theme.js +0 -0
  161. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/static/js/vendor/pdf.min.js +0 -0
  162. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/static/js/vendor/pdf.worker.min.js +0 -0
  163. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/static/js/vendor/qrcode-browser.js +0 -0
  164. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/templates/_admin_tabs.html +0 -0
  165. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/templates/_theme_early.html +0 -0
  166. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/templates/_theme_login_corner.html +0 -0
  167. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird/utils/__init__.py +0 -0
  168. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird.egg-info/dependency_links.txt +0 -0
  169. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird.egg-info/entry_points.txt +0 -0
  170. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/aird.egg-info/top_level.txt +0 -0
  171. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/setup.cfg +0 -0
  172. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/tests/__init__.py +0 -0
  173. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/tests/conftest.py +0 -0
  174. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/tests/handler_helpers.py +0 -0
  175. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/tests/test_admin_handlers.py +0 -0
  176. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/tests/test_api_handlers.py +0 -0
  177. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/tests/test_architecture_conformance.py +0 -0
  178. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/tests/test_auth_handlers.py +0 -0
  179. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/tests/test_auth_handlers_extended.py +0 -0
  180. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/tests/test_base_handler.py +0 -0
  181. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/tests/test_base_handler_pep.py +0 -0
  182. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/tests/test_cli.py +0 -0
  183. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/tests/test_cloud.py +0 -0
  184. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/tests/test_config.py +0 -0
  185. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/tests/test_core_file_operations.py +0 -0
  186. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/tests/test_database_db.py +0 -0
  187. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/tests/test_database_feature_flags.py +0 -0
  188. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/tests/test_database_ldap.py +0 -0
  189. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/tests/test_database_shares.py +0 -0
  190. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/tests/test_database_users.py +0 -0
  191. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/tests/test_database_users_hashing.py +0 -0
  192. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/tests/test_db.py +0 -0
  193. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/tests/test_email_service.py +0 -0
  194. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/tests/test_file_op_handlers.py +0 -0
  195. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/tests/test_filter_expression.py +0 -0
  196. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/tests/test_main.py +0 -0
  197. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/tests/test_multi_user.py +0 -0
  198. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/tests/test_network_shares.py +0 -0
  199. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/tests/test_p2p_handlers.py +0 -0
  200. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/tests/test_password_hashing.py +0 -0
  201. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/tests/test_policy_service.py +0 -0
  202. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/tests/test_rate_limit.py +0 -0
  203. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/tests/test_security.py +0 -0
  204. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/tests/test_security_comprehensive.py +0 -0
  205. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/tests/test_server_runtime.py +0 -0
  206. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/tests/test_share_handlers.py +0 -0
  207. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/tests/test_share_ownership.py +0 -0
  208. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/tests/test_super_search_handler.py +0 -0
  209. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/tests/test_tag_service.py +0 -0
  210. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/tests/test_transfer_ws_handlers.py +0 -0
  211. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/tests/test_util.py +0 -0
  212. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/tests/test_view_handlers.py +0 -0
  213. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/tests/test_websocket_manager.py +0 -0
  214. {aird-0.4.23.dev12 → aird-0.4.23.dev18}/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.dev18
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
@@ -13,6 +13,7 @@ Requires-Python: >=3.10
13
13
  Description-Content-Type: text/markdown
14
14
  License-File: LICENSE
15
15
  Requires-Dist: tornado>=6.5.1
16
+ Requires-Dist: uvloop>=0.19.0; sys_platform == "linux"
16
17
  Requires-Dist: ldap3>=2.9.1
17
18
  Requires-Dist: aiofiles>=23.0.0
18
19
  Requires-Dist: argon2-cffi>=23.1.0
@@ -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
@@ -62,7 +63,7 @@ UPLOAD_CONFIG = {
62
63
 
63
64
  # File operation constants (derived from UPLOAD_CONFIG at startup)
64
65
  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
+ # HTTP /upload body limit (browser uploads stream POST /upload; CLI may POST whole file)
66
67
  UPLOAD_REQUEST_MAX_BODY_SIZE = MAX_FILE_SIZE + (1024 * 1024)
67
68
  MAX_READABLE_FILE_SIZE = 50 * 1024 * 1024 # 50 MB
68
69
 
@@ -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]:
@@ -0,0 +1,34 @@
1
+ """Optional asyncio event loop tuning (Linux uvloop)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import sys
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ _uvloop_installed = False
11
+
12
+
13
+ def install_uvloop_if_linux() -> bool:
14
+ """Use libuv-backed asyncio on Linux when uvloop is installed."""
15
+ global _uvloop_installed
16
+ if _uvloop_installed:
17
+ return True
18
+ if not sys.platform.startswith("linux"):
19
+ return False
20
+ try:
21
+ import asyncio
22
+
23
+ import uvloop
24
+
25
+ asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
26
+ _uvloop_installed = True
27
+ logger.info("Using uvloop event loop (Linux)")
28
+ return True
29
+ except ImportError:
30
+ logger.debug("uvloop not installed; using default asyncio loop")
31
+ return False
32
+ except Exception:
33
+ logger.warning("Failed to enable uvloop; using default asyncio loop", exc_info=True)
34
+ return False
@@ -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):
@@ -349,7 +349,7 @@ def _process_bulk_action(
349
349
 
350
350
  @tornado.web.stream_request_body
351
351
  class UploadHandler(BaseHandler):
352
- """Single-request HTTP upload (CLI / direct origin). Browser uses WebSocket."""
352
+ """Streamed HTTP upload (browser, CLI, reverse proxy, or direct origin)."""
353
353
 
354
354
  def check_xsrf_cookie(self) -> None:
355
355
  """Streamed uploads send raw body; accept X-XSRFToken header or ?_xsrf= query param."""
@@ -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),
@@ -595,6 +595,10 @@ def _start_server(ssl_options, port: int, hostname: str, worker_count: int) -> N
595
595
 
596
596
 
597
597
  def main():
598
+ from aird.event_loop import install_uvloop_if_linux
599
+
600
+ install_uvloop_if_linux()
601
+
598
602
  print_banner()
599
603
  config.init_config()
600
604