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.
- django_nativemojo-0.1.16.dist-info/METADATA +138 -0
- django_nativemojo-0.1.16.dist-info/RECORD +302 -0
- mojo/__init__.py +1 -1
- mojo/apps/account/management/__init__.py +5 -0
- mojo/apps/account/management/commands/__init__.py +6 -0
- mojo/apps/account/management/commands/serializer_admin.py +651 -0
- mojo/apps/account/migrations/0004_user_avatar.py +20 -0
- mojo/apps/account/migrations/0005_group_last_activity.py +18 -0
- mojo/apps/account/migrations/0006_add_device_tracking_models.py +72 -0
- mojo/apps/account/migrations/0007_delete_userdevicelocation.py +16 -0
- mojo/apps/account/migrations/0008_userdevicelocation.py +33 -0
- mojo/apps/account/migrations/0009_geolocatedip_subnet.py +18 -0
- mojo/apps/account/migrations/0010_group_avatar.py +20 -0
- mojo/apps/account/migrations/0011_user_org_registereddevice_pushconfig_and_more.py +118 -0
- mojo/apps/account/migrations/0012_remove_pushconfig_apns_key_file_and_more.py +21 -0
- mojo/apps/account/migrations/0013_pushconfig_test_mode_alter_pushconfig_apns_enabled_and_more.py +28 -0
- mojo/apps/account/migrations/0014_notificationdelivery_data_payload_and_more.py +48 -0
- mojo/apps/account/models/__init__.py +2 -0
- mojo/apps/account/models/device.py +281 -0
- mojo/apps/account/models/group.py +319 -15
- mojo/apps/account/models/member.py +29 -5
- mojo/apps/account/models/push/__init__.py +4 -0
- mojo/apps/account/models/push/config.py +112 -0
- mojo/apps/account/models/push/delivery.py +93 -0
- mojo/apps/account/models/push/device.py +66 -0
- mojo/apps/account/models/push/template.py +99 -0
- mojo/apps/account/models/user.py +369 -19
- mojo/apps/account/rest/__init__.py +2 -0
- mojo/apps/account/rest/device.py +39 -0
- mojo/apps/account/rest/group.py +9 -0
- mojo/apps/account/rest/push.py +187 -0
- mojo/apps/account/rest/user.py +100 -6
- mojo/apps/account/services/__init__.py +1 -0
- mojo/apps/account/services/push.py +363 -0
- mojo/apps/aws/migrations/0001_initial.py +206 -0
- mojo/apps/aws/migrations/0002_emaildomain_can_recv_emaildomain_can_send_and_more.py +28 -0
- mojo/apps/aws/migrations/0003_mailbox_is_domain_default_mailbox_is_system_default_and_more.py +31 -0
- mojo/apps/aws/migrations/0004_s3bucket.py +39 -0
- mojo/apps/aws/migrations/0005_alter_emaildomain_region_delete_s3bucket.py +21 -0
- mojo/apps/aws/models/__init__.py +19 -0
- mojo/apps/aws/models/email_attachment.py +99 -0
- mojo/apps/aws/models/email_domain.py +218 -0
- mojo/apps/aws/models/email_template.py +132 -0
- mojo/apps/aws/models/incoming_email.py +197 -0
- mojo/apps/aws/models/mailbox.py +288 -0
- mojo/apps/aws/models/sent_message.py +175 -0
- mojo/apps/aws/rest/__init__.py +7 -0
- mojo/apps/aws/rest/email.py +33 -0
- mojo/apps/aws/rest/email_ops.py +183 -0
- mojo/apps/aws/rest/messages.py +32 -0
- mojo/apps/aws/rest/s3.py +64 -0
- mojo/apps/aws/rest/send.py +101 -0
- mojo/apps/aws/rest/sns.py +403 -0
- mojo/apps/aws/rest/templates.py +19 -0
- mojo/apps/aws/services/__init__.py +32 -0
- mojo/apps/aws/services/email.py +390 -0
- mojo/apps/aws/services/email_ops.py +548 -0
- mojo/apps/docit/__init__.py +6 -0
- mojo/apps/docit/markdown_plugins/syntax_highlight.py +25 -0
- mojo/apps/docit/markdown_plugins/toc.py +12 -0
- mojo/apps/docit/migrations/0001_initial.py +113 -0
- mojo/apps/docit/migrations/0002_alter_book_modified_by_alter_page_modified_by.py +26 -0
- mojo/apps/docit/migrations/0003_alter_book_group.py +20 -0
- mojo/apps/docit/models/__init__.py +17 -0
- mojo/apps/docit/models/asset.py +231 -0
- mojo/apps/docit/models/book.py +227 -0
- mojo/apps/docit/models/page.py +319 -0
- mojo/apps/docit/models/page_revision.py +203 -0
- mojo/apps/docit/rest/__init__.py +10 -0
- mojo/apps/docit/rest/asset.py +17 -0
- mojo/apps/docit/rest/book.py +22 -0
- mojo/apps/docit/rest/page.py +22 -0
- mojo/apps/docit/rest/page_revision.py +17 -0
- mojo/apps/docit/services/__init__.py +11 -0
- mojo/apps/docit/services/docit.py +315 -0
- mojo/apps/docit/services/markdown.py +44 -0
- mojo/apps/fileman/README.md +8 -8
- mojo/apps/fileman/backends/base.py +76 -70
- mojo/apps/fileman/backends/filesystem.py +86 -86
- mojo/apps/fileman/backends/s3.py +409 -108
- mojo/apps/fileman/migrations/0001_initial.py +106 -0
- mojo/apps/fileman/migrations/0002_filemanager_parent_alter_filemanager_max_file_size.py +24 -0
- mojo/apps/fileman/migrations/0003_remove_file_fileman_fil_upload__c4bc35_idx_and_more.py +25 -0
- mojo/apps/fileman/migrations/0004_remove_file_original_filename_and_more.py +39 -0
- mojo/apps/fileman/migrations/0005_alter_file_upload_token.py +18 -0
- mojo/apps/fileman/migrations/0006_file_download_url_filemanager_forever_urls.py +23 -0
- mojo/apps/fileman/migrations/0007_remove_filemanager_forever_urls_and_more.py +22 -0
- mojo/apps/fileman/migrations/0008_file_category.py +18 -0
- mojo/apps/fileman/migrations/0009_rename_file_path_file_storage_file_path.py +18 -0
- mojo/apps/fileman/migrations/0010_filerendition.py +33 -0
- mojo/apps/fileman/migrations/0011_alter_filerendition_original_file.py +19 -0
- mojo/apps/fileman/models/__init__.py +1 -5
- mojo/apps/fileman/models/file.py +240 -58
- mojo/apps/fileman/models/manager.py +427 -31
- mojo/apps/fileman/models/rendition.py +118 -0
- mojo/apps/fileman/renderer/__init__.py +111 -0
- mojo/apps/fileman/renderer/audio.py +403 -0
- mojo/apps/fileman/renderer/base.py +205 -0
- mojo/apps/fileman/renderer/document.py +404 -0
- mojo/apps/fileman/renderer/image.py +222 -0
- mojo/apps/fileman/renderer/utils.py +297 -0
- mojo/apps/fileman/renderer/video.py +304 -0
- mojo/apps/fileman/rest/__init__.py +1 -18
- mojo/apps/fileman/rest/upload.py +22 -32
- mojo/apps/fileman/signals.py +58 -0
- mojo/apps/fileman/tasks.py +254 -0
- mojo/apps/fileman/utils/__init__.py +40 -16
- mojo/apps/incident/migrations/0005_incidenthistory.py +39 -0
- mojo/apps/incident/migrations/0006_alter_incident_state.py +18 -0
- mojo/apps/incident/migrations/0007_event_uid.py +18 -0
- mojo/apps/incident/migrations/0008_ticket_ticketnote.py +55 -0
- mojo/apps/incident/migrations/0009_incident_status.py +18 -0
- mojo/apps/incident/migrations/0010_event_country_code.py +18 -0
- mojo/apps/incident/migrations/0011_incident_country_code.py +18 -0
- mojo/apps/incident/migrations/0012_alter_incident_status.py +18 -0
- mojo/apps/incident/models/__init__.py +2 -0
- mojo/apps/incident/models/event.py +35 -0
- mojo/apps/incident/models/history.py +36 -0
- mojo/apps/incident/models/incident.py +3 -1
- mojo/apps/incident/models/ticket.py +62 -0
- mojo/apps/incident/reporter.py +21 -1
- mojo/apps/incident/rest/__init__.py +1 -0
- mojo/apps/incident/rest/event.py +7 -1
- mojo/apps/incident/rest/ticket.py +43 -0
- mojo/apps/jobs/__init__.py +489 -0
- mojo/apps/jobs/adapters.py +24 -0
- mojo/apps/jobs/cli.py +616 -0
- mojo/apps/jobs/daemon.py +370 -0
- mojo/apps/jobs/examples/sample_jobs.py +376 -0
- mojo/apps/jobs/examples/webhook_examples.py +203 -0
- mojo/apps/jobs/handlers/__init__.py +5 -0
- mojo/apps/jobs/handlers/webhook.py +317 -0
- mojo/apps/jobs/job_engine.py +734 -0
- mojo/apps/jobs/keys.py +203 -0
- mojo/apps/jobs/local_queue.py +363 -0
- mojo/apps/jobs/management/__init__.py +3 -0
- mojo/apps/jobs/management/commands/__init__.py +3 -0
- mojo/apps/jobs/manager.py +1327 -0
- mojo/apps/jobs/migrations/0001_initial.py +97 -0
- mojo/apps/jobs/migrations/0002_alter_job_max_retries_joblog.py +39 -0
- mojo/apps/jobs/models/__init__.py +6 -0
- mojo/apps/jobs/models/job.py +441 -0
- mojo/apps/jobs/rest/__init__.py +2 -0
- mojo/apps/jobs/rest/control.py +466 -0
- mojo/apps/jobs/rest/jobs.py +421 -0
- mojo/apps/jobs/scheduler.py +571 -0
- mojo/apps/jobs/services/__init__.py +6 -0
- mojo/apps/jobs/services/job_actions.py +465 -0
- mojo/apps/jobs/settings.py +209 -0
- mojo/apps/logit/migrations/0004_alter_log_level.py +18 -0
- mojo/apps/logit/models/log.py +7 -1
- mojo/apps/metrics/__init__.py +8 -1
- mojo/apps/metrics/redis_metrics.py +198 -0
- mojo/apps/metrics/rest/__init__.py +3 -0
- mojo/apps/metrics/rest/categories.py +266 -0
- mojo/apps/metrics/rest/helpers.py +48 -0
- mojo/apps/metrics/rest/permissions.py +99 -0
- mojo/apps/metrics/rest/values.py +277 -0
- mojo/apps/metrics/utils.py +19 -2
- mojo/decorators/auth.py +6 -1
- mojo/decorators/http.py +47 -3
- mojo/helpers/aws/__init__.py +45 -0
- mojo/helpers/aws/ec2.py +804 -0
- mojo/helpers/aws/iam.py +748 -0
- mojo/helpers/aws/inbound_email.py +309 -0
- mojo/helpers/aws/kms.py +413 -0
- mojo/helpers/aws/s3.py +451 -11
- mojo/helpers/aws/ses.py +483 -0
- mojo/helpers/aws/ses_domain.py +959 -0
- mojo/helpers/aws/sns.py +461 -0
- mojo/helpers/crypto/__init__.py +1 -1
- mojo/helpers/crypto/utils.py +15 -0
- mojo/helpers/dates.py +18 -0
- mojo/helpers/location/__init__.py +2 -0
- mojo/helpers/location/countries.py +262 -0
- mojo/helpers/location/geolocation.py +196 -0
- mojo/helpers/logit.py +37 -0
- mojo/helpers/redis/__init__.py +2 -0
- mojo/helpers/redis/adapter.py +606 -0
- mojo/helpers/redis/client.py +48 -0
- mojo/helpers/redis/pool.py +225 -0
- mojo/helpers/request.py +8 -0
- mojo/helpers/response.py +14 -2
- mojo/helpers/settings/__init__.py +2 -0
- mojo/helpers/{settings.py → settings/helper.py} +1 -37
- mojo/helpers/settings/parser.py +132 -0
- mojo/middleware/auth.py +1 -1
- mojo/middleware/cors.py +40 -0
- mojo/middleware/logging.py +131 -12
- mojo/middleware/mojo.py +10 -0
- mojo/models/rest.py +494 -65
- mojo/models/secrets.py +98 -3
- mojo/serializers/__init__.py +106 -0
- mojo/serializers/core/__init__.py +90 -0
- mojo/serializers/core/cache/__init__.py +121 -0
- mojo/serializers/core/cache/backends.py +518 -0
- mojo/serializers/core/cache/base.py +102 -0
- mojo/serializers/core/cache/disabled.py +181 -0
- mojo/serializers/core/cache/memory.py +287 -0
- mojo/serializers/core/cache/redis.py +533 -0
- mojo/serializers/core/cache/utils.py +454 -0
- mojo/serializers/core/manager.py +550 -0
- mojo/serializers/core/serializer.py +475 -0
- mojo/serializers/examples/settings.py +322 -0
- mojo/serializers/formats/csv.py +393 -0
- mojo/serializers/formats/localizers.py +509 -0
- mojo/serializers/{models.py → simple.py} +38 -15
- mojo/serializers/suggested_improvements.md +388 -0
- testit/client.py +1 -1
- testit/helpers.py +35 -4
- testit/runner.py +23 -6
- django_nativemojo-0.1.10.dist-info/METADATA +0 -96
- django_nativemojo-0.1.10.dist-info/RECORD +0 -194
- mojo/apps/metrics/rest/db.py +0 -0
- mojo/apps/notify/README.md +0 -91
- mojo/apps/notify/README_NOTIFICATIONS.md +0 -566
- mojo/apps/notify/admin.py +0 -52
- mojo/apps/notify/handlers/example_handlers.py +0 -516
- mojo/apps/notify/handlers/ses/__init__.py +0 -25
- mojo/apps/notify/handlers/ses/bounce.py +0 -0
- mojo/apps/notify/handlers/ses/complaint.py +0 -25
- mojo/apps/notify/handlers/ses/message.py +0 -86
- mojo/apps/notify/management/commands/__init__.py +0 -1
- mojo/apps/notify/management/commands/process_notifications.py +0 -370
- mojo/apps/notify/mod +0 -0
- mojo/apps/notify/models/__init__.py +0 -12
- mojo/apps/notify/models/account.py +0 -128
- mojo/apps/notify/models/attachment.py +0 -24
- mojo/apps/notify/models/bounce.py +0 -68
- mojo/apps/notify/models/complaint.py +0 -40
- mojo/apps/notify/models/inbox.py +0 -113
- mojo/apps/notify/models/inbox_message.py +0 -173
- mojo/apps/notify/models/outbox.py +0 -129
- mojo/apps/notify/models/outbox_message.py +0 -288
- mojo/apps/notify/models/template.py +0 -30
- mojo/apps/notify/providers/aws.py +0 -73
- mojo/apps/notify/rest/ses.py +0 -0
- mojo/apps/notify/utils/__init__.py +0 -2
- mojo/apps/notify/utils/notifications.py +0 -404
- mojo/apps/notify/utils/parsing.py +0 -202
- mojo/apps/notify/utils/render.py +0 -144
- mojo/apps/tasks/README.md +0 -118
- mojo/apps/tasks/__init__.py +0 -11
- mojo/apps/tasks/manager.py +0 -489
- mojo/apps/tasks/rest/__init__.py +0 -2
- mojo/apps/tasks/rest/hooks.py +0 -0
- mojo/apps/tasks/rest/tasks.py +0 -62
- mojo/apps/tasks/runner.py +0 -174
- mojo/apps/tasks/tq_handlers.py +0 -14
- mojo/helpers/aws/setup_email.py +0 -0
- mojo/helpers/redis.py +0 -10
- mojo/models/meta.py +0 -262
- mojo/ws4redis/README.md +0 -174
- mojo/ws4redis/__init__.py +0 -2
- mojo/ws4redis/client.py +0 -283
- mojo/ws4redis/connection.py +0 -327
- mojo/ws4redis/exceptions.py +0 -32
- mojo/ws4redis/redis.py +0 -183
- mojo/ws4redis/servers/base.py +0 -86
- mojo/ws4redis/servers/django.py +0 -171
- mojo/ws4redis/servers/uwsgi.py +0 -63
- mojo/ws4redis/settings.py +0 -45
- mojo/ws4redis/utf8validator.py +0 -128
- mojo/ws4redis/websocket.py +0 -403
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/LICENSE +0 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/NOTICE +0 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/WHEEL +0 -0
- /mojo/apps/{notify → aws}/__init__.py +0 -0
- /mojo/apps/{notify/handlers → aws/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/management → docit/markdown_plugins}/__init__.py +0 -0
- /mojo/apps/{notify/providers → docit/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/rest → fileman/migrations}/__init__.py +0 -0
- /mojo/{ws4redis/servers → apps/jobs/examples}/__init__.py +0 -0
- /mojo/apps/{fileman/models/render.py → jobs/migrations/__init__.py} +0 -0
- /mojo/{serializers → rest}/openapi.py +0 -0
- /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,
|
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(
|
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
|