Decided to keep python based, added S3 support
This commit is contained in:
parent
c7586d1aa1
commit
c752138fb0
9 changed files with 293 additions and 37 deletions
92
storage.py
Normal file
92
storage.py
Normal file
|
@ -0,0 +1,92 @@
|
|||
import os
|
||||
import fsspec
|
||||
import asyncio
|
||||
from typing import BinaryIO, Optional, Union
|
||||
from urllib.parse import urlparse
|
||||
from flask import current_app
|
||||
from werkzeug.datastructures import FileStorage
|
||||
|
||||
class StorageBackend:
|
||||
def __init__(self, storage_url: str):
|
||||
"""
|
||||
Initialize storage backend with a URL.
|
||||
Examples:
|
||||
- file:///path/to/storage (local filesystem)
|
||||
- s3://bucket-name/path (S3 compatible)
|
||||
"""
|
||||
self.storage_url = storage_url
|
||||
self.parsed_url = urlparse(storage_url)
|
||||
self.protocol = self.parsed_url.scheme or 'file'
|
||||
|
||||
# Configure filesystem
|
||||
if self.protocol == 's3':
|
||||
self.fs = fsspec.filesystem(
|
||||
's3',
|
||||
key=os.getenv('S3_ACCESS_KEY'),
|
||||
secret=os.getenv('S3_SECRET_KEY'),
|
||||
endpoint_url=os.getenv('S3_ENDPOINT_URL'),
|
||||
client_kwargs={
|
||||
'endpoint_url': os.getenv('S3_ENDPOINT_URL')
|
||||
} if os.getenv('S3_ENDPOINT_URL') else None
|
||||
)
|
||||
self.bucket = self.parsed_url.netloc
|
||||
self.base_path = self.parsed_url.path.lstrip('/')
|
||||
else:
|
||||
self.fs = fsspec.filesystem('file')
|
||||
self.base_path = self.parsed_url.path or '/uploads'
|
||||
|
||||
def _get_full_path(self, filename: str) -> str:
|
||||
"""Get full path for a file"""
|
||||
if self.protocol == 's3':
|
||||
return os.path.join(self.base_path, filename)
|
||||
return os.path.join(current_app.root_path, self.base_path, filename)
|
||||
|
||||
def save(self, file_storage: FileStorage, filename: str) -> str:
|
||||
"""Save a file to storage"""
|
||||
full_path = self._get_full_path(filename)
|
||||
|
||||
if self.protocol == 's3':
|
||||
with self.fs.open(f"{self.bucket}/{full_path}", 'wb') as f:
|
||||
file_storage.save(f)
|
||||
return f"s3://{self.bucket}/{full_path}"
|
||||
else:
|
||||
os.makedirs(os.path.dirname(full_path), exist_ok=True)
|
||||
file_storage.save(full_path)
|
||||
return f"file://{full_path}"
|
||||
|
||||
def open(self, filename: str, mode: str = 'rb') -> BinaryIO:
|
||||
"""Open a file from storage"""
|
||||
full_path = self._get_full_path(filename)
|
||||
if self.protocol == 's3':
|
||||
return self.fs.open(f"{self.bucket}/{full_path}", mode)
|
||||
return self.fs.open(full_path, mode)
|
||||
|
||||
def delete(self, filename: str) -> None:
|
||||
"""Delete a file from storage"""
|
||||
full_path = self._get_full_path(filename)
|
||||
if self.protocol == 's3':
|
||||
self.fs.delete(f"{self.bucket}/{full_path}")
|
||||
else:
|
||||
self.fs.delete(full_path)
|
||||
|
||||
def url_for(self, filename: str) -> str:
|
||||
"""Get URL for a file"""
|
||||
if self.protocol == 's3':
|
||||
full_path = self._get_full_path(filename)
|
||||
if os.getenv('S3_PUBLIC_URL'):
|
||||
# For public buckets with a custom domain
|
||||
return f"{os.getenv('S3_PUBLIC_URL')}/{full_path}"
|
||||
elif os.getenv('S3_ENDPOINT_URL'):
|
||||
# For MinIO/S3, construct direct URL
|
||||
endpoint = os.getenv('S3_ENDPOINT_URL').rstrip('/')
|
||||
return f"{endpoint}/{self.bucket}/{full_path}"
|
||||
return f"s3://{self.bucket}/{full_path}"
|
||||
else:
|
||||
return f"/uploads/{filename}"
|
||||
|
||||
def exists(self, filename: str) -> bool:
|
||||
"""Check if a file exists"""
|
||||
full_path = self._get_full_path(filename)
|
||||
if self.protocol == 's3':
|
||||
return self.fs.exists(f"{self.bucket}/{full_path}")
|
||||
return self.fs.exists(full_path)
|
Loading…
Add table
Add a link
Reference in a new issue