django-nativemojo 0.1.10__py3-none-any.whl → 0.1.16__py3-none-any.whl

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 (276) hide show
  1. django_nativemojo-0.1.16.dist-info/METADATA +138 -0
  2. django_nativemojo-0.1.16.dist-info/RECORD +302 -0
  3. mojo/__init__.py +1 -1
  4. mojo/apps/account/management/__init__.py +5 -0
  5. mojo/apps/account/management/commands/__init__.py +6 -0
  6. mojo/apps/account/management/commands/serializer_admin.py +651 -0
  7. mojo/apps/account/migrations/0004_user_avatar.py +20 -0
  8. mojo/apps/account/migrations/0005_group_last_activity.py +18 -0
  9. mojo/apps/account/migrations/0006_add_device_tracking_models.py +72 -0
  10. mojo/apps/account/migrations/0007_delete_userdevicelocation.py +16 -0
  11. mojo/apps/account/migrations/0008_userdevicelocation.py +33 -0
  12. mojo/apps/account/migrations/0009_geolocatedip_subnet.py +18 -0
  13. mojo/apps/account/migrations/0010_group_avatar.py +20 -0
  14. mojo/apps/account/migrations/0011_user_org_registereddevice_pushconfig_and_more.py +118 -0
  15. mojo/apps/account/migrations/0012_remove_pushconfig_apns_key_file_and_more.py +21 -0
  16. mojo/apps/account/migrations/0013_pushconfig_test_mode_alter_pushconfig_apns_enabled_and_more.py +28 -0
  17. mojo/apps/account/migrations/0014_notificationdelivery_data_payload_and_more.py +48 -0
  18. mojo/apps/account/models/__init__.py +2 -0
  19. mojo/apps/account/models/device.py +281 -0
  20. mojo/apps/account/models/group.py +319 -15
  21. mojo/apps/account/models/member.py +29 -5
  22. mojo/apps/account/models/push/__init__.py +4 -0
  23. mojo/apps/account/models/push/config.py +112 -0
  24. mojo/apps/account/models/push/delivery.py +93 -0
  25. mojo/apps/account/models/push/device.py +66 -0
  26. mojo/apps/account/models/push/template.py +99 -0
  27. mojo/apps/account/models/user.py +369 -19
  28. mojo/apps/account/rest/__init__.py +2 -0
  29. mojo/apps/account/rest/device.py +39 -0
  30. mojo/apps/account/rest/group.py +9 -0
  31. mojo/apps/account/rest/push.py +187 -0
  32. mojo/apps/account/rest/user.py +100 -6
  33. mojo/apps/account/services/__init__.py +1 -0
  34. mojo/apps/account/services/push.py +363 -0
  35. mojo/apps/aws/migrations/0001_initial.py +206 -0
  36. mojo/apps/aws/migrations/0002_emaildomain_can_recv_emaildomain_can_send_and_more.py +28 -0
  37. mojo/apps/aws/migrations/0003_mailbox_is_domain_default_mailbox_is_system_default_and_more.py +31 -0
  38. mojo/apps/aws/migrations/0004_s3bucket.py +39 -0
  39. mojo/apps/aws/migrations/0005_alter_emaildomain_region_delete_s3bucket.py +21 -0
  40. mojo/apps/aws/models/__init__.py +19 -0
  41. mojo/apps/aws/models/email_attachment.py +99 -0
  42. mojo/apps/aws/models/email_domain.py +218 -0
  43. mojo/apps/aws/models/email_template.py +132 -0
  44. mojo/apps/aws/models/incoming_email.py +197 -0
  45. mojo/apps/aws/models/mailbox.py +288 -0
  46. mojo/apps/aws/models/sent_message.py +175 -0
  47. mojo/apps/aws/rest/__init__.py +7 -0
  48. mojo/apps/aws/rest/email.py +33 -0
  49. mojo/apps/aws/rest/email_ops.py +183 -0
  50. mojo/apps/aws/rest/messages.py +32 -0
  51. mojo/apps/aws/rest/s3.py +64 -0
  52. mojo/apps/aws/rest/send.py +101 -0
  53. mojo/apps/aws/rest/sns.py +403 -0
  54. mojo/apps/aws/rest/templates.py +19 -0
  55. mojo/apps/aws/services/__init__.py +32 -0
  56. mojo/apps/aws/services/email.py +390 -0
  57. mojo/apps/aws/services/email_ops.py +548 -0
  58. mojo/apps/docit/__init__.py +6 -0
  59. mojo/apps/docit/markdown_plugins/syntax_highlight.py +25 -0
  60. mojo/apps/docit/markdown_plugins/toc.py +12 -0
  61. mojo/apps/docit/migrations/0001_initial.py +113 -0
  62. mojo/apps/docit/migrations/0002_alter_book_modified_by_alter_page_modified_by.py +26 -0
  63. mojo/apps/docit/migrations/0003_alter_book_group.py +20 -0
  64. mojo/apps/docit/models/__init__.py +17 -0
  65. mojo/apps/docit/models/asset.py +231 -0
  66. mojo/apps/docit/models/book.py +227 -0
  67. mojo/apps/docit/models/page.py +319 -0
  68. mojo/apps/docit/models/page_revision.py +203 -0
  69. mojo/apps/docit/rest/__init__.py +10 -0
  70. mojo/apps/docit/rest/asset.py +17 -0
  71. mojo/apps/docit/rest/book.py +22 -0
  72. mojo/apps/docit/rest/page.py +22 -0
  73. mojo/apps/docit/rest/page_revision.py +17 -0
  74. mojo/apps/docit/services/__init__.py +11 -0
  75. mojo/apps/docit/services/docit.py +315 -0
  76. mojo/apps/docit/services/markdown.py +44 -0
  77. mojo/apps/fileman/README.md +8 -8
  78. mojo/apps/fileman/backends/base.py +76 -70
  79. mojo/apps/fileman/backends/filesystem.py +86 -86
  80. mojo/apps/fileman/backends/s3.py +409 -108
  81. mojo/apps/fileman/migrations/0001_initial.py +106 -0
  82. mojo/apps/fileman/migrations/0002_filemanager_parent_alter_filemanager_max_file_size.py +24 -0
  83. mojo/apps/fileman/migrations/0003_remove_file_fileman_fil_upload__c4bc35_idx_and_more.py +25 -0
  84. mojo/apps/fileman/migrations/0004_remove_file_original_filename_and_more.py +39 -0
  85. mojo/apps/fileman/migrations/0005_alter_file_upload_token.py +18 -0
  86. mojo/apps/fileman/migrations/0006_file_download_url_filemanager_forever_urls.py +23 -0
  87. mojo/apps/fileman/migrations/0007_remove_filemanager_forever_urls_and_more.py +22 -0
  88. mojo/apps/fileman/migrations/0008_file_category.py +18 -0
  89. mojo/apps/fileman/migrations/0009_rename_file_path_file_storage_file_path.py +18 -0
  90. mojo/apps/fileman/migrations/0010_filerendition.py +33 -0
  91. mojo/apps/fileman/migrations/0011_alter_filerendition_original_file.py +19 -0
  92. mojo/apps/fileman/models/__init__.py +1 -5
  93. mojo/apps/fileman/models/file.py +240 -58
  94. mojo/apps/fileman/models/manager.py +427 -31
  95. mojo/apps/fileman/models/rendition.py +118 -0
  96. mojo/apps/fileman/renderer/__init__.py +111 -0
  97. mojo/apps/fileman/renderer/audio.py +403 -0
  98. mojo/apps/fileman/renderer/base.py +205 -0
  99. mojo/apps/fileman/renderer/document.py +404 -0
  100. mojo/apps/fileman/renderer/image.py +222 -0
  101. mojo/apps/fileman/renderer/utils.py +297 -0
  102. mojo/apps/fileman/renderer/video.py +304 -0
  103. mojo/apps/fileman/rest/__init__.py +1 -18
  104. mojo/apps/fileman/rest/upload.py +22 -32
  105. mojo/apps/fileman/signals.py +58 -0
  106. mojo/apps/fileman/tasks.py +254 -0
  107. mojo/apps/fileman/utils/__init__.py +40 -16
  108. mojo/apps/incident/migrations/0005_incidenthistory.py +39 -0
  109. mojo/apps/incident/migrations/0006_alter_incident_state.py +18 -0
  110. mojo/apps/incident/migrations/0007_event_uid.py +18 -0
  111. mojo/apps/incident/migrations/0008_ticket_ticketnote.py +55 -0
  112. mojo/apps/incident/migrations/0009_incident_status.py +18 -0
  113. mojo/apps/incident/migrations/0010_event_country_code.py +18 -0
  114. mojo/apps/incident/migrations/0011_incident_country_code.py +18 -0
  115. mojo/apps/incident/migrations/0012_alter_incident_status.py +18 -0
  116. mojo/apps/incident/models/__init__.py +2 -0
  117. mojo/apps/incident/models/event.py +35 -0
  118. mojo/apps/incident/models/history.py +36 -0
  119. mojo/apps/incident/models/incident.py +3 -1
  120. mojo/apps/incident/models/ticket.py +62 -0
  121. mojo/apps/incident/reporter.py +21 -1
  122. mojo/apps/incident/rest/__init__.py +1 -0
  123. mojo/apps/incident/rest/event.py +7 -1
  124. mojo/apps/incident/rest/ticket.py +43 -0
  125. mojo/apps/jobs/__init__.py +489 -0
  126. mojo/apps/jobs/adapters.py +24 -0
  127. mojo/apps/jobs/cli.py +616 -0
  128. mojo/apps/jobs/daemon.py +370 -0
  129. mojo/apps/jobs/examples/sample_jobs.py +376 -0
  130. mojo/apps/jobs/examples/webhook_examples.py +203 -0
  131. mojo/apps/jobs/handlers/__init__.py +5 -0
  132. mojo/apps/jobs/handlers/webhook.py +317 -0
  133. mojo/apps/jobs/job_engine.py +734 -0
  134. mojo/apps/jobs/keys.py +203 -0
  135. mojo/apps/jobs/local_queue.py +363 -0
  136. mojo/apps/jobs/management/__init__.py +3 -0
  137. mojo/apps/jobs/management/commands/__init__.py +3 -0
  138. mojo/apps/jobs/manager.py +1327 -0
  139. mojo/apps/jobs/migrations/0001_initial.py +97 -0
  140. mojo/apps/jobs/migrations/0002_alter_job_max_retries_joblog.py +39 -0
  141. mojo/apps/jobs/models/__init__.py +6 -0
  142. mojo/apps/jobs/models/job.py +441 -0
  143. mojo/apps/jobs/rest/__init__.py +2 -0
  144. mojo/apps/jobs/rest/control.py +466 -0
  145. mojo/apps/jobs/rest/jobs.py +421 -0
  146. mojo/apps/jobs/scheduler.py +571 -0
  147. mojo/apps/jobs/services/__init__.py +6 -0
  148. mojo/apps/jobs/services/job_actions.py +465 -0
  149. mojo/apps/jobs/settings.py +209 -0
  150. mojo/apps/logit/migrations/0004_alter_log_level.py +18 -0
  151. mojo/apps/logit/models/log.py +7 -1
  152. mojo/apps/metrics/__init__.py +8 -1
  153. mojo/apps/metrics/redis_metrics.py +198 -0
  154. mojo/apps/metrics/rest/__init__.py +3 -0
  155. mojo/apps/metrics/rest/categories.py +266 -0
  156. mojo/apps/metrics/rest/helpers.py +48 -0
  157. mojo/apps/metrics/rest/permissions.py +99 -0
  158. mojo/apps/metrics/rest/values.py +277 -0
  159. mojo/apps/metrics/utils.py +19 -2
  160. mojo/decorators/auth.py +6 -1
  161. mojo/decorators/http.py +47 -3
  162. mojo/helpers/aws/__init__.py +45 -0
  163. mojo/helpers/aws/ec2.py +804 -0
  164. mojo/helpers/aws/iam.py +748 -0
  165. mojo/helpers/aws/inbound_email.py +309 -0
  166. mojo/helpers/aws/kms.py +413 -0
  167. mojo/helpers/aws/s3.py +451 -11
  168. mojo/helpers/aws/ses.py +483 -0
  169. mojo/helpers/aws/ses_domain.py +959 -0
  170. mojo/helpers/aws/sns.py +461 -0
  171. mojo/helpers/crypto/__init__.py +1 -1
  172. mojo/helpers/crypto/utils.py +15 -0
  173. mojo/helpers/dates.py +18 -0
  174. mojo/helpers/location/__init__.py +2 -0
  175. mojo/helpers/location/countries.py +262 -0
  176. mojo/helpers/location/geolocation.py +196 -0
  177. mojo/helpers/logit.py +37 -0
  178. mojo/helpers/redis/__init__.py +2 -0
  179. mojo/helpers/redis/adapter.py +606 -0
  180. mojo/helpers/redis/client.py +48 -0
  181. mojo/helpers/redis/pool.py +225 -0
  182. mojo/helpers/request.py +8 -0
  183. mojo/helpers/response.py +14 -2
  184. mojo/helpers/settings/__init__.py +2 -0
  185. mojo/helpers/{settings.py → settings/helper.py} +1 -37
  186. mojo/helpers/settings/parser.py +132 -0
  187. mojo/middleware/auth.py +1 -1
  188. mojo/middleware/cors.py +40 -0
  189. mojo/middleware/logging.py +131 -12
  190. mojo/middleware/mojo.py +10 -0
  191. mojo/models/rest.py +494 -65
  192. mojo/models/secrets.py +98 -3
  193. mojo/serializers/__init__.py +106 -0
  194. mojo/serializers/core/__init__.py +90 -0
  195. mojo/serializers/core/cache/__init__.py +121 -0
  196. mojo/serializers/core/cache/backends.py +518 -0
  197. mojo/serializers/core/cache/base.py +102 -0
  198. mojo/serializers/core/cache/disabled.py +181 -0
  199. mojo/serializers/core/cache/memory.py +287 -0
  200. mojo/serializers/core/cache/redis.py +533 -0
  201. mojo/serializers/core/cache/utils.py +454 -0
  202. mojo/serializers/core/manager.py +550 -0
  203. mojo/serializers/core/serializer.py +475 -0
  204. mojo/serializers/examples/settings.py +322 -0
  205. mojo/serializers/formats/csv.py +393 -0
  206. mojo/serializers/formats/localizers.py +509 -0
  207. mojo/serializers/{models.py → simple.py} +38 -15
  208. mojo/serializers/suggested_improvements.md +388 -0
  209. testit/client.py +1 -1
  210. testit/helpers.py +35 -4
  211. testit/runner.py +23 -6
  212. django_nativemojo-0.1.10.dist-info/METADATA +0 -96
  213. django_nativemojo-0.1.10.dist-info/RECORD +0 -194
  214. mojo/apps/metrics/rest/db.py +0 -0
  215. mojo/apps/notify/README.md +0 -91
  216. mojo/apps/notify/README_NOTIFICATIONS.md +0 -566
  217. mojo/apps/notify/admin.py +0 -52
  218. mojo/apps/notify/handlers/example_handlers.py +0 -516
  219. mojo/apps/notify/handlers/ses/__init__.py +0 -25
  220. mojo/apps/notify/handlers/ses/bounce.py +0 -0
  221. mojo/apps/notify/handlers/ses/complaint.py +0 -25
  222. mojo/apps/notify/handlers/ses/message.py +0 -86
  223. mojo/apps/notify/management/commands/__init__.py +0 -1
  224. mojo/apps/notify/management/commands/process_notifications.py +0 -370
  225. mojo/apps/notify/mod +0 -0
  226. mojo/apps/notify/models/__init__.py +0 -12
  227. mojo/apps/notify/models/account.py +0 -128
  228. mojo/apps/notify/models/attachment.py +0 -24
  229. mojo/apps/notify/models/bounce.py +0 -68
  230. mojo/apps/notify/models/complaint.py +0 -40
  231. mojo/apps/notify/models/inbox.py +0 -113
  232. mojo/apps/notify/models/inbox_message.py +0 -173
  233. mojo/apps/notify/models/outbox.py +0 -129
  234. mojo/apps/notify/models/outbox_message.py +0 -288
  235. mojo/apps/notify/models/template.py +0 -30
  236. mojo/apps/notify/providers/aws.py +0 -73
  237. mojo/apps/notify/rest/ses.py +0 -0
  238. mojo/apps/notify/utils/__init__.py +0 -2
  239. mojo/apps/notify/utils/notifications.py +0 -404
  240. mojo/apps/notify/utils/parsing.py +0 -202
  241. mojo/apps/notify/utils/render.py +0 -144
  242. mojo/apps/tasks/README.md +0 -118
  243. mojo/apps/tasks/__init__.py +0 -11
  244. mojo/apps/tasks/manager.py +0 -489
  245. mojo/apps/tasks/rest/__init__.py +0 -2
  246. mojo/apps/tasks/rest/hooks.py +0 -0
  247. mojo/apps/tasks/rest/tasks.py +0 -62
  248. mojo/apps/tasks/runner.py +0 -174
  249. mojo/apps/tasks/tq_handlers.py +0 -14
  250. mojo/helpers/aws/setup_email.py +0 -0
  251. mojo/helpers/redis.py +0 -10
  252. mojo/models/meta.py +0 -262
  253. mojo/ws4redis/README.md +0 -174
  254. mojo/ws4redis/__init__.py +0 -2
  255. mojo/ws4redis/client.py +0 -283
  256. mojo/ws4redis/connection.py +0 -327
  257. mojo/ws4redis/exceptions.py +0 -32
  258. mojo/ws4redis/redis.py +0 -183
  259. mojo/ws4redis/servers/base.py +0 -86
  260. mojo/ws4redis/servers/django.py +0 -171
  261. mojo/ws4redis/servers/uwsgi.py +0 -63
  262. mojo/ws4redis/settings.py +0 -45
  263. mojo/ws4redis/utf8validator.py +0 -128
  264. mojo/ws4redis/websocket.py +0 -403
  265. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/LICENSE +0 -0
  266. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/NOTICE +0 -0
  267. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/WHEEL +0 -0
  268. /mojo/apps/{notify → aws}/__init__.py +0 -0
  269. /mojo/apps/{notify/handlers → aws/migrations}/__init__.py +0 -0
  270. /mojo/apps/{notify/management → docit/markdown_plugins}/__init__.py +0 -0
  271. /mojo/apps/{notify/providers → docit/migrations}/__init__.py +0 -0
  272. /mojo/apps/{notify/rest → fileman/migrations}/__init__.py +0 -0
  273. /mojo/{ws4redis/servers → apps/jobs/examples}/__init__.py +0 -0
  274. /mojo/apps/{fileman/models/render.py → jobs/migrations/__init__.py} +0 -0
  275. /mojo/{serializers → rest}/openapi.py +0 -0
  276. /mojo/{apps/fileman/rest/__init__ → serializers/formats/__init__.py} +0 -0
@@ -14,54 +14,54 @@ class FileSystemStorageBackend(StorageBackend):
14
14
  """
15
15
  Local file system storage backend implementation
16
16
  """
17
-
17
+
18
18
  def __init__(self, file_manager, **kwargs):
19
19
  super().__init__(file_manager, **kwargs)
20
-
20
+
21
21
  # File system configuration
22
22
  self.base_path = self.get_setting('base_path', '/tmp/fileman')
23
23
  self.base_url = self.get_setting('base_url', '/media/')
24
24
  self.create_directories = self.get_setting('create_directories', True)
25
25
  self.permissions = self.get_setting('permissions', 0o644)
26
26
  self.directory_permissions = self.get_setting('directory_permissions', 0o755)
27
-
28
- # Upload configuration
27
+
28
+ # Upload configuration
29
29
  self.upload_expires_in = self.get_setting('upload_expires_in', 3600) # 1 hour
30
30
  self.temp_upload_path = self.get_setting('temp_upload_path', os.path.join(self.base_path, 'uploads'))
31
-
31
+
32
32
  # Ensure base paths exist
33
33
  if self.create_directories:
34
34
  os.makedirs(self.base_path, mode=self.directory_permissions, exist_ok=True)
35
35
  os.makedirs(self.temp_upload_path, mode=self.directory_permissions, exist_ok=True)
36
-
36
+
37
37
  def _get_full_path(self, file_path: str) -> str:
38
38
  """Get the full file system path for a file"""
39
39
  # Normalize the path to prevent directory traversal
40
40
  normalized_path = os.path.normpath(file_path.lstrip('/'))
41
-
41
+
42
42
  # Ensure the path doesn't escape the base directory
43
43
  full_path = os.path.join(self.base_path, normalized_path)
44
44
  if not full_path.startswith(self.base_path):
45
45
  raise ValueError(f"Invalid file path: {file_path}")
46
-
46
+
47
47
  return full_path
48
-
48
+
49
49
  def _ensure_directory(self, file_path: str):
50
50
  """Ensure the directory for a file path exists"""
51
51
  directory = os.path.dirname(file_path)
52
52
  if directory and not os.path.exists(directory):
53
53
  os.makedirs(directory, mode=self.directory_permissions, exist_ok=True)
54
-
55
- def save(self, file_obj, filename: str, **kwargs) -> str:
54
+
55
+ def save(self, file_obj, file_path: str, content_type: Optional[str] = None, metadata: Optional[dict] = None) -> str:
56
56
  """Save a file to the local file system"""
57
57
  try:
58
58
  # Generate file path
59
- file_path = self.generate_file_path(filename, kwargs.get('group_id'))
59
+ file_path = self.generate_file_path(file_path)
60
60
  full_path = self._get_full_path(file_path)
61
-
61
+
62
62
  # Ensure directory exists
63
63
  self._ensure_directory(full_path)
64
-
64
+
65
65
  # Save the file
66
66
  with open(full_path, 'wb') as dest:
67
67
  if hasattr(file_obj, 'read'):
@@ -71,22 +71,22 @@ class FileSystemStorageBackend(StorageBackend):
71
71
  else:
72
72
  # Bytes data
73
73
  dest.write(file_obj)
74
-
74
+
75
75
  # Set file permissions
76
76
  os.chmod(full_path, self.permissions)
77
-
77
+
78
78
  return file_path
79
-
79
+
80
80
  except Exception as e:
81
81
  raise Exception(f"Failed to save file to filesystem: {e}")
82
-
82
+
83
83
  def delete(self, file_path: str) -> bool:
84
84
  """Delete a file from the file system"""
85
85
  try:
86
86
  full_path = self._get_full_path(file_path)
87
87
  if os.path.exists(full_path):
88
88
  os.remove(full_path)
89
-
89
+
90
90
  # Try to remove empty parent directories
91
91
  parent_dir = os.path.dirname(full_path)
92
92
  while parent_dir != self.base_path:
@@ -98,12 +98,12 @@ class FileSystemStorageBackend(StorageBackend):
98
98
  break
99
99
  except OSError:
100
100
  break
101
-
101
+
102
102
  return True
103
103
  return False
104
104
  except Exception:
105
105
  return False
106
-
106
+
107
107
  def exists(self, file_path: str) -> bool:
108
108
  """Check if a file exists in the file system"""
109
109
  try:
@@ -111,7 +111,7 @@ class FileSystemStorageBackend(StorageBackend):
111
111
  return os.path.isfile(full_path)
112
112
  except Exception:
113
113
  return False
114
-
114
+
115
115
  def get_file_size(self, file_path: str) -> Optional[int]:
116
116
  """Get the size of a file in bytes"""
117
117
  try:
@@ -121,16 +121,16 @@ class FileSystemStorageBackend(StorageBackend):
121
121
  return None
122
122
  except Exception:
123
123
  return None
124
-
124
+
125
125
  def get_url(self, file_path: str, expires_in: Optional[int] = None) -> str:
126
126
  """Get a URL to access the file"""
127
127
  # For file system, we just return a static URL
128
128
  # In a real implementation, you might want to generate signed URLs
129
129
  # or check permissions here
130
130
  return urljoin(self.base_url, file_path)
131
-
132
- def generate_upload_url(self, file_path: str, content_type: str,
133
- file_size: Optional[int] = None,
131
+
132
+ def generate_upload_url(self, file_path: str, content_type: str,
133
+ file_size: Optional[int] = None,
134
134
  expires_in: int = 3600) -> Dict[str, Any]:
135
135
  """
136
136
  Generate an upload URL for file system backend
@@ -140,12 +140,12 @@ class FileSystemStorageBackend(StorageBackend):
140
140
  try:
141
141
  # Generate upload token
142
142
  upload_token = hashlib.sha256(f"{file_path}{uuid.uuid4()}{datetime.now()}".encode()).hexdigest()[:32]
143
-
143
+
144
144
  # Create temporary upload directory if needed
145
145
  temp_path = os.path.join(self.temp_upload_path, upload_token)
146
146
  if self.create_directories:
147
147
  os.makedirs(temp_path, mode=self.directory_permissions, exist_ok=True)
148
-
148
+
149
149
  # Store upload metadata in a temporary file
150
150
  metadata = {
151
151
  'file_path': file_path,
@@ -154,12 +154,12 @@ class FileSystemStorageBackend(StorageBackend):
154
154
  'expires_at': (datetime.now() + timedelta(seconds=expires_in)).isoformat(),
155
155
  'created_at': datetime.now().isoformat()
156
156
  }
157
-
157
+
158
158
  metadata_path = os.path.join(temp_path, 'metadata.json')
159
159
  import json
160
160
  with open(metadata_path, 'w') as f:
161
161
  json.dump(metadata, f)
162
-
162
+
163
163
  # Return upload information
164
164
  # The upload_url would point to a custom Django view that handles the upload
165
165
  return {
@@ -173,138 +173,138 @@ class FileSystemStorageBackend(StorageBackend):
173
173
  'Content-Type': content_type
174
174
  }
175
175
  }
176
-
176
+
177
177
  except Exception as e:
178
178
  raise Exception(f"Failed to generate upload URL: {e}")
179
-
179
+
180
180
  def validate_upload_token(self, upload_token: str) -> Tuple[bool, Optional[Dict[str, Any]]]:
181
181
  """Validate an upload token and return metadata"""
182
182
  try:
183
183
  temp_path = os.path.join(self.temp_upload_path, upload_token)
184
184
  metadata_path = os.path.join(temp_path, 'metadata.json')
185
-
185
+
186
186
  if not os.path.exists(metadata_path):
187
187
  return False, None
188
-
188
+
189
189
  import json
190
190
  with open(metadata_path, 'r') as f:
191
191
  metadata = json.load(f)
192
-
192
+
193
193
  # Check if expired
194
194
  expires_at = datetime.fromisoformat(metadata['expires_at'])
195
195
  if datetime.now() > expires_at:
196
196
  # Clean up expired token
197
197
  shutil.rmtree(temp_path, ignore_errors=True)
198
198
  return False, None
199
-
199
+
200
200
  return True, metadata
201
-
201
+
202
202
  except Exception:
203
203
  return False, None
204
-
204
+
205
205
  def finalize_upload(self, upload_token: str, uploaded_file_path: str) -> bool:
206
206
  """Move uploaded file from temp location to final location"""
207
207
  try:
208
208
  is_valid, metadata = self.validate_upload_token(upload_token)
209
209
  if not is_valid or not metadata:
210
210
  return False
211
-
211
+
212
212
  temp_path = os.path.join(self.temp_upload_path, upload_token)
213
213
  temp_file_path = os.path.join(temp_path, 'uploaded_file')
214
-
214
+
215
215
  if not os.path.exists(temp_file_path):
216
216
  return False
217
-
217
+
218
218
  # Move file to final location
219
219
  final_path = self._get_full_path(metadata['file_path'])
220
220
  self._ensure_directory(final_path)
221
-
221
+
222
222
  shutil.move(temp_file_path, final_path)
223
223
  os.chmod(final_path, self.permissions)
224
-
224
+
225
225
  # Clean up temp directory
226
226
  shutil.rmtree(temp_path, ignore_errors=True)
227
-
227
+
228
228
  return True
229
-
229
+
230
230
  except Exception:
231
231
  return False
232
-
232
+
233
233
  def open(self, file_path: str, mode: str = 'rb'):
234
234
  """Open a file from the file system"""
235
235
  full_path = self._get_full_path(file_path)
236
236
  return open(full_path, mode)
237
-
237
+
238
238
  def list_files(self, path_prefix: str = "", limit: int = 1000) -> List[str]:
239
239
  """List files in the file system with optional path prefix"""
240
240
  try:
241
241
  search_path = self._get_full_path(path_prefix) if path_prefix else self.base_path
242
-
242
+
243
243
  files = []
244
244
  for root, dirs, filenames in os.walk(search_path):
245
245
  for filename in filenames:
246
246
  if len(files) >= limit:
247
247
  break
248
-
248
+
249
249
  full_path = os.path.join(root, filename)
250
250
  # Get relative path from base_path
251
251
  rel_path = os.path.relpath(full_path, self.base_path)
252
252
  files.append(rel_path.replace(os.sep, '/')) # Use forward slashes
253
-
253
+
254
254
  if len(files) >= limit:
255
255
  break
256
-
256
+
257
257
  return files[:limit]
258
-
258
+
259
259
  except Exception:
260
260
  return []
261
-
261
+
262
262
  def copy_file(self, source_path: str, dest_path: str) -> bool:
263
263
  """Copy a file within the file system"""
264
264
  try:
265
265
  source_full_path = self._get_full_path(source_path)
266
266
  dest_full_path = self._get_full_path(dest_path)
267
-
267
+
268
268
  if not os.path.exists(source_full_path):
269
269
  return False
270
-
270
+
271
271
  self._ensure_directory(dest_full_path)
272
272
  shutil.copy2(source_full_path, dest_full_path)
273
273
  os.chmod(dest_full_path, self.permissions)
274
-
274
+
275
275
  return True
276
-
276
+
277
277
  except Exception:
278
278
  return False
279
-
279
+
280
280
  def move_file(self, source_path: str, dest_path: str) -> bool:
281
281
  """Move a file within the file system"""
282
282
  try:
283
283
  source_full_path = self._get_full_path(source_path)
284
284
  dest_full_path = self._get_full_path(dest_path)
285
-
285
+
286
286
  if not os.path.exists(source_full_path):
287
287
  return False
288
-
288
+
289
289
  self._ensure_directory(dest_full_path)
290
290
  shutil.move(source_full_path, dest_full_path)
291
291
  os.chmod(dest_full_path, self.permissions)
292
-
292
+
293
293
  return True
294
-
294
+
295
295
  except Exception:
296
296
  return False
297
-
297
+
298
298
  def get_file_metadata(self, file_path: str) -> Dict[str, Any]:
299
299
  """Get comprehensive metadata for a file"""
300
300
  try:
301
301
  full_path = self._get_full_path(file_path)
302
-
302
+
303
303
  if not os.path.exists(full_path):
304
304
  return {'exists': False, 'path': file_path}
305
-
305
+
306
306
  stat = os.stat(full_path)
307
-
307
+
308
308
  metadata = {
309
309
  'exists': True,
310
310
  'path': file_path,
@@ -315,35 +315,35 @@ class FileSystemStorageBackend(StorageBackend):
315
315
  'is_file': os.path.isfile(full_path),
316
316
  'is_directory': os.path.isdir(full_path)
317
317
  }
318
-
318
+
319
319
  return metadata
320
-
320
+
321
321
  except Exception:
322
322
  return {'exists': False, 'path': file_path}
323
-
323
+
324
324
  def cleanup_expired_uploads(self, before_date: Optional[datetime] = None):
325
325
  """Clean up expired upload tokens and temporary files"""
326
326
  if before_date is None:
327
327
  before_date = datetime.now() - timedelta(hours=1)
328
-
328
+
329
329
  try:
330
330
  if not os.path.exists(self.temp_upload_path):
331
331
  return
332
-
332
+
333
333
  for token_dir in os.listdir(self.temp_upload_path):
334
334
  token_path = os.path.join(self.temp_upload_path, token_dir)
335
-
335
+
336
336
  if not os.path.isdir(token_path):
337
337
  continue
338
-
338
+
339
339
  metadata_path = os.path.join(token_path, 'metadata.json')
340
-
340
+
341
341
  try:
342
342
  if os.path.exists(metadata_path):
343
343
  import json
344
344
  with open(metadata_path, 'r') as f:
345
345
  metadata = json.load(f)
346
-
346
+
347
347
  expires_at = datetime.fromisoformat(metadata['expires_at'])
348
348
  if expires_at < before_date:
349
349
  shutil.rmtree(token_path, ignore_errors=True)
@@ -352,14 +352,14 @@ class FileSystemStorageBackend(StorageBackend):
352
352
  stat = os.stat(token_path)
353
353
  if datetime.fromtimestamp(stat.st_mtime) < before_date:
354
354
  shutil.rmtree(token_path, ignore_errors=True)
355
-
355
+
356
356
  except Exception:
357
357
  # If we can't process the directory, skip it
358
358
  continue
359
-
359
+
360
360
  except Exception:
361
361
  pass # Silently ignore cleanup errors
362
-
362
+
363
363
  def get_available_space(self) -> Optional[int]:
364
364
  """Get available disk space in bytes"""
365
365
  try:
@@ -367,14 +367,14 @@ class FileSystemStorageBackend(StorageBackend):
367
367
  return statvfs.f_frsize * statvfs.f_bavail
368
368
  except Exception:
369
369
  return None
370
-
370
+
371
371
  def validate_configuration(self) -> Tuple[bool, List[str]]:
372
372
  """Validate file system configuration"""
373
373
  errors = []
374
-
374
+
375
375
  if not self.base_path:
376
376
  errors.append("Base path is required for file system backend")
377
-
377
+
378
378
  try:
379
379
  # Check if base path is accessible
380
380
  if not os.path.exists(self.base_path):
@@ -382,16 +382,16 @@ class FileSystemStorageBackend(StorageBackend):
382
382
  os.makedirs(self.base_path, mode=self.directory_permissions)
383
383
  else:
384
384
  errors.append(f"Base path does not exist: {self.base_path}")
385
-
385
+
386
386
  # Check write permissions
387
387
  if os.path.exists(self.base_path):
388
388
  if not os.access(self.base_path, os.W_OK):
389
389
  errors.append(f"No write permission for base path: {self.base_path}")
390
-
390
+
391
391
  if not os.access(self.base_path, os.R_OK):
392
392
  errors.append(f"No read permission for base path: {self.base_path}")
393
-
393
+
394
394
  except Exception as e:
395
395
  errors.append(f"Error accessing base path: {e}")
396
-
397
- return len(errors) == 0, errors
396
+
397
+ return len(errors) == 0, errors