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
@@ -1,6 +1,10 @@
|
|
1
|
+
import os
|
2
|
+
import boto3
|
3
|
+
from botocore.exceptions import ClientError
|
1
4
|
from django.db import models
|
2
5
|
from mojo.models import MojoModel, MojoSecrets
|
3
|
-
|
6
|
+
from urllib.parse import urlparse
|
7
|
+
from mojo.helpers.settings import settings
|
4
8
|
|
5
9
|
class FileManager(MojoSecrets, MojoModel):
|
6
10
|
"""
|
@@ -11,7 +15,8 @@ class FileManager(MojoSecrets, MojoModel):
|
|
11
15
|
CAN_SAVE = CAN_CREATE = True
|
12
16
|
CAN_DELETE = True
|
13
17
|
DEFAULT_SORT = "-id"
|
14
|
-
|
18
|
+
POST_SAVE_ACTIONS = ["test_connection", "fix_cors", "clone", "check_cors"]
|
19
|
+
VIEW_PERMS = ["view_fileman", "manage_files"]
|
15
20
|
SEARCH_FIELDS = ["name", "backend_type", "description"]
|
16
21
|
SEARCH_TERMS = [
|
17
22
|
"name", "backend_type", "description",
|
@@ -19,12 +24,21 @@ class FileManager(MojoSecrets, MojoModel):
|
|
19
24
|
|
20
25
|
GRAPHS = {
|
21
26
|
"default": {
|
27
|
+
"extra": ["aws_region", "aws_key", "aws_secret_masked", "allowed_origins"],
|
28
|
+
"fields": [
|
29
|
+
"created", "id", "name", "backend_type", "backend_url",
|
30
|
+
"is_active", "is_default"],
|
22
31
|
"graphs": {
|
32
|
+
"user": "basic",
|
23
33
|
"group": "basic"
|
24
34
|
}
|
25
35
|
},
|
26
36
|
"list": {
|
37
|
+
"extra": ["aws_region", "aws_key", "aws_secret_masked", "allowed_origins"],
|
38
|
+
"fields": ["created", "id", "name", "backend_type", "backend_url",
|
39
|
+
"is_active", "is_default"],
|
27
40
|
"graphs": {
|
41
|
+
"user": "basic",
|
28
42
|
"group": "basic"
|
29
43
|
}
|
30
44
|
}
|
@@ -58,6 +72,16 @@ class FileManager(MojoSecrets, MojoModel):
|
|
58
72
|
help_text="Group that owns this file manager configuration"
|
59
73
|
)
|
60
74
|
|
75
|
+
user = models.ForeignKey(
|
76
|
+
"account.User",
|
77
|
+
related_name="file_managers",
|
78
|
+
null=True,
|
79
|
+
blank=True,
|
80
|
+
default=None,
|
81
|
+
on_delete=models.CASCADE,
|
82
|
+
help_text="User that owns this file manager configuration"
|
83
|
+
)
|
84
|
+
|
61
85
|
name = models.CharField(
|
62
86
|
max_length=255,
|
63
87
|
db_index=True,
|
@@ -88,7 +112,7 @@ class FileManager(MojoSecrets, MojoModel):
|
|
88
112
|
)
|
89
113
|
|
90
114
|
max_file_size = models.BigIntegerField(
|
91
|
-
default=
|
115
|
+
default=1000 * 1024 * 1024, # 100MB default
|
92
116
|
help_text="Maximum file size in bytes (0 for unlimited)"
|
93
117
|
)
|
94
118
|
|
@@ -114,6 +138,19 @@ class FileManager(MojoSecrets, MojoModel):
|
|
114
138
|
help_text="Whether this is the default file manager for the group or user"
|
115
139
|
)
|
116
140
|
|
141
|
+
is_public = models.BooleanField(
|
142
|
+
default=True,
|
143
|
+
help_text="Whether this allows public access to the files"
|
144
|
+
)
|
145
|
+
|
146
|
+
parent = models.ForeignKey(
|
147
|
+
'self',
|
148
|
+
on_delete=models.CASCADE,
|
149
|
+
null=True,
|
150
|
+
blank=True,
|
151
|
+
help_text="Used if this file manager is a child of another file manager, and inherits settings from its parent"
|
152
|
+
)
|
153
|
+
|
117
154
|
class Meta:
|
118
155
|
unique_together = [
|
119
156
|
['group', 'name'],
|
@@ -131,16 +168,115 @@ class FileManager(MojoSecrets, MojoModel):
|
|
131
168
|
|
132
169
|
def get_setting(self, key, default=None):
|
133
170
|
"""Get a specific setting value"""
|
134
|
-
|
171
|
+
value = self.get_secret(key, default)
|
172
|
+
if value is None:
|
173
|
+
value = self.primary_parent.get_secret(key, default)
|
174
|
+
return value
|
135
175
|
|
136
176
|
def set_setting(self, key, value):
|
137
177
|
"""Set a specific setting value"""
|
138
178
|
self.set_secret(key, value)
|
139
179
|
|
180
|
+
def set_settings(self, value):
|
181
|
+
"""Set a specific setting value"""
|
182
|
+
self.set_secrets(value)
|
183
|
+
|
184
|
+
def set_backend_url(self, url, *args):
|
185
|
+
"""Set the backend URL"""
|
186
|
+
self.backend_url = os.path.join(url, *args)
|
187
|
+
self.backend_type = self.backend_url.split(':')[0]
|
188
|
+
|
189
|
+
def _update_default(self):
|
190
|
+
if self.is_default:
|
191
|
+
if self.pk is None:
|
192
|
+
FileManager.objects.filter(
|
193
|
+
group=self.group,
|
194
|
+
user=self.user,
|
195
|
+
is_default=True
|
196
|
+
).update(is_default=False)
|
197
|
+
else:
|
198
|
+
FileManager.objects.filter(
|
199
|
+
group=self.group,
|
200
|
+
user=self.user,
|
201
|
+
is_default=True
|
202
|
+
).exclude(pk=self.pk).update(is_default=False)
|
203
|
+
|
204
|
+
_backend = None
|
205
|
+
|
206
|
+
@property
|
207
|
+
def aws_key(self):
|
208
|
+
return self.get_secret('aws_key')
|
209
|
+
|
210
|
+
@property
|
211
|
+
def aws_secret(self):
|
212
|
+
return self.get_secret('aws_secret')
|
213
|
+
|
214
|
+
@property
|
215
|
+
def aws_secret_masked(self):
|
216
|
+
secret = self.get_secret('aws_secret', '')
|
217
|
+
if len(secret) > 4:
|
218
|
+
return '*' * (len(secret) - 4) + secret[-4:]
|
219
|
+
return secret
|
220
|
+
|
221
|
+
@property
|
222
|
+
def aws_region(self):
|
223
|
+
return self.get_secret('aws_region')
|
224
|
+
|
225
|
+
@property
|
226
|
+
def is_verified(self):
|
227
|
+
return self.status in ["verified", "ready"]
|
228
|
+
|
229
|
+
def set_aws_key(self, key):
|
230
|
+
self.set_secret('aws_key', key)
|
231
|
+
|
232
|
+
def set_aws_secret(self, secret):
|
233
|
+
self.set_secret('aws_secret', secret)
|
234
|
+
|
235
|
+
def set_aws_region(self, secret):
|
236
|
+
self.set_secret('aws_region', secret)
|
237
|
+
|
238
|
+
def set_allowed_origins(self, origins):
|
239
|
+
if isinstance(origins, str) and "," in origins:
|
240
|
+
origins = [origin.strip() for origin in origins.split(',')]
|
241
|
+
self.set_secret('allowed_origins', origins)
|
242
|
+
|
243
|
+
@property
|
244
|
+
def allowed_origins(self):
|
245
|
+
return self.get_secret('allowed_origins')
|
246
|
+
|
247
|
+
@property
|
248
|
+
def backend(self):
|
249
|
+
"""Get the backend instance"""
|
250
|
+
from mojo.apps.fileman import backends
|
251
|
+
if not self._backend:
|
252
|
+
self._backend = backends.get_backend(self)
|
253
|
+
return self._backend
|
254
|
+
|
140
255
|
@property
|
141
256
|
def settings(self):
|
142
257
|
return self.secrets
|
143
258
|
|
259
|
+
@property
|
260
|
+
def primary_settings(self):
|
261
|
+
return self.primary_parent.secrets
|
262
|
+
|
263
|
+
@property
|
264
|
+
def primary_parent(self):
|
265
|
+
parent = self
|
266
|
+
while parent.parent:
|
267
|
+
parent = parent.parent
|
268
|
+
return parent
|
269
|
+
|
270
|
+
@property
|
271
|
+
def root_path(self):
|
272
|
+
purl = urlparse(self.backend_url)
|
273
|
+
return purl.path.lstrip('/')
|
274
|
+
|
275
|
+
@property
|
276
|
+
def root_location(self):
|
277
|
+
purl = urlparse(self.backend_url)
|
278
|
+
return purl.netloc
|
279
|
+
|
144
280
|
@property
|
145
281
|
def is_file_system(self):
|
146
282
|
return self.backend_type == self.FILE_SYSTEM
|
@@ -185,43 +321,303 @@ class FileManager(MojoSecrets, MojoModel):
|
|
185
321
|
return True
|
186
322
|
return mime_type.lower() in [mt.lower() for mt in self.allowed_mime_types]
|
187
323
|
|
188
|
-
def
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
324
|
+
def on_rest_created(self):
|
325
|
+
self._update_default()
|
326
|
+
|
327
|
+
def on_rest_pre_save(self, changed_fields, created):
|
328
|
+
self._update_default()
|
329
|
+
if not self.name:
|
330
|
+
self.name = self.generate_name()
|
331
|
+
if created:
|
332
|
+
if not self.aws_region:
|
333
|
+
self.set_aws_region(settings.get("AWS_REGION", "us-east-1"))
|
334
|
+
if not self.aws_key:
|
335
|
+
self.set_aws_key(settings.get("AWS_KEY", None))
|
336
|
+
if not self.aws_secret:
|
337
|
+
self.set_aws_secret(settings.get("AWS_SECRET", None))
|
338
|
+
if created or "is_default" in changed_fields:
|
339
|
+
self._update_default()
|
340
|
+
|
341
|
+
def on_rest_saved(self, changed_fields, created):
|
342
|
+
self._update_default()
|
343
|
+
if not self.name:
|
344
|
+
self.name = self.generate_name()
|
345
|
+
if "is_public" in changed_fields or created:
|
346
|
+
if self.is_public:
|
347
|
+
self.backend.make_path_public()
|
348
|
+
else:
|
349
|
+
self.backend.make_path_private()
|
350
|
+
|
351
|
+
def generate_name(self):
|
352
|
+
if self.user and self.group:
|
353
|
+
return f"{self.user.username}@{self.group.name}'s {self.backend_type} FileManager"
|
354
|
+
elif self.user:
|
355
|
+
return f"{self.user.username}'s {self.backend_type} FileManager"
|
356
|
+
elif self.group:
|
357
|
+
return f"{self.group.name}'s {self.backend_type} FileManager"
|
358
|
+
return f"{self.backend_type} FileManager"
|
359
|
+
|
360
|
+
def on_action_test_connection(self, value):
|
361
|
+
try:
|
362
|
+
self.backend.test_connection()
|
363
|
+
return dict(status=True)
|
364
|
+
except Exception as e:
|
365
|
+
return dict(status=False, error=str(e))
|
366
|
+
|
367
|
+
def on_action_fix_cors(self, value):
|
368
|
+
try:
|
369
|
+
if not self.is_s3:
|
370
|
+
return dict(status=False, error="CORS management is only supported for S3 backends.")
|
371
|
+
# Validate connectivity first
|
372
|
+
self.backend.test_connection()
|
373
|
+
allowed_origins = self._resolve_allowed_origins_from_value_or_settings(value or {})
|
374
|
+
result = self.update_cors(allowed_origins)
|
375
|
+
return dict(status=True, result=result)
|
376
|
+
except Exception as e:
|
377
|
+
return dict(status=False, error=str(e))
|
378
|
+
|
379
|
+
def on_action_check_cors(self, value):
|
380
|
+
try:
|
381
|
+
if not self.is_s3:
|
382
|
+
return dict(status=False, error="CORS management is only supported for S3 backends.")
|
383
|
+
self.backend.test_connection()
|
384
|
+
# allowed_origins = self._resolve_allowed_origins_from_value_or_settings(value or {})
|
385
|
+
result = self.check_cors_config(allowed_origins=self.allowed_origins)
|
386
|
+
return dict(status=True, result=result)
|
387
|
+
except Exception as e:
|
388
|
+
return dict(status=False, error=str(e))
|
389
|
+
|
390
|
+
def on_action_clone(self, value):
|
391
|
+
secrets = self.secrets
|
392
|
+
new_manager = FileManager(user=self.user, group=self.group)
|
393
|
+
new_manager.name = f"Clone of {self.name}"
|
394
|
+
new_manager.backend_url = self.backend_url
|
395
|
+
new_manager.backend_type = self.backend_type
|
396
|
+
new_manager.set_secrets(secrets)
|
397
|
+
new_manager.save()
|
398
|
+
return dict(status=True, id=new_manager.id)
|
399
|
+
|
400
|
+
def fix_cors(self):
|
401
|
+
"""
|
402
|
+
Ensure bucket CORS allows direct uploads from configured origins.
|
403
|
+
This uses manager settings and does not require manual AWS console changes.
|
404
|
+
"""
|
405
|
+
if not self.is_s3:
|
406
|
+
return
|
407
|
+
allowed_origins = self._resolve_allowed_origins_from_value_or_settings({})
|
408
|
+
self.update_cors(allowed_origins)
|
409
|
+
|
410
|
+
# --- CORS helpers for S3 direct upload ---
|
411
|
+
def _s3_client(self):
|
412
|
+
if not self.is_s3:
|
413
|
+
raise ValueError("CORS management is only supported for S3 backends.")
|
414
|
+
session = boto3.Session(
|
415
|
+
aws_access_key_id=self.aws_key,
|
416
|
+
aws_secret_access_key=self.aws_secret,
|
417
|
+
region_name=self.aws_region or "us-east-1",
|
418
|
+
)
|
419
|
+
endpoint_url = self.get_setting("endpoint_url", None)
|
420
|
+
return session.client("s3", endpoint_url=endpoint_url)
|
421
|
+
|
422
|
+
def _resolve_allowed_origins_from_value_or_settings(self, value):
|
423
|
+
"""
|
424
|
+
Resolve a list of allowed origins from action value or global settings.
|
425
|
+
Accepts 'origins', 'allowed_origins', 'domains', or 'list_of_domains' keys.
|
426
|
+
Falls back to settings such as CORS_ALLOWED_ORIGINS, ALLOWED_ORIGINS, FRONTEND_ORIGIN/URL.
|
427
|
+
"""
|
428
|
+
origins = []
|
429
|
+
|
430
|
+
if isinstance(value, dict):
|
431
|
+
for key in ("origins", "allowed_origins", "domains", "list_of_domains"):
|
432
|
+
v = value.get(key)
|
433
|
+
if v:
|
434
|
+
if isinstance(v, str):
|
435
|
+
origins.extend([s.strip() for s in v.split(",") if s.strip()])
|
436
|
+
elif isinstance(v, (list, tuple)):
|
437
|
+
origins.extend([str(s).strip() for s in v if str(s).strip()])
|
438
|
+
break
|
439
|
+
|
440
|
+
for key in ("CORS_ALLOWED_ORIGINS", "ALLOWED_ORIGINS"):
|
441
|
+
v = settings.get(key)
|
442
|
+
if v:
|
443
|
+
if isinstance(v, str):
|
444
|
+
origins.extend([s.strip() for s in v.split(",") if s.strip()])
|
445
|
+
elif isinstance(v, (list, tuple)):
|
446
|
+
origins.extend([str(s).strip() for s in v if str(s).strip()])
|
447
|
+
|
448
|
+
for key in ("FRONTEND_ORIGIN", "FRONTEND_URL", "SITE_URL", "BASE_URL"):
|
449
|
+
v = settings.get(key)
|
450
|
+
if v:
|
451
|
+
origins.append(str(v).strip())
|
452
|
+
|
453
|
+
# Normalize: dedupe, drop trailing slash
|
454
|
+
cleaned = []
|
455
|
+
seen = set()
|
456
|
+
for o in origins:
|
457
|
+
if not o:
|
458
|
+
continue
|
459
|
+
if o.endswith("/"):
|
460
|
+
o = o[:-1]
|
461
|
+
if o not in seen:
|
462
|
+
seen.add(o)
|
463
|
+
cleaned.append(o)
|
464
|
+
|
465
|
+
if not cleaned:
|
466
|
+
raise ValueError("No allowed origins provided. Please pass at least one origin.")
|
467
|
+
return cleaned
|
468
|
+
|
469
|
+
def check_cors_config(self, allowed_origins=None, required_methods=None, required_headers=None):
|
470
|
+
"""
|
471
|
+
Check the current CORS configuration to ensure it supports direct uploads.
|
472
|
+
Note: S3 CORS is bucket-wide. Prefix-level restriction must be enforced by IAM/policy and presigned URLs.
|
473
|
+
"""
|
474
|
+
if not self.is_s3:
|
475
|
+
raise ValueError("CORS management is only supported for S3 backends.")
|
476
|
+
|
477
|
+
s3 = self._s3_client()
|
478
|
+
bucket = self.root_location
|
479
|
+
|
480
|
+
try:
|
481
|
+
resp = s3.get_bucket_cors(Bucket=bucket)
|
482
|
+
config = resp
|
483
|
+
except ClientError as e:
|
484
|
+
if e.response.get("Error", {}).get("Code") == "NoSuchCORSConfiguration":
|
485
|
+
return {"ok": False, "issues": ["No CORS configuration set on this bucket."], "config": None}
|
486
|
+
return {"ok": False, "issues": [str(e)], "config": None}
|
487
|
+
|
488
|
+
if allowed_origins is None:
|
489
|
+
allowed_origins = self._resolve_allowed_origins_from_value_or_settings({})
|
490
|
+
if not allowed_origins:
|
491
|
+
raise ValueError("No allowed origins provided. Please pass at least one origin.")
|
492
|
+
|
493
|
+
required_methods = [m.upper() for m in (required_methods or ["GET", "PUT", "POST", "HEAD"])]
|
494
|
+
required_headers = [h.lower() for h in (required_headers or ["content-type"])]
|
495
|
+
|
496
|
+
rules = config.get("CORSRules", [])
|
497
|
+
issues = []
|
498
|
+
|
499
|
+
def origin_covered(origin: str) -> bool:
|
500
|
+
for r in rules:
|
501
|
+
origins = r.get("AllowedOrigins", [])
|
502
|
+
if "*" in origins or origin in origins:
|
503
|
+
methods = [m.upper() for m in r.get("AllowedMethods", [])]
|
504
|
+
if not all(m in methods for m in required_methods):
|
505
|
+
continue
|
506
|
+
headers = [h.lower() for h in r.get("AllowedHeaders", [])]
|
507
|
+
if "*" in headers or all(h in headers for h in required_headers):
|
508
|
+
return True
|
509
|
+
return False
|
510
|
+
|
511
|
+
for origin in allowed_origins:
|
512
|
+
if not origin_covered(origin):
|
513
|
+
issues.append(f"Origin not covered for direct upload: {origin}")
|
514
|
+
|
515
|
+
return {"ok": len(issues) == 0, "issues": issues, "config": config}
|
516
|
+
|
517
|
+
def update_cors(self, allowed_origins, merge=True, allowed_methods=None, allowed_headers=None, expose_headers=None, max_age_seconds=3000):
|
518
|
+
"""
|
519
|
+
Update bucket CORS to support direct uploads from allowed_origins.
|
520
|
+
If merge=True, append our rule to any existing rules; otherwise replace entirely.
|
521
|
+
"""
|
522
|
+
if not self.is_s3:
|
523
|
+
raise ValueError("CORS management is only supported for S3 backends.")
|
524
|
+
|
525
|
+
s3 = self._s3_client()
|
526
|
+
bucket = self.root_location
|
527
|
+
if not allowed_origins:
|
528
|
+
raise ValueError("No allowed origins provided. Please pass at least one origin.")
|
529
|
+
|
530
|
+
if allowed_methods is None:
|
531
|
+
allowed_methods = ["POST", "HEAD"] if getattr(self.backend, "server_side_encryption", None) else ["PUT", "HEAD"]
|
532
|
+
allowed_methods = [m.upper() for m in allowed_methods]
|
533
|
+
allowed_headers = [h for h in (allowed_headers or ["*"])]
|
534
|
+
expose_headers = expose_headers or ["ETag", "x-amz-request-id", "x-amz-id-2", "x-amz-version-id"]
|
535
|
+
|
536
|
+
desired = {
|
537
|
+
"CORSRules": [
|
538
|
+
{
|
539
|
+
"AllowedOrigins": allowed_origins,
|
540
|
+
"AllowedMethods": allowed_methods,
|
541
|
+
"AllowedHeaders": allowed_headers,
|
542
|
+
"ExposeHeaders": expose_headers,
|
543
|
+
"MaxAgeSeconds": max_age_seconds,
|
544
|
+
}
|
545
|
+
]
|
546
|
+
}
|
196
547
|
|
197
|
-
|
548
|
+
# If already compliant, no change
|
549
|
+
verify_required_headers = [] if getattr(self.backend, "server_side_encryption", None) else ["content-type"]
|
550
|
+
check = self.check_cors_config(allowed_origins, required_methods=allowed_methods, required_headers=verify_required_headers)
|
551
|
+
if check["ok"]:
|
552
|
+
return {"changed": False, "message": "Existing CORS already supports direct uploads.", "current": check["config"]}
|
553
|
+
|
554
|
+
current = None
|
555
|
+
try:
|
556
|
+
current = s3.get_bucket_cors(Bucket=bucket)
|
557
|
+
except ClientError as e:
|
558
|
+
if e.response.get("Error", {}).get("Code") != "NoSuchCORSConfiguration":
|
559
|
+
raise
|
560
|
+
|
561
|
+
if merge and current:
|
562
|
+
merged = {"CORSRules": current.get("CORSRules", []) + desired["CORSRules"]}
|
563
|
+
s3.put_bucket_cors(Bucket=bucket, CORSConfiguration=merged)
|
564
|
+
applied = merged
|
565
|
+
else:
|
566
|
+
s3.put_bucket_cors(Bucket=bucket, CORSConfiguration=desired)
|
567
|
+
applied = desired
|
568
|
+
|
569
|
+
verify_required_headers = [] if getattr(self.backend, "server_side_encryption", None) else ["content-type"]
|
570
|
+
verify = self.check_cors_config(allowed_origins, required_methods=allowed_methods, required_headers=verify_required_headers)
|
571
|
+
return {"changed": True, "applied": applied, "verified": verify["ok"], "post_update_issues": verify["issues"]}
|
198
572
|
|
199
573
|
@classmethod
|
200
574
|
def get_from_request(cls, request):
|
201
575
|
"""Get the file manager from the request"""
|
202
|
-
if request.DATA.fileman:
|
203
|
-
return cls.objects.get(pk=request.DATA.fileman)
|
204
|
-
if request.DATA.use_groups_fileman:
|
576
|
+
if request.DATA.get(["fileman", "filemanager"]):
|
577
|
+
return cls.objects.get(pk=request.DATA.get(["fileman", "filemanager"]))
|
578
|
+
if request.DATA.use_groups_fileman and request.group:
|
205
579
|
return cls.get_for_user_group(group=request.group)
|
206
580
|
return cls.get_for_user_group(user=request.user, group=request.group)
|
207
581
|
|
208
582
|
@classmethod
|
209
|
-
def
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
group=group,
|
220
|
-
|
583
|
+
def get_for_user(cls, user, group=None):
|
584
|
+
file_manager = cls.objects.filter(
|
585
|
+
user=user, group=group, is_default=True, is_active=True
|
586
|
+
).first()
|
587
|
+
if file_manager is None:
|
588
|
+
if group:
|
589
|
+
sys_manager = cls.get_for_group(group=group)
|
590
|
+
else:
|
591
|
+
sys_manager = cls.objects.filter(user=None, group=None, is_default=True, is_active=True).first()
|
592
|
+
if sys_manager is not None:
|
593
|
+
file_manager = cls(user=user, is_default=True, group=group, parent=sys_manager)
|
594
|
+
file_manager.set_backend_url(sys_manager.backend_url, user.uuid.hex)
|
595
|
+
file_manager.save()
|
596
|
+
return file_manager
|
221
597
|
|
222
|
-
|
223
|
-
|
224
|
-
|
598
|
+
@classmethod
|
599
|
+
def get_for_group(cls, group=None):
|
600
|
+
file_manager = cls.objects.filter(
|
601
|
+
user=None, group=group, is_default=True, is_active=True
|
602
|
+
).first()
|
603
|
+
if file_manager is None:
|
604
|
+
sys_manager = cls.objects.filter(
|
605
|
+
user=None, group=None, is_default=True, is_active=True
|
225
606
|
).first()
|
607
|
+
if sys_manager is not None:
|
608
|
+
file_manager = cls(group=group, is_default=True, user=None, parent=sys_manager)
|
609
|
+
file_manager.set_backend_url(sys_manager.backend_url, group.uuid.hex)
|
610
|
+
file_manager.save()
|
611
|
+
return file_manager
|
226
612
|
|
613
|
+
@classmethod
|
614
|
+
def get_for_user_group(cls, user=None, group=None):
|
615
|
+
"""Get the file manager from the user and/or group"""
|
616
|
+
file_manager = None
|
617
|
+
if user and group is None:
|
618
|
+
file_manager = cls.get_for_user(user=user)
|
619
|
+
if not file_manager and group and user is None:
|
620
|
+
file_manager = cls.get_for_user_group(group=group)
|
621
|
+
if not file_manager and group and user:
|
622
|
+
file_manager = cls.get_for_user(user=user, group=group)
|
227
623
|
return file_manager
|
@@ -0,0 +1,118 @@
|
|
1
|
+
from django.db import models
|
2
|
+
from mojo.models import MojoModel
|
3
|
+
import uuid
|
4
|
+
import hashlib
|
5
|
+
import mimetypes
|
6
|
+
from datetime import datetime
|
7
|
+
import os
|
8
|
+
from mojo.apps.fileman import utils
|
9
|
+
from mojo.apps.fileman.models import FileManager
|
10
|
+
from typing import Text
|
11
|
+
|
12
|
+
|
13
|
+
class FileRendition(models.Model, MojoModel):
|
14
|
+
"""
|
15
|
+
File model representing uploaded files with metadata and storage information
|
16
|
+
"""
|
17
|
+
|
18
|
+
class RestMeta:
|
19
|
+
CAN_SAVE = CAN_CREATE = True
|
20
|
+
CAN_DELETE = True
|
21
|
+
DEFAULT_SORT = "-created"
|
22
|
+
VIEW_PERMS = ["view_fileman", "manage_files"]
|
23
|
+
SEARCH_FIELDS = ["filename", "content_type"]
|
24
|
+
SEARCH_TERMS = [
|
25
|
+
"filename", "content_type",
|
26
|
+
("group", "group__name"),
|
27
|
+
("file_manager", "file_manager__name")]
|
28
|
+
|
29
|
+
GRAPHS = {
|
30
|
+
"upload": {
|
31
|
+
"fields": ["id", "filename", "content_type", "file_size"],
|
32
|
+
},
|
33
|
+
"default": {
|
34
|
+
"extra": ["url"],
|
35
|
+
},
|
36
|
+
"list": {
|
37
|
+
"extra": ["url"],
|
38
|
+
}
|
39
|
+
}
|
40
|
+
|
41
|
+
# Upload status choices
|
42
|
+
PENDING = 'pending'
|
43
|
+
RENDERING = 'rendering'
|
44
|
+
COMPLETED = 'completed'
|
45
|
+
FAILED = 'failed'
|
46
|
+
EXPIRED = 'expired'
|
47
|
+
|
48
|
+
created = models.DateTimeField(auto_now_add=True, editable=False, db_index=True)
|
49
|
+
modified = models.DateTimeField(auto_now=True)
|
50
|
+
|
51
|
+
original_file = models.ForeignKey(
|
52
|
+
"fileman.File",
|
53
|
+
related_name="file_renditions",
|
54
|
+
on_delete=models.CASCADE,
|
55
|
+
help_text="The parent file"
|
56
|
+
)
|
57
|
+
|
58
|
+
filename = models.CharField(
|
59
|
+
max_length=255,
|
60
|
+
db_index=True,
|
61
|
+
help_text="rendition filename"
|
62
|
+
)
|
63
|
+
|
64
|
+
storage_path = models.TextField(
|
65
|
+
help_text="Storage path and filename",
|
66
|
+
)
|
67
|
+
|
68
|
+
download_url = models.TextField(
|
69
|
+
blank=True,
|
70
|
+
null=True,
|
71
|
+
default=None,
|
72
|
+
help_text="Persistent URL for downloading the file, (if allowed)"
|
73
|
+
)
|
74
|
+
|
75
|
+
file_size = models.BigIntegerField(
|
76
|
+
null=True,
|
77
|
+
blank=True,
|
78
|
+
help_text="File size in bytes"
|
79
|
+
)
|
80
|
+
|
81
|
+
content_type = models.CharField(
|
82
|
+
max_length=255,
|
83
|
+
help_text="MIME type of the file"
|
84
|
+
)
|
85
|
+
|
86
|
+
category = models.CharField(
|
87
|
+
max_length=255,
|
88
|
+
help_text="A category for the file, like 'image', 'document', 'video', etc."
|
89
|
+
)
|
90
|
+
|
91
|
+
role = models.CharField(
|
92
|
+
max_length=255,
|
93
|
+
db_index=True,
|
94
|
+
help_text="The role of the file, like 'thumbnail', 'preview', 'full', etc."
|
95
|
+
)
|
96
|
+
|
97
|
+
upload_status = models.CharField(
|
98
|
+
max_length=32,
|
99
|
+
default=PENDING,
|
100
|
+
db_index=True,
|
101
|
+
help_text="Current status of rendering"
|
102
|
+
)
|
103
|
+
|
104
|
+
@property
|
105
|
+
def file_manager(self):
|
106
|
+
return self.original_file.file_manager
|
107
|
+
|
108
|
+
@property
|
109
|
+
def url(self):
|
110
|
+
return self.generate_download_url()
|
111
|
+
|
112
|
+
def generate_download_url(self):
|
113
|
+
if self.download_url:
|
114
|
+
return self.download_url
|
115
|
+
if self.file_manager.is_public:
|
116
|
+
self.download_url = self.file_manager.backend.get_url(self.storage_path)
|
117
|
+
return self.download_url
|
118
|
+
return self.file_manager.backend.get_url(self.storage_path, self.get_setting("urls_expire_in", 3600))
|