django-nativemojo 0.1.10__py3-none-any.whl → 0.1.15__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 (120) hide show
  1. django_nativemojo-0.1.15.dist-info/METADATA +136 -0
  2. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/RECORD +105 -65
  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 +531 -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/models/group.py +25 -7
  10. mojo/apps/account/models/member.py +15 -4
  11. mojo/apps/account/models/user.py +197 -20
  12. mojo/apps/account/rest/group.py +1 -0
  13. mojo/apps/account/rest/user.py +6 -2
  14. mojo/apps/aws/rest/__init__.py +1 -0
  15. mojo/apps/aws/rest/s3.py +64 -0
  16. mojo/apps/fileman/README.md +8 -8
  17. mojo/apps/fileman/backends/base.py +76 -70
  18. mojo/apps/fileman/backends/filesystem.py +86 -86
  19. mojo/apps/fileman/backends/s3.py +200 -108
  20. mojo/apps/fileman/migrations/0001_initial.py +106 -0
  21. mojo/apps/fileman/migrations/0002_filemanager_parent_alter_filemanager_max_file_size.py +24 -0
  22. mojo/apps/fileman/migrations/0003_remove_file_fileman_fil_upload__c4bc35_idx_and_more.py +25 -0
  23. mojo/apps/fileman/migrations/0004_remove_file_original_filename_and_more.py +39 -0
  24. mojo/apps/fileman/migrations/0005_alter_file_upload_token.py +18 -0
  25. mojo/apps/fileman/migrations/0006_file_download_url_filemanager_forever_urls.py +23 -0
  26. mojo/apps/fileman/migrations/0007_remove_filemanager_forever_urls_and_more.py +22 -0
  27. mojo/apps/fileman/migrations/0008_file_category.py +18 -0
  28. mojo/apps/fileman/migrations/0009_rename_file_path_file_storage_file_path.py +18 -0
  29. mojo/apps/fileman/migrations/0010_filerendition.py +33 -0
  30. mojo/apps/fileman/migrations/0011_alter_filerendition_original_file.py +19 -0
  31. mojo/apps/fileman/models/__init__.py +1 -5
  32. mojo/apps/fileman/models/file.py +204 -58
  33. mojo/apps/fileman/models/manager.py +161 -31
  34. mojo/apps/fileman/models/rendition.py +118 -0
  35. mojo/apps/fileman/renderer/__init__.py +111 -0
  36. mojo/apps/fileman/renderer/audio.py +403 -0
  37. mojo/apps/fileman/renderer/base.py +205 -0
  38. mojo/apps/fileman/renderer/document.py +404 -0
  39. mojo/apps/fileman/renderer/image.py +222 -0
  40. mojo/apps/fileman/renderer/utils.py +297 -0
  41. mojo/apps/fileman/renderer/video.py +304 -0
  42. mojo/apps/fileman/rest/__init__.py +1 -18
  43. mojo/apps/fileman/rest/upload.py +22 -32
  44. mojo/apps/fileman/signals.py +58 -0
  45. mojo/apps/fileman/tasks.py +254 -0
  46. mojo/apps/fileman/utils/__init__.py +40 -16
  47. mojo/apps/incident/migrations/0005_incidenthistory.py +39 -0
  48. mojo/apps/incident/migrations/0006_alter_incident_state.py +18 -0
  49. mojo/apps/incident/models/__init__.py +1 -0
  50. mojo/apps/incident/models/history.py +36 -0
  51. mojo/apps/incident/models/incident.py +1 -1
  52. mojo/apps/incident/reporter.py +3 -1
  53. mojo/apps/incident/rest/event.py +7 -1
  54. mojo/apps/logit/migrations/0004_alter_log_level.py +18 -0
  55. mojo/apps/logit/models/log.py +4 -1
  56. mojo/apps/metrics/utils.py +2 -2
  57. mojo/apps/notify/handlers/ses/message.py +1 -1
  58. mojo/apps/notify/providers/aws.py +2 -2
  59. mojo/apps/tasks/__init__.py +34 -1
  60. mojo/apps/tasks/manager.py +200 -45
  61. mojo/apps/tasks/rest/tasks.py +24 -10
  62. mojo/apps/tasks/runner.py +283 -18
  63. mojo/apps/tasks/task.py +99 -0
  64. mojo/apps/tasks/tq_handlers.py +118 -0
  65. mojo/decorators/auth.py +6 -1
  66. mojo/decorators/http.py +7 -2
  67. mojo/helpers/aws/__init__.py +41 -0
  68. mojo/helpers/aws/ec2.py +804 -0
  69. mojo/helpers/aws/iam.py +748 -0
  70. mojo/helpers/aws/s3.py +451 -11
  71. mojo/helpers/aws/ses.py +483 -0
  72. mojo/helpers/aws/sns.py +461 -0
  73. mojo/helpers/crypto/__pycache__/hash.cpython-310.pyc +0 -0
  74. mojo/helpers/crypto/__pycache__/sign.cpython-310.pyc +0 -0
  75. mojo/helpers/crypto/__pycache__/utils.cpython-310.pyc +0 -0
  76. mojo/helpers/dates.py +18 -0
  77. mojo/helpers/response.py +6 -2
  78. mojo/helpers/settings/__init__.py +2 -0
  79. mojo/helpers/{settings.py → settings/helper.py} +1 -37
  80. mojo/helpers/settings/parser.py +132 -0
  81. mojo/middleware/logging.py +1 -1
  82. mojo/middleware/mojo.py +5 -0
  83. mojo/models/rest.py +261 -46
  84. mojo/models/secrets.py +13 -4
  85. mojo/serializers/__init__.py +100 -0
  86. mojo/serializers/advanced/README.md +363 -0
  87. mojo/serializers/advanced/__init__.py +247 -0
  88. mojo/serializers/advanced/formats/__init__.py +28 -0
  89. mojo/serializers/advanced/formats/csv.py +416 -0
  90. mojo/serializers/advanced/formats/excel.py +516 -0
  91. mojo/serializers/advanced/formats/json.py +239 -0
  92. mojo/serializers/advanced/formats/localizers.py +509 -0
  93. mojo/serializers/advanced/formats/response.py +485 -0
  94. mojo/serializers/advanced/serializer.py +568 -0
  95. mojo/serializers/manager.py +501 -0
  96. mojo/serializers/optimized.py +618 -0
  97. mojo/serializers/settings_example.py +322 -0
  98. mojo/serializers/{models.py → simple.py} +38 -15
  99. testit/helpers.py +21 -4
  100. django_nativemojo-0.1.10.dist-info/METADATA +0 -96
  101. mojo/apps/metrics/rest/db.py +0 -0
  102. mojo/helpers/aws/setup_email.py +0 -0
  103. mojo/ws4redis/README.md +0 -174
  104. mojo/ws4redis/__init__.py +0 -2
  105. mojo/ws4redis/client.py +0 -283
  106. mojo/ws4redis/connection.py +0 -327
  107. mojo/ws4redis/exceptions.py +0 -32
  108. mojo/ws4redis/redis.py +0 -183
  109. mojo/ws4redis/servers/base.py +0 -86
  110. mojo/ws4redis/servers/django.py +0 -171
  111. mojo/ws4redis/servers/uwsgi.py +0 -63
  112. mojo/ws4redis/settings.py +0 -45
  113. mojo/ws4redis/utf8validator.py +0 -128
  114. mojo/ws4redis/websocket.py +0 -403
  115. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/LICENSE +0 -0
  116. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/NOTICE +0 -0
  117. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/WHEEL +0 -0
  118. /mojo/{ws4redis/servers → apps/aws}/__init__.py +0 -0
  119. /mojo/apps/{fileman/models/render.py → aws/models/__init__.py} +0 -0
  120. /mojo/apps/fileman/{rest/__init__ → migrations/__init__.py} +0 -0
@@ -0,0 +1,297 @@
1
+ import os
2
+ import mimetypes
3
+ import tempfile
4
+ import logging
5
+ import shutil
6
+ import hashlib
7
+ from typing import Dict, List, Tuple, Optional, Union, Any
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ # Additional MIME type mappings that might not be in the standard library
12
+ ADDITIONAL_MIME_TYPES = {
13
+ '.webp': 'image/webp',
14
+ '.heic': 'image/heic',
15
+ '.heif': 'image/heif',
16
+ '.webm': 'video/webm',
17
+ '.mkv': 'video/x-matroska',
18
+ '.m4a': 'audio/mp4',
19
+ '.flac': 'audio/flac',
20
+ }
21
+
22
+ # Register additional MIME types
23
+ for ext, type_name in ADDITIONAL_MIME_TYPES.items():
24
+ mimetypes.add_type(type_name, ext)
25
+
26
+ # Map of file categories by MIME type prefix
27
+ CATEGORY_MAP = {
28
+ 'image/': 'image',
29
+ 'video/': 'video',
30
+ 'audio/': 'audio',
31
+ 'text/': 'document',
32
+ 'application/pdf': 'pdf',
33
+ 'application/msword': 'document',
34
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml': 'document',
35
+ 'application/vnd.ms-excel': 'spreadsheet',
36
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml': 'spreadsheet',
37
+ 'application/vnd.ms-powerpoint': 'presentation',
38
+ 'application/vnd.openxmlformats-officedocument.presentationml': 'presentation',
39
+ 'application/zip': 'archive',
40
+ 'application/x-rar-compressed': 'archive',
41
+ 'application/x-7z-compressed': 'archive',
42
+ 'application/x-tar': 'archive',
43
+ 'application/gzip': 'archive',
44
+ }
45
+
46
+ def get_file_category(mime_type: str) -> str:
47
+ """
48
+ Determine the category of a file based on its MIME type.
49
+
50
+ Args:
51
+ mime_type: The MIME type of the file
52
+
53
+ Returns:
54
+ str: The category of the file (image, video, audio, document, etc.)
55
+ """
56
+ if not mime_type:
57
+ return 'unknown'
58
+
59
+ # Check for exact matches
60
+ if mime_type in CATEGORY_MAP:
61
+ return CATEGORY_MAP[mime_type]
62
+
63
+ # Check for prefix matches
64
+ for prefix, category in CATEGORY_MAP.items():
65
+ if prefix.endswith('/'):
66
+ if mime_type.startswith(prefix):
67
+ return category
68
+
69
+ # Default category
70
+ return 'other'
71
+
72
+ def get_file_info(file_path: str) -> Dict[str, Any]:
73
+ """
74
+ Get information about a file
75
+
76
+ Args:
77
+ file_path: Path to the file
78
+
79
+ Returns:
80
+ Dict with file information (size, mime_type, category, etc.)
81
+ """
82
+ if not os.path.exists(file_path):
83
+ return {
84
+ 'exists': False,
85
+ 'size': 0,
86
+ 'mime_type': None,
87
+ 'category': 'unknown',
88
+ }
89
+
90
+ file_size = os.path.getsize(file_path)
91
+ mime_type, _ = mimetypes.guess_type(file_path)
92
+ category = get_file_category(mime_type)
93
+
94
+ return {
95
+ 'exists': True,
96
+ 'size': file_size,
97
+ 'mime_type': mime_type,
98
+ 'category': category,
99
+ 'extension': os.path.splitext(file_path)[1].lower(),
100
+ }
101
+
102
+ def create_temp_directory() -> str:
103
+ """
104
+ Create a temporary directory for file processing
105
+
106
+ Returns:
107
+ str: Path to the temporary directory
108
+ """
109
+ return tempfile.mkdtemp()
110
+
111
+ def create_temp_file(suffix: str = '') -> str:
112
+ """
113
+ Create a temporary file for processing
114
+
115
+ Args:
116
+ suffix: Optional suffix for the temp file (e.g., '.jpg')
117
+
118
+ Returns:
119
+ str: Path to the temporary file
120
+ """
121
+ fd, path = tempfile.mkstemp(suffix=suffix)
122
+ os.close(fd)
123
+ return path
124
+
125
+ def cleanup_temp_files(paths: List[str]):
126
+ """
127
+ Clean up temporary files and directories
128
+
129
+ Args:
130
+ paths: List of paths to clean up
131
+ """
132
+ for path in paths:
133
+ if not path:
134
+ continue
135
+ try:
136
+ if os.path.isdir(path):
137
+ shutil.rmtree(path, ignore_errors=True)
138
+ elif os.path.exists(path):
139
+ os.unlink(path)
140
+ except Exception as e:
141
+ logger.warning(f"Failed to clean up temporary path {path}: {str(e)}")
142
+
143
+ def calculate_file_hash(file_path: str, algorithm: str = 'md5') -> str:
144
+ """
145
+ Calculate hash for a file
146
+
147
+ Args:
148
+ file_path: Path to the file
149
+ algorithm: Hash algorithm to use (md5, sha1, sha256)
150
+
151
+ Returns:
152
+ str: Hex digest of the hash
153
+ """
154
+ if algorithm == 'md5':
155
+ hash_obj = hashlib.md5()
156
+ elif algorithm == 'sha1':
157
+ hash_obj = hashlib.sha1()
158
+ elif algorithm == 'sha256':
159
+ hash_obj = hashlib.sha256()
160
+ else:
161
+ raise ValueError(f"Unsupported hash algorithm: {algorithm}")
162
+
163
+ with open(file_path, 'rb') as f:
164
+ for chunk in iter(lambda: f.read(4096), b''):
165
+ hash_obj.update(chunk)
166
+
167
+ return hash_obj.hexdigest()
168
+
169
+ def calculate_dimensions(original_width: int, original_height: int,
170
+ target_width: int, target_height: int,
171
+ mode: str = 'contain') -> Tuple[int, int]:
172
+ """
173
+ Calculate dimensions for resizing an image or video
174
+
175
+ Args:
176
+ original_width: Original width
177
+ original_height: Original height
178
+ target_width: Target width
179
+ target_height: Target height
180
+ mode: Resize mode ('contain', 'cover', 'stretch')
181
+
182
+ Returns:
183
+ Tuple[int, int]: (new_width, new_height)
184
+ """
185
+ if mode == 'stretch':
186
+ return target_width, target_height
187
+
188
+ # Calculate aspect ratios
189
+ original_ratio = original_width / original_height
190
+ target_ratio = target_width / target_height
191
+
192
+ if mode == 'contain':
193
+ # Fit within target dimensions while maintaining aspect ratio
194
+ if original_ratio > target_ratio:
195
+ # Width is the limiting factor
196
+ new_width = target_width
197
+ new_height = int(new_width / original_ratio)
198
+ else:
199
+ # Height is the limiting factor
200
+ new_height = target_height
201
+ new_width = int(new_height * original_ratio)
202
+ else: # cover
203
+ # Fill target dimensions while maintaining aspect ratio
204
+ if original_ratio > target_ratio:
205
+ # Height is the limiting factor
206
+ new_height = target_height
207
+ new_width = int(new_height * original_ratio)
208
+ else:
209
+ # Width is the limiting factor
210
+ new_width = target_width
211
+ new_height = int(new_width / original_ratio)
212
+
213
+ return new_width, new_height
214
+
215
+ def get_video_duration(video_path: str) -> Optional[float]:
216
+ """
217
+ Get the duration of a video file in seconds
218
+
219
+ Args:
220
+ video_path: Path to the video file
221
+
222
+ Returns:
223
+ float: Duration in seconds, or None if duration couldn't be determined
224
+ """
225
+ try:
226
+ import subprocess
227
+
228
+ # Use ffprobe to get video duration
229
+ cmd = [
230
+ "ffprobe",
231
+ "-v", "error",
232
+ "-show_entries", "format=duration",
233
+ "-of", "default=noprint_wrappers=1:nokey=1",
234
+ video_path
235
+ ]
236
+
237
+ result = subprocess.run(cmd,
238
+ stdout=subprocess.PIPE,
239
+ stderr=subprocess.PIPE,
240
+ text=True,
241
+ check=True)
242
+
243
+ duration = float(result.stdout.strip())
244
+ return duration
245
+ except Exception as e:
246
+ logger.error(f"Failed to get video duration: {str(e)}")
247
+ return None
248
+
249
+ def get_audio_duration(audio_path: str) -> Optional[float]:
250
+ """
251
+ Get the duration of an audio file in seconds
252
+
253
+ Args:
254
+ audio_path: Path to the audio file
255
+
256
+ Returns:
257
+ float: Duration in seconds, or None if duration couldn't be determined
258
+ """
259
+ return get_video_duration(audio_path) # Use the same ffprobe method
260
+
261
+ def get_image_dimensions(image_path: str) -> Optional[Tuple[int, int]]:
262
+ """
263
+ Get dimensions of an image
264
+
265
+ Args:
266
+ image_path: Path to the image file
267
+
268
+ Returns:
269
+ Tuple[int, int]: (width, height), or None if dimensions couldn't be determined
270
+ """
271
+ try:
272
+ from PIL import Image
273
+ with Image.open(image_path) as img:
274
+ return img.size
275
+ except Exception as e:
276
+ logger.error(f"Failed to get image dimensions: {str(e)}")
277
+ return None
278
+
279
+ def format_filesize(size_in_bytes: int) -> str:
280
+ """
281
+ Format a file size in bytes to a human-readable string
282
+
283
+ Args:
284
+ size_in_bytes: Size in bytes
285
+
286
+ Returns:
287
+ str: Human-readable file size (e.g., "1.2 MB")
288
+ """
289
+ if size_in_bytes < 1024:
290
+ return f"{size_in_bytes} B"
291
+
292
+ for unit in ['KB', 'MB', 'GB', 'TB', 'PB']:
293
+ size_in_bytes /= 1024.0
294
+ if size_in_bytes < 1024.0:
295
+ return f"{size_in_bytes:.1f} {unit}"
296
+
297
+ return f"{size_in_bytes:.1f} PB"
@@ -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>')