Files

233 lines
8.1 KiB
Python
Raw Permalink Normal View History

2025-12-30 17:37:14 +03:00
import hashlib
import urllib3
from pathlib import Path
from typing import Optional
import boto3
from botocore.config import Config as BotoConfig
from botocore.exceptions import ClientError
from .config import settings
# Suppress SSL warnings for S3 endpoint
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
class S3Storage:
"""S3-compatible storage service for media files."""
def __init__(self):
self.client = boto3.client(
"s3",
endpoint_url=settings.s3_endpoint,
aws_access_key_id=settings.s3_access_key,
aws_secret_access_key=settings.s3_secret_key,
region_name=settings.s3_region,
config=BotoConfig(signature_version="s3v4"),
verify=False, # Disable SSL verification for FirstVDS S3
)
self.bucket = settings.s3_bucket
self.cache_path = settings.cache_path
self._ensure_bucket_exists()
def _ensure_bucket_exists(self):
"""Create bucket if it doesn't exist."""
try:
self.client.head_bucket(Bucket=self.bucket)
except ClientError as e:
error_code = e.response.get("Error", {}).get("Code")
if error_code in ("404", "NoSuchBucket"):
try:
self.client.create_bucket(Bucket=self.bucket)
print(f"Created S3 bucket: {self.bucket}")
except ClientError as create_error:
print(f"Failed to create bucket {self.bucket}: {create_error}")
def _get_cache_path(self, key: str) -> Path:
"""Get local cache path for a file."""
# Use hash of key for cache filename to avoid path issues
key_hash = hashlib.md5(key.encode()).hexdigest()[:16]
ext = Path(key).suffix
return self.cache_path / f"{key_hash}{ext}"
def list_files(self, prefix: str, extensions: Optional[list[str]] = None) -> list[str]:
"""List files in S3 bucket with given prefix."""
try:
response = self.client.list_objects_v2(
Bucket=self.bucket,
Prefix=prefix,
)
files = []
for obj in response.get("Contents", []):
key = obj["Key"]
filename = key.replace(prefix, "").lstrip("/")
if filename: # Skip the prefix itself
if extensions:
if any(filename.lower().endswith(ext) for ext in extensions):
files.append(filename)
else:
files.append(filename)
return sorted(files)
except ClientError as e:
print(f"Error listing S3 files: {e}")
return []
def download_file(self, key: str) -> Optional[Path]:
"""Download file from S3 to local cache."""
cache_file = self._get_cache_path(key)
# Return cached file if exists
if cache_file.exists():
return cache_file
try:
self.client.download_file(
Bucket=self.bucket,
Key=key,
Filename=str(cache_file),
)
return cache_file
except ClientError as e:
print(f"Error downloading {key} from S3: {e}")
return None
def get_audio_file(self, filename: str) -> Optional[Path]:
"""Download audio file from S3."""
key = f"audio/{filename}"
return self.download_file(key)
def get_background_file(self, filename: str) -> Optional[Path]:
"""Download background video from S3."""
key = f"backgrounds/{filename}"
return self.download_file(key)
def get_poster_file(self, filename: str) -> Optional[Path]:
"""Download poster image from S3."""
key = f"posters/{filename}"
return self.download_file(key)
def get_transition_file(self, filename: str) -> Optional[Path]:
"""Download transition sound from S3."""
key = f"transitions/{filename}"
return self.download_file(key)
def list_audio_files(self) -> list[str]:
"""List available audio files."""
return self.list_files("audio/", [".mp3", ".wav", ".ogg", ".m4a", ".webm"])
2025-12-30 17:37:14 +03:00
def list_background_videos(self) -> list[str]:
"""List available background videos."""
return self.list_files("backgrounds/", [".mp4", ".mov", ".avi"])
def list_posters(self) -> list[str]:
"""List available poster images."""
return self.list_files("posters/", [".jpg", ".jpeg", ".png", ".webp"])
def list_transition_sounds(self) -> list[str]:
"""List available transition sounds."""
return self.list_files("transitions/", [".mp3", ".wav", ".ogg"])
def file_exists(self, key: str) -> bool:
"""Check if file exists in S3."""
try:
self.client.head_object(Bucket=self.bucket, Key=key)
return True
except ClientError:
return False
def clear_cache(self):
"""Clear local cache."""
for file in self.cache_path.glob("*"):
try:
file.unlink()
except Exception:
pass
def upload_file(self, key: str, file_data: bytes, content_type: str = None) -> bool:
"""Upload file to S3."""
try:
extra_args = {}
if content_type:
extra_args["ContentType"] = content_type
self.client.put_object(
Bucket=self.bucket,
Key=key,
Body=file_data,
**extra_args
)
return True
except ClientError as e:
print(f"Error uploading {key} to S3: {e}")
return False
def upload_audio(self, filename: str, file_data: bytes) -> bool:
"""Upload audio file to S3."""
ext = filename.lower().split(".")[-1]
content_types = {
"mp3": "audio/mpeg",
"wav": "audio/wav",
"ogg": "audio/ogg",
"m4a": "audio/mp4",
"webm": "video/webm",
}
content_type = content_types.get(ext, "audio/mpeg")
2025-12-30 17:37:14 +03:00
return self.upload_file(f"audio/{filename}", file_data, content_type)
def upload_background(self, filename: str, file_data: bytes) -> bool:
"""Upload background video to S3."""
return self.upload_file(f"backgrounds/{filename}", file_data, "video/mp4")
def upload_poster(self, filename: str, file_data: bytes) -> bool:
"""Upload poster image to S3."""
ext = filename.lower().split(".")[-1]
content_types = {
"jpg": "image/jpeg",
"jpeg": "image/jpeg",
"png": "image/png",
"webp": "image/webp"
}
content_type = content_types.get(ext, "application/octet-stream")
return self.upload_file(f"posters/{filename}", file_data, content_type)
def upload_transition(self, filename: str, file_data: bytes) -> bool:
"""Upload transition sound to S3."""
return self.upload_file(f"transitions/{filename}", file_data, "audio/mpeg")
def delete_file(self, key: str) -> bool:
"""Delete file from S3."""
try:
self.client.delete_object(Bucket=self.bucket, Key=key)
# Also remove from cache
cache_file = self._get_cache_path(key)
if cache_file.exists():
cache_file.unlink()
return True
except ClientError as e:
print(f"Error deleting {key} from S3: {e}")
return False
def delete_audio(self, filename: str) -> bool:
"""Delete audio file from S3."""
return self.delete_file(f"audio/{filename}")
def delete_background(self, filename: str) -> bool:
"""Delete background video from S3."""
return self.delete_file(f"backgrounds/{filename}")
def delete_poster(self, filename: str) -> bool:
"""Delete poster image from S3."""
return self.delete_file(f"posters/{filename}")
def delete_transition(self, filename: str) -> bool:
"""Delete transition sound from S3."""
return self.delete_file(f"transitions/{filename}")
# Global storage instance
storage = S3Storage()