django-nativemojo 0.1.10__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 (194) hide show
  1. django_nativemojo-0.1.10.dist-info/LICENSE +19 -0
  2. django_nativemojo-0.1.10.dist-info/METADATA +96 -0
  3. django_nativemojo-0.1.10.dist-info/NOTICE +8 -0
  4. django_nativemojo-0.1.10.dist-info/RECORD +194 -0
  5. django_nativemojo-0.1.10.dist-info/WHEEL +4 -0
  6. mojo/__init__.py +3 -0
  7. mojo/apps/account/__init__.py +1 -0
  8. mojo/apps/account/admin.py +91 -0
  9. mojo/apps/account/apps.py +16 -0
  10. mojo/apps/account/migrations/0001_initial.py +77 -0
  11. mojo/apps/account/migrations/0002_user_is_email_verified_user_is_phone_verified.py +23 -0
  12. mojo/apps/account/migrations/0003_group_mojo_secrets_user_mojo_secrets.py +23 -0
  13. mojo/apps/account/migrations/__init__.py +0 -0
  14. mojo/apps/account/models/__init__.py +3 -0
  15. mojo/apps/account/models/group.py +98 -0
  16. mojo/apps/account/models/member.py +95 -0
  17. mojo/apps/account/models/pkey.py +18 -0
  18. mojo/apps/account/models/user.py +211 -0
  19. mojo/apps/account/rest/__init__.py +3 -0
  20. mojo/apps/account/rest/group.py +25 -0
  21. mojo/apps/account/rest/user.py +47 -0
  22. mojo/apps/account/utils/__init__.py +0 -0
  23. mojo/apps/account/utils/jwtoken.py +72 -0
  24. mojo/apps/account/utils/passkeys.py +54 -0
  25. mojo/apps/fileman/README.md +549 -0
  26. mojo/apps/fileman/__init__.py +0 -0
  27. mojo/apps/fileman/apps.py +15 -0
  28. mojo/apps/fileman/backends/__init__.py +117 -0
  29. mojo/apps/fileman/backends/base.py +319 -0
  30. mojo/apps/fileman/backends/filesystem.py +397 -0
  31. mojo/apps/fileman/backends/s3.py +398 -0
  32. mojo/apps/fileman/examples/configurations.py +378 -0
  33. mojo/apps/fileman/examples/usage_example.py +665 -0
  34. mojo/apps/fileman/management/__init__.py +1 -0
  35. mojo/apps/fileman/management/commands/__init__.py +1 -0
  36. mojo/apps/fileman/management/commands/cleanup_expired_uploads.py +222 -0
  37. mojo/apps/fileman/models/__init__.py +7 -0
  38. mojo/apps/fileman/models/file.py +292 -0
  39. mojo/apps/fileman/models/manager.py +227 -0
  40. mojo/apps/fileman/models/render.py +0 -0
  41. mojo/apps/fileman/rest/__init__ +0 -0
  42. mojo/apps/fileman/rest/__init__.py +23 -0
  43. mojo/apps/fileman/rest/fileman.py +13 -0
  44. mojo/apps/fileman/rest/upload.py +92 -0
  45. mojo/apps/fileman/utils/__init__.py +19 -0
  46. mojo/apps/fileman/utils/upload.py +616 -0
  47. mojo/apps/incident/__init__.py +1 -0
  48. mojo/apps/incident/handlers/__init__.py +3 -0
  49. mojo/apps/incident/handlers/event_handlers.py +142 -0
  50. mojo/apps/incident/migrations/0001_initial.py +83 -0
  51. mojo/apps/incident/migrations/0002_rename_bundle_ruleset_bundle_minutes_event_hostname_and_more.py +44 -0
  52. mojo/apps/incident/migrations/0003_alter_event_model_id.py +18 -0
  53. mojo/apps/incident/migrations/0004_alter_incident_model_id.py +18 -0
  54. mojo/apps/incident/migrations/__init__.py +0 -0
  55. mojo/apps/incident/models/__init__.py +3 -0
  56. mojo/apps/incident/models/event.py +135 -0
  57. mojo/apps/incident/models/incident.py +33 -0
  58. mojo/apps/incident/models/rule.py +247 -0
  59. mojo/apps/incident/parsers/__init__.py +0 -0
  60. mojo/apps/incident/parsers/ossec/__init__.py +1 -0
  61. mojo/apps/incident/parsers/ossec/core.py +82 -0
  62. mojo/apps/incident/parsers/ossec/parsed.py +23 -0
  63. mojo/apps/incident/parsers/ossec/rules.py +124 -0
  64. mojo/apps/incident/parsers/ossec/utils.py +169 -0
  65. mojo/apps/incident/reporter.py +42 -0
  66. mojo/apps/incident/rest/__init__.py +2 -0
  67. mojo/apps/incident/rest/event.py +23 -0
  68. mojo/apps/incident/rest/ossec.py +22 -0
  69. mojo/apps/logit/__init__.py +0 -0
  70. mojo/apps/logit/admin.py +37 -0
  71. mojo/apps/logit/migrations/0001_initial.py +32 -0
  72. mojo/apps/logit/migrations/0002_log_duid_log_payload_log_username.py +28 -0
  73. mojo/apps/logit/migrations/0003_log_level.py +18 -0
  74. mojo/apps/logit/migrations/__init__.py +0 -0
  75. mojo/apps/logit/models/__init__.py +1 -0
  76. mojo/apps/logit/models/log.py +57 -0
  77. mojo/apps/logit/rest.py +9 -0
  78. mojo/apps/metrics/README.md +79 -0
  79. mojo/apps/metrics/__init__.py +12 -0
  80. mojo/apps/metrics/redis_metrics.py +331 -0
  81. mojo/apps/metrics/rest/__init__.py +1 -0
  82. mojo/apps/metrics/rest/base.py +152 -0
  83. mojo/apps/metrics/rest/db.py +0 -0
  84. mojo/apps/metrics/utils.py +227 -0
  85. mojo/apps/notify/README.md +91 -0
  86. mojo/apps/notify/README_NOTIFICATIONS.md +566 -0
  87. mojo/apps/notify/__init__.py +0 -0
  88. mojo/apps/notify/admin.py +52 -0
  89. mojo/apps/notify/handlers/__init__.py +0 -0
  90. mojo/apps/notify/handlers/example_handlers.py +516 -0
  91. mojo/apps/notify/handlers/ses/__init__.py +25 -0
  92. mojo/apps/notify/handlers/ses/bounce.py +0 -0
  93. mojo/apps/notify/handlers/ses/complaint.py +25 -0
  94. mojo/apps/notify/handlers/ses/message.py +86 -0
  95. mojo/apps/notify/management/__init__.py +0 -0
  96. mojo/apps/notify/management/commands/__init__.py +1 -0
  97. mojo/apps/notify/management/commands/process_notifications.py +370 -0
  98. mojo/apps/notify/mod +0 -0
  99. mojo/apps/notify/models/__init__.py +12 -0
  100. mojo/apps/notify/models/account.py +128 -0
  101. mojo/apps/notify/models/attachment.py +24 -0
  102. mojo/apps/notify/models/bounce.py +68 -0
  103. mojo/apps/notify/models/complaint.py +40 -0
  104. mojo/apps/notify/models/inbox.py +113 -0
  105. mojo/apps/notify/models/inbox_message.py +173 -0
  106. mojo/apps/notify/models/outbox.py +129 -0
  107. mojo/apps/notify/models/outbox_message.py +288 -0
  108. mojo/apps/notify/models/template.py +30 -0
  109. mojo/apps/notify/providers/__init__.py +0 -0
  110. mojo/apps/notify/providers/aws.py +73 -0
  111. mojo/apps/notify/rest/__init__.py +0 -0
  112. mojo/apps/notify/rest/ses.py +0 -0
  113. mojo/apps/notify/utils/__init__.py +2 -0
  114. mojo/apps/notify/utils/notifications.py +404 -0
  115. mojo/apps/notify/utils/parsing.py +202 -0
  116. mojo/apps/notify/utils/render.py +144 -0
  117. mojo/apps/tasks/README.md +118 -0
  118. mojo/apps/tasks/__init__.py +11 -0
  119. mojo/apps/tasks/manager.py +489 -0
  120. mojo/apps/tasks/rest/__init__.py +2 -0
  121. mojo/apps/tasks/rest/hooks.py +0 -0
  122. mojo/apps/tasks/rest/tasks.py +62 -0
  123. mojo/apps/tasks/runner.py +174 -0
  124. mojo/apps/tasks/tq_handlers.py +14 -0
  125. mojo/decorators/__init__.py +3 -0
  126. mojo/decorators/auth.py +25 -0
  127. mojo/decorators/cron.py +31 -0
  128. mojo/decorators/http.py +132 -0
  129. mojo/decorators/validate.py +14 -0
  130. mojo/errors.py +88 -0
  131. mojo/helpers/__init__.py +0 -0
  132. mojo/helpers/aws/__init__.py +0 -0
  133. mojo/helpers/aws/client.py +8 -0
  134. mojo/helpers/aws/s3.py +268 -0
  135. mojo/helpers/aws/setup_email.py +0 -0
  136. mojo/helpers/cron.py +79 -0
  137. mojo/helpers/crypto/__init__.py +4 -0
  138. mojo/helpers/crypto/aes.py +60 -0
  139. mojo/helpers/crypto/hash.py +59 -0
  140. mojo/helpers/crypto/privpub/__init__.py +1 -0
  141. mojo/helpers/crypto/privpub/hybrid.py +97 -0
  142. mojo/helpers/crypto/privpub/rsa.py +104 -0
  143. mojo/helpers/crypto/sign.py +36 -0
  144. mojo/helpers/crypto/too.l.py +25 -0
  145. mojo/helpers/crypto/utils.py +26 -0
  146. mojo/helpers/daemon.py +94 -0
  147. mojo/helpers/dates.py +69 -0
  148. mojo/helpers/dns/__init__.py +0 -0
  149. mojo/helpers/dns/godaddy.py +62 -0
  150. mojo/helpers/filetypes.py +128 -0
  151. mojo/helpers/logit.py +310 -0
  152. mojo/helpers/modules.py +95 -0
  153. mojo/helpers/paths.py +63 -0
  154. mojo/helpers/redis.py +10 -0
  155. mojo/helpers/request.py +89 -0
  156. mojo/helpers/request_parser.py +269 -0
  157. mojo/helpers/response.py +14 -0
  158. mojo/helpers/settings.py +146 -0
  159. mojo/helpers/sysinfo.py +140 -0
  160. mojo/helpers/ua.py +0 -0
  161. mojo/middleware/__init__.py +0 -0
  162. mojo/middleware/auth.py +26 -0
  163. mojo/middleware/logging.py +55 -0
  164. mojo/middleware/mojo.py +21 -0
  165. mojo/migrations/0001_initial.py +32 -0
  166. mojo/migrations/__init__.py +0 -0
  167. mojo/models/__init__.py +2 -0
  168. mojo/models/meta.py +262 -0
  169. mojo/models/rest.py +538 -0
  170. mojo/models/secrets.py +59 -0
  171. mojo/rest/__init__.py +1 -0
  172. mojo/rest/info.py +26 -0
  173. mojo/serializers/__init__.py +0 -0
  174. mojo/serializers/models.py +165 -0
  175. mojo/serializers/openapi.py +188 -0
  176. mojo/urls.py +38 -0
  177. mojo/ws4redis/README.md +174 -0
  178. mojo/ws4redis/__init__.py +2 -0
  179. mojo/ws4redis/client.py +283 -0
  180. mojo/ws4redis/connection.py +327 -0
  181. mojo/ws4redis/exceptions.py +32 -0
  182. mojo/ws4redis/redis.py +183 -0
  183. mojo/ws4redis/servers/__init__.py +0 -0
  184. mojo/ws4redis/servers/base.py +86 -0
  185. mojo/ws4redis/servers/django.py +171 -0
  186. mojo/ws4redis/servers/uwsgi.py +63 -0
  187. mojo/ws4redis/settings.py +45 -0
  188. mojo/ws4redis/utf8validator.py +128 -0
  189. mojo/ws4redis/websocket.py +403 -0
  190. testit/__init__.py +0 -0
  191. testit/client.py +147 -0
  192. testit/faker.py +20 -0
  193. testit/helpers.py +198 -0
  194. testit/runner.py +262 -0
@@ -0,0 +1,616 @@
1
+ from django.http import JsonResponse, HttpResponseRedirect
2
+ from django.shortcuts import get_object_or_404
3
+ from django.conf import settings
4
+ import json
5
+ from datetime import datetime, timedelta
6
+ from typing import List, Dict, Any, Optional, Tuple, Union
7
+ import hashlib
8
+ import uuid
9
+
10
+ from ..models import FileManager, File
11
+ from ..backends import get_backend
12
+
13
+
14
+ def get_file_manager(request, file_manager_id=None, group=None):
15
+ """
16
+ Get FileManager instance based on ID, group, or default
17
+
18
+ Args:
19
+ request: The HTTP request
20
+ file_manager_id: Optional ID of specific file manager
21
+ group: Optional group to find file manager for
22
+
23
+ Returns:
24
+ FileManager instance
25
+
26
+ Raises:
27
+ ValueError: If no active file manager is available
28
+ """
29
+ if file_manager_id:
30
+ return get_object_or_404(
31
+ FileManager.objects.filter(is_active=True),
32
+ id=file_manager_id
33
+ )
34
+
35
+ # Get user's group or use provided group
36
+ user_group = group or getattr(request.user, 'group', None)
37
+
38
+ # Try to get default file manager for the group
39
+ if user_group:
40
+ file_manager = FileManager.objects.filter(
41
+ group=user_group,
42
+ is_default=True,
43
+ is_active=True
44
+ ).first()
45
+
46
+ if file_manager:
47
+ return file_manager
48
+
49
+ # Fall back to global default
50
+ file_manager = FileManager.objects.filter(
51
+ group__isnull=True,
52
+ is_default=True,
53
+ is_active=True
54
+ ).first()
55
+
56
+ if not file_manager:
57
+ # If no default, get any active file manager
58
+ file_manager = FileManager.objects.filter(is_active=True).first()
59
+
60
+ if not file_manager:
61
+ raise ValueError("No active file manager available")
62
+
63
+ return file_manager
64
+
65
+
66
+ def validate_file_request(file_manager, filename, content_type, file_size=None) -> Tuple[bool, str]:
67
+ """
68
+ Validate a file upload request
69
+
70
+ Args:
71
+ file_manager: FileManager instance
72
+ filename: Name of the file
73
+ content_type: MIME type of the file
74
+ file_size: Optional size of the file in bytes
75
+
76
+ Returns:
77
+ Tuple of (is_valid, error_message)
78
+ """
79
+ # Check file manager restrictions
80
+ can_upload, error_msg = file_manager.can_upload_file(filename, file_size)
81
+ if not can_upload:
82
+ return False, error_msg
83
+
84
+ # Check MIME type
85
+ if not file_manager.can_upload_mime_type(content_type):
86
+ return False, f"MIME type '{content_type}' is not allowed"
87
+
88
+ return True, "File upload request is valid"
89
+
90
+
91
+ def initiate_upload(request, data) -> Dict[str, Any]:
92
+ """
93
+ Initiate file upload(s) and return upload URLs and metadata
94
+
95
+ Args:
96
+ request: The HTTP request
97
+ data: JSON data containing file information
98
+
99
+ Returns:
100
+ Dictionary with response data
101
+
102
+ Example request data:
103
+ {
104
+ "files": [
105
+ {
106
+ "filename": "document.pdf",
107
+ "content_type": "application/pdf",
108
+ "size": 1024000
109
+ }
110
+ ],
111
+ "file_manager_id": 123, // optional
112
+ "group_id": 456, // optional
113
+ "metadata": { // optional global metadata
114
+ "source": "web_upload",
115
+ "category": "documents"
116
+ }
117
+ }
118
+ """
119
+ # Parse request data
120
+ files_data = data.get('files', [])
121
+ file_manager_id = data.get('file_manager_id')
122
+ group_id = data.get('group_id')
123
+ global_metadata = data.get('metadata', {})
124
+
125
+ if not files_data:
126
+ return {
127
+ 'success': False,
128
+ 'error': 'No files specified',
129
+ 'status_code': 400
130
+ }
131
+
132
+ # Get file manager
133
+ try:
134
+ file_manager = get_file_manager(
135
+ request,
136
+ file_manager_id=file_manager_id,
137
+ group=getattr(request.user, 'groups', None).filter(id=group_id).first() if group_id else None
138
+ )
139
+ except Exception as e:
140
+ return {
141
+ 'success': False,
142
+ 'error': f'File manager error: {str(e)}',
143
+ 'status_code': 400
144
+ }
145
+
146
+ # Get storage backend
147
+ try:
148
+ backend = get_backend(file_manager)
149
+ except Exception as e:
150
+ return {
151
+ 'success': False,
152
+ 'error': f'Storage backend error: {str(e)}',
153
+ 'status_code': 500
154
+ }
155
+
156
+ # Process each file
157
+ upload_files = []
158
+ errors = []
159
+
160
+ for file_data in files_data:
161
+ filename = file_data.get('filename')
162
+ content_type = file_data.get('content_type')
163
+ file_size = file_data.get('size')
164
+ file_metadata = file_data.get('metadata', {})
165
+
166
+ if not filename or not content_type:
167
+ errors.append({
168
+ 'filename': filename,
169
+ 'error': 'Filename and content_type are required'
170
+ })
171
+ continue
172
+
173
+ # Validate file request
174
+ is_valid, error_msg = validate_file_request(
175
+ file_manager, filename, content_type, file_size
176
+ )
177
+
178
+ if not is_valid:
179
+ errors.append({
180
+ 'filename': filename,
181
+ 'error': error_msg
182
+ })
183
+ continue
184
+
185
+ try:
186
+ # Create File record
187
+ file_obj = File(
188
+ group=file_manager.group,
189
+ uploaded_by=request.user,
190
+ file_manager=file_manager,
191
+ original_filename=filename,
192
+ content_type=content_type,
193
+ file_size=file_size,
194
+ upload_status=File.PENDING
195
+ )
196
+
197
+ # Combine metadata
198
+ combined_metadata = {**global_metadata, **file_metadata}
199
+ if combined_metadata:
200
+ file_obj.metadata = combined_metadata
201
+
202
+ # Generate file path
203
+ file_obj.file_path = backend.generate_file_path(
204
+ file_obj.generate_unique_filename(),
205
+ group_id=file_manager.group.id if file_manager.group else None
206
+ )
207
+
208
+ # Save to generate upload token
209
+ file_obj.save()
210
+
211
+ # Generate upload URL if backend supports it
212
+ upload_info = None
213
+ if backend.supports_direct_upload():
214
+ try:
215
+ upload_info = backend.generate_upload_url(
216
+ file_obj.file_path,
217
+ content_type,
218
+ file_size,
219
+ expires_in=file_manager.get_setting('upload_expires_in', 3600)
220
+ )
221
+
222
+ # Update file record with upload URL and expiration
223
+ file_obj.upload_url = upload_info['upload_url']
224
+ file_obj.upload_expires_at = datetime.now() + timedelta(
225
+ seconds=file_manager.get_setting('upload_expires_in', 3600)
226
+ )
227
+ file_obj.save()
228
+
229
+ except Exception as e:
230
+ errors.append({
231
+ 'filename': filename,
232
+ 'error': f'Failed to generate upload URL: {str(e)}'
233
+ })
234
+ continue
235
+
236
+ # Prepare response data
237
+ file_response = {
238
+ 'id': file_obj.id,
239
+ 'filename': file_obj.filename,
240
+ 'original_filename': file_obj.original_filename,
241
+ 'upload_token': file_obj.upload_token,
242
+ 'file_path': file_obj.file_path,
243
+ 'content_type': file_obj.content_type,
244
+ 'upload_status': file_obj.upload_status
245
+ }
246
+
247
+ if upload_info:
248
+ file_response.update({
249
+ 'upload_url': upload_info['upload_url'],
250
+ 'method': upload_info['method'],
251
+ 'fields': upload_info.get('fields', {}),
252
+ 'headers': upload_info.get('headers', {}),
253
+ 'expires_at': file_obj.upload_expires_at.isoformat() if file_obj.upload_expires_at else None
254
+ })
255
+ else:
256
+ # For backends that don't support direct upload
257
+ file_response.update({
258
+ 'upload_url': f'/fileman/upload/{file_obj.upload_token}/',
259
+ 'method': 'POST',
260
+ 'fields': {},
261
+ 'headers': {}
262
+ })
263
+
264
+ upload_files.append(file_response)
265
+
266
+ except Exception as e:
267
+ errors.append({
268
+ 'filename': filename,
269
+ 'error': f'Failed to initiate upload: {str(e)}'
270
+ })
271
+
272
+ # Prepare response
273
+ response_data = {
274
+ 'success': len(upload_files) > 0,
275
+ 'files': upload_files,
276
+ 'file_manager': {
277
+ 'id': file_manager.id,
278
+ 'name': file_manager.name,
279
+ 'backend_type': file_manager.backend_type,
280
+ 'supports_direct_upload': file_manager.supports_direct_upload
281
+ }
282
+ }
283
+
284
+ if errors:
285
+ response_data['errors'] = errors
286
+
287
+ status_code = 200 if upload_files else 400
288
+ response_data['status_code'] = status_code
289
+
290
+ return response_data
291
+
292
+
293
+ def finalize_upload(request, data) -> Dict[str, Any]:
294
+ """
295
+ Finalize file upload and confirm successful upload
296
+
297
+ Args:
298
+ request: The HTTP request
299
+ data: JSON data containing upload information
300
+
301
+ Returns:
302
+ Dictionary with response data
303
+
304
+ Example request data:
305
+ {
306
+ "upload_token": "abc123...",
307
+ "file_size": 1024000, // optional
308
+ "checksum": "md5:abcdef...", // optional
309
+ "metadata": { // optional additional metadata
310
+ "processing_complete": true
311
+ }
312
+ }
313
+ """
314
+ # Parse request data
315
+ upload_token = data.get('upload_token')
316
+ reported_size = data.get('file_size')
317
+ reported_checksum = data.get('checksum')
318
+ additional_metadata = data.get('metadata', {})
319
+
320
+ if not upload_token:
321
+ return {
322
+ 'success': False,
323
+ 'error': 'Upload token is required',
324
+ 'status_code': 400
325
+ }
326
+
327
+ # Get file record
328
+ try:
329
+ file_obj = File.objects.get(
330
+ upload_token=upload_token,
331
+ is_active=True
332
+ )
333
+ except File.DoesNotExist:
334
+ return {
335
+ 'success': False,
336
+ 'error': 'Invalid upload token',
337
+ 'status_code': 404
338
+ }
339
+
340
+ # Check if upload has expired
341
+ if file_obj.is_upload_expired:
342
+ file_obj.mark_as_expired()
343
+ return {
344
+ 'success': False,
345
+ 'error': 'Upload has expired',
346
+ 'status_code': 410
347
+ }
348
+
349
+ # Check permissions
350
+ if file_obj.uploaded_by != request.user:
351
+ return {
352
+ 'success': False,
353
+ 'error': 'Permission denied',
354
+ 'status_code': 403
355
+ }
356
+
357
+ # Get storage backend
358
+ try:
359
+ backend = get_backend(file_obj.file_manager)
360
+ except Exception as e:
361
+ return {
362
+ 'success': False,
363
+ 'error': f'Storage backend error: {str(e)}',
364
+ 'status_code': 500
365
+ }
366
+
367
+ try:
368
+ # Mark as uploading
369
+ file_obj.mark_as_uploading()
370
+
371
+ # Validate upload
372
+ is_valid, error_msg = backend.validate_upload(
373
+ file_obj.file_path,
374
+ upload_token,
375
+ expected_size=reported_size,
376
+ expected_checksum=reported_checksum
377
+ )
378
+
379
+ if not is_valid:
380
+ file_obj.mark_as_failed(error_msg)
381
+ return {
382
+ 'success': False,
383
+ 'error': f'Upload validation failed: {error_msg}',
384
+ 'status_code': 400
385
+ }
386
+
387
+ # Get actual file metadata from backend
388
+ file_metadata = backend.get_file_metadata(file_obj.file_path)
389
+ actual_size = file_metadata.get('size')
390
+
391
+ # Update file record
392
+ file_obj.file_size = actual_size or reported_size
393
+
394
+ # Calculate checksum if not provided
395
+ if not reported_checksum and backend.exists(file_obj.file_path):
396
+ try:
397
+ calculated_checksum = backend.get_file_checksum(file_obj.file_path)
398
+ if calculated_checksum:
399
+ file_obj.checksum = f"md5:{calculated_checksum}"
400
+ except Exception:
401
+ pass # Checksum calculation is optional
402
+ else:
403
+ file_obj.checksum = reported_checksum or ""
404
+
405
+ # Add additional metadata
406
+ if additional_metadata:
407
+ file_obj.metadata.update(additional_metadata)
408
+
409
+ # Mark as completed
410
+ file_obj.mark_as_completed()
411
+
412
+ # Generate download URL
413
+ try:
414
+ download_url = backend.get_url(file_obj.file_path)
415
+ except Exception:
416
+ download_url = f'/fileman/download/{upload_token}/'
417
+
418
+ # Prepare response
419
+ response_data = {
420
+ 'success': True,
421
+ 'file': {
422
+ 'id': file_obj.id,
423
+ 'filename': file_obj.filename,
424
+ 'original_filename': file_obj.original_filename,
425
+ 'file_path': file_obj.file_path,
426
+ 'file_size': file_obj.file_size,
427
+ 'content_type': file_obj.content_type,
428
+ 'upload_status': file_obj.upload_status,
429
+ 'upload_token': file_obj.upload_token,
430
+ 'checksum': file_obj.checksum,
431
+ 'download_url': download_url,
432
+ 'metadata': file_obj.metadata,
433
+ 'created': file_obj.created.isoformat(),
434
+ 'modified': file_obj.modified.isoformat()
435
+ },
436
+ 'status_code': 200
437
+ }
438
+
439
+ return response_data
440
+
441
+ except Exception as e:
442
+ file_obj.mark_as_failed(str(e))
443
+ return {
444
+ 'success': False,
445
+ 'error': f'Failed to finalize upload: {str(e)}',
446
+ 'status_code': 500
447
+ }
448
+
449
+
450
+ def direct_upload(request, upload_token, file_data) -> Dict[str, Any]:
451
+ """
452
+ Handle direct file uploads for backends that don't support pre-signed URLs
453
+
454
+ Args:
455
+ request: The HTTP request
456
+ upload_token: The upload token
457
+ file_data: The uploaded file data
458
+
459
+ Returns:
460
+ Dictionary with response data
461
+ """
462
+ # Get file record
463
+ try:
464
+ file_obj = File.objects.get(
465
+ upload_token=upload_token,
466
+ is_active=True
467
+ )
468
+ except File.DoesNotExist:
469
+ return {
470
+ 'success': False,
471
+ 'error': 'Invalid upload token',
472
+ 'status_code': 404
473
+ }
474
+
475
+ # Check if upload has expired
476
+ if file_obj.is_upload_expired:
477
+ file_obj.mark_as_expired()
478
+ return {
479
+ 'success': False,
480
+ 'error': 'Upload has expired',
481
+ 'status_code': 410
482
+ }
483
+
484
+ # Get uploaded file
485
+ uploaded_file = file_data
486
+ if not uploaded_file:
487
+ return {
488
+ 'success': False,
489
+ 'error': 'No file uploaded',
490
+ 'status_code': 400
491
+ }
492
+
493
+ # Get storage backend
494
+ try:
495
+ backend = get_backend(file_obj.file_manager)
496
+ except Exception as e:
497
+ return {
498
+ 'success': False,
499
+ 'error': f'Storage backend error: {str(e)}',
500
+ 'status_code': 500
501
+ }
502
+
503
+ try:
504
+ # Mark as uploading
505
+ file_obj.mark_as_uploading()
506
+
507
+ # Save file using backend
508
+ file_path = backend.save(
509
+ uploaded_file,
510
+ file_obj.filename,
511
+ content_type=file_obj.content_type,
512
+ group_id=file_obj.group.id if file_obj.group else None,
513
+ metadata=file_obj.metadata
514
+ )
515
+
516
+ # Update file record with actual path
517
+ file_obj.file_path = file_path
518
+ file_obj.file_size = uploaded_file.size
519
+
520
+ # Calculate checksum
521
+ try:
522
+ uploaded_file.seek(0) # Reset file pointer
523
+ md5_hash = hashlib.md5()
524
+ for chunk in uploaded_file.chunks():
525
+ md5_hash.update(chunk)
526
+ file_obj.checksum = f"md5:{md5_hash.hexdigest()}"
527
+ except Exception:
528
+ pass # Checksum calculation is optional
529
+
530
+ # Mark as completed
531
+ file_obj.mark_as_completed()
532
+
533
+ return {
534
+ 'success': True,
535
+ 'message': 'File uploaded successfully',
536
+ 'upload_token': upload_token,
537
+ 'status_code': 200
538
+ }
539
+
540
+ except Exception as e:
541
+ file_obj.mark_as_failed(str(e))
542
+ return {
543
+ 'success': False,
544
+ 'error': f'Failed to upload file: {str(e)}',
545
+ 'status_code': 500
546
+ }
547
+
548
+
549
+ def get_download_url(request, upload_token) -> Dict[str, Any]:
550
+ """
551
+ Generate download URL for a file
552
+
553
+ Args:
554
+ request: The HTTP request
555
+ upload_token: The upload token
556
+
557
+ Returns:
558
+ Dictionary with download URL or error information
559
+ """
560
+ # Get file record
561
+ try:
562
+ file_obj = File.objects.get(
563
+ upload_token=upload_token,
564
+ is_active=True,
565
+ upload_status=File.COMPLETED
566
+ )
567
+ except File.DoesNotExist:
568
+ return {
569
+ 'success': False,
570
+ 'error': 'File not found',
571
+ 'status_code': 404
572
+ }
573
+
574
+ # Check permissions
575
+ if not file_obj.can_be_accessed_by(request.user, getattr(request.user, 'group', None)):
576
+ return {
577
+ 'success': False,
578
+ 'error': 'Permission denied',
579
+ 'status_code': 403
580
+ }
581
+
582
+ # Get storage backend
583
+ try:
584
+ backend = get_backend(file_obj.file_manager)
585
+ except Exception as e:
586
+ return {
587
+ 'success': False,
588
+ 'error': f'Storage backend error: {str(e)}',
589
+ 'status_code': 500
590
+ }
591
+
592
+ # Generate download URL
593
+ try:
594
+ download_url = backend.get_url(
595
+ file_obj.file_path,
596
+ expires_in=3600 # 1 hour
597
+ )
598
+
599
+ return {
600
+ 'success': True,
601
+ 'download_url': download_url,
602
+ 'file': {
603
+ 'id': file_obj.id,
604
+ 'filename': file_obj.filename,
605
+ 'original_filename': file_obj.original_filename,
606
+ 'content_type': file_obj.content_type
607
+ },
608
+ 'status_code': 200
609
+ }
610
+
611
+ except Exception as e:
612
+ return {
613
+ 'success': False,
614
+ 'error': f'Failed to generate download URL: {str(e)}',
615
+ 'status_code': 500
616
+ }
@@ -0,0 +1 @@
1
+ from .reporter import report_event
@@ -0,0 +1,3 @@
1
+ from .event_handlers import TaskHandler, EmailHandler, NotifyHandler
2
+
3
+ __all__ = ['TaskHandler', 'EmailHandler', 'NotifyHandler']