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
@@ -0,0 +1,304 @@
1
+ import os
2
+ import io
3
+ import subprocess
4
+ import tempfile
5
+ import logging
6
+ import mimetypes
7
+ import shutil
8
+ from typing import Dict, Optional, Tuple, Union, BinaryIO, List
9
+
10
+ from mojo.apps.fileman.models import File, FileRendition
11
+ from mojo.apps.fileman.renderer.base import BaseRenderer, RenditionRole
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ class VideoRenderer(BaseRenderer):
16
+ """
17
+ Renderer for video files
18
+
19
+ Creates various renditions like thumbnails, previews, and different formats using ffmpeg
20
+ """
21
+
22
+ # Video file categories
23
+ supported_categories = ['video']
24
+
25
+ # Default rendition definitions with options
26
+ default_renditions = {
27
+ RenditionRole.VIDEO_THUMBNAIL: {
28
+ 'width': 300,
29
+ 'height': 169,
30
+ 'time_offset': '00:00:03',
31
+ 'format': 'jpg'
32
+ },
33
+ RenditionRole.THUMBNAIL: {
34
+ 'width': 300,
35
+ 'height': 169,
36
+ 'time_offset': '00:00:03',
37
+ 'format': 'jpg'
38
+ },
39
+ RenditionRole.VIDEO_PREVIEW: {
40
+ 'width': 640,
41
+ 'height': 360,
42
+ 'bitrate': '500k',
43
+ 'duration': 10,
44
+ 'format': 'mp4',
45
+ 'audio': True,
46
+ },
47
+ RenditionRole.VIDEO_MP4: {
48
+ 'width': 1280,
49
+ 'height': 720,
50
+ 'bitrate': '2000k',
51
+ 'format': 'mp4',
52
+ 'audio': True,
53
+ },
54
+ RenditionRole.VIDEO_WEBM: {
55
+ 'width': 1280,
56
+ 'height': 720,
57
+ 'bitrate': '2000k',
58
+ 'format': 'webm',
59
+ 'audio': True,
60
+ },
61
+ }
62
+
63
+ def __init__(self, file: File):
64
+ super().__init__(file)
65
+ # Check if ffmpeg is available
66
+ self._check_ffmpeg()
67
+
68
+ def _check_ffmpeg(self):
69
+ """Check if ffmpeg is available in the system"""
70
+ try:
71
+ subprocess.run(["ffmpeg", "-version"],
72
+ stdout=subprocess.PIPE,
73
+ stderr=subprocess.PIPE,
74
+ check=True)
75
+ except (subprocess.SubprocessError, FileNotFoundError):
76
+ logger.warning("ffmpeg is not available. Video rendering may not work properly.")
77
+
78
+ def _download_original(self) -> Union[str, None]:
79
+ """
80
+ Download the original file to a temporary location
81
+
82
+ Returns:
83
+ str: Path to the downloaded file, or None if download failed
84
+ """
85
+ try:
86
+ file_manager = self.file.file_manager
87
+ backend = file_manager.backend
88
+
89
+ # Get file extension
90
+ _, ext = os.path.splitext(self.file.filename)
91
+ temp_path = self.get_temp_path(ext)
92
+
93
+ # Download file from storage
94
+ with open(temp_path, 'wb') as f:
95
+ backend.download(self.file.storage_file_path, f)
96
+
97
+ return temp_path
98
+ except Exception as e:
99
+ logger.error(f"Failed to download original video file: {str(e)}")
100
+ return None
101
+
102
+ def _create_thumbnail(self, source_path: str, width: int, height: int,
103
+ time_offset: str, output_format: str) -> Tuple[str, str, int]:
104
+ """
105
+ Create a thumbnail from a video at specified time offset
106
+
107
+ Args:
108
+ source_path: Path to the source video
109
+ width: Target width
110
+ height: Target height
111
+ time_offset: Time offset for thumbnail (format: HH:MM:SS)
112
+ output_format: Output format (jpg, png)
113
+
114
+ Returns:
115
+ Tuple[str, str, int]: (Output path, mime type, file size)
116
+ """
117
+ temp_output = self.get_temp_path(f".{output_format}")
118
+
119
+ try:
120
+ # Use ffmpeg to extract a frame
121
+ cmd = [
122
+ "ffmpeg",
123
+ "-y", # Overwrite output files
124
+ "-ss", time_offset, # Seek to time offset
125
+ "-i", source_path, # Input file
126
+ "-vframes", "1", # Extract one frame
127
+ "-s", f"{width}x{height}", # Set size
128
+ "-f", "image2", # Force image2 format
129
+ temp_output # Output file
130
+ ]
131
+
132
+ subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
133
+
134
+ # Get file size
135
+ file_size = os.path.getsize(temp_output)
136
+
137
+ # Get mime type
138
+ mime_type = mimetypes.guess_type(f"file.{output_format}")[0]
139
+
140
+ return temp_output, mime_type, file_size
141
+
142
+ except subprocess.SubprocessError as e:
143
+ logger.error(f"Failed to create video thumbnail: {str(e)}")
144
+ if os.path.exists(temp_output):
145
+ os.unlink(temp_output)
146
+ raise
147
+
148
+ def _create_video_rendition(self, source_path: str, options: Dict) -> Tuple[str, str, int]:
149
+ """
150
+ Create a video rendition with specified options
151
+
152
+ Args:
153
+ source_path: Path to the source video
154
+ options: Video processing options
155
+
156
+ Returns:
157
+ Tuple[str, str, int]: (Output path, mime type, file size)
158
+ """
159
+ width = options.get('width', 1280)
160
+ height = options.get('height', 720)
161
+ bitrate = options.get('bitrate', '2000k')
162
+ output_format = options.get('format', 'mp4')
163
+ duration = options.get('duration') # Optional duration limit in seconds
164
+ audio = options.get('audio', True)
165
+
166
+ temp_output = self.get_temp_path(f".{output_format}")
167
+
168
+ try:
169
+ # Build ffmpeg command
170
+ cmd = [
171
+ "ffmpeg",
172
+ "-y", # Overwrite output files
173
+ "-i", source_path, # Input file
174
+ ]
175
+
176
+ # Add duration limit if specified
177
+ if duration:
178
+ cmd.extend(["-t", str(duration)])
179
+
180
+ # Video settings
181
+ cmd.extend([
182
+ "-vf", f"scale={width}:{height}",
183
+ "-c:v", "libx264" if output_format == "mp4" else "libvpx",
184
+ "-b:v", bitrate,
185
+ ])
186
+
187
+ # Audio settings
188
+ if audio:
189
+ if output_format == "mp4":
190
+ cmd.extend(["-c:a", "aac", "-b:a", "128k"])
191
+ else: # webm
192
+ cmd.extend(["-c:a", "libvorbis", "-b:a", "128k"])
193
+ else:
194
+ cmd.extend(["-an"]) # No audio
195
+
196
+ # Add output file
197
+ cmd.append(temp_output)
198
+
199
+ subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
200
+
201
+ # Get file size
202
+ file_size = os.path.getsize(temp_output)
203
+
204
+ # Get mime type
205
+ mime_type = mimetypes.guess_type(f"file.{output_format}")[0]
206
+
207
+ return temp_output, mime_type, file_size
208
+
209
+ except subprocess.SubprocessError as e:
210
+ logger.error(f"Failed to create video rendition: {str(e)}")
211
+ if os.path.exists(temp_output):
212
+ os.unlink(temp_output)
213
+ raise
214
+
215
+ def create_rendition(self, role: str, options: Dict = None) -> Optional[FileRendition]:
216
+ """
217
+ Create a video rendition for the specified role
218
+
219
+ Args:
220
+ role: The role of the rendition
221
+ options: Additional options for creating the rendition
222
+
223
+ Returns:
224
+ FileRendition: The created rendition, or None if creation failed
225
+ """
226
+ try:
227
+ # Get rendition settings
228
+ settings = self.default_renditions.get(role, {})
229
+ if options:
230
+ settings.update(options)
231
+
232
+ # Download the original file
233
+ source_path = self._download_original()
234
+ if not source_path:
235
+ return None
236
+
237
+ try:
238
+ temp_output = None
239
+ mime_type = None
240
+ file_size = None
241
+
242
+ # Process based on role type
243
+ if role in [RenditionRole.THUMBNAIL, RenditionRole.VIDEO_THUMBNAIL]:
244
+ # Create thumbnail image
245
+ width = settings.get('width', 300)
246
+ height = settings.get('height', 169)
247
+ time_offset = settings.get('time_offset', '00:00:03')
248
+ output_format = settings.get('format', 'jpg')
249
+
250
+ temp_output, mime_type, file_size = self._create_thumbnail(
251
+ source_path, width, height, time_offset, output_format
252
+ )
253
+
254
+ # Set filename
255
+ name, _ = os.path.splitext(self.file.filename)
256
+ filename = f"{name}_{role}.{output_format}"
257
+ category = 'image' # Thumbnails are images
258
+
259
+ else:
260
+ # Create video rendition
261
+ temp_output, mime_type, file_size = self._create_video_rendition(
262
+ source_path, settings
263
+ )
264
+
265
+ # Set filename
266
+ name, _ = os.path.splitext(self.file.filename)
267
+ output_format = settings.get('format', 'mp4')
268
+ filename = f"{name}_{role}.{output_format}"
269
+ category = 'video'
270
+
271
+ # Save to storage
272
+ file_manager = self.file.file_manager
273
+ backend = file_manager.backend
274
+ storage_path = os.path.join(
275
+ os.path.dirname(self.file.storage_file_path),
276
+ filename
277
+ )
278
+
279
+ # Upload to storage
280
+ with open(temp_output, 'rb') as f:
281
+ backend.save(f, storage_path, mime_type)
282
+
283
+ # Create rendition record
284
+ rendition = self._create_rendition_object(
285
+ role=role,
286
+ filename=filename,
287
+ storage_path=storage_path,
288
+ content_type=mime_type,
289
+ category=category,
290
+ file_size=file_size
291
+ )
292
+
293
+ return rendition
294
+
295
+ finally:
296
+ # Clean up temporary files
297
+ if source_path and os.path.exists(source_path):
298
+ os.unlink(source_path)
299
+ if temp_output and os.path.exists(temp_output):
300
+ os.unlink(temp_output)
301
+
302
+ except Exception as e:
303
+ logger.error(f"Failed to create video rendition '{role}': {str(e)}")
304
+ return None
@@ -3,21 +3,4 @@ File Manager REST API endpoints
3
3
  """
4
4
 
5
5
  from .fileman import on_filemanager, on_file
6
- from .upload import (
7
- on_upload_initiate,
8
- on_upload_finalize,
9
- on_direct_upload,
10
- on_download
11
- )
12
-
13
- __all__ = [
14
- # File manager model endpoints
15
- 'on_filemanager',
16
- 'on_file',
17
-
18
- # File upload/download endpoints
19
- 'on_upload_initiate',
20
- 'on_upload_finalize',
21
- 'on_direct_upload',
22
- 'on_download'
23
- ]
6
+ from .upload import *
@@ -1,12 +1,7 @@
1
1
  from mojo import decorators as md
2
2
  from mojo import JsonResponse
3
+ import mojo.errors
3
4
  from mojo.apps.fileman.models import File, FileManager
4
- from mojo.apps.fileman.utils.upload import (
5
- initiate_upload,
6
- finalize_upload,
7
- direct_upload,
8
- get_download_url
9
- )
10
5
 
11
6
 
12
7
  @md.POST('upload/initiate')
@@ -20,40 +15,35 @@ def on_upload_initiate(request):
20
15
  {
21
16
  "filename": "document.pdf",
22
17
  "content_type": "application/pdf",
23
- "size": 1024000
18
+ "file_size": 1024000
24
19
  }
25
20
  ],
26
- "file_manager_id": 123, // optional
27
- "group_id": 456, // optional
21
+ "file_manager": 123, // optional
22
+ "group": 456, // optional
23
+ "user": 789, // optional
28
24
  "metadata": { // optional global metadata
29
25
  "source": "web_upload",
30
26
  "category": "documents"
31
27
  }
32
28
  }
33
29
  """
34
- response_data = initiate_upload(request, request.DATA)
35
- status_code = response_data.pop('status_code', 200)
36
- return JsonResponse(response_data, status=status_code)
37
-
38
-
39
- @md.POST('upload/finalize')
40
- def on_upload_finalize(request):
41
- """
42
- Finalize a file upload
43
-
44
- Request body format:
45
- {
46
- "upload_token": "abc123...",
47
- "file_size": 1024000, // optional
48
- "checksum": "md5:abcdef...", // optional
49
- "metadata": { // optional additional metadata
50
- "processing_complete": true
51
- }
52
- }
53
- """
54
- response_data = finalize_upload(request, request.DATA)
55
- status_code = response_data.pop('status_code', 200)
56
- return JsonResponse(response_data, status=status_code)
30
+ # first we need to get the correct file manager
31
+ file_manager = FileManager.get_from_request(request)
32
+ if file_manager is None:
33
+ raise mojo.errors.ValueException("No file manager found")
34
+ # new lets create a new file
35
+ file = File(
36
+ filename=request.DATA['filename'],
37
+ content_type=request.DATA['content_type'],
38
+ file_size=request.DATA['file_size'],
39
+ file_manager=file_manager,
40
+ group=file_manager.group,
41
+ user=request.user)
42
+ file.on_rest_pre_save({}, True)
43
+ file.mark_as_uploading()
44
+ file.save()
45
+ file.request_upload_url()
46
+ return file.on_rest_get(request, "upload")
57
47
 
58
48
 
59
49
  @md.POST('upload/<str:upload_token>')
@@ -0,0 +1,58 @@
1
+ import logging
2
+ from django.db.models.signals import post_save, post_delete
3
+ from django.dispatch import receiver
4
+ from mojo.apps.fileman.models import File
5
+ from mojo.apps.fileman.tasks import process_file_renditions, cleanup_renditions
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ @receiver(post_save, sender=File)
10
+ def handle_file_upload_completed(sender, instance, created, **kwargs):
11
+ """
12
+ When a file is marked as completed, automatically create renditions
13
+
14
+ This connects to the post_save signal of the File model and checks if the
15
+ file has been marked as completed. If so, it queues a task to create renditions.
16
+ """
17
+ # Only process if the file upload is completed
18
+ if instance.is_completed:
19
+ # Check if this is a status change to completed
20
+ if not created:
21
+ try:
22
+ # Get the previous instance from the database
23
+ old_instance = sender.objects.get(pk=instance.pk)
24
+ if old_instance.upload_status != instance.upload_status and instance.upload_status == File.COMPLETED:
25
+ logger.info(f"File {instance.id} ({instance.filename}) marked as completed, creating renditions")
26
+ # Queue task to create renditions
27
+ process_file_renditions.delay(instance.id)
28
+ except Exception as e:
29
+ logger.error(f"Error checking file status change: {str(e)}")
30
+ else:
31
+ # For new files that are already completed
32
+ if instance.upload_status == File.COMPLETED:
33
+ logger.info(f"New file {instance.id} ({instance.filename}) is completed, creating renditions")
34
+ # Queue task to create renditions
35
+ process_file_renditions.delay(instance.id)
36
+
37
+ @receiver(post_delete, sender=File)
38
+ def handle_file_deleted(sender, instance, **kwargs):
39
+ """
40
+ When a file is deleted, clean up its renditions
41
+
42
+ This connects to the post_delete signal of the File model and ensures that
43
+ when a file is deleted, its renditions are also removed from storage.
44
+ """
45
+ try:
46
+ # Check if we have any renditions to clean up
47
+ from mojo.apps.fileman.models import FileRendition
48
+ if FileRendition.objects.filter(original_file_id=instance.id).exists():
49
+ logger.info(f"File {instance.id} ({instance.filename}) deleted, cleaning up renditions")
50
+ # Queue task to clean up renditions
51
+ # Note: We can't use instance.id directly in the task since the object is being deleted
52
+ # So we pass the ID value
53
+ cleanup_renditions.delay(instance.id)
54
+ except Exception as e:
55
+ logger.error(f"Error queueing rendition cleanup for deleted file: {str(e)}")
56
+
57
+ # Note: No need to manually connect signals as Django will automatically
58
+ # discover and connect properly decorated signal handlers