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.
Files changed (194) hide show
  1. django_nativemojo-0.1.10.dist-info/LICENSE +19 -0
  2. django_nativemojo-0.1.10.dist-info/METADATA +96 -0
  3. django_nativemojo-0.1.10.dist-info/NOTICE +8 -0
  4. django_nativemojo-0.1.10.dist-info/RECORD +194 -0
  5. django_nativemojo-0.1.10.dist-info/WHEEL +4 -0
  6. mojo/__init__.py +3 -0
  7. mojo/apps/account/__init__.py +1 -0
  8. mojo/apps/account/admin.py +91 -0
  9. mojo/apps/account/apps.py +16 -0
  10. mojo/apps/account/migrations/0001_initial.py +77 -0
  11. mojo/apps/account/migrations/0002_user_is_email_verified_user_is_phone_verified.py +23 -0
  12. mojo/apps/account/migrations/0003_group_mojo_secrets_user_mojo_secrets.py +23 -0
  13. mojo/apps/account/migrations/__init__.py +0 -0
  14. mojo/apps/account/models/__init__.py +3 -0
  15. mojo/apps/account/models/group.py +98 -0
  16. mojo/apps/account/models/member.py +95 -0
  17. mojo/apps/account/models/pkey.py +18 -0
  18. mojo/apps/account/models/user.py +211 -0
  19. mojo/apps/account/rest/__init__.py +3 -0
  20. mojo/apps/account/rest/group.py +25 -0
  21. mojo/apps/account/rest/user.py +47 -0
  22. mojo/apps/account/utils/__init__.py +0 -0
  23. mojo/apps/account/utils/jwtoken.py +72 -0
  24. mojo/apps/account/utils/passkeys.py +54 -0
  25. mojo/apps/fileman/README.md +549 -0
  26. mojo/apps/fileman/__init__.py +0 -0
  27. mojo/apps/fileman/apps.py +15 -0
  28. mojo/apps/fileman/backends/__init__.py +117 -0
  29. mojo/apps/fileman/backends/base.py +319 -0
  30. mojo/apps/fileman/backends/filesystem.py +397 -0
  31. mojo/apps/fileman/backends/s3.py +398 -0
  32. mojo/apps/fileman/examples/configurations.py +378 -0
  33. mojo/apps/fileman/examples/usage_example.py +665 -0
  34. mojo/apps/fileman/management/__init__.py +1 -0
  35. mojo/apps/fileman/management/commands/__init__.py +1 -0
  36. mojo/apps/fileman/management/commands/cleanup_expired_uploads.py +222 -0
  37. mojo/apps/fileman/models/__init__.py +7 -0
  38. mojo/apps/fileman/models/file.py +292 -0
  39. mojo/apps/fileman/models/manager.py +227 -0
  40. mojo/apps/fileman/models/render.py +0 -0
  41. mojo/apps/fileman/rest/__init__ +0 -0
  42. mojo/apps/fileman/rest/__init__.py +23 -0
  43. mojo/apps/fileman/rest/fileman.py +13 -0
  44. mojo/apps/fileman/rest/upload.py +92 -0
  45. mojo/apps/fileman/utils/__init__.py +19 -0
  46. mojo/apps/fileman/utils/upload.py +616 -0
  47. mojo/apps/incident/__init__.py +1 -0
  48. mojo/apps/incident/handlers/__init__.py +3 -0
  49. mojo/apps/incident/handlers/event_handlers.py +142 -0
  50. mojo/apps/incident/migrations/0001_initial.py +83 -0
  51. mojo/apps/incident/migrations/0002_rename_bundle_ruleset_bundle_minutes_event_hostname_and_more.py +44 -0
  52. mojo/apps/incident/migrations/0003_alter_event_model_id.py +18 -0
  53. mojo/apps/incident/migrations/0004_alter_incident_model_id.py +18 -0
  54. mojo/apps/incident/migrations/__init__.py +0 -0
  55. mojo/apps/incident/models/__init__.py +3 -0
  56. mojo/apps/incident/models/event.py +135 -0
  57. mojo/apps/incident/models/incident.py +33 -0
  58. mojo/apps/incident/models/rule.py +247 -0
  59. mojo/apps/incident/parsers/__init__.py +0 -0
  60. mojo/apps/incident/parsers/ossec/__init__.py +1 -0
  61. mojo/apps/incident/parsers/ossec/core.py +82 -0
  62. mojo/apps/incident/parsers/ossec/parsed.py +23 -0
  63. mojo/apps/incident/parsers/ossec/rules.py +124 -0
  64. mojo/apps/incident/parsers/ossec/utils.py +169 -0
  65. mojo/apps/incident/reporter.py +42 -0
  66. mojo/apps/incident/rest/__init__.py +2 -0
  67. mojo/apps/incident/rest/event.py +23 -0
  68. mojo/apps/incident/rest/ossec.py +22 -0
  69. mojo/apps/logit/__init__.py +0 -0
  70. mojo/apps/logit/admin.py +37 -0
  71. mojo/apps/logit/migrations/0001_initial.py +32 -0
  72. mojo/apps/logit/migrations/0002_log_duid_log_payload_log_username.py +28 -0
  73. mojo/apps/logit/migrations/0003_log_level.py +18 -0
  74. mojo/apps/logit/migrations/__init__.py +0 -0
  75. mojo/apps/logit/models/__init__.py +1 -0
  76. mojo/apps/logit/models/log.py +57 -0
  77. mojo/apps/logit/rest.py +9 -0
  78. mojo/apps/metrics/README.md +79 -0
  79. mojo/apps/metrics/__init__.py +12 -0
  80. mojo/apps/metrics/redis_metrics.py +331 -0
  81. mojo/apps/metrics/rest/__init__.py +1 -0
  82. mojo/apps/metrics/rest/base.py +152 -0
  83. mojo/apps/metrics/rest/db.py +0 -0
  84. mojo/apps/metrics/utils.py +227 -0
  85. mojo/apps/notify/README.md +91 -0
  86. mojo/apps/notify/README_NOTIFICATIONS.md +566 -0
  87. mojo/apps/notify/__init__.py +0 -0
  88. mojo/apps/notify/admin.py +52 -0
  89. mojo/apps/notify/handlers/__init__.py +0 -0
  90. mojo/apps/notify/handlers/example_handlers.py +516 -0
  91. mojo/apps/notify/handlers/ses/__init__.py +25 -0
  92. mojo/apps/notify/handlers/ses/bounce.py +0 -0
  93. mojo/apps/notify/handlers/ses/complaint.py +25 -0
  94. mojo/apps/notify/handlers/ses/message.py +86 -0
  95. mojo/apps/notify/management/__init__.py +0 -0
  96. mojo/apps/notify/management/commands/__init__.py +1 -0
  97. mojo/apps/notify/management/commands/process_notifications.py +370 -0
  98. mojo/apps/notify/mod +0 -0
  99. mojo/apps/notify/models/__init__.py +12 -0
  100. mojo/apps/notify/models/account.py +128 -0
  101. mojo/apps/notify/models/attachment.py +24 -0
  102. mojo/apps/notify/models/bounce.py +68 -0
  103. mojo/apps/notify/models/complaint.py +40 -0
  104. mojo/apps/notify/models/inbox.py +113 -0
  105. mojo/apps/notify/models/inbox_message.py +173 -0
  106. mojo/apps/notify/models/outbox.py +129 -0
  107. mojo/apps/notify/models/outbox_message.py +288 -0
  108. mojo/apps/notify/models/template.py +30 -0
  109. mojo/apps/notify/providers/__init__.py +0 -0
  110. mojo/apps/notify/providers/aws.py +73 -0
  111. mojo/apps/notify/rest/__init__.py +0 -0
  112. mojo/apps/notify/rest/ses.py +0 -0
  113. mojo/apps/notify/utils/__init__.py +2 -0
  114. mojo/apps/notify/utils/notifications.py +404 -0
  115. mojo/apps/notify/utils/parsing.py +202 -0
  116. mojo/apps/notify/utils/render.py +144 -0
  117. mojo/apps/tasks/README.md +118 -0
  118. mojo/apps/tasks/__init__.py +11 -0
  119. mojo/apps/tasks/manager.py +489 -0
  120. mojo/apps/tasks/rest/__init__.py +2 -0
  121. mojo/apps/tasks/rest/hooks.py +0 -0
  122. mojo/apps/tasks/rest/tasks.py +62 -0
  123. mojo/apps/tasks/runner.py +174 -0
  124. mojo/apps/tasks/tq_handlers.py +14 -0
  125. mojo/decorators/__init__.py +3 -0
  126. mojo/decorators/auth.py +25 -0
  127. mojo/decorators/cron.py +31 -0
  128. mojo/decorators/http.py +132 -0
  129. mojo/decorators/validate.py +14 -0
  130. mojo/errors.py +88 -0
  131. mojo/helpers/__init__.py +0 -0
  132. mojo/helpers/aws/__init__.py +0 -0
  133. mojo/helpers/aws/client.py +8 -0
  134. mojo/helpers/aws/s3.py +268 -0
  135. mojo/helpers/aws/setup_email.py +0 -0
  136. mojo/helpers/cron.py +79 -0
  137. mojo/helpers/crypto/__init__.py +4 -0
  138. mojo/helpers/crypto/aes.py +60 -0
  139. mojo/helpers/crypto/hash.py +59 -0
  140. mojo/helpers/crypto/privpub/__init__.py +1 -0
  141. mojo/helpers/crypto/privpub/hybrid.py +97 -0
  142. mojo/helpers/crypto/privpub/rsa.py +104 -0
  143. mojo/helpers/crypto/sign.py +36 -0
  144. mojo/helpers/crypto/too.l.py +25 -0
  145. mojo/helpers/crypto/utils.py +26 -0
  146. mojo/helpers/daemon.py +94 -0
  147. mojo/helpers/dates.py +69 -0
  148. mojo/helpers/dns/__init__.py +0 -0
  149. mojo/helpers/dns/godaddy.py +62 -0
  150. mojo/helpers/filetypes.py +128 -0
  151. mojo/helpers/logit.py +310 -0
  152. mojo/helpers/modules.py +95 -0
  153. mojo/helpers/paths.py +63 -0
  154. mojo/helpers/redis.py +10 -0
  155. mojo/helpers/request.py +89 -0
  156. mojo/helpers/request_parser.py +269 -0
  157. mojo/helpers/response.py +14 -0
  158. mojo/helpers/settings.py +146 -0
  159. mojo/helpers/sysinfo.py +140 -0
  160. mojo/helpers/ua.py +0 -0
  161. mojo/middleware/__init__.py +0 -0
  162. mojo/middleware/auth.py +26 -0
  163. mojo/middleware/logging.py +55 -0
  164. mojo/middleware/mojo.py +21 -0
  165. mojo/migrations/0001_initial.py +32 -0
  166. mojo/migrations/__init__.py +0 -0
  167. mojo/models/__init__.py +2 -0
  168. mojo/models/meta.py +262 -0
  169. mojo/models/rest.py +538 -0
  170. mojo/models/secrets.py +59 -0
  171. mojo/rest/__init__.py +1 -0
  172. mojo/rest/info.py +26 -0
  173. mojo/serializers/__init__.py +0 -0
  174. mojo/serializers/models.py +165 -0
  175. mojo/serializers/openapi.py +188 -0
  176. mojo/urls.py +38 -0
  177. mojo/ws4redis/README.md +174 -0
  178. mojo/ws4redis/__init__.py +2 -0
  179. mojo/ws4redis/client.py +283 -0
  180. mojo/ws4redis/connection.py +327 -0
  181. mojo/ws4redis/exceptions.py +32 -0
  182. mojo/ws4redis/redis.py +183 -0
  183. mojo/ws4redis/servers/__init__.py +0 -0
  184. mojo/ws4redis/servers/base.py +86 -0
  185. mojo/ws4redis/servers/django.py +171 -0
  186. mojo/ws4redis/servers/uwsgi.py +63 -0
  187. mojo/ws4redis/settings.py +45 -0
  188. mojo/ws4redis/utf8validator.py +128 -0
  189. mojo/ws4redis/websocket.py +403 -0
  190. testit/__init__.py +0 -0
  191. testit/client.py +147 -0
  192. testit/faker.py +20 -0
  193. testit/helpers.py +198 -0
  194. 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