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,549 @@
|
|
1
|
+
# Django File Manager (fileman)
|
2
|
+
|
3
|
+
A comprehensive file management system for Django that supports multiple storage backends with direct upload capabilities to services like AWS S3, avoiding server bottlenecks.
|
4
|
+
|
5
|
+
## Features
|
6
|
+
|
7
|
+
- **Multiple Storage Backends**: File system, AWS S3, Azure Blob, Google Cloud Storage
|
8
|
+
- **Direct Upload Support**: Generate pre-signed URLs for client-side uploads to cloud services
|
9
|
+
- **Flexible Configuration**: Per-group file managers with different backends and restrictions
|
10
|
+
- **File Validation**: Size limits, extension filtering, MIME type validation
|
11
|
+
- **Upload Lifecycle Management**: Track upload status from initiation to completion
|
12
|
+
- **Automatic Cleanup**: Management commands for cleaning expired uploads
|
13
|
+
- **Security**: Token-based uploads, access controls, encryption support
|
14
|
+
|
15
|
+
## Quick Start
|
16
|
+
|
17
|
+
### 1. Add to Django Settings
|
18
|
+
|
19
|
+
```python
|
20
|
+
INSTALLED_APPS = [
|
21
|
+
# ... other apps
|
22
|
+
'mojo.apps.fileman',
|
23
|
+
]
|
24
|
+
|
25
|
+
# Optional: Configure default settings
|
26
|
+
FILEMAN_SETTINGS = {
|
27
|
+
'DEFAULT_MAX_FILE_SIZE': 100 * 1024 * 1024, # 100MB
|
28
|
+
'DEFAULT_UPLOAD_EXPIRES_IN': 3600, # 1 hour
|
29
|
+
'CLEANUP_EXPIRED_UPLOADS': True,
|
30
|
+
}
|
31
|
+
```
|
32
|
+
|
33
|
+
### 2. Run Migrations
|
34
|
+
|
35
|
+
```bash
|
36
|
+
python manage.py migrate fileman
|
37
|
+
```
|
38
|
+
|
39
|
+
### 3. Create a FileManager
|
40
|
+
|
41
|
+
```python
|
42
|
+
from mojo.apps.fileman.models import FileManager
|
43
|
+
from mojo.apps.account.models import Group
|
44
|
+
|
45
|
+
# Create a file manager for S3
|
46
|
+
file_manager = FileManager.objects.create(
|
47
|
+
name="AWS S3 Storage",
|
48
|
+
backend_type="s3",
|
49
|
+
backend_url="s3://my-bucket/",
|
50
|
+
supports_direct_upload=True,
|
51
|
+
max_file_size=100 * 1024 * 1024, # 100MB
|
52
|
+
settings={
|
53
|
+
"bucket_name": "my-bucket",
|
54
|
+
"region_name": "us-east-1",
|
55
|
+
"access_key_id": "YOUR_ACCESS_KEY",
|
56
|
+
"secret_access_key": "YOUR_SECRET_KEY",
|
57
|
+
},
|
58
|
+
is_default=True,
|
59
|
+
is_active=True
|
60
|
+
)
|
61
|
+
```
|
62
|
+
|
63
|
+
## API Usage
|
64
|
+
|
65
|
+
### 1. Initiate Upload
|
66
|
+
|
67
|
+
**POST** `/fileman/initiate-upload/`
|
68
|
+
|
69
|
+
```javascript
|
70
|
+
const response = await fetch('/fileman/initiate-upload/', {
|
71
|
+
method: 'POST',
|
72
|
+
headers: {
|
73
|
+
'Content-Type': 'application/json',
|
74
|
+
'X-CSRFToken': csrfToken,
|
75
|
+
},
|
76
|
+
body: JSON.stringify({
|
77
|
+
files: [
|
78
|
+
{
|
79
|
+
filename: 'document.pdf',
|
80
|
+
content_type: 'application/pdf',
|
81
|
+
size: 1024000
|
82
|
+
},
|
83
|
+
{
|
84
|
+
filename: 'image.jpg',
|
85
|
+
content_type: 'image/jpeg',
|
86
|
+
size: 512000
|
87
|
+
}
|
88
|
+
],
|
89
|
+
metadata: {
|
90
|
+
source: 'web_upload',
|
91
|
+
category: 'documents'
|
92
|
+
}
|
93
|
+
})
|
94
|
+
});
|
95
|
+
|
96
|
+
const data = await response.json();
|
97
|
+
```
|
98
|
+
|
99
|
+
**Response:**
|
100
|
+
```json
|
101
|
+
{
|
102
|
+
"success": true,
|
103
|
+
"files": [
|
104
|
+
{
|
105
|
+
"id": 123,
|
106
|
+
"filename": "document_20231201_abc12345.pdf",
|
107
|
+
"original_filename": "document.pdf",
|
108
|
+
"upload_token": "a1b2c3d4e5f6...",
|
109
|
+
"upload_url": "https://s3.amazonaws.com/my-bucket/...",
|
110
|
+
"method": "POST",
|
111
|
+
"fields": {
|
112
|
+
"key": "uploads/document_20231201_abc12345.pdf",
|
113
|
+
"policy": "eyJ...",
|
114
|
+
"x-amz-algorithm": "AWS4-HMAC-SHA256",
|
115
|
+
"x-amz-credential": "...",
|
116
|
+
"x-amz-date": "20231201T120000Z",
|
117
|
+
"x-amz-signature": "..."
|
118
|
+
},
|
119
|
+
"expires_at": "2023-12-01T13:00:00Z"
|
120
|
+
}
|
121
|
+
],
|
122
|
+
"file_manager": {
|
123
|
+
"id": 1,
|
124
|
+
"name": "AWS S3 Storage",
|
125
|
+
"backend_type": "s3"
|
126
|
+
}
|
127
|
+
}
|
128
|
+
```
|
129
|
+
|
130
|
+
### 2. Upload File to Pre-signed URL
|
131
|
+
|
132
|
+
```javascript
|
133
|
+
// For S3 direct upload
|
134
|
+
const formData = new FormData();
|
135
|
+
|
136
|
+
// Add all the fields from the response
|
137
|
+
Object.entries(fileData.fields).forEach(([key, value]) => {
|
138
|
+
formData.append(key, value);
|
139
|
+
});
|
140
|
+
|
141
|
+
// Add the file last
|
142
|
+
formData.append('file', selectedFile);
|
143
|
+
|
144
|
+
// Upload directly to S3
|
145
|
+
const uploadResponse = await fetch(fileData.upload_url, {
|
146
|
+
method: fileData.method,
|
147
|
+
body: formData
|
148
|
+
});
|
149
|
+
```
|
150
|
+
|
151
|
+
### 3. Finalize Upload
|
152
|
+
|
153
|
+
**POST** `/fileman/finalize-upload/`
|
154
|
+
|
155
|
+
```javascript
|
156
|
+
const finalizeResponse = await fetch('/fileman/finalize-upload/', {
|
157
|
+
method: 'POST',
|
158
|
+
headers: {
|
159
|
+
'Content-Type': 'application/json',
|
160
|
+
'X-CSRFToken': csrfToken,
|
161
|
+
},
|
162
|
+
body: JSON.stringify({
|
163
|
+
upload_token: fileData.upload_token,
|
164
|
+
file_size: selectedFile.size,
|
165
|
+
checksum: 'md5:' + calculatedMD5,
|
166
|
+
metadata: {
|
167
|
+
processing_complete: true
|
168
|
+
}
|
169
|
+
})
|
170
|
+
});
|
171
|
+
|
172
|
+
const result = await finalizeResponse.json();
|
173
|
+
```
|
174
|
+
|
175
|
+
**Response:**
|
176
|
+
```json
|
177
|
+
{
|
178
|
+
"success": true,
|
179
|
+
"file": {
|
180
|
+
"id": 123,
|
181
|
+
"filename": "document_20231201_abc12345.pdf",
|
182
|
+
"original_filename": "document.pdf",
|
183
|
+
"file_path": "uploads/document_20231201_abc12345.pdf",
|
184
|
+
"file_size": 1024000,
|
185
|
+
"content_type": "application/pdf",
|
186
|
+
"upload_status": "completed",
|
187
|
+
"download_url": "https://s3.amazonaws.com/my-bucket/...",
|
188
|
+
"checksum": "md5:abcdef123456...",
|
189
|
+
"metadata": {...},
|
190
|
+
"created": "2023-12-01T12:00:00Z"
|
191
|
+
}
|
192
|
+
}
|
193
|
+
```
|
194
|
+
|
195
|
+
## Storage Backend Configuration
|
196
|
+
|
197
|
+
### AWS S3
|
198
|
+
|
199
|
+
```python
|
200
|
+
{
|
201
|
+
"name": "AWS S3 Production",
|
202
|
+
"backend_type": "s3",
|
203
|
+
"backend_url": "s3://my-bucket/",
|
204
|
+
"supports_direct_upload": True,
|
205
|
+
"settings": {
|
206
|
+
"bucket_name": "my-bucket",
|
207
|
+
"region_name": "us-east-1",
|
208
|
+
"access_key_id": "AKIAIOSFODNN7EXAMPLE",
|
209
|
+
"secret_access_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
|
210
|
+
"server_side_encryption": "AES256",
|
211
|
+
"upload_expires_in": 3600,
|
212
|
+
"download_expires_in": 3600
|
213
|
+
}
|
214
|
+
}
|
215
|
+
```
|
216
|
+
|
217
|
+
### Local File System
|
218
|
+
|
219
|
+
```python
|
220
|
+
{
|
221
|
+
"name": "Local Storage",
|
222
|
+
"backend_type": "file",
|
223
|
+
"backend_url": "file:///app/media/uploads/",
|
224
|
+
"supports_direct_upload": False,
|
225
|
+
"settings": {
|
226
|
+
"base_path": "/app/media/uploads",
|
227
|
+
"base_url": "/media/uploads/",
|
228
|
+
"create_directories": True,
|
229
|
+
"permissions": 0o644,
|
230
|
+
"directory_permissions": 0o755
|
231
|
+
}
|
232
|
+
}
|
233
|
+
```
|
234
|
+
|
235
|
+
### S3-Compatible Services (MinIO, DigitalOcean Spaces)
|
236
|
+
|
237
|
+
```python
|
238
|
+
{
|
239
|
+
"name": "DigitalOcean Spaces",
|
240
|
+
"backend_type": "s3",
|
241
|
+
"backend_url": "https://nyc3.digitaloceanspaces.com/my-space/",
|
242
|
+
"supports_direct_upload": True,
|
243
|
+
"settings": {
|
244
|
+
"bucket_name": "my-space",
|
245
|
+
"region_name": "nyc3",
|
246
|
+
"endpoint_url": "https://nyc3.digitaloceanspaces.com",
|
247
|
+
"access_key_id": "DO00EXAMPLE",
|
248
|
+
"secret_access_key": "EXAMPLE_SECRET_KEY"
|
249
|
+
}
|
250
|
+
}
|
251
|
+
```
|
252
|
+
|
253
|
+
## JavaScript Upload Helper
|
254
|
+
|
255
|
+
```javascript
|
256
|
+
class FileUploader {
|
257
|
+
constructor(csrfToken) {
|
258
|
+
this.csrfToken = csrfToken;
|
259
|
+
}
|
260
|
+
|
261
|
+
async uploadFiles(files, options = {}) {
|
262
|
+
// 1. Initiate upload
|
263
|
+
const initResponse = await this.initiateUpload(files, options);
|
264
|
+
if (!initResponse.success) {
|
265
|
+
throw new Error('Failed to initiate upload');
|
266
|
+
}
|
267
|
+
|
268
|
+
// 2. Upload each file
|
269
|
+
const uploadPromises = initResponse.files.map(async (fileData, index) => {
|
270
|
+
const file = files[index];
|
271
|
+
await this.uploadFile(file, fileData);
|
272
|
+
return this.finalizeUpload(fileData.upload_token, file);
|
273
|
+
});
|
274
|
+
|
275
|
+
// 3. Wait for all uploads to complete
|
276
|
+
return Promise.all(uploadPromises);
|
277
|
+
}
|
278
|
+
|
279
|
+
async initiateUpload(files, options) {
|
280
|
+
const response = await fetch('/fileman/initiate-upload/', {
|
281
|
+
method: 'POST',
|
282
|
+
headers: {
|
283
|
+
'Content-Type': 'application/json',
|
284
|
+
'X-CSRFToken': this.csrfToken,
|
285
|
+
},
|
286
|
+
body: JSON.stringify({
|
287
|
+
files: files.map(file => ({
|
288
|
+
filename: file.name,
|
289
|
+
content_type: file.type,
|
290
|
+
size: file.size
|
291
|
+
})),
|
292
|
+
...options
|
293
|
+
})
|
294
|
+
});
|
295
|
+
return response.json();
|
296
|
+
}
|
297
|
+
|
298
|
+
async uploadFile(file, uploadData) {
|
299
|
+
const formData = new FormData();
|
300
|
+
|
301
|
+
// Add fields for S3 or other backends
|
302
|
+
Object.entries(uploadData.fields || {}).forEach(([key, value]) => {
|
303
|
+
formData.append(key, value);
|
304
|
+
});
|
305
|
+
|
306
|
+
formData.append('file', file);
|
307
|
+
|
308
|
+
const response = await fetch(uploadData.upload_url, {
|
309
|
+
method: uploadData.method,
|
310
|
+
body: formData
|
311
|
+
});
|
312
|
+
|
313
|
+
if (!response.ok) {
|
314
|
+
throw new Error(`Upload failed: ${response.statusText}`);
|
315
|
+
}
|
316
|
+
}
|
317
|
+
|
318
|
+
async finalizeUpload(uploadToken, file) {
|
319
|
+
const response = await fetch('/fileman/finalize-upload/', {
|
320
|
+
method: 'POST',
|
321
|
+
headers: {
|
322
|
+
'Content-Type': 'application/json',
|
323
|
+
'X-CSRFToken': this.csrfToken,
|
324
|
+
},
|
325
|
+
body: JSON.stringify({
|
326
|
+
upload_token: uploadToken,
|
327
|
+
file_size: file.size
|
328
|
+
})
|
329
|
+
});
|
330
|
+
return response.json();
|
331
|
+
}
|
332
|
+
}
|
333
|
+
|
334
|
+
// Usage
|
335
|
+
const uploader = new FileUploader(document.querySelector('[name=csrfmiddlewaretoken]').value);
|
336
|
+
const fileInput = document.getElementById('file-input');
|
337
|
+
|
338
|
+
fileInput.addEventListener('change', async (e) => {
|
339
|
+
const files = Array.from(e.target.files);
|
340
|
+
try {
|
341
|
+
const results = await uploader.uploadFiles(files, {
|
342
|
+
metadata: { source: 'web_form' }
|
343
|
+
});
|
344
|
+
console.log('Upload completed:', results);
|
345
|
+
} catch (error) {
|
346
|
+
console.error('Upload failed:', error);
|
347
|
+
}
|
348
|
+
});
|
349
|
+
```
|
350
|
+
|
351
|
+
## File Access and Downloads
|
352
|
+
|
353
|
+
### Download Files
|
354
|
+
|
355
|
+
```python
|
356
|
+
from mojo.apps.fileman.models import File
|
357
|
+
from mojo.apps.fileman.backends import get_backend
|
358
|
+
|
359
|
+
# Get file by upload token
|
360
|
+
file_obj = File.objects.get(upload_token='abc123...', upload_status=File.COMPLETED)
|
361
|
+
|
362
|
+
# Generate download URL
|
363
|
+
backend = get_backend(file_obj.file_manager)
|
364
|
+
download_url = backend.get_url(file_obj.file_path, expires_in=3600)
|
365
|
+
|
366
|
+
# Or use the built-in download view
|
367
|
+
# /fileman/download/abc123.../
|
368
|
+
```
|
369
|
+
|
370
|
+
### Check File Access Permissions
|
371
|
+
|
372
|
+
```python
|
373
|
+
# Check if user can access file
|
374
|
+
can_access = file_obj.can_be_accessed_by(user=request.user, group=user_group)
|
375
|
+
|
376
|
+
# Public files
|
377
|
+
file_obj.is_public = True
|
378
|
+
file_obj.save()
|
379
|
+
```
|
380
|
+
|
381
|
+
## Management Commands
|
382
|
+
|
383
|
+
### Clean Up Expired Uploads
|
384
|
+
|
385
|
+
```bash
|
386
|
+
# Clean up uploads older than 1 day (default)
|
387
|
+
python manage.py cleanup_expired_uploads
|
388
|
+
|
389
|
+
# Clean up uploads older than 6 hours
|
390
|
+
python manage.py cleanup_expired_uploads --hours 6
|
391
|
+
|
392
|
+
# Dry run to see what would be cleaned
|
393
|
+
python manage.py cleanup_expired_uploads --dry-run
|
394
|
+
|
395
|
+
# Clean up only failed uploads
|
396
|
+
python manage.py cleanup_expired_uploads --status failed
|
397
|
+
|
398
|
+
# Verbose output
|
399
|
+
python manage.py cleanup_expired_uploads --verbose
|
400
|
+
```
|
401
|
+
|
402
|
+
## File Validation
|
403
|
+
|
404
|
+
### Configure File Restrictions
|
405
|
+
|
406
|
+
```python
|
407
|
+
file_manager = FileManager.objects.create(
|
408
|
+
name="Image Storage",
|
409
|
+
backend_type="s3",
|
410
|
+
max_file_size=10 * 1024 * 1024, # 10MB
|
411
|
+
allowed_extensions=["jpg", "jpeg", "png", "gif", "webp"],
|
412
|
+
allowed_mime_types=[
|
413
|
+
"image/jpeg",
|
414
|
+
"image/png",
|
415
|
+
"image/gif",
|
416
|
+
"image/webp"
|
417
|
+
],
|
418
|
+
# ... other settings
|
419
|
+
)
|
420
|
+
```
|
421
|
+
|
422
|
+
### Custom Validation
|
423
|
+
|
424
|
+
```python
|
425
|
+
from mojo.apps.fileman.models import File
|
426
|
+
|
427
|
+
# Custom validation in your views
|
428
|
+
def validate_upload(request, file_data):
|
429
|
+
# Custom business logic
|
430
|
+
if file_data['filename'].startswith('temp_'):
|
431
|
+
raise ValidationError('Temporary files not allowed')
|
432
|
+
|
433
|
+
# Check file size against user's quota
|
434
|
+
user_files_size = File.objects.filter(
|
435
|
+
uploaded_by=request.user,
|
436
|
+
upload_status=File.COMPLETED
|
437
|
+
).aggregate(total=Sum('file_size'))['total'] or 0
|
438
|
+
|
439
|
+
if user_files_size + file_data['size'] > USER_QUOTA:
|
440
|
+
raise ValidationError('Upload would exceed user quota')
|
441
|
+
```
|
442
|
+
|
443
|
+
## Security Considerations
|
444
|
+
|
445
|
+
### Environment Variables
|
446
|
+
|
447
|
+
```bash
|
448
|
+
# Production environment variables
|
449
|
+
export AWS_ACCESS_KEY_ID="AKIAIOSFODNN7EXAMPLE"
|
450
|
+
export AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
|
451
|
+
export AWS_DEFAULT_REGION="us-east-1"
|
452
|
+
export KMS_KEY_ID="arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012"
|
453
|
+
```
|
454
|
+
|
455
|
+
### IAM Policy for S3
|
456
|
+
|
457
|
+
```json
|
458
|
+
{
|
459
|
+
"Version": "2012-10-17",
|
460
|
+
"Statement": [
|
461
|
+
{
|
462
|
+
"Effect": "Allow",
|
463
|
+
"Action": [
|
464
|
+
"s3:GetObject",
|
465
|
+
"s3:PutObject",
|
466
|
+
"s3:DeleteObject"
|
467
|
+
],
|
468
|
+
"Resource": "arn:aws:s3:::my-bucket/*"
|
469
|
+
},
|
470
|
+
{
|
471
|
+
"Effect": "Allow",
|
472
|
+
"Action": [
|
473
|
+
"s3:ListBucket"
|
474
|
+
],
|
475
|
+
"Resource": "arn:aws:s3:::my-bucket"
|
476
|
+
}
|
477
|
+
]
|
478
|
+
}
|
479
|
+
```
|
480
|
+
|
481
|
+
### CORS Configuration for S3
|
482
|
+
|
483
|
+
```json
|
484
|
+
[
|
485
|
+
{
|
486
|
+
"AllowedHeaders": ["*"],
|
487
|
+
"AllowedMethods": ["GET", "POST", "PUT"],
|
488
|
+
"AllowedOrigins": ["https://yourdomain.com"],
|
489
|
+
"ExposeHeaders": ["ETag"],
|
490
|
+
"MaxAgeSeconds": 3000
|
491
|
+
}
|
492
|
+
]
|
493
|
+
```
|
494
|
+
|
495
|
+
## Troubleshooting
|
496
|
+
|
497
|
+
### Common Issues
|
498
|
+
|
499
|
+
1. **Upload URL Expired**
|
500
|
+
- Check `upload_expires_in` setting
|
501
|
+
- Ensure client uploads immediately after getting URL
|
502
|
+
|
503
|
+
2. **CORS Errors on S3**
|
504
|
+
- Configure CORS policy on S3 bucket
|
505
|
+
- Ensure your domain is in AllowedOrigins
|
506
|
+
|
507
|
+
3. **File Not Found After Upload**
|
508
|
+
- Check that finalize-upload was called successfully
|
509
|
+
- Verify file exists in storage backend
|
510
|
+
|
511
|
+
4. **Permission Denied Errors**
|
512
|
+
- Check IAM policies for S3
|
513
|
+
- Verify file system permissions for local storage
|
514
|
+
|
515
|
+
5. **Large File Upload Failures**
|
516
|
+
- Increase `max_file_size` setting
|
517
|
+
- Consider enabling S3 multipart uploads for large files
|
518
|
+
|
519
|
+
### Debug Mode
|
520
|
+
|
521
|
+
```python
|
522
|
+
# Enable debug logging
|
523
|
+
import logging
|
524
|
+
logging.getLogger('mojo.apps.fileman').setLevel(logging.DEBUG)
|
525
|
+
|
526
|
+
# Check file status
|
527
|
+
file_obj = File.objects.get(upload_token='abc123...')
|
528
|
+
print(f"Status: {file_obj.upload_status}")
|
529
|
+
print(f"Path: {file_obj.file_path}")
|
530
|
+
print(f"Metadata: {file_obj.metadata}")
|
531
|
+
|
532
|
+
# Test backend connection
|
533
|
+
from mojo.apps.fileman.backends import get_backend
|
534
|
+
backend = get_backend(file_obj.file_manager)
|
535
|
+
is_valid, errors = backend.validate_configuration()
|
536
|
+
print(f"Backend valid: {is_valid}, Errors: {errors}")
|
537
|
+
```
|
538
|
+
|
539
|
+
## Contributing
|
540
|
+
|
541
|
+
1. Fork the repository
|
542
|
+
2. Create a feature branch
|
543
|
+
3. Add tests for new functionality
|
544
|
+
4. Ensure all tests pass
|
545
|
+
5. Submit a pull request
|
546
|
+
|
547
|
+
## License
|
548
|
+
|
549
|
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
File without changes
|
@@ -0,0 +1,15 @@
|
|
1
|
+
from django.apps import AppConfig
|
2
|
+
|
3
|
+
|
4
|
+
class FilemanConfig(AppConfig):
|
5
|
+
default_auto_field = 'django.db.models.BigAutoField'
|
6
|
+
name = 'mojo.apps.fileman'
|
7
|
+
verbose_name = 'File Manager'
|
8
|
+
|
9
|
+
def ready(self):
|
10
|
+
"""
|
11
|
+
Perform initialization tasks when the app is ready
|
12
|
+
"""
|
13
|
+
# Import signal handlers if any
|
14
|
+
# from . import signals
|
15
|
+
pass
|
@@ -0,0 +1,117 @@
|
|
1
|
+
from typing import Dict, Type
|
2
|
+
from .base import StorageBackend
|
3
|
+
from .filesystem import FileSystemStorageBackend
|
4
|
+
from .s3 import S3StorageBackend
|
5
|
+
|
6
|
+
|
7
|
+
# Registry of available storage backends
|
8
|
+
BACKEND_REGISTRY: Dict[str, Type[StorageBackend]] = {
|
9
|
+
'file': FileSystemStorageBackend,
|
10
|
+
's3': S3StorageBackend,
|
11
|
+
}
|
12
|
+
|
13
|
+
|
14
|
+
def register_backend(backend_type: str, backend_class: Type[StorageBackend]):
|
15
|
+
"""
|
16
|
+
Register a custom storage backend
|
17
|
+
|
18
|
+
Args:
|
19
|
+
backend_type: Backend type identifier
|
20
|
+
backend_class: Backend class that inherits from StorageBackend
|
21
|
+
"""
|
22
|
+
if not issubclass(backend_class, StorageBackend):
|
23
|
+
raise ValueError("Backend class must inherit from StorageBackend")
|
24
|
+
|
25
|
+
BACKEND_REGISTRY[backend_type] = backend_class
|
26
|
+
|
27
|
+
|
28
|
+
def get_backend(file_manager, **kwargs) -> StorageBackend:
|
29
|
+
"""
|
30
|
+
Get a storage backend instance for the given FileManager
|
31
|
+
|
32
|
+
Args:
|
33
|
+
file_manager: FileManager instance with backend configuration
|
34
|
+
**kwargs: Additional backend-specific configuration
|
35
|
+
|
36
|
+
Returns:
|
37
|
+
StorageBackend: Configured storage backend instance
|
38
|
+
|
39
|
+
Raises:
|
40
|
+
ValueError: If backend type is not supported
|
41
|
+
Exception: If backend initialization fails
|
42
|
+
"""
|
43
|
+
backend_type = file_manager.backend_type
|
44
|
+
|
45
|
+
if backend_type not in BACKEND_REGISTRY:
|
46
|
+
available_backends = ', '.join(BACKEND_REGISTRY.keys())
|
47
|
+
raise ValueError(
|
48
|
+
f"Unsupported backend type '{backend_type}'. "
|
49
|
+
f"Available backends: {available_backends}"
|
50
|
+
)
|
51
|
+
|
52
|
+
backend_class = BACKEND_REGISTRY[backend_type]
|
53
|
+
|
54
|
+
try:
|
55
|
+
return backend_class(file_manager, **kwargs)
|
56
|
+
except Exception as e:
|
57
|
+
raise Exception(f"Failed to initialize {backend_type} backend: {e}")
|
58
|
+
|
59
|
+
|
60
|
+
def get_available_backends() -> Dict[str, Type[StorageBackend]]:
|
61
|
+
"""
|
62
|
+
Get all available storage backends
|
63
|
+
|
64
|
+
Returns:
|
65
|
+
Dict mapping backend type to backend class
|
66
|
+
"""
|
67
|
+
return BACKEND_REGISTRY.copy()
|
68
|
+
|
69
|
+
|
70
|
+
def is_backend_supported(backend_type: str) -> bool:
|
71
|
+
"""
|
72
|
+
Check if a backend type is supported
|
73
|
+
|
74
|
+
Args:
|
75
|
+
backend_type: Backend type to check
|
76
|
+
|
77
|
+
Returns:
|
78
|
+
bool: True if backend is supported
|
79
|
+
"""
|
80
|
+
return backend_type in BACKEND_REGISTRY
|
81
|
+
|
82
|
+
|
83
|
+
def validate_backend_config(file_manager) -> tuple[bool, list[str]]:
|
84
|
+
"""
|
85
|
+
Validate backend configuration for a FileManager
|
86
|
+
|
87
|
+
Args:
|
88
|
+
file_manager: FileManager instance to validate
|
89
|
+
|
90
|
+
Returns:
|
91
|
+
tuple: (is_valid, list_of_errors)
|
92
|
+
"""
|
93
|
+
try:
|
94
|
+
backend = get_backend(file_manager)
|
95
|
+
|
96
|
+
# Check if backend has a validate_configuration method
|
97
|
+
if hasattr(backend, 'validate_configuration') and callable(getattr(backend, 'validate_configuration')):
|
98
|
+
return backend.validate_configuration()
|
99
|
+
else:
|
100
|
+
# Basic validation - just check if we can create the backend
|
101
|
+
return True, []
|
102
|
+
|
103
|
+
except Exception as e:
|
104
|
+
return False, [str(e)]
|
105
|
+
|
106
|
+
|
107
|
+
__all__ = [
|
108
|
+
'StorageBackend',
|
109
|
+
'FileSystemStorageBackend',
|
110
|
+
'S3StorageBackend',
|
111
|
+
'BACKEND_REGISTRY',
|
112
|
+
'register_backend',
|
113
|
+
'get_backend',
|
114
|
+
'get_available_backends',
|
115
|
+
'is_backend_supported',
|
116
|
+
'validate_backend_config',
|
117
|
+
]
|