django-nativemojo 0.1.10__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.10.dist-info/LICENSE +19 -0
- django_nativemojo-0.1.10.dist-info/METADATA +96 -0
- django_nativemojo-0.1.10.dist-info/NOTICE +8 -0
- django_nativemojo-0.1.10.dist-info/RECORD +194 -0
- django_nativemojo-0.1.10.dist-info/WHEEL +4 -0
- mojo/__init__.py +3 -0
- mojo/apps/account/__init__.py +1 -0
- mojo/apps/account/admin.py +91 -0
- mojo/apps/account/apps.py +16 -0
- mojo/apps/account/migrations/0001_initial.py +77 -0
- mojo/apps/account/migrations/0002_user_is_email_verified_user_is_phone_verified.py +23 -0
- mojo/apps/account/migrations/0003_group_mojo_secrets_user_mojo_secrets.py +23 -0
- mojo/apps/account/migrations/__init__.py +0 -0
- mojo/apps/account/models/__init__.py +3 -0
- mojo/apps/account/models/group.py +98 -0
- mojo/apps/account/models/member.py +95 -0
- mojo/apps/account/models/pkey.py +18 -0
- mojo/apps/account/models/user.py +211 -0
- mojo/apps/account/rest/__init__.py +3 -0
- mojo/apps/account/rest/group.py +25 -0
- mojo/apps/account/rest/user.py +47 -0
- mojo/apps/account/utils/__init__.py +0 -0
- mojo/apps/account/utils/jwtoken.py +72 -0
- mojo/apps/account/utils/passkeys.py +54 -0
- mojo/apps/fileman/README.md +549 -0
- mojo/apps/fileman/__init__.py +0 -0
- mojo/apps/fileman/apps.py +15 -0
- mojo/apps/fileman/backends/__init__.py +117 -0
- mojo/apps/fileman/backends/base.py +319 -0
- mojo/apps/fileman/backends/filesystem.py +397 -0
- mojo/apps/fileman/backends/s3.py +398 -0
- mojo/apps/fileman/examples/configurations.py +378 -0
- mojo/apps/fileman/examples/usage_example.py +665 -0
- mojo/apps/fileman/management/__init__.py +1 -0
- mojo/apps/fileman/management/commands/__init__.py +1 -0
- mojo/apps/fileman/management/commands/cleanup_expired_uploads.py +222 -0
- mojo/apps/fileman/models/__init__.py +7 -0
- mojo/apps/fileman/models/file.py +292 -0
- mojo/apps/fileman/models/manager.py +227 -0
- mojo/apps/fileman/models/render.py +0 -0
- mojo/apps/fileman/rest/__init__ +0 -0
- mojo/apps/fileman/rest/__init__.py +23 -0
- mojo/apps/fileman/rest/fileman.py +13 -0
- mojo/apps/fileman/rest/upload.py +92 -0
- mojo/apps/fileman/utils/__init__.py +19 -0
- mojo/apps/fileman/utils/upload.py +616 -0
- mojo/apps/incident/__init__.py +1 -0
- mojo/apps/incident/handlers/__init__.py +3 -0
- mojo/apps/incident/handlers/event_handlers.py +142 -0
- mojo/apps/incident/migrations/0001_initial.py +83 -0
- mojo/apps/incident/migrations/0002_rename_bundle_ruleset_bundle_minutes_event_hostname_and_more.py +44 -0
- mojo/apps/incident/migrations/0003_alter_event_model_id.py +18 -0
- mojo/apps/incident/migrations/0004_alter_incident_model_id.py +18 -0
- mojo/apps/incident/migrations/__init__.py +0 -0
- mojo/apps/incident/models/__init__.py +3 -0
- mojo/apps/incident/models/event.py +135 -0
- mojo/apps/incident/models/incident.py +33 -0
- mojo/apps/incident/models/rule.py +247 -0
- mojo/apps/incident/parsers/__init__.py +0 -0
- mojo/apps/incident/parsers/ossec/__init__.py +1 -0
- mojo/apps/incident/parsers/ossec/core.py +82 -0
- mojo/apps/incident/parsers/ossec/parsed.py +23 -0
- mojo/apps/incident/parsers/ossec/rules.py +124 -0
- mojo/apps/incident/parsers/ossec/utils.py +169 -0
- mojo/apps/incident/reporter.py +42 -0
- mojo/apps/incident/rest/__init__.py +2 -0
- mojo/apps/incident/rest/event.py +23 -0
- mojo/apps/incident/rest/ossec.py +22 -0
- mojo/apps/logit/__init__.py +0 -0
- mojo/apps/logit/admin.py +37 -0
- mojo/apps/logit/migrations/0001_initial.py +32 -0
- mojo/apps/logit/migrations/0002_log_duid_log_payload_log_username.py +28 -0
- mojo/apps/logit/migrations/0003_log_level.py +18 -0
- mojo/apps/logit/migrations/__init__.py +0 -0
- mojo/apps/logit/models/__init__.py +1 -0
- mojo/apps/logit/models/log.py +57 -0
- mojo/apps/logit/rest.py +9 -0
- mojo/apps/metrics/README.md +79 -0
- mojo/apps/metrics/__init__.py +12 -0
- mojo/apps/metrics/redis_metrics.py +331 -0
- mojo/apps/metrics/rest/__init__.py +1 -0
- mojo/apps/metrics/rest/base.py +152 -0
- mojo/apps/metrics/rest/db.py +0 -0
- mojo/apps/metrics/utils.py +227 -0
- mojo/apps/notify/README.md +91 -0
- mojo/apps/notify/README_NOTIFICATIONS.md +566 -0
- mojo/apps/notify/__init__.py +0 -0
- mojo/apps/notify/admin.py +52 -0
- mojo/apps/notify/handlers/__init__.py +0 -0
- mojo/apps/notify/handlers/example_handlers.py +516 -0
- mojo/apps/notify/handlers/ses/__init__.py +25 -0
- mojo/apps/notify/handlers/ses/bounce.py +0 -0
- mojo/apps/notify/handlers/ses/complaint.py +25 -0
- mojo/apps/notify/handlers/ses/message.py +86 -0
- mojo/apps/notify/management/__init__.py +0 -0
- mojo/apps/notify/management/commands/__init__.py +1 -0
- mojo/apps/notify/management/commands/process_notifications.py +370 -0
- mojo/apps/notify/mod +0 -0
- mojo/apps/notify/models/__init__.py +12 -0
- mojo/apps/notify/models/account.py +128 -0
- mojo/apps/notify/models/attachment.py +24 -0
- mojo/apps/notify/models/bounce.py +68 -0
- mojo/apps/notify/models/complaint.py +40 -0
- mojo/apps/notify/models/inbox.py +113 -0
- mojo/apps/notify/models/inbox_message.py +173 -0
- mojo/apps/notify/models/outbox.py +129 -0
- mojo/apps/notify/models/outbox_message.py +288 -0
- mojo/apps/notify/models/template.py +30 -0
- mojo/apps/notify/providers/__init__.py +0 -0
- mojo/apps/notify/providers/aws.py +73 -0
- mojo/apps/notify/rest/__init__.py +0 -0
- mojo/apps/notify/rest/ses.py +0 -0
- mojo/apps/notify/utils/__init__.py +2 -0
- mojo/apps/notify/utils/notifications.py +404 -0
- mojo/apps/notify/utils/parsing.py +202 -0
- mojo/apps/notify/utils/render.py +144 -0
- mojo/apps/tasks/README.md +118 -0
- mojo/apps/tasks/__init__.py +11 -0
- mojo/apps/tasks/manager.py +489 -0
- mojo/apps/tasks/rest/__init__.py +2 -0
- mojo/apps/tasks/rest/hooks.py +0 -0
- mojo/apps/tasks/rest/tasks.py +62 -0
- mojo/apps/tasks/runner.py +174 -0
- mojo/apps/tasks/tq_handlers.py +14 -0
- mojo/decorators/__init__.py +3 -0
- mojo/decorators/auth.py +25 -0
- mojo/decorators/cron.py +31 -0
- mojo/decorators/http.py +132 -0
- mojo/decorators/validate.py +14 -0
- mojo/errors.py +88 -0
- mojo/helpers/__init__.py +0 -0
- mojo/helpers/aws/__init__.py +0 -0
- mojo/helpers/aws/client.py +8 -0
- mojo/helpers/aws/s3.py +268 -0
- mojo/helpers/aws/setup_email.py +0 -0
- mojo/helpers/cron.py +79 -0
- mojo/helpers/crypto/__init__.py +4 -0
- mojo/helpers/crypto/aes.py +60 -0
- mojo/helpers/crypto/hash.py +59 -0
- mojo/helpers/crypto/privpub/__init__.py +1 -0
- mojo/helpers/crypto/privpub/hybrid.py +97 -0
- mojo/helpers/crypto/privpub/rsa.py +104 -0
- mojo/helpers/crypto/sign.py +36 -0
- mojo/helpers/crypto/too.l.py +25 -0
- mojo/helpers/crypto/utils.py +26 -0
- mojo/helpers/daemon.py +94 -0
- mojo/helpers/dates.py +69 -0
- mojo/helpers/dns/__init__.py +0 -0
- mojo/helpers/dns/godaddy.py +62 -0
- mojo/helpers/filetypes.py +128 -0
- mojo/helpers/logit.py +310 -0
- mojo/helpers/modules.py +95 -0
- mojo/helpers/paths.py +63 -0
- mojo/helpers/redis.py +10 -0
- mojo/helpers/request.py +89 -0
- mojo/helpers/request_parser.py +269 -0
- mojo/helpers/response.py +14 -0
- mojo/helpers/settings.py +146 -0
- mojo/helpers/sysinfo.py +140 -0
- mojo/helpers/ua.py +0 -0
- mojo/middleware/__init__.py +0 -0
- mojo/middleware/auth.py +26 -0
- mojo/middleware/logging.py +55 -0
- mojo/middleware/mojo.py +21 -0
- mojo/migrations/0001_initial.py +32 -0
- mojo/migrations/__init__.py +0 -0
- mojo/models/__init__.py +2 -0
- mojo/models/meta.py +262 -0
- mojo/models/rest.py +538 -0
- mojo/models/secrets.py +59 -0
- mojo/rest/__init__.py +1 -0
- mojo/rest/info.py +26 -0
- mojo/serializers/__init__.py +0 -0
- mojo/serializers/models.py +165 -0
- mojo/serializers/openapi.py +188 -0
- mojo/urls.py +38 -0
- mojo/ws4redis/README.md +174 -0
- mojo/ws4redis/__init__.py +2 -0
- mojo/ws4redis/client.py +283 -0
- mojo/ws4redis/connection.py +327 -0
- mojo/ws4redis/exceptions.py +32 -0
- mojo/ws4redis/redis.py +183 -0
- mojo/ws4redis/servers/__init__.py +0 -0
- mojo/ws4redis/servers/base.py +86 -0
- mojo/ws4redis/servers/django.py +171 -0
- mojo/ws4redis/servers/uwsgi.py +63 -0
- mojo/ws4redis/settings.py +45 -0
- mojo/ws4redis/utf8validator.py +128 -0
- mojo/ws4redis/websocket.py +403 -0
- testit/__init__.py +0 -0
- testit/client.py +147 -0
- testit/faker.py +20 -0
- testit/helpers.py +198 -0
- testit/runner.py +262 -0
@@ -0,0 +1,222 @@
|
|
1
|
+
from django.core.management.base import BaseCommand, CommandError
|
2
|
+
from django.utils import timezone
|
3
|
+
from datetime import datetime, timedelta
|
4
|
+
from django.db import transaction
|
5
|
+
import logging
|
6
|
+
|
7
|
+
from mojo.apps.fileman.models import File, FileManager
|
8
|
+
from mojo.apps.fileman.backends import get_backend
|
9
|
+
|
10
|
+
|
11
|
+
logger = logging.getLogger(__name__)
|
12
|
+
|
13
|
+
|
14
|
+
class Command(BaseCommand):
|
15
|
+
help = 'Clean up expired file uploads and temporary files'
|
16
|
+
|
17
|
+
def add_arguments(self, parser):
|
18
|
+
parser.add_argument(
|
19
|
+
'--days',
|
20
|
+
type=int,
|
21
|
+
default=1,
|
22
|
+
help='Clean up uploads older than this many days (default: 1)'
|
23
|
+
)
|
24
|
+
|
25
|
+
parser.add_argument(
|
26
|
+
'--hours',
|
27
|
+
type=int,
|
28
|
+
help='Clean up uploads older than this many hours (overrides --days)'
|
29
|
+
)
|
30
|
+
|
31
|
+
parser.add_argument(
|
32
|
+
'--dry-run',
|
33
|
+
action='store_true',
|
34
|
+
help='Show what would be cleaned up without actually doing it'
|
35
|
+
)
|
36
|
+
|
37
|
+
parser.add_argument(
|
38
|
+
'--backend-cleanup',
|
39
|
+
action='store_true',
|
40
|
+
default=True,
|
41
|
+
help='Also run backend-specific cleanup (default: True)'
|
42
|
+
)
|
43
|
+
|
44
|
+
parser.add_argument(
|
45
|
+
'--no-backend-cleanup',
|
46
|
+
action='store_false',
|
47
|
+
dest='backend_cleanup',
|
48
|
+
help='Skip backend-specific cleanup'
|
49
|
+
)
|
50
|
+
|
51
|
+
parser.add_argument(
|
52
|
+
'--status',
|
53
|
+
choices=['pending', 'uploading', 'failed', 'expired', 'all'],
|
54
|
+
default='all',
|
55
|
+
help='Which upload statuses to clean up (default: all non-completed)'
|
56
|
+
)
|
57
|
+
|
58
|
+
parser.add_argument(
|
59
|
+
'--verbose',
|
60
|
+
action='store_true',
|
61
|
+
help='Verbose output'
|
62
|
+
)
|
63
|
+
|
64
|
+
def handle(self, *args, **options):
|
65
|
+
self.dry_run = options['dry_run']
|
66
|
+
self.verbose = options['verbose']
|
67
|
+
|
68
|
+
# Calculate cutoff date
|
69
|
+
if options['hours']:
|
70
|
+
cutoff_date = timezone.now() - timedelta(hours=options['hours'])
|
71
|
+
cutoff_desc = f"{options['hours']} hours"
|
72
|
+
else:
|
73
|
+
cutoff_date = timezone.now() - timedelta(days=options['days'])
|
74
|
+
cutoff_desc = f"{options['days']} days"
|
75
|
+
|
76
|
+
if self.dry_run:
|
77
|
+
self.stdout.write(
|
78
|
+
self.style.WARNING(f"DRY RUN: Showing what would be cleaned up")
|
79
|
+
)
|
80
|
+
|
81
|
+
self.stdout.write(
|
82
|
+
f"Cleaning up uploads older than {cutoff_desc} (before {cutoff_date})"
|
83
|
+
)
|
84
|
+
|
85
|
+
# Clean up File records
|
86
|
+
files_cleaned = self.cleanup_files(cutoff_date, options['status'])
|
87
|
+
|
88
|
+
# Clean up backend temporary files
|
89
|
+
if options['backend_cleanup']:
|
90
|
+
backends_cleaned = self.cleanup_backends(cutoff_date)
|
91
|
+
else:
|
92
|
+
backends_cleaned = 0
|
93
|
+
|
94
|
+
# Summary
|
95
|
+
if self.dry_run:
|
96
|
+
self.stdout.write(
|
97
|
+
self.style.SUCCESS(
|
98
|
+
f"DRY RUN COMPLETE: Would clean up {files_cleaned} file records "
|
99
|
+
f"and {backends_cleaned} backend temporary files"
|
100
|
+
)
|
101
|
+
)
|
102
|
+
else:
|
103
|
+
self.stdout.write(
|
104
|
+
self.style.SUCCESS(
|
105
|
+
f"CLEANUP COMPLETE: Cleaned up {files_cleaned} file records "
|
106
|
+
f"and {backends_cleaned} backend temporary files"
|
107
|
+
)
|
108
|
+
)
|
109
|
+
|
110
|
+
def cleanup_files(self, cutoff_date, status_filter):
|
111
|
+
"""Clean up File model records"""
|
112
|
+
|
113
|
+
# Build query based on status filter
|
114
|
+
query = File.objects.filter(created__lt=cutoff_date)
|
115
|
+
|
116
|
+
if status_filter == 'pending':
|
117
|
+
query = query.filter(upload_status=File.PENDING)
|
118
|
+
elif status_filter == 'uploading':
|
119
|
+
query = query.filter(upload_status=File.UPLOADING)
|
120
|
+
elif status_filter == 'failed':
|
121
|
+
query = query.filter(upload_status=File.FAILED)
|
122
|
+
elif status_filter == 'expired':
|
123
|
+
query = query.filter(upload_status=File.EXPIRED)
|
124
|
+
elif status_filter == 'all':
|
125
|
+
# Clean up all non-completed uploads
|
126
|
+
query = query.exclude(upload_status=File.COMPLETED)
|
127
|
+
|
128
|
+
# Also include files with expired upload URLs
|
129
|
+
expired_url_query = File.objects.filter(
|
130
|
+
upload_expires_at__lt=timezone.now(),
|
131
|
+
upload_status__in=[File.PENDING, File.UPLOADING]
|
132
|
+
)
|
133
|
+
|
134
|
+
# Combine queries
|
135
|
+
files_to_cleanup = query.union(expired_url_query).distinct()
|
136
|
+
|
137
|
+
if self.verbose:
|
138
|
+
self.stdout.write(f"Found {files_to_cleanup.count()} file records to clean up")
|
139
|
+
|
140
|
+
cleaned_count = 0
|
141
|
+
|
142
|
+
for file_obj in files_to_cleanup:
|
143
|
+
if self.verbose:
|
144
|
+
self.stdout.write(
|
145
|
+
f" File: {file_obj.original_filename} "
|
146
|
+
f"(status: {file_obj.upload_status}, "
|
147
|
+
f"created: {file_obj.created})"
|
148
|
+
)
|
149
|
+
|
150
|
+
if not self.dry_run:
|
151
|
+
try:
|
152
|
+
with transaction.atomic():
|
153
|
+
# Try to delete the actual file from storage if it exists
|
154
|
+
if file_obj.file_path and file_obj.upload_status == File.COMPLETED:
|
155
|
+
try:
|
156
|
+
backend = get_backend(file_obj.file_manager)
|
157
|
+
if backend.exists(file_obj.file_path):
|
158
|
+
backend.delete(file_obj.file_path)
|
159
|
+
if self.verbose:
|
160
|
+
self.stdout.write(f" Deleted file from storage: {file_obj.file_path}")
|
161
|
+
except Exception as e:
|
162
|
+
logger.warning(f"Failed to delete file from storage: {e}")
|
163
|
+
if self.verbose:
|
164
|
+
self.stdout.write(
|
165
|
+
self.style.WARNING(f" Warning: Could not delete from storage: {e}")
|
166
|
+
)
|
167
|
+
|
168
|
+
# Mark as expired or delete the record
|
169
|
+
if file_obj.upload_status in [File.PENDING, File.UPLOADING]:
|
170
|
+
file_obj.mark_as_expired()
|
171
|
+
if self.verbose:
|
172
|
+
self.stdout.write(f" Marked as expired")
|
173
|
+
else:
|
174
|
+
file_obj.delete()
|
175
|
+
if self.verbose:
|
176
|
+
self.stdout.write(f" Deleted record")
|
177
|
+
|
178
|
+
cleaned_count += 1
|
179
|
+
|
180
|
+
except Exception as e:
|
181
|
+
logger.error(f"Error cleaning up file {file_obj.id}: {e}")
|
182
|
+
if self.verbose:
|
183
|
+
self.stdout.write(
|
184
|
+
self.style.ERROR(f" Error: {e}")
|
185
|
+
)
|
186
|
+
else:
|
187
|
+
cleaned_count += 1
|
188
|
+
|
189
|
+
return cleaned_count
|
190
|
+
|
191
|
+
def cleanup_backends(self, cutoff_date):
|
192
|
+
"""Run backend-specific cleanup"""
|
193
|
+
cleaned_count = 0
|
194
|
+
|
195
|
+
# Get all active file managers
|
196
|
+
file_managers = FileManager.objects.filter(is_active=True)
|
197
|
+
|
198
|
+
if self.verbose:
|
199
|
+
self.stdout.write(f"Running backend cleanup for {file_managers.count()} file managers")
|
200
|
+
|
201
|
+
for file_manager in file_managers:
|
202
|
+
try:
|
203
|
+
backend = get_backend(file_manager)
|
204
|
+
|
205
|
+
if self.verbose:
|
206
|
+
self.stdout.write(
|
207
|
+
f" Cleaning up {file_manager.name} ({file_manager.backend_type})"
|
208
|
+
)
|
209
|
+
|
210
|
+
if not self.dry_run:
|
211
|
+
backend.cleanup_expired_uploads(cutoff_date)
|
212
|
+
|
213
|
+
cleaned_count += 1
|
214
|
+
|
215
|
+
except Exception as e:
|
216
|
+
logger.error(f"Error running backend cleanup for {file_manager.name}: {e}")
|
217
|
+
if self.verbose:
|
218
|
+
self.stdout.write(
|
219
|
+
self.style.ERROR(f" Error: {e}")
|
220
|
+
)
|
221
|
+
|
222
|
+
return cleaned_count
|
@@ -0,0 +1,292 @@
|
|
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
|
+
|
8
|
+
|
9
|
+
class File(models.Model, MojoModel):
|
10
|
+
"""
|
11
|
+
File model representing uploaded files with metadata and storage information
|
12
|
+
"""
|
13
|
+
|
14
|
+
class RestMeta:
|
15
|
+
CAN_SAVE = CAN_CREATE = True
|
16
|
+
CAN_DELETE = True
|
17
|
+
DEFAULT_SORT = "-created"
|
18
|
+
VIEW_PERMS = ["view_fileman"]
|
19
|
+
SEARCH_FIELDS = ["filename", "original_filename", "content_type"]
|
20
|
+
SEARCH_TERMS = [
|
21
|
+
"filename", "original_filename", "content_type",
|
22
|
+
("group", "group__name"),
|
23
|
+
("file_manager", "file_manager__name")]
|
24
|
+
|
25
|
+
GRAPHS = {
|
26
|
+
"default": {
|
27
|
+
"graphs": {
|
28
|
+
"group": "basic",
|
29
|
+
"file_manager": "basic",
|
30
|
+
"uploaded_by": "basic"
|
31
|
+
}
|
32
|
+
},
|
33
|
+
"list": {
|
34
|
+
"graphs": {
|
35
|
+
"group": "basic",
|
36
|
+
"file_manager": "basic"
|
37
|
+
}
|
38
|
+
}
|
39
|
+
}
|
40
|
+
|
41
|
+
# Upload status choices
|
42
|
+
PENDING = 'pending'
|
43
|
+
UPLOADING = 'uploading'
|
44
|
+
COMPLETED = 'completed'
|
45
|
+
FAILED = 'failed'
|
46
|
+
EXPIRED = 'expired'
|
47
|
+
|
48
|
+
STATUS_CHOICES = [
|
49
|
+
(PENDING, 'Pending Upload'),
|
50
|
+
(UPLOADING, 'Uploading'),
|
51
|
+
(COMPLETED, 'Upload Completed'),
|
52
|
+
(FAILED, 'Upload Failed'),
|
53
|
+
(EXPIRED, 'Upload Expired'),
|
54
|
+
]
|
55
|
+
|
56
|
+
created = models.DateTimeField(auto_now_add=True, editable=False, db_index=True)
|
57
|
+
modified = models.DateTimeField(auto_now=True)
|
58
|
+
|
59
|
+
group = models.ForeignKey(
|
60
|
+
"account.Group",
|
61
|
+
related_name="files",
|
62
|
+
null=True,
|
63
|
+
blank=True,
|
64
|
+
default=None,
|
65
|
+
on_delete=models.CASCADE,
|
66
|
+
help_text="Group that owns this file"
|
67
|
+
)
|
68
|
+
|
69
|
+
uploaded_by = models.ForeignKey(
|
70
|
+
"account.User",
|
71
|
+
related_name="uploaded_files",
|
72
|
+
null=True,
|
73
|
+
blank=True,
|
74
|
+
default=None,
|
75
|
+
on_delete=models.SET_NULL,
|
76
|
+
help_text="User who uploaded this file"
|
77
|
+
)
|
78
|
+
|
79
|
+
file_manager = models.ForeignKey(
|
80
|
+
"fileman.FileManager",
|
81
|
+
related_name="files",
|
82
|
+
on_delete=models.CASCADE,
|
83
|
+
help_text="File manager configuration used for this file"
|
84
|
+
)
|
85
|
+
|
86
|
+
filename = models.CharField(
|
87
|
+
max_length=255,
|
88
|
+
db_index=True,
|
89
|
+
help_text="Final filename used for storage"
|
90
|
+
)
|
91
|
+
|
92
|
+
original_filename = models.CharField(
|
93
|
+
max_length=255,
|
94
|
+
help_text="Original filename as uploaded by user"
|
95
|
+
)
|
96
|
+
|
97
|
+
file_path = models.TextField(
|
98
|
+
help_text="Full path to file in storage backend"
|
99
|
+
)
|
100
|
+
|
101
|
+
file_size = models.BigIntegerField(
|
102
|
+
null=True,
|
103
|
+
blank=True,
|
104
|
+
help_text="File size in bytes"
|
105
|
+
)
|
106
|
+
|
107
|
+
content_type = models.CharField(
|
108
|
+
max_length=255,
|
109
|
+
db_index=True,
|
110
|
+
help_text="MIME type of the file"
|
111
|
+
)
|
112
|
+
|
113
|
+
checksum = models.CharField(
|
114
|
+
max_length=128,
|
115
|
+
blank=True,
|
116
|
+
default="",
|
117
|
+
help_text="File checksum (MD5, SHA256, etc.)"
|
118
|
+
)
|
119
|
+
|
120
|
+
upload_token = models.CharField(
|
121
|
+
max_length=64,
|
122
|
+
unique=True,
|
123
|
+
db_index=True,
|
124
|
+
help_text="Unique token for tracking direct uploads"
|
125
|
+
)
|
126
|
+
|
127
|
+
upload_status = models.CharField(
|
128
|
+
max_length=32,
|
129
|
+
choices=STATUS_CHOICES,
|
130
|
+
default=PENDING,
|
131
|
+
db_index=True,
|
132
|
+
help_text="Current status of the file upload"
|
133
|
+
)
|
134
|
+
|
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
|
+
metadata = models.JSONField(
|
148
|
+
default=dict,
|
149
|
+
blank=True,
|
150
|
+
help_text="Additional file metadata and custom properties"
|
151
|
+
)
|
152
|
+
|
153
|
+
is_active = models.BooleanField(
|
154
|
+
default=True,
|
155
|
+
help_text="Whether this file is active and accessible"
|
156
|
+
)
|
157
|
+
|
158
|
+
is_public = models.BooleanField(
|
159
|
+
default=False,
|
160
|
+
help_text="Whether this file can be accessed without authentication"
|
161
|
+
)
|
162
|
+
|
163
|
+
class Meta:
|
164
|
+
indexes = [
|
165
|
+
models.Index(fields=['upload_status', 'created']),
|
166
|
+
models.Index(fields=['file_manager', 'upload_status']),
|
167
|
+
models.Index(fields=['group', 'is_active']),
|
168
|
+
models.Index(fields=['content_type', 'is_active']),
|
169
|
+
models.Index(fields=['upload_expires_at']),
|
170
|
+
]
|
171
|
+
|
172
|
+
def __str__(self):
|
173
|
+
return f"{self.original_filename} ({self.get_upload_status_display()})"
|
174
|
+
|
175
|
+
def save(self, *args, **kwargs):
|
176
|
+
"""Custom save to generate upload token and set defaults"""
|
177
|
+
if not self.upload_token:
|
178
|
+
self.upload_token = self.generate_upload_token()
|
179
|
+
|
180
|
+
if not self.filename:
|
181
|
+
self.filename = self.generate_unique_filename()
|
182
|
+
|
183
|
+
if not self.content_type:
|
184
|
+
self.content_type = mimetypes.guess_type(self.filename)[0] or 'application/octet-stream'
|
185
|
+
|
186
|
+
super().save(*args, **kwargs)
|
187
|
+
|
188
|
+
@classmethod
|
189
|
+
def generate_upload_token(cls):
|
190
|
+
"""Generate a unique upload token"""
|
191
|
+
return hashlib.sha256(f"{uuid.uuid4()}{datetime.now()}".encode()).hexdigest()[:32]
|
192
|
+
|
193
|
+
def generate_unique_filename(self):
|
194
|
+
"""Generate a unique filename for storage"""
|
195
|
+
import os
|
196
|
+
name, ext = os.path.splitext(self.original_filename)
|
197
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
198
|
+
unique_id = str(uuid.uuid4())[:8]
|
199
|
+
return f"{name}_{timestamp}_{unique_id}{ext}"
|
200
|
+
|
201
|
+
def get_metadata(self, key, default=None):
|
202
|
+
"""Get a specific metadata value"""
|
203
|
+
return self.metadata.get(key, default)
|
204
|
+
|
205
|
+
def set_metadata(self, key, value):
|
206
|
+
"""Set a specific metadata value"""
|
207
|
+
self.metadata[key] = value
|
208
|
+
|
209
|
+
@property
|
210
|
+
def is_pending(self):
|
211
|
+
return self.upload_status == self.PENDING
|
212
|
+
|
213
|
+
@property
|
214
|
+
def is_uploading(self):
|
215
|
+
return self.upload_status == self.UPLOADING
|
216
|
+
|
217
|
+
@property
|
218
|
+
def is_completed(self):
|
219
|
+
return self.upload_status == self.COMPLETED
|
220
|
+
|
221
|
+
@property
|
222
|
+
def is_failed(self):
|
223
|
+
return self.upload_status == self.FAILED
|
224
|
+
|
225
|
+
@property
|
226
|
+
def is_expired(self):
|
227
|
+
return self.upload_status == self.EXPIRED
|
228
|
+
|
229
|
+
@property
|
230
|
+
def is_upload_expired(self):
|
231
|
+
"""Check if the upload URL has expired"""
|
232
|
+
if not self.upload_expires_at:
|
233
|
+
return False
|
234
|
+
return datetime.now() > self.upload_expires_at
|
235
|
+
|
236
|
+
def mark_as_uploading(self):
|
237
|
+
"""Mark file as currently being uploaded"""
|
238
|
+
self.upload_status = self.UPLOADING
|
239
|
+
self.save(update_fields=['upload_status', 'modified'])
|
240
|
+
|
241
|
+
def mark_as_completed(self, file_size=None, checksum=None):
|
242
|
+
"""Mark file upload as completed"""
|
243
|
+
self.upload_status = self.COMPLETED
|
244
|
+
if file_size:
|
245
|
+
self.file_size = file_size
|
246
|
+
if checksum:
|
247
|
+
self.checksum = checksum
|
248
|
+
self.save(update_fields=['upload_status', 'file_size', 'checksum', 'modified'])
|
249
|
+
|
250
|
+
def mark_as_failed(self, error_message=None):
|
251
|
+
"""Mark file upload as failed"""
|
252
|
+
self.upload_status = self.FAILED
|
253
|
+
if error_message:
|
254
|
+
self.set_metadata('error_message', error_message)
|
255
|
+
self.save(update_fields=['upload_status', 'metadata', 'modified'])
|
256
|
+
|
257
|
+
def mark_as_expired(self):
|
258
|
+
"""Mark file upload as expired"""
|
259
|
+
self.upload_status = self.EXPIRED
|
260
|
+
self.save(update_fields=['upload_status', 'modified'])
|
261
|
+
|
262
|
+
def get_file_extension(self):
|
263
|
+
"""Get the file extension"""
|
264
|
+
import os
|
265
|
+
return os.path.splitext(self.original_filename)[1].lower()
|
266
|
+
|
267
|
+
def get_human_readable_size(self):
|
268
|
+
"""Get human readable file size"""
|
269
|
+
if not self.file_size:
|
270
|
+
return "Unknown"
|
271
|
+
|
272
|
+
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
|
273
|
+
if self.file_size < 1024.0:
|
274
|
+
return f"{self.file_size:.1f} {unit}"
|
275
|
+
self.file_size /= 1024.0
|
276
|
+
return f"{self.file_size:.1f} PB"
|
277
|
+
|
278
|
+
def can_be_accessed_by(self, user=None, group=None):
|
279
|
+
"""Check if file can be accessed by user/group"""
|
280
|
+
if not self.is_active:
|
281
|
+
return False
|
282
|
+
|
283
|
+
if self.is_public:
|
284
|
+
return True
|
285
|
+
|
286
|
+
if user and self.uploaded_by == user:
|
287
|
+
return True
|
288
|
+
|
289
|
+
if group and self.group == group:
|
290
|
+
return True
|
291
|
+
|
292
|
+
return False
|