django-nativemojo 0.1.10__py3-none-any.whl → 0.1.15__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_nativemojo-0.1.15.dist-info/METADATA +136 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/RECORD +105 -65
- mojo/__init__.py +1 -1
- mojo/apps/account/management/__init__.py +5 -0
- mojo/apps/account/management/commands/__init__.py +6 -0
- mojo/apps/account/management/commands/serializer_admin.py +531 -0
- mojo/apps/account/migrations/0004_user_avatar.py +20 -0
- mojo/apps/account/migrations/0005_group_last_activity.py +18 -0
- mojo/apps/account/models/group.py +25 -7
- mojo/apps/account/models/member.py +15 -4
- mojo/apps/account/models/user.py +197 -20
- mojo/apps/account/rest/group.py +1 -0
- mojo/apps/account/rest/user.py +6 -2
- mojo/apps/aws/rest/__init__.py +1 -0
- mojo/apps/aws/rest/s3.py +64 -0
- mojo/apps/fileman/README.md +8 -8
- mojo/apps/fileman/backends/base.py +76 -70
- mojo/apps/fileman/backends/filesystem.py +86 -86
- mojo/apps/fileman/backends/s3.py +200 -108
- mojo/apps/fileman/migrations/0001_initial.py +106 -0
- mojo/apps/fileman/migrations/0002_filemanager_parent_alter_filemanager_max_file_size.py +24 -0
- mojo/apps/fileman/migrations/0003_remove_file_fileman_fil_upload__c4bc35_idx_and_more.py +25 -0
- mojo/apps/fileman/migrations/0004_remove_file_original_filename_and_more.py +39 -0
- mojo/apps/fileman/migrations/0005_alter_file_upload_token.py +18 -0
- mojo/apps/fileman/migrations/0006_file_download_url_filemanager_forever_urls.py +23 -0
- mojo/apps/fileman/migrations/0007_remove_filemanager_forever_urls_and_more.py +22 -0
- mojo/apps/fileman/migrations/0008_file_category.py +18 -0
- mojo/apps/fileman/migrations/0009_rename_file_path_file_storage_file_path.py +18 -0
- mojo/apps/fileman/migrations/0010_filerendition.py +33 -0
- mojo/apps/fileman/migrations/0011_alter_filerendition_original_file.py +19 -0
- mojo/apps/fileman/models/__init__.py +1 -5
- mojo/apps/fileman/models/file.py +204 -58
- mojo/apps/fileman/models/manager.py +161 -31
- mojo/apps/fileman/models/rendition.py +118 -0
- mojo/apps/fileman/renderer/__init__.py +111 -0
- mojo/apps/fileman/renderer/audio.py +403 -0
- mojo/apps/fileman/renderer/base.py +205 -0
- mojo/apps/fileman/renderer/document.py +404 -0
- mojo/apps/fileman/renderer/image.py +222 -0
- mojo/apps/fileman/renderer/utils.py +297 -0
- mojo/apps/fileman/renderer/video.py +304 -0
- mojo/apps/fileman/rest/__init__.py +1 -18
- mojo/apps/fileman/rest/upload.py +22 -32
- mojo/apps/fileman/signals.py +58 -0
- mojo/apps/fileman/tasks.py +254 -0
- mojo/apps/fileman/utils/__init__.py +40 -16
- mojo/apps/incident/migrations/0005_incidenthistory.py +39 -0
- mojo/apps/incident/migrations/0006_alter_incident_state.py +18 -0
- mojo/apps/incident/models/__init__.py +1 -0
- mojo/apps/incident/models/history.py +36 -0
- mojo/apps/incident/models/incident.py +1 -1
- mojo/apps/incident/reporter.py +3 -1
- mojo/apps/incident/rest/event.py +7 -1
- mojo/apps/logit/migrations/0004_alter_log_level.py +18 -0
- mojo/apps/logit/models/log.py +4 -1
- mojo/apps/metrics/utils.py +2 -2
- mojo/apps/notify/handlers/ses/message.py +1 -1
- mojo/apps/notify/providers/aws.py +2 -2
- mojo/apps/tasks/__init__.py +34 -1
- mojo/apps/tasks/manager.py +200 -45
- mojo/apps/tasks/rest/tasks.py +24 -10
- mojo/apps/tasks/runner.py +283 -18
- mojo/apps/tasks/task.py +99 -0
- mojo/apps/tasks/tq_handlers.py +118 -0
- mojo/decorators/auth.py +6 -1
- mojo/decorators/http.py +7 -2
- mojo/helpers/aws/__init__.py +41 -0
- mojo/helpers/aws/ec2.py +804 -0
- mojo/helpers/aws/iam.py +748 -0
- mojo/helpers/aws/s3.py +451 -11
- mojo/helpers/aws/ses.py +483 -0
- mojo/helpers/aws/sns.py +461 -0
- mojo/helpers/crypto/__pycache__/hash.cpython-310.pyc +0 -0
- mojo/helpers/crypto/__pycache__/sign.cpython-310.pyc +0 -0
- mojo/helpers/crypto/__pycache__/utils.cpython-310.pyc +0 -0
- mojo/helpers/dates.py +18 -0
- mojo/helpers/response.py +6 -2
- mojo/helpers/settings/__init__.py +2 -0
- mojo/helpers/{settings.py → settings/helper.py} +1 -37
- mojo/helpers/settings/parser.py +132 -0
- mojo/middleware/logging.py +1 -1
- mojo/middleware/mojo.py +5 -0
- mojo/models/rest.py +261 -46
- mojo/models/secrets.py +13 -4
- mojo/serializers/__init__.py +100 -0
- mojo/serializers/advanced/README.md +363 -0
- mojo/serializers/advanced/__init__.py +247 -0
- mojo/serializers/advanced/formats/__init__.py +28 -0
- mojo/serializers/advanced/formats/csv.py +416 -0
- mojo/serializers/advanced/formats/excel.py +516 -0
- mojo/serializers/advanced/formats/json.py +239 -0
- mojo/serializers/advanced/formats/localizers.py +509 -0
- mojo/serializers/advanced/formats/response.py +485 -0
- mojo/serializers/advanced/serializer.py +568 -0
- mojo/serializers/manager.py +501 -0
- mojo/serializers/optimized.py +618 -0
- mojo/serializers/settings_example.py +322 -0
- mojo/serializers/{models.py → simple.py} +38 -15
- testit/helpers.py +21 -4
- django_nativemojo-0.1.10.dist-info/METADATA +0 -96
- mojo/apps/metrics/rest/db.py +0 -0
- mojo/helpers/aws/setup_email.py +0 -0
- mojo/ws4redis/README.md +0 -174
- mojo/ws4redis/__init__.py +0 -2
- mojo/ws4redis/client.py +0 -283
- mojo/ws4redis/connection.py +0 -327
- mojo/ws4redis/exceptions.py +0 -32
- mojo/ws4redis/redis.py +0 -183
- mojo/ws4redis/servers/base.py +0 -86
- mojo/ws4redis/servers/django.py +0 -171
- mojo/ws4redis/servers/uwsgi.py +0 -63
- mojo/ws4redis/settings.py +0 -45
- mojo/ws4redis/utf8validator.py +0 -128
- mojo/ws4redis/websocket.py +0 -403
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/LICENSE +0 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/NOTICE +0 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/WHEEL +0 -0
- /mojo/{ws4redis/servers → apps/aws}/__init__.py +0 -0
- /mojo/apps/{fileman/models/render.py → aws/models/__init__.py} +0 -0
- /mojo/apps/fileman/{rest/__init__ → migrations/__init__.py} +0 -0
mojo/helpers/aws/s3.py
CHANGED
@@ -1,23 +1,22 @@
|
|
1
|
-
from
|
1
|
+
from mojo.helpers.settings import settings
|
2
|
+
from mojo.helpers import logit
|
2
3
|
from objict import objict
|
3
4
|
import boto3
|
4
5
|
import botocore
|
5
6
|
from urllib.parse import urlparse
|
6
7
|
import io
|
7
|
-
import sys
|
8
|
-
from medialib import utils
|
9
|
-
import threading
|
10
8
|
import tempfile
|
11
|
-
import
|
12
|
-
from typing import Optional, Union, BinaryIO, Dict, List, Any
|
9
|
+
import json
|
10
|
+
from typing import Optional, Union, BinaryIO, Dict, List, Any, Tuple
|
13
11
|
|
14
|
-
logger =
|
12
|
+
logger = logit.get_logger(__name__)
|
15
13
|
|
16
14
|
class S3Config:
|
17
15
|
"""S3 configuration holder with lazy initialization of clients and resources."""
|
18
|
-
def __init__(self, key: str, secret: str):
|
16
|
+
def __init__(self, key: str, secret: str, region_name: str):
|
19
17
|
self.key = key
|
20
18
|
self.secret = secret
|
19
|
+
self.region_name = region_name
|
21
20
|
self._resource = None
|
22
21
|
self._client = None
|
23
22
|
|
@@ -26,7 +25,8 @@ class S3Config:
|
|
26
25
|
if self._resource is None:
|
27
26
|
self._resource = boto3.resource('s3',
|
28
27
|
aws_access_key_id=self.key,
|
29
|
-
aws_secret_access_key=self.secret
|
28
|
+
aws_secret_access_key=self.secret,
|
29
|
+
region_name=self.region_name)
|
30
30
|
return self._resource
|
31
31
|
|
32
32
|
@property
|
@@ -34,11 +34,36 @@ class S3Config:
|
|
34
34
|
if self._client is None:
|
35
35
|
self._client = boto3.client('s3',
|
36
36
|
aws_access_key_id=self.key,
|
37
|
-
aws_secret_access_key=self.secret
|
37
|
+
aws_secret_access_key=self.secret,
|
38
|
+
region_name=self.region_name)
|
38
39
|
return self._client
|
39
40
|
|
41
|
+
@staticmethod
|
42
|
+
def get_bucket(bucket_name):
|
43
|
+
if not bucket_name:
|
44
|
+
raise ValueError("Bucket name cannot be empty")
|
45
|
+
return S3.resource.Bucket(bucket_name)
|
46
|
+
|
47
|
+
@staticmethod
|
48
|
+
def list_all_buckets():
|
49
|
+
"""
|
50
|
+
List all S3 buckets.
|
51
|
+
|
52
|
+
Returns:
|
53
|
+
List of bucket names
|
54
|
+
"""
|
55
|
+
try:
|
56
|
+
response = S3.client.list_buckets()
|
57
|
+
return [
|
58
|
+
{"id": b["Name"], "name": b["Name"], "created": b["CreationDate"].timestamp()}
|
59
|
+
for b in response.get("Buckets", [])
|
60
|
+
]
|
61
|
+
except botocore.exceptions.ClientError as e:
|
62
|
+
logger.error(f"Failed to list buckets: {e}")
|
63
|
+
return []
|
64
|
+
|
40
65
|
# Initialize the global S3 configuration
|
41
|
-
S3 = S3Config(settings.AWS_KEY, settings.AWS_SECRET)
|
66
|
+
S3 = S3Config(settings.AWS_KEY, settings.AWS_SECRET, settings.AWS_REGION)
|
42
67
|
|
43
68
|
|
44
69
|
class S3Item:
|
@@ -266,3 +291,418 @@ def delete(url: str) -> None:
|
|
266
291
|
def path(url: str) -> str:
|
267
292
|
"""Extract the path component from a URL."""
|
268
293
|
return urlparse(url).path
|
294
|
+
|
295
|
+
|
296
|
+
class S3Bucket:
|
297
|
+
"""
|
298
|
+
Simple interface for S3 bucket management.
|
299
|
+
|
300
|
+
This class provides methods to create, configure, and manage S3 buckets
|
301
|
+
with sensible defaults.
|
302
|
+
"""
|
303
|
+
|
304
|
+
def __init__(self, name: str):
|
305
|
+
"""
|
306
|
+
Initialize a bucket manager for the specified bucket.
|
307
|
+
|
308
|
+
Args:
|
309
|
+
name: The name of the S3 bucket
|
310
|
+
"""
|
311
|
+
self.name = name
|
312
|
+
self.exists = self._check_exists()
|
313
|
+
|
314
|
+
def _check_exists(self) -> bool:
|
315
|
+
"""Check if the bucket exists."""
|
316
|
+
try:
|
317
|
+
S3.client.head_bucket(Bucket=self.name)
|
318
|
+
return True
|
319
|
+
except botocore.exceptions.ClientError as e:
|
320
|
+
if e.response['Error']['Code'] == '404':
|
321
|
+
return False
|
322
|
+
# If it's a different error (e.g., 403 forbidden), still return False
|
323
|
+
# but log the error
|
324
|
+
logger.warning(f"Error checking bucket existence: {e}")
|
325
|
+
return False
|
326
|
+
|
327
|
+
def create(self, region: Optional[str] = None, public_access: bool = False) -> bool:
|
328
|
+
"""
|
329
|
+
Create the S3 bucket with optional configuration.
|
330
|
+
|
331
|
+
Args:
|
332
|
+
region: AWS region for the bucket. If None, uses configured region.
|
333
|
+
public_access: Whether to allow public access to the bucket.
|
334
|
+
|
335
|
+
Returns:
|
336
|
+
True if bucket was created, False if it already exists
|
337
|
+
"""
|
338
|
+
if self.exists:
|
339
|
+
logger.info(f"Bucket {self.name} already exists")
|
340
|
+
return False
|
341
|
+
|
342
|
+
# Use configured region if none specified
|
343
|
+
if region is None:
|
344
|
+
region = S3.region_name
|
345
|
+
|
346
|
+
create_params = {'Bucket': self.name}
|
347
|
+
|
348
|
+
# Add region configuration if specified
|
349
|
+
if region and region != 'us-east-1':
|
350
|
+
create_params['CreateBucketConfiguration'] = {
|
351
|
+
'LocationConstraint': region
|
352
|
+
}
|
353
|
+
|
354
|
+
try:
|
355
|
+
S3.client.create_bucket(**create_params)
|
356
|
+
self.exists = True
|
357
|
+
|
358
|
+
# Configure public access blocking based on the public_access parameter
|
359
|
+
if not public_access:
|
360
|
+
S3.client.put_public_access_block(
|
361
|
+
Bucket=self.name,
|
362
|
+
PublicAccessBlockConfiguration={
|
363
|
+
'BlockPublicAcls': True,
|
364
|
+
'IgnorePublicAcls': True,
|
365
|
+
'BlockPublicPolicy': True,
|
366
|
+
'RestrictPublicBuckets': True
|
367
|
+
}
|
368
|
+
)
|
369
|
+
return True
|
370
|
+
except botocore.exceptions.ClientError as e:
|
371
|
+
logger.error(f"Failed to create bucket {self.name}: {e}")
|
372
|
+
return False
|
373
|
+
|
374
|
+
def delete(self, force: bool = False) -> bool:
|
375
|
+
"""
|
376
|
+
Delete the bucket.
|
377
|
+
|
378
|
+
Args:
|
379
|
+
force: If True, delete all objects in the bucket first
|
380
|
+
|
381
|
+
Returns:
|
382
|
+
True if successfully deleted, False otherwise
|
383
|
+
"""
|
384
|
+
if not self.exists:
|
385
|
+
logger.info(f"Bucket {self.name} does not exist")
|
386
|
+
return False
|
387
|
+
|
388
|
+
try:
|
389
|
+
if force:
|
390
|
+
self.delete_all_objects()
|
391
|
+
|
392
|
+
S3.client.delete_bucket(Bucket=self.name)
|
393
|
+
self.exists = False
|
394
|
+
return True
|
395
|
+
except botocore.exceptions.ClientError as e:
|
396
|
+
logger.error(f"Failed to delete bucket {self.name}: {e}")
|
397
|
+
return False
|
398
|
+
|
399
|
+
def delete_all_objects(self) -> int:
|
400
|
+
"""
|
401
|
+
Delete all objects in the bucket.
|
402
|
+
|
403
|
+
Returns:
|
404
|
+
Number of objects deleted
|
405
|
+
"""
|
406
|
+
count = 0
|
407
|
+
try:
|
408
|
+
# List objects in the bucket
|
409
|
+
paginator = S3.client.get_paginator('list_objects_v2')
|
410
|
+
|
411
|
+
for page in paginator.paginate(Bucket=self.name):
|
412
|
+
if 'Contents' not in page:
|
413
|
+
continue
|
414
|
+
|
415
|
+
# Delete objects in batches of 1000 (S3 API limit)
|
416
|
+
objects = [{'Key': obj['Key']} for obj in page['Contents']]
|
417
|
+
if objects:
|
418
|
+
S3.client.delete_objects(
|
419
|
+
Bucket=self.name,
|
420
|
+
Delete={'Objects': objects}
|
421
|
+
)
|
422
|
+
count += len(objects)
|
423
|
+
|
424
|
+
return count
|
425
|
+
except botocore.exceptions.ClientError as e:
|
426
|
+
logger.error(f"Failed to delete objects in bucket {self.name}: {e}")
|
427
|
+
return count
|
428
|
+
|
429
|
+
def set_policy(self, policy: Union[Dict, str]) -> bool:
|
430
|
+
"""
|
431
|
+
Set a bucket policy.
|
432
|
+
|
433
|
+
Args:
|
434
|
+
policy: Policy document as dict or JSON string
|
435
|
+
|
436
|
+
Returns:
|
437
|
+
True if successful, False otherwise
|
438
|
+
"""
|
439
|
+
if not self.exists:
|
440
|
+
logger.warning(f"Bucket {self.name} does not exist")
|
441
|
+
return False
|
442
|
+
|
443
|
+
try:
|
444
|
+
# Convert dict to JSON string if needed
|
445
|
+
policy_str = policy if isinstance(policy, str) else json.dumps(policy)
|
446
|
+
|
447
|
+
S3.client.put_bucket_policy(
|
448
|
+
Bucket=self.name,
|
449
|
+
Policy=policy_str
|
450
|
+
)
|
451
|
+
return True
|
452
|
+
except botocore.exceptions.ClientError as e:
|
453
|
+
logger.error(f"Failed to set policy for bucket {self.name}: {e}")
|
454
|
+
return False
|
455
|
+
|
456
|
+
def make_public(self) -> bool:
|
457
|
+
"""
|
458
|
+
Make the bucket publicly readable.
|
459
|
+
|
460
|
+
Returns:
|
461
|
+
True if successful, False otherwise
|
462
|
+
"""
|
463
|
+
if not self.exists:
|
464
|
+
logger.warning(f"Bucket {self.name} does not exist")
|
465
|
+
return False
|
466
|
+
|
467
|
+
try:
|
468
|
+
# Remove public access block
|
469
|
+
S3.client.put_public_access_block(
|
470
|
+
Bucket=self.name,
|
471
|
+
PublicAccessBlockConfiguration={
|
472
|
+
'BlockPublicAcls': False,
|
473
|
+
'IgnorePublicAcls': False,
|
474
|
+
'BlockPublicPolicy': False,
|
475
|
+
'RestrictPublicBuckets': False
|
476
|
+
}
|
477
|
+
)
|
478
|
+
|
479
|
+
# Set public read policy
|
480
|
+
policy = {
|
481
|
+
"Version": "2012-10-17",
|
482
|
+
"Statement": [
|
483
|
+
{
|
484
|
+
"Sid": "PublicReadGetObject",
|
485
|
+
"Effect": "Allow",
|
486
|
+
"Principal": "*",
|
487
|
+
"Action": "s3:GetObject",
|
488
|
+
"Resource": f"arn:aws:s3:::{self.name}/*"
|
489
|
+
}
|
490
|
+
]
|
491
|
+
}
|
492
|
+
|
493
|
+
return self.set_policy(policy)
|
494
|
+
except botocore.exceptions.ClientError as e:
|
495
|
+
logger.error(f"Failed to make bucket {self.name} public: {e}")
|
496
|
+
return False
|
497
|
+
|
498
|
+
def enable_website(self, index_document: str = 'index.html',
|
499
|
+
error_document: Optional[str] = None) -> bool:
|
500
|
+
"""
|
501
|
+
Configure the bucket for static website hosting.
|
502
|
+
|
503
|
+
Args:
|
504
|
+
index_document: Default index document
|
505
|
+
error_document: Custom error document
|
506
|
+
|
507
|
+
Returns:
|
508
|
+
True if successful, False otherwise
|
509
|
+
"""
|
510
|
+
if not self.exists:
|
511
|
+
logger.warning(f"Bucket {self.name} does not exist")
|
512
|
+
return False
|
513
|
+
|
514
|
+
try:
|
515
|
+
website_config = {
|
516
|
+
'IndexDocument': {'Suffix': index_document}
|
517
|
+
}
|
518
|
+
|
519
|
+
if error_document:
|
520
|
+
website_config['ErrorDocument'] = {'Key': error_document}
|
521
|
+
|
522
|
+
S3.client.put_bucket_website(
|
523
|
+
Bucket=self.name,
|
524
|
+
WebsiteConfiguration=website_config
|
525
|
+
)
|
526
|
+
return True
|
527
|
+
except botocore.exceptions.ClientError as e:
|
528
|
+
logger.error(f"Failed to configure website for bucket {self.name}: {e}")
|
529
|
+
return False
|
530
|
+
|
531
|
+
def get_website_url(self) -> str:
|
532
|
+
"""
|
533
|
+
Get the website URL for this bucket.
|
534
|
+
|
535
|
+
Returns:
|
536
|
+
The website URL
|
537
|
+
"""
|
538
|
+
region = S3.region_name
|
539
|
+
|
540
|
+
# Special URL format for us-east-1
|
541
|
+
if region == 'us-east-1':
|
542
|
+
return f"http://{self.name}.s3-website-{region}.amazonaws.com"
|
543
|
+
else:
|
544
|
+
return f"http://{self.name}.s3-website.{region}.amazonaws.com"
|
545
|
+
|
546
|
+
def list_objects(self, prefix: str = '', max_keys: int = 1000) -> List[Dict]:
|
547
|
+
"""
|
548
|
+
List objects in the bucket.
|
549
|
+
|
550
|
+
Args:
|
551
|
+
prefix: Filter objects by prefix
|
552
|
+
max_keys: Maximum number of keys to return
|
553
|
+
|
554
|
+
Returns:
|
555
|
+
List of object metadata dictionaries
|
556
|
+
"""
|
557
|
+
if not self.exists:
|
558
|
+
logger.warning(f"Bucket {self.name} does not exist")
|
559
|
+
return []
|
560
|
+
|
561
|
+
try:
|
562
|
+
response = S3.client.list_objects_v2(
|
563
|
+
Bucket=self.name,
|
564
|
+
Prefix=prefix,
|
565
|
+
MaxKeys=max_keys
|
566
|
+
)
|
567
|
+
|
568
|
+
return response.get('Contents', [])
|
569
|
+
except botocore.exceptions.ClientError as e:
|
570
|
+
logger.error(f"Failed to list objects in bucket {self.name}: {e}")
|
571
|
+
return []
|
572
|
+
|
573
|
+
def enable_versioning(self) -> bool:
|
574
|
+
"""
|
575
|
+
Enable versioning on the bucket.
|
576
|
+
|
577
|
+
Returns:
|
578
|
+
True if successful, False otherwise
|
579
|
+
"""
|
580
|
+
if not self.exists:
|
581
|
+
logger.warning(f"Bucket {self.name} does not exist")
|
582
|
+
return False
|
583
|
+
|
584
|
+
try:
|
585
|
+
S3.client.put_bucket_versioning(
|
586
|
+
Bucket=self.name,
|
587
|
+
VersioningConfiguration={'Status': 'Enabled'}
|
588
|
+
)
|
589
|
+
return True
|
590
|
+
except botocore.exceptions.ClientError as e:
|
591
|
+
logger.error(f"Failed to enable versioning for bucket {self.name}: {e}")
|
592
|
+
return False
|
593
|
+
|
594
|
+
|
595
|
+
def make_path_public(self, prefix: str):
|
596
|
+
# Get the current bucket policy (if any)
|
597
|
+
try:
|
598
|
+
current_policy = json.loads(S3.client.get_bucket_policy(Bucket=self.name)["Policy"])
|
599
|
+
statements = current_policy.get("Statement", [])
|
600
|
+
except S3.client.exceptions.from_code('NoSuchBucketPolicy'):
|
601
|
+
current_policy = {"Version": "2012-10-17", "Statement": []}
|
602
|
+
statements = []
|
603
|
+
|
604
|
+
# Check if our public-read rule for the prefix already exists
|
605
|
+
public_read_sid = f"AllowPublicReadForPrefix_{prefix.strip('/')}"
|
606
|
+
already_exists = any(
|
607
|
+
stmt.get("Sid") == public_read_sid for stmt in statements
|
608
|
+
)
|
609
|
+
|
610
|
+
if already_exists:
|
611
|
+
print(f"Policy for prefix '{prefix}' already exists.")
|
612
|
+
return
|
613
|
+
|
614
|
+
# Construct the public read statement for the given prefix
|
615
|
+
new_statement = {
|
616
|
+
"Sid": public_read_sid,
|
617
|
+
"Effect": "Allow",
|
618
|
+
"Principal": "*",
|
619
|
+
"Action": "s3:GetObject",
|
620
|
+
"Resource": f"arn:aws:s3:::{self.name}/{prefix}*"
|
621
|
+
}
|
622
|
+
|
623
|
+
# Add and apply the new policy
|
624
|
+
current_policy["Statement"].append(new_statement)
|
625
|
+
S3.client.put_bucket_policy(
|
626
|
+
Bucket=self.name,
|
627
|
+
Policy=json.dumps(current_policy)
|
628
|
+
)
|
629
|
+
|
630
|
+
def make_private(self):
|
631
|
+
try:
|
632
|
+
S3.client.put_public_access_block(
|
633
|
+
Bucket=self.name,
|
634
|
+
PublicAccessBlockConfiguration={
|
635
|
+
'BlockPublicAcls': True,
|
636
|
+
'IgnorePublicAcls': True,
|
637
|
+
'BlockPublicPolicy': True,
|
638
|
+
'RestrictPublicBuckets': True
|
639
|
+
}
|
640
|
+
)
|
641
|
+
return True
|
642
|
+
except botocore.exceptions.ClientError as e:
|
643
|
+
logger.error(f"Failed to make bucket {self.name} private: {e}")
|
644
|
+
return False
|
645
|
+
|
646
|
+
|
647
|
+
def enable_cors(self):
|
648
|
+
try:
|
649
|
+
S3.client.put_bucket_cors(
|
650
|
+
Bucket=self.name,
|
651
|
+
CORSConfiguration={
|
652
|
+
'CORSRules': [
|
653
|
+
{
|
654
|
+
'AllowedHeaders': ['*'],
|
655
|
+
'AllowedMethods': ['GET', 'HEAD', 'PUT', 'POST', 'DELETE'],
|
656
|
+
'AllowedOrigins': ['*'],
|
657
|
+
'ExposeHeaders': ['ETag', 'x-amz-version-id'],
|
658
|
+
'MaxAgeSeconds': 3000
|
659
|
+
}
|
660
|
+
]
|
661
|
+
}
|
662
|
+
)
|
663
|
+
return True
|
664
|
+
except botocore.exceptions.ClientError as e:
|
665
|
+
logger.error(f"Failed to enable CORS for bucket {self.name}: {e}")
|
666
|
+
return False
|
667
|
+
|
668
|
+
def enable_lifecycle(self):
|
669
|
+
try:
|
670
|
+
S3.client.put_bucket_lifecycle_configuration(
|
671
|
+
Bucket=self.name,
|
672
|
+
LifecycleConfiguration={
|
673
|
+
'Rules': [
|
674
|
+
{
|
675
|
+
'Expiration': {'Days': 30},
|
676
|
+
'ID': 'DeleteAfter30Days',
|
677
|
+
'Filter': {'Prefix': 'logs/'},
|
678
|
+
'Status': 'Enabled'
|
679
|
+
}
|
680
|
+
]
|
681
|
+
}
|
682
|
+
)
|
683
|
+
return True
|
684
|
+
except botocore.exceptions.ClientError as e:
|
685
|
+
logger.error(f"Failed to enable lifecycle for bucket {self.name}: {e}")
|
686
|
+
return False
|
687
|
+
|
688
|
+
def get_url(self, key: str, presigned: bool = False,
|
689
|
+
expires: int = 3600) -> str:
|
690
|
+
"""
|
691
|
+
Get a URL for an object in the bucket.
|
692
|
+
|
693
|
+
Args:
|
694
|
+
key: Object key
|
695
|
+
presigned: Whether to generate a presigned URL
|
696
|
+
expires: Expiration time in seconds for presigned URLs
|
697
|
+
|
698
|
+
Returns:
|
699
|
+
URL for the object
|
700
|
+
"""
|
701
|
+
if presigned:
|
702
|
+
return S3.client.generate_presigned_url(
|
703
|
+
'get_object',
|
704
|
+
Params={'Bucket': self.name, 'Key': key},
|
705
|
+
ExpiresIn=expires
|
706
|
+
)
|
707
|
+
else:
|
708
|
+
return f"https://{self.name}.s3.amazonaws.com/{key}"
|