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.
- django_nativemojo-0.1.15.dist-info/METADATA +136 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/RECORD +105 -65
- 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 +531 -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/models/group.py +25 -7
- mojo/apps/account/models/member.py +15 -4
- mojo/apps/account/models/user.py +197 -20
- mojo/apps/account/rest/group.py +1 -0
- mojo/apps/account/rest/user.py +6 -2
- mojo/apps/aws/rest/__init__.py +1 -0
- mojo/apps/aws/rest/s3.py +64 -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 +200 -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 +204 -58
- mojo/apps/fileman/models/manager.py +161 -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/models/__init__.py +1 -0
- mojo/apps/incident/models/history.py +36 -0
- mojo/apps/incident/models/incident.py +1 -1
- mojo/apps/incident/reporter.py +3 -1
- mojo/apps/incident/rest/event.py +7 -1
- mojo/apps/logit/migrations/0004_alter_log_level.py +18 -0
- mojo/apps/logit/models/log.py +4 -1
- mojo/apps/metrics/utils.py +2 -2
- mojo/apps/notify/handlers/ses/message.py +1 -1
- mojo/apps/notify/providers/aws.py +2 -2
- mojo/apps/tasks/__init__.py +34 -1
- mojo/apps/tasks/manager.py +200 -45
- mojo/apps/tasks/rest/tasks.py +24 -10
- mojo/apps/tasks/runner.py +283 -18
- mojo/apps/tasks/task.py +99 -0
- mojo/apps/tasks/tq_handlers.py +118 -0
- mojo/decorators/auth.py +6 -1
- mojo/decorators/http.py +7 -2
- mojo/helpers/aws/__init__.py +41 -0
- mojo/helpers/aws/ec2.py +804 -0
- mojo/helpers/aws/iam.py +748 -0
- mojo/helpers/aws/s3.py +451 -11
- mojo/helpers/aws/ses.py +483 -0
- mojo/helpers/aws/sns.py +461 -0
- mojo/helpers/crypto/__pycache__/hash.cpython-310.pyc +0 -0
- mojo/helpers/crypto/__pycache__/sign.cpython-310.pyc +0 -0
- mojo/helpers/crypto/__pycache__/utils.cpython-310.pyc +0 -0
- mojo/helpers/dates.py +18 -0
- mojo/helpers/response.py +6 -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/logging.py +1 -1
- mojo/middleware/mojo.py +5 -0
- mojo/models/rest.py +261 -46
- mojo/models/secrets.py +13 -4
- mojo/serializers/__init__.py +100 -0
- mojo/serializers/advanced/README.md +363 -0
- mojo/serializers/advanced/__init__.py +247 -0
- mojo/serializers/advanced/formats/__init__.py +28 -0
- mojo/serializers/advanced/formats/csv.py +416 -0
- mojo/serializers/advanced/formats/excel.py +516 -0
- mojo/serializers/advanced/formats/json.py +239 -0
- mojo/serializers/advanced/formats/localizers.py +509 -0
- mojo/serializers/advanced/formats/response.py +485 -0
- mojo/serializers/advanced/serializer.py +568 -0
- mojo/serializers/manager.py +501 -0
- mojo/serializers/optimized.py +618 -0
- mojo/serializers/settings_example.py +322 -0
- mojo/serializers/{models.py → simple.py} +38 -15
- testit/helpers.py +21 -4
- django_nativemojo-0.1.10.dist-info/METADATA +0 -96
- mojo/apps/metrics/rest/db.py +0 -0
- mojo/helpers/aws/setup_email.py +0 -0
- 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.15.dist-info}/LICENSE +0 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/NOTICE +0 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/WHEEL +0 -0
- /mojo/{ws4redis/servers → apps/aws}/__init__.py +0 -0
- /mojo/apps/{fileman/models/render.py → aws/models/__init__.py} +0 -0
- /mojo/apps/fileman/{rest/__init__ → migrations/__init__.py} +0 -0
@@ -0,0 +1,24 @@
|
|
1
|
+
# Generated by Django 4.2.21 on 2025-06-07 20:02
|
2
|
+
|
3
|
+
from django.db import migrations, models
|
4
|
+
import django.db.models.deletion
|
5
|
+
|
6
|
+
|
7
|
+
class Migration(migrations.Migration):
|
8
|
+
|
9
|
+
dependencies = [
|
10
|
+
('fileman', '0001_initial'),
|
11
|
+
]
|
12
|
+
|
13
|
+
operations = [
|
14
|
+
migrations.AddField(
|
15
|
+
model_name='filemanager',
|
16
|
+
name='parent',
|
17
|
+
field=models.ForeignKey(blank=True, help_text='Used if this file manager is a child of another file manager, and inherits settings from its parent', null=True, on_delete=django.db.models.deletion.CASCADE, to='fileman.filemanager'),
|
18
|
+
),
|
19
|
+
migrations.AlterField(
|
20
|
+
model_name='filemanager',
|
21
|
+
name='max_file_size',
|
22
|
+
field=models.BigIntegerField(default=1048576000, help_text='Maximum file size in bytes (0 for unlimited)'),
|
23
|
+
),
|
24
|
+
]
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# Generated by Django 4.2.21 on 2025-06-08 04:49
|
2
|
+
|
3
|
+
from django.db import migrations
|
4
|
+
|
5
|
+
|
6
|
+
class Migration(migrations.Migration):
|
7
|
+
|
8
|
+
dependencies = [
|
9
|
+
('fileman', '0002_filemanager_parent_alter_filemanager_max_file_size'),
|
10
|
+
]
|
11
|
+
|
12
|
+
operations = [
|
13
|
+
migrations.RemoveIndex(
|
14
|
+
model_name='file',
|
15
|
+
name='fileman_fil_upload__c4bc35_idx',
|
16
|
+
),
|
17
|
+
migrations.RemoveField(
|
18
|
+
model_name='file',
|
19
|
+
name='upload_expires_at',
|
20
|
+
),
|
21
|
+
migrations.RemoveField(
|
22
|
+
model_name='file',
|
23
|
+
name='upload_url',
|
24
|
+
),
|
25
|
+
]
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# Generated by Django 4.2.21 on 2025-06-08 05:14
|
2
|
+
|
3
|
+
from django.conf import settings
|
4
|
+
from django.db import migrations, models
|
5
|
+
import django.db.models.deletion
|
6
|
+
|
7
|
+
|
8
|
+
class Migration(migrations.Migration):
|
9
|
+
|
10
|
+
dependencies = [
|
11
|
+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
12
|
+
('fileman', '0003_remove_file_fileman_fil_upload__c4bc35_idx_and_more'),
|
13
|
+
]
|
14
|
+
|
15
|
+
operations = [
|
16
|
+
migrations.RemoveField(
|
17
|
+
model_name='file',
|
18
|
+
name='original_filename',
|
19
|
+
),
|
20
|
+
migrations.RemoveField(
|
21
|
+
model_name='file',
|
22
|
+
name='uploaded_by',
|
23
|
+
),
|
24
|
+
migrations.AddField(
|
25
|
+
model_name='file',
|
26
|
+
name='storage_filename',
|
27
|
+
field=models.CharField(blank=True, default=None, help_text='Storage filename', max_length=255, null=True),
|
28
|
+
),
|
29
|
+
migrations.AddField(
|
30
|
+
model_name='file',
|
31
|
+
name='user',
|
32
|
+
field=models.ForeignKey(blank=True, default=None, help_text='User who uploaded this file', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='files', to=settings.AUTH_USER_MODEL),
|
33
|
+
),
|
34
|
+
migrations.AlterField(
|
35
|
+
model_name='file',
|
36
|
+
name='filename',
|
37
|
+
field=models.CharField(db_index=True, help_text='User-provided filename', max_length=255),
|
38
|
+
),
|
39
|
+
]
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# Generated by Django 4.2.21 on 2025-06-08 12:55
|
2
|
+
|
3
|
+
from django.db import migrations, models
|
4
|
+
|
5
|
+
|
6
|
+
class Migration(migrations.Migration):
|
7
|
+
|
8
|
+
dependencies = [
|
9
|
+
('fileman', '0004_remove_file_original_filename_and_more'),
|
10
|
+
]
|
11
|
+
|
12
|
+
operations = [
|
13
|
+
migrations.AlterField(
|
14
|
+
model_name='file',
|
15
|
+
name='upload_token',
|
16
|
+
field=models.CharField(db_index=True, help_text='Unique token for tracking direct uploads', max_length=64),
|
17
|
+
),
|
18
|
+
]
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# Generated by Django 4.2.21 on 2025-06-08 13:25
|
2
|
+
|
3
|
+
from django.db import migrations, models
|
4
|
+
|
5
|
+
|
6
|
+
class Migration(migrations.Migration):
|
7
|
+
|
8
|
+
dependencies = [
|
9
|
+
('fileman', '0005_alter_file_upload_token'),
|
10
|
+
]
|
11
|
+
|
12
|
+
operations = [
|
13
|
+
migrations.AddField(
|
14
|
+
model_name='file',
|
15
|
+
name='download_url',
|
16
|
+
field=models.TextField(blank=True, default=None, help_text='Persistent URL for downloading the file, (if allowed)', null=True),
|
17
|
+
),
|
18
|
+
migrations.AddField(
|
19
|
+
model_name='filemanager',
|
20
|
+
name='forever_urls',
|
21
|
+
field=models.BooleanField(default=False, help_text='Allow URLs to be permanent and not expire'),
|
22
|
+
),
|
23
|
+
]
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# Generated by Django 4.2.21 on 2025-06-08 16:40
|
2
|
+
|
3
|
+
from django.db import migrations, models
|
4
|
+
|
5
|
+
|
6
|
+
class Migration(migrations.Migration):
|
7
|
+
|
8
|
+
dependencies = [
|
9
|
+
('fileman', '0006_file_download_url_filemanager_forever_urls'),
|
10
|
+
]
|
11
|
+
|
12
|
+
operations = [
|
13
|
+
migrations.RemoveField(
|
14
|
+
model_name='filemanager',
|
15
|
+
name='forever_urls',
|
16
|
+
),
|
17
|
+
migrations.AddField(
|
18
|
+
model_name='filemanager',
|
19
|
+
name='is_public',
|
20
|
+
field=models.BooleanField(default=True, help_text='Whether this allows public access to the files'),
|
21
|
+
),
|
22
|
+
]
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# Generated by Django 4.2.21 on 2025-06-08 16:59
|
2
|
+
|
3
|
+
from django.db import migrations, models
|
4
|
+
|
5
|
+
|
6
|
+
class Migration(migrations.Migration):
|
7
|
+
|
8
|
+
dependencies = [
|
9
|
+
('fileman', '0007_remove_filemanager_forever_urls_and_more'),
|
10
|
+
]
|
11
|
+
|
12
|
+
operations = [
|
13
|
+
migrations.AddField(
|
14
|
+
model_name='file',
|
15
|
+
name='category',
|
16
|
+
field=models.CharField(blank=True, db_index=True, default=None, help_text="A category for the file, like 'image', 'document', 'video', etc.", max_length=255, null=True),
|
17
|
+
),
|
18
|
+
]
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# Generated by Django 4.2.21 on 2025-06-08 17:32
|
2
|
+
|
3
|
+
from django.db import migrations
|
4
|
+
|
5
|
+
|
6
|
+
class Migration(migrations.Migration):
|
7
|
+
|
8
|
+
dependencies = [
|
9
|
+
('fileman', '0008_file_category'),
|
10
|
+
]
|
11
|
+
|
12
|
+
operations = [
|
13
|
+
migrations.RenameField(
|
14
|
+
model_name='file',
|
15
|
+
old_name='file_path',
|
16
|
+
new_name='storage_file_path',
|
17
|
+
),
|
18
|
+
]
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# Generated by Django 4.2.21 on 2025-06-09 03:49
|
2
|
+
|
3
|
+
from django.db import migrations, models
|
4
|
+
import django.db.models.deletion
|
5
|
+
import mojo.models.rest
|
6
|
+
|
7
|
+
|
8
|
+
class Migration(migrations.Migration):
|
9
|
+
|
10
|
+
dependencies = [
|
11
|
+
('fileman', '0009_rename_file_path_file_storage_file_path'),
|
12
|
+
]
|
13
|
+
|
14
|
+
operations = [
|
15
|
+
migrations.CreateModel(
|
16
|
+
name='FileRendition',
|
17
|
+
fields=[
|
18
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
19
|
+
('created', models.DateTimeField(auto_now_add=True, db_index=True)),
|
20
|
+
('modified', models.DateTimeField(auto_now=True)),
|
21
|
+
('filename', models.CharField(db_index=True, help_text='rendition filename', max_length=255)),
|
22
|
+
('storage_path', models.TextField(help_text='Storage path and filename')),
|
23
|
+
('download_url', models.TextField(blank=True, default=None, help_text='Persistent URL for downloading the file, (if allowed)', null=True)),
|
24
|
+
('file_size', models.BigIntegerField(blank=True, help_text='File size in bytes', null=True)),
|
25
|
+
('content_type', models.CharField(help_text='MIME type of the file', max_length=255)),
|
26
|
+
('category', models.CharField(help_text="A category for the file, like 'image', 'document', 'video', etc.", max_length=255)),
|
27
|
+
('role', models.CharField(db_index=True, help_text="The role of the file, like 'thumbnail', 'preview', 'full', etc.", max_length=255)),
|
28
|
+
('upload_status', models.CharField(db_index=True, default='pending', help_text='Current status of rendering', max_length=32)),
|
29
|
+
('original_file', models.ForeignKey(help_text='The parent file', on_delete=django.db.models.deletion.CASCADE, related_name='renditions', to='fileman.file')),
|
30
|
+
],
|
31
|
+
bases=(models.Model, mojo.models.rest.MojoModel),
|
32
|
+
),
|
33
|
+
]
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# Generated by Django 4.2.21 on 2025-06-09 05:37
|
2
|
+
|
3
|
+
from django.db import migrations, models
|
4
|
+
import django.db.models.deletion
|
5
|
+
|
6
|
+
|
7
|
+
class Migration(migrations.Migration):
|
8
|
+
|
9
|
+
dependencies = [
|
10
|
+
('fileman', '0010_filerendition'),
|
11
|
+
]
|
12
|
+
|
13
|
+
operations = [
|
14
|
+
migrations.AlterField(
|
15
|
+
model_name='filerendition',
|
16
|
+
name='original_file',
|
17
|
+
field=models.ForeignKey(help_text='The parent file', on_delete=django.db.models.deletion.CASCADE, related_name='file_renditions', to='fileman.file'),
|
18
|
+
),
|
19
|
+
]
|
mojo/apps/fileman/models/file.py
CHANGED
@@ -1,9 +1,16 @@
|
|
1
1
|
from django.db import models
|
2
2
|
from mojo.models import MojoModel
|
3
|
+
from objict import objict
|
4
|
+
import io
|
3
5
|
import uuid
|
4
6
|
import hashlib
|
7
|
+
import base64
|
8
|
+
import magic
|
5
9
|
import mimetypes
|
6
10
|
from datetime import datetime
|
11
|
+
import os
|
12
|
+
from mojo.apps.fileman import utils
|
13
|
+
from mojo.apps.fileman.models import FileManager
|
7
14
|
|
8
15
|
|
9
16
|
class File(models.Model, MojoModel):
|
@@ -15,25 +22,38 @@ class File(models.Model, MojoModel):
|
|
15
22
|
CAN_SAVE = CAN_CREATE = True
|
16
23
|
CAN_DELETE = True
|
17
24
|
DEFAULT_SORT = "-created"
|
18
|
-
VIEW_PERMS = ["view_fileman"]
|
19
|
-
SEARCH_FIELDS = ["filename", "
|
25
|
+
VIEW_PERMS = ["view_fileman", "manage_files"]
|
26
|
+
SEARCH_FIELDS = ["filename", "content_type"]
|
20
27
|
SEARCH_TERMS = [
|
21
|
-
"filename",
|
28
|
+
"filename", "content_type",
|
22
29
|
("group", "group__name"),
|
23
30
|
("file_manager", "file_manager__name")]
|
24
31
|
|
25
32
|
GRAPHS = {
|
26
|
-
"
|
33
|
+
"upload": {
|
34
|
+
"fields": ["id", "filename", "content_type", "file_size", "upload_url"],
|
35
|
+
},
|
36
|
+
"detailed": {
|
37
|
+
"extra": ["url", "renditions"],
|
27
38
|
"graphs": {
|
28
39
|
"group": "basic",
|
29
40
|
"file_manager": "basic",
|
30
|
-
"
|
41
|
+
"user": "basic"
|
31
42
|
}
|
32
43
|
},
|
44
|
+
"basic": {
|
45
|
+
"fields": ["id", "filename", "content_type", "category"],
|
46
|
+
"extra": ["url", "renditions"],
|
47
|
+
},
|
48
|
+
"default": {
|
49
|
+
"extra": ["url", "renditions"],
|
50
|
+
},
|
33
51
|
"list": {
|
52
|
+
"extra": ["url", "renditions"],
|
34
53
|
"graphs": {
|
35
54
|
"group": "basic",
|
36
|
-
"file_manager": "basic"
|
55
|
+
"file_manager": "basic",
|
56
|
+
"user": "basic"
|
37
57
|
}
|
38
58
|
}
|
39
59
|
}
|
@@ -66,9 +86,9 @@ class File(models.Model, MojoModel):
|
|
66
86
|
help_text="Group that owns this file"
|
67
87
|
)
|
68
88
|
|
69
|
-
|
89
|
+
user = models.ForeignKey(
|
70
90
|
"account.User",
|
71
|
-
related_name="
|
91
|
+
related_name="files",
|
72
92
|
null=True,
|
73
93
|
blank=True,
|
74
94
|
default=None,
|
@@ -86,18 +106,28 @@ class File(models.Model, MojoModel):
|
|
86
106
|
filename = models.CharField(
|
87
107
|
max_length=255,
|
88
108
|
db_index=True,
|
89
|
-
help_text="
|
109
|
+
help_text="User-provided filename"
|
90
110
|
)
|
91
111
|
|
92
|
-
|
112
|
+
storage_filename = models.CharField(
|
93
113
|
max_length=255,
|
94
|
-
help_text="
|
114
|
+
help_text="Storage filename",
|
115
|
+
default=None,
|
116
|
+
blank=True,
|
117
|
+
null=True,
|
95
118
|
)
|
96
119
|
|
97
|
-
|
120
|
+
storage_file_path = models.TextField(
|
98
121
|
help_text="Full path to file in storage backend"
|
99
122
|
)
|
100
123
|
|
124
|
+
download_url = models.TextField(
|
125
|
+
blank=True,
|
126
|
+
null=True,
|
127
|
+
default=None,
|
128
|
+
help_text="Persistent URL for downloading the file, (if allowed)"
|
129
|
+
)
|
130
|
+
|
101
131
|
file_size = models.BigIntegerField(
|
102
132
|
null=True,
|
103
133
|
blank=True,
|
@@ -110,6 +140,15 @@ class File(models.Model, MojoModel):
|
|
110
140
|
help_text="MIME type of the file"
|
111
141
|
)
|
112
142
|
|
143
|
+
category = models.CharField(
|
144
|
+
max_length=255,
|
145
|
+
db_index=True,
|
146
|
+
default=None,
|
147
|
+
blank=True,
|
148
|
+
null=True,
|
149
|
+
help_text="A category for the file, like 'image', 'document', 'video', etc."
|
150
|
+
)
|
151
|
+
|
113
152
|
checksum = models.CharField(
|
114
153
|
max_length=128,
|
115
154
|
blank=True,
|
@@ -119,7 +158,6 @@ class File(models.Model, MojoModel):
|
|
119
158
|
|
120
159
|
upload_token = models.CharField(
|
121
160
|
max_length=64,
|
122
|
-
unique=True,
|
123
161
|
db_index=True,
|
124
162
|
help_text="Unique token for tracking direct uploads"
|
125
163
|
)
|
@@ -132,18 +170,6 @@ class File(models.Model, MojoModel):
|
|
132
170
|
help_text="Current status of the file upload"
|
133
171
|
)
|
134
172
|
|
135
|
-
upload_url = models.TextField(
|
136
|
-
blank=True,
|
137
|
-
default="",
|
138
|
-
help_text="Pre-signed URL for direct upload (temporary)"
|
139
|
-
)
|
140
|
-
|
141
|
-
upload_expires_at = models.DateTimeField(
|
142
|
-
null=True,
|
143
|
-
blank=True,
|
144
|
-
help_text="When the upload URL expires"
|
145
|
-
)
|
146
|
-
|
147
173
|
metadata = models.JSONField(
|
148
174
|
default=dict,
|
149
175
|
blank=True,
|
@@ -160,43 +186,64 @@ class File(models.Model, MojoModel):
|
|
160
186
|
help_text="Whether this file can be accessed without authentication"
|
161
187
|
)
|
162
188
|
|
189
|
+
upload_url = None
|
190
|
+
|
163
191
|
class Meta:
|
164
192
|
indexes = [
|
165
193
|
models.Index(fields=['upload_status', 'created']),
|
166
194
|
models.Index(fields=['file_manager', 'upload_status']),
|
167
195
|
models.Index(fields=['group', 'is_active']),
|
168
196
|
models.Index(fields=['content_type', 'is_active']),
|
169
|
-
models.Index(fields=['upload_expires_at']),
|
170
197
|
]
|
171
198
|
|
172
199
|
def __str__(self):
|
173
|
-
return f"{self.
|
174
|
-
|
175
|
-
def
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
self.
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
200
|
+
return f"{self.filename} ({self.get_upload_status_display()})"
|
201
|
+
|
202
|
+
def on_rest_pre_save(self, changed_fields, created):
|
203
|
+
if created:
|
204
|
+
if not hasattr(self, "file_manager") or self.file_manager is None:
|
205
|
+
self.file_manager = FileManager.get_from_request(self.active_request)
|
206
|
+
if not self.content_type:
|
207
|
+
self.content_type = mimetypes.guess_type(self.filename)[0] or 'application/octet-stream'
|
208
|
+
self.category = utils.get_file_category(self.content_type)
|
209
|
+
if not self.storage_filename:
|
210
|
+
self.generate_storage_filename()
|
211
|
+
|
212
|
+
def on_rest_pre_delete(self):
|
213
|
+
# we need to handle the deletion of the file from storage
|
214
|
+
if self.storage_file_path:
|
215
|
+
name, ext = os.path.splitext(self.filename)
|
216
|
+
renditions_path = os.path.join(self.file_manager.root_path, name)
|
217
|
+
self.file_manager.backend.delete_folder(renditions_path)
|
218
|
+
self.file_manager.backend.delete(self.storage_file_path)
|
219
|
+
|
220
|
+
def generate_upload_token(self, commit=False):
|
190
221
|
"""Generate a unique upload token"""
|
191
|
-
|
222
|
+
self.upload_token = hashlib.sha256(f"{uuid.uuid4()}{datetime.now()}".encode()).hexdigest()[:32]
|
223
|
+
if commit:
|
224
|
+
self.save()
|
192
225
|
|
193
|
-
def
|
226
|
+
def generate_storage_filename(self):
|
194
227
|
"""Generate a unique filename for storage"""
|
195
|
-
|
196
|
-
|
197
|
-
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
228
|
+
name, ext = os.path.splitext(self.filename)
|
229
|
+
# timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
198
230
|
unique_id = str(uuid.uuid4())[:8]
|
199
|
-
|
231
|
+
self.storage_filename = f"{name}_{unique_id}{ext}"
|
232
|
+
self.storage_file_path = os.path.join(self.file_manager.root_path, self.storage_filename)
|
233
|
+
|
234
|
+
def request_upload_url(self):
|
235
|
+
"""Request a pre-signed URL for direct upload"""
|
236
|
+
if not self.file_manager.backend.supports_direct_upload:
|
237
|
+
self.generate_upload_token(True)
|
238
|
+
self.upload_url = f"/api/fileman/upload/{self.upload_token}"
|
239
|
+
else:
|
240
|
+
data = self.file_manager.backend.generate_upload_url(self.storage_file_path, self.content_type, self.file_size)
|
241
|
+
self.debug("request_upload_url", data)
|
242
|
+
if "url" in data:
|
243
|
+
self.upload_url = data['url']
|
244
|
+
else:
|
245
|
+
self.upload_url = data
|
246
|
+
return self.upload_url
|
200
247
|
|
201
248
|
def get_metadata(self, key, default=None):
|
202
249
|
"""Get a specific metadata value"""
|
@@ -206,6 +253,13 @@ class File(models.Model, MojoModel):
|
|
206
253
|
"""Set a specific metadata value"""
|
207
254
|
self.metadata[key] = value
|
208
255
|
|
256
|
+
_renditions = None
|
257
|
+
@property
|
258
|
+
def renditions(self):
|
259
|
+
if self._renditions is None:
|
260
|
+
self._renditions = objict.from_dict({r.role: r.to_dict() for r in self.file_renditions.all()})
|
261
|
+
return self._renditions
|
262
|
+
|
209
263
|
@property
|
210
264
|
def is_pending(self):
|
211
265
|
return self.upload_status == self.PENDING
|
@@ -233,26 +287,65 @@ class File(models.Model, MojoModel):
|
|
233
287
|
return False
|
234
288
|
return datetime.now() > self.upload_expires_at
|
235
289
|
|
236
|
-
|
290
|
+
@property
|
291
|
+
def url(self):
|
292
|
+
return self.generate_download_url()
|
293
|
+
|
294
|
+
def generate_download_url(self):
|
295
|
+
if self.download_url:
|
296
|
+
return self.download_url
|
297
|
+
if self.file_manager.is_public:
|
298
|
+
self.download_url = self.file_manager.backend.get_url(self.storage_file_path)
|
299
|
+
return self.download_url
|
300
|
+
return self.file_manager.backend.get_url(self.storage_file_path, self.get_setting("urls_expire_in", 3600))
|
301
|
+
|
302
|
+
def set_action(self, action):
|
303
|
+
if action == "mark_as_completed":
|
304
|
+
self.mark_as_completed()
|
305
|
+
elif action == "mark_as_failed":
|
306
|
+
self.mark_as_failed()
|
307
|
+
elif action == "mark_as_uploading":
|
308
|
+
self.mark_as_uploading()
|
309
|
+
|
310
|
+
def set_filename(self, filename):
|
311
|
+
self.filename = filename
|
312
|
+
if not self.content_type:
|
313
|
+
self.content_type = mimetypes.guess_type(filename)[0]
|
314
|
+
self.category = utils.get_file_category(self.content_type)
|
315
|
+
|
316
|
+
|
317
|
+
def create_renditions(self):
|
318
|
+
"""Create renditions for the file"""
|
319
|
+
from mojo.apps.fileman import renderer
|
320
|
+
renderer.create_all_renditions(self)
|
321
|
+
|
322
|
+
def mark_as_uploading(self, commit=False):
|
237
323
|
"""Mark file as currently being uploaded"""
|
238
324
|
self.upload_status = self.UPLOADING
|
239
|
-
|
325
|
+
if commit:
|
326
|
+
self.atomic_save()
|
240
327
|
|
241
|
-
def mark_as_completed(self, file_size=None, checksum=None):
|
328
|
+
def mark_as_completed(self, file_size=None, checksum=None, commit=False):
|
242
329
|
"""Mark file upload as completed"""
|
243
|
-
self.upload_status = self.COMPLETED
|
244
330
|
if file_size:
|
245
331
|
self.file_size = file_size
|
246
332
|
if checksum:
|
247
333
|
self.checksum = checksum
|
248
|
-
self.
|
249
|
-
|
250
|
-
|
334
|
+
if self.file_manager.backend.exists(self.storage_file_path):
|
335
|
+
self.upload_status = self.COMPLETED
|
336
|
+
self.create_renditions()
|
337
|
+
else:
|
338
|
+
self.upload_status = self.FAILED
|
339
|
+
if commit:
|
340
|
+
self.atomic_save()
|
341
|
+
|
342
|
+
def mark_as_failed(self, error_message=None, commit=False):
|
251
343
|
"""Mark file upload as failed"""
|
252
344
|
self.upload_status = self.FAILED
|
253
345
|
if error_message:
|
254
346
|
self.set_metadata('error_message', error_message)
|
255
|
-
|
347
|
+
if commit:
|
348
|
+
self.atomic_save()
|
256
349
|
|
257
350
|
def mark_as_expired(self):
|
258
351
|
"""Mark file upload as expired"""
|
@@ -262,7 +355,7 @@ class File(models.Model, MojoModel):
|
|
262
355
|
def get_file_extension(self):
|
263
356
|
"""Get the file extension"""
|
264
357
|
import os
|
265
|
-
return os.path.splitext(self.
|
358
|
+
return os.path.splitext(self.filename)[1].lower()
|
266
359
|
|
267
360
|
def get_human_readable_size(self):
|
268
361
|
"""Get human readable file size"""
|
@@ -290,3 +383,56 @@ class File(models.Model, MojoModel):
|
|
290
383
|
return True
|
291
384
|
|
292
385
|
return False
|
386
|
+
|
387
|
+
def on_rest_save_file(self, name, file):
|
388
|
+
self.content_type = file.content_type
|
389
|
+
self.category = utils.get_file_category(self.content_type)
|
390
|
+
self.set_filename(file.name)
|
391
|
+
self.file_manager = FileManager.get_from_request(self.active_request)
|
392
|
+
self.generate_storage_filename()
|
393
|
+
self.mark_as_uploading(True)
|
394
|
+
self.file_manager.backend.save(file, self.storage_file_path, self.content_type)
|
395
|
+
self.mark_as_completed(commit=True)
|
396
|
+
|
397
|
+
@classmethod
|
398
|
+
def create_from_file(cls, file, name, request=None, user=None, group=None):
|
399
|
+
"""Create a new file instance from a file"""
|
400
|
+
if request:
|
401
|
+
file_manager = FileManager.get_from_request(request)
|
402
|
+
else:
|
403
|
+
file_manager = FileManager.get_for_user_group(user, group)
|
404
|
+
instance = cls()
|
405
|
+
instance.filename = file.name
|
406
|
+
instance.file_size = file.size
|
407
|
+
instance.file_manager = file_manager
|
408
|
+
instance.user = user
|
409
|
+
instance.group = group
|
410
|
+
instance.set_filename(file.name)
|
411
|
+
instance.category = utils.get_file_category(instance.content_type)
|
412
|
+
instance.on_rest_pre_save({}, True)
|
413
|
+
instance.save()
|
414
|
+
|
415
|
+
# now we need to upload the file
|
416
|
+
instance.on_rest_save_file(name, file)
|
417
|
+
|
418
|
+
return instance
|
419
|
+
|
420
|
+
@classmethod
|
421
|
+
def on_rest_related_save(cls, related_instance, related_field_name, field_value, current_instance=None):
|
422
|
+
# this allows us to handle json posts with inline base64 file data
|
423
|
+
if isinstance(field_value, str):
|
424
|
+
# assume base64 encoded data
|
425
|
+
file_bytes = base64.b64decode(field_value)
|
426
|
+
mime_type = magic.from_buffer(file_bytes, mime=True)
|
427
|
+
ext = mimetypes.guess_extension(mime_type)
|
428
|
+
file_obj = io.BytesIO(file_bytes)
|
429
|
+
file_obj.name = f"{related_field_name}{ext}"
|
430
|
+
file_obj.content_type = mime_type
|
431
|
+
file_obj.size = len(file_bytes)
|
432
|
+
# now we need to upload the file
|
433
|
+
instance = cls.create_from_file(file_obj, file_obj.name)
|
434
|
+
setattr(related_instance, related_field_name, instance)
|
435
|
+
elif isinstance(field_value, int):
|
436
|
+
# assume file id
|
437
|
+
instance = File.objects.get(id=field_value)
|
438
|
+
setattr(related_instance, related_field_name, instance)
|