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,397 @@
|
|
1
|
+
import os
|
2
|
+
import shutil
|
3
|
+
import hashlib
|
4
|
+
from pathlib import Path
|
5
|
+
from typing import Dict, Any, Optional, Tuple, List
|
6
|
+
from datetime import datetime, timedelta
|
7
|
+
from urllib.parse import urljoin
|
8
|
+
import uuid
|
9
|
+
|
10
|
+
from .base import StorageBackend
|
11
|
+
|
12
|
+
|
13
|
+
class FileSystemStorageBackend(StorageBackend):
|
14
|
+
"""
|
15
|
+
Local file system storage backend implementation
|
16
|
+
"""
|
17
|
+
|
18
|
+
def __init__(self, file_manager, **kwargs):
|
19
|
+
super().__init__(file_manager, **kwargs)
|
20
|
+
|
21
|
+
# File system configuration
|
22
|
+
self.base_path = self.get_setting('base_path', '/tmp/fileman')
|
23
|
+
self.base_url = self.get_setting('base_url', '/media/')
|
24
|
+
self.create_directories = self.get_setting('create_directories', True)
|
25
|
+
self.permissions = self.get_setting('permissions', 0o644)
|
26
|
+
self.directory_permissions = self.get_setting('directory_permissions', 0o755)
|
27
|
+
|
28
|
+
# Upload configuration
|
29
|
+
self.upload_expires_in = self.get_setting('upload_expires_in', 3600) # 1 hour
|
30
|
+
self.temp_upload_path = self.get_setting('temp_upload_path', os.path.join(self.base_path, 'uploads'))
|
31
|
+
|
32
|
+
# Ensure base paths exist
|
33
|
+
if self.create_directories:
|
34
|
+
os.makedirs(self.base_path, mode=self.directory_permissions, exist_ok=True)
|
35
|
+
os.makedirs(self.temp_upload_path, mode=self.directory_permissions, exist_ok=True)
|
36
|
+
|
37
|
+
def _get_full_path(self, file_path: str) -> str:
|
38
|
+
"""Get the full file system path for a file"""
|
39
|
+
# Normalize the path to prevent directory traversal
|
40
|
+
normalized_path = os.path.normpath(file_path.lstrip('/'))
|
41
|
+
|
42
|
+
# Ensure the path doesn't escape the base directory
|
43
|
+
full_path = os.path.join(self.base_path, normalized_path)
|
44
|
+
if not full_path.startswith(self.base_path):
|
45
|
+
raise ValueError(f"Invalid file path: {file_path}")
|
46
|
+
|
47
|
+
return full_path
|
48
|
+
|
49
|
+
def _ensure_directory(self, file_path: str):
|
50
|
+
"""Ensure the directory for a file path exists"""
|
51
|
+
directory = os.path.dirname(file_path)
|
52
|
+
if directory and not os.path.exists(directory):
|
53
|
+
os.makedirs(directory, mode=self.directory_permissions, exist_ok=True)
|
54
|
+
|
55
|
+
def save(self, file_obj, filename: str, **kwargs) -> str:
|
56
|
+
"""Save a file to the local file system"""
|
57
|
+
try:
|
58
|
+
# Generate file path
|
59
|
+
file_path = self.generate_file_path(filename, kwargs.get('group_id'))
|
60
|
+
full_path = self._get_full_path(file_path)
|
61
|
+
|
62
|
+
# Ensure directory exists
|
63
|
+
self._ensure_directory(full_path)
|
64
|
+
|
65
|
+
# Save the file
|
66
|
+
with open(full_path, 'wb') as dest:
|
67
|
+
if hasattr(file_obj, 'read'):
|
68
|
+
# File-like object
|
69
|
+
for chunk in iter(lambda: file_obj.read(4096), b''):
|
70
|
+
dest.write(chunk)
|
71
|
+
else:
|
72
|
+
# Bytes data
|
73
|
+
dest.write(file_obj)
|
74
|
+
|
75
|
+
# Set file permissions
|
76
|
+
os.chmod(full_path, self.permissions)
|
77
|
+
|
78
|
+
return file_path
|
79
|
+
|
80
|
+
except Exception as e:
|
81
|
+
raise Exception(f"Failed to save file to filesystem: {e}")
|
82
|
+
|
83
|
+
def delete(self, file_path: str) -> bool:
|
84
|
+
"""Delete a file from the file system"""
|
85
|
+
try:
|
86
|
+
full_path = self._get_full_path(file_path)
|
87
|
+
if os.path.exists(full_path):
|
88
|
+
os.remove(full_path)
|
89
|
+
|
90
|
+
# Try to remove empty parent directories
|
91
|
+
parent_dir = os.path.dirname(full_path)
|
92
|
+
while parent_dir != self.base_path:
|
93
|
+
try:
|
94
|
+
if os.path.exists(parent_dir) and not os.listdir(parent_dir):
|
95
|
+
os.rmdir(parent_dir)
|
96
|
+
parent_dir = os.path.dirname(parent_dir)
|
97
|
+
else:
|
98
|
+
break
|
99
|
+
except OSError:
|
100
|
+
break
|
101
|
+
|
102
|
+
return True
|
103
|
+
return False
|
104
|
+
except Exception:
|
105
|
+
return False
|
106
|
+
|
107
|
+
def exists(self, file_path: str) -> bool:
|
108
|
+
"""Check if a file exists in the file system"""
|
109
|
+
try:
|
110
|
+
full_path = self._get_full_path(file_path)
|
111
|
+
return os.path.isfile(full_path)
|
112
|
+
except Exception:
|
113
|
+
return False
|
114
|
+
|
115
|
+
def get_file_size(self, file_path: str) -> Optional[int]:
|
116
|
+
"""Get the size of a file in bytes"""
|
117
|
+
try:
|
118
|
+
full_path = self._get_full_path(file_path)
|
119
|
+
if os.path.isfile(full_path):
|
120
|
+
return os.path.getsize(full_path)
|
121
|
+
return None
|
122
|
+
except Exception:
|
123
|
+
return None
|
124
|
+
|
125
|
+
def get_url(self, file_path: str, expires_in: Optional[int] = None) -> str:
|
126
|
+
"""Get a URL to access the file"""
|
127
|
+
# For file system, we just return a static URL
|
128
|
+
# In a real implementation, you might want to generate signed URLs
|
129
|
+
# or check permissions here
|
130
|
+
return urljoin(self.base_url, file_path)
|
131
|
+
|
132
|
+
def generate_upload_url(self, file_path: str, content_type: str,
|
133
|
+
file_size: Optional[int] = None,
|
134
|
+
expires_in: int = 3600) -> Dict[str, Any]:
|
135
|
+
"""
|
136
|
+
Generate an upload URL for file system backend
|
137
|
+
Note: File system doesn't natively support pre-signed URLs like S3,
|
138
|
+
so this creates a temporary upload token that can be used with a custom endpoint
|
139
|
+
"""
|
140
|
+
try:
|
141
|
+
# Generate upload token
|
142
|
+
upload_token = hashlib.sha256(f"{file_path}{uuid.uuid4()}{datetime.now()}".encode()).hexdigest()[:32]
|
143
|
+
|
144
|
+
# Create temporary upload directory if needed
|
145
|
+
temp_path = os.path.join(self.temp_upload_path, upload_token)
|
146
|
+
if self.create_directories:
|
147
|
+
os.makedirs(temp_path, mode=self.directory_permissions, exist_ok=True)
|
148
|
+
|
149
|
+
# Store upload metadata in a temporary file
|
150
|
+
metadata = {
|
151
|
+
'file_path': file_path,
|
152
|
+
'content_type': content_type,
|
153
|
+
'file_size': file_size,
|
154
|
+
'expires_at': (datetime.now() + timedelta(seconds=expires_in)).isoformat(),
|
155
|
+
'created_at': datetime.now().isoformat()
|
156
|
+
}
|
157
|
+
|
158
|
+
metadata_path = os.path.join(temp_path, 'metadata.json')
|
159
|
+
import json
|
160
|
+
with open(metadata_path, 'w') as f:
|
161
|
+
json.dump(metadata, f)
|
162
|
+
|
163
|
+
# Return upload information
|
164
|
+
# The upload_url would point to a custom Django view that handles the upload
|
165
|
+
return {
|
166
|
+
'upload_url': f'/fileman/upload/{upload_token}/',
|
167
|
+
'method': 'POST',
|
168
|
+
'fields': {
|
169
|
+
'upload_token': upload_token,
|
170
|
+
'content_type': content_type
|
171
|
+
},
|
172
|
+
'headers': {
|
173
|
+
'Content-Type': content_type
|
174
|
+
}
|
175
|
+
}
|
176
|
+
|
177
|
+
except Exception as e:
|
178
|
+
raise Exception(f"Failed to generate upload URL: {e}")
|
179
|
+
|
180
|
+
def validate_upload_token(self, upload_token: str) -> Tuple[bool, Optional[Dict[str, Any]]]:
|
181
|
+
"""Validate an upload token and return metadata"""
|
182
|
+
try:
|
183
|
+
temp_path = os.path.join(self.temp_upload_path, upload_token)
|
184
|
+
metadata_path = os.path.join(temp_path, 'metadata.json')
|
185
|
+
|
186
|
+
if not os.path.exists(metadata_path):
|
187
|
+
return False, None
|
188
|
+
|
189
|
+
import json
|
190
|
+
with open(metadata_path, 'r') as f:
|
191
|
+
metadata = json.load(f)
|
192
|
+
|
193
|
+
# Check if expired
|
194
|
+
expires_at = datetime.fromisoformat(metadata['expires_at'])
|
195
|
+
if datetime.now() > expires_at:
|
196
|
+
# Clean up expired token
|
197
|
+
shutil.rmtree(temp_path, ignore_errors=True)
|
198
|
+
return False, None
|
199
|
+
|
200
|
+
return True, metadata
|
201
|
+
|
202
|
+
except Exception:
|
203
|
+
return False, None
|
204
|
+
|
205
|
+
def finalize_upload(self, upload_token: str, uploaded_file_path: str) -> bool:
|
206
|
+
"""Move uploaded file from temp location to final location"""
|
207
|
+
try:
|
208
|
+
is_valid, metadata = self.validate_upload_token(upload_token)
|
209
|
+
if not is_valid or not metadata:
|
210
|
+
return False
|
211
|
+
|
212
|
+
temp_path = os.path.join(self.temp_upload_path, upload_token)
|
213
|
+
temp_file_path = os.path.join(temp_path, 'uploaded_file')
|
214
|
+
|
215
|
+
if not os.path.exists(temp_file_path):
|
216
|
+
return False
|
217
|
+
|
218
|
+
# Move file to final location
|
219
|
+
final_path = self._get_full_path(metadata['file_path'])
|
220
|
+
self._ensure_directory(final_path)
|
221
|
+
|
222
|
+
shutil.move(temp_file_path, final_path)
|
223
|
+
os.chmod(final_path, self.permissions)
|
224
|
+
|
225
|
+
# Clean up temp directory
|
226
|
+
shutil.rmtree(temp_path, ignore_errors=True)
|
227
|
+
|
228
|
+
return True
|
229
|
+
|
230
|
+
except Exception:
|
231
|
+
return False
|
232
|
+
|
233
|
+
def open(self, file_path: str, mode: str = 'rb'):
|
234
|
+
"""Open a file from the file system"""
|
235
|
+
full_path = self._get_full_path(file_path)
|
236
|
+
return open(full_path, mode)
|
237
|
+
|
238
|
+
def list_files(self, path_prefix: str = "", limit: int = 1000) -> List[str]:
|
239
|
+
"""List files in the file system with optional path prefix"""
|
240
|
+
try:
|
241
|
+
search_path = self._get_full_path(path_prefix) if path_prefix else self.base_path
|
242
|
+
|
243
|
+
files = []
|
244
|
+
for root, dirs, filenames in os.walk(search_path):
|
245
|
+
for filename in filenames:
|
246
|
+
if len(files) >= limit:
|
247
|
+
break
|
248
|
+
|
249
|
+
full_path = os.path.join(root, filename)
|
250
|
+
# Get relative path from base_path
|
251
|
+
rel_path = os.path.relpath(full_path, self.base_path)
|
252
|
+
files.append(rel_path.replace(os.sep, '/')) # Use forward slashes
|
253
|
+
|
254
|
+
if len(files) >= limit:
|
255
|
+
break
|
256
|
+
|
257
|
+
return files[:limit]
|
258
|
+
|
259
|
+
except Exception:
|
260
|
+
return []
|
261
|
+
|
262
|
+
def copy_file(self, source_path: str, dest_path: str) -> bool:
|
263
|
+
"""Copy a file within the file system"""
|
264
|
+
try:
|
265
|
+
source_full_path = self._get_full_path(source_path)
|
266
|
+
dest_full_path = self._get_full_path(dest_path)
|
267
|
+
|
268
|
+
if not os.path.exists(source_full_path):
|
269
|
+
return False
|
270
|
+
|
271
|
+
self._ensure_directory(dest_full_path)
|
272
|
+
shutil.copy2(source_full_path, dest_full_path)
|
273
|
+
os.chmod(dest_full_path, self.permissions)
|
274
|
+
|
275
|
+
return True
|
276
|
+
|
277
|
+
except Exception:
|
278
|
+
return False
|
279
|
+
|
280
|
+
def move_file(self, source_path: str, dest_path: str) -> bool:
|
281
|
+
"""Move a file within the file system"""
|
282
|
+
try:
|
283
|
+
source_full_path = self._get_full_path(source_path)
|
284
|
+
dest_full_path = self._get_full_path(dest_path)
|
285
|
+
|
286
|
+
if not os.path.exists(source_full_path):
|
287
|
+
return False
|
288
|
+
|
289
|
+
self._ensure_directory(dest_full_path)
|
290
|
+
shutil.move(source_full_path, dest_full_path)
|
291
|
+
os.chmod(dest_full_path, self.permissions)
|
292
|
+
|
293
|
+
return True
|
294
|
+
|
295
|
+
except Exception:
|
296
|
+
return False
|
297
|
+
|
298
|
+
def get_file_metadata(self, file_path: str) -> Dict[str, Any]:
|
299
|
+
"""Get comprehensive metadata for a file"""
|
300
|
+
try:
|
301
|
+
full_path = self._get_full_path(file_path)
|
302
|
+
|
303
|
+
if not os.path.exists(full_path):
|
304
|
+
return {'exists': False, 'path': file_path}
|
305
|
+
|
306
|
+
stat = os.stat(full_path)
|
307
|
+
|
308
|
+
metadata = {
|
309
|
+
'exists': True,
|
310
|
+
'path': file_path,
|
311
|
+
'size': stat.st_size,
|
312
|
+
'last_modified': datetime.fromtimestamp(stat.st_mtime),
|
313
|
+
'created': datetime.fromtimestamp(stat.st_ctime),
|
314
|
+
'permissions': oct(stat.st_mode)[-3:],
|
315
|
+
'is_file': os.path.isfile(full_path),
|
316
|
+
'is_directory': os.path.isdir(full_path)
|
317
|
+
}
|
318
|
+
|
319
|
+
return metadata
|
320
|
+
|
321
|
+
except Exception:
|
322
|
+
return {'exists': False, 'path': file_path}
|
323
|
+
|
324
|
+
def cleanup_expired_uploads(self, before_date: Optional[datetime] = None):
|
325
|
+
"""Clean up expired upload tokens and temporary files"""
|
326
|
+
if before_date is None:
|
327
|
+
before_date = datetime.now() - timedelta(hours=1)
|
328
|
+
|
329
|
+
try:
|
330
|
+
if not os.path.exists(self.temp_upload_path):
|
331
|
+
return
|
332
|
+
|
333
|
+
for token_dir in os.listdir(self.temp_upload_path):
|
334
|
+
token_path = os.path.join(self.temp_upload_path, token_dir)
|
335
|
+
|
336
|
+
if not os.path.isdir(token_path):
|
337
|
+
continue
|
338
|
+
|
339
|
+
metadata_path = os.path.join(token_path, 'metadata.json')
|
340
|
+
|
341
|
+
try:
|
342
|
+
if os.path.exists(metadata_path):
|
343
|
+
import json
|
344
|
+
with open(metadata_path, 'r') as f:
|
345
|
+
metadata = json.load(f)
|
346
|
+
|
347
|
+
expires_at = datetime.fromisoformat(metadata['expires_at'])
|
348
|
+
if expires_at < before_date:
|
349
|
+
shutil.rmtree(token_path, ignore_errors=True)
|
350
|
+
else:
|
351
|
+
# If no metadata file, check directory modification time
|
352
|
+
stat = os.stat(token_path)
|
353
|
+
if datetime.fromtimestamp(stat.st_mtime) < before_date:
|
354
|
+
shutil.rmtree(token_path, ignore_errors=True)
|
355
|
+
|
356
|
+
except Exception:
|
357
|
+
# If we can't process the directory, skip it
|
358
|
+
continue
|
359
|
+
|
360
|
+
except Exception:
|
361
|
+
pass # Silently ignore cleanup errors
|
362
|
+
|
363
|
+
def get_available_space(self) -> Optional[int]:
|
364
|
+
"""Get available disk space in bytes"""
|
365
|
+
try:
|
366
|
+
statvfs = os.statvfs(self.base_path)
|
367
|
+
return statvfs.f_frsize * statvfs.f_bavail
|
368
|
+
except Exception:
|
369
|
+
return None
|
370
|
+
|
371
|
+
def validate_configuration(self) -> Tuple[bool, List[str]]:
|
372
|
+
"""Validate file system configuration"""
|
373
|
+
errors = []
|
374
|
+
|
375
|
+
if not self.base_path:
|
376
|
+
errors.append("Base path is required for file system backend")
|
377
|
+
|
378
|
+
try:
|
379
|
+
# Check if base path is accessible
|
380
|
+
if not os.path.exists(self.base_path):
|
381
|
+
if self.create_directories:
|
382
|
+
os.makedirs(self.base_path, mode=self.directory_permissions)
|
383
|
+
else:
|
384
|
+
errors.append(f"Base path does not exist: {self.base_path}")
|
385
|
+
|
386
|
+
# Check write permissions
|
387
|
+
if os.path.exists(self.base_path):
|
388
|
+
if not os.access(self.base_path, os.W_OK):
|
389
|
+
errors.append(f"No write permission for base path: {self.base_path}")
|
390
|
+
|
391
|
+
if not os.access(self.base_path, os.R_OK):
|
392
|
+
errors.append(f"No read permission for base path: {self.base_path}")
|
393
|
+
|
394
|
+
except Exception as e:
|
395
|
+
errors.append(f"Error accessing base path: {e}")
|
396
|
+
|
397
|
+
return len(errors) == 0, errors
|