feat(storage): support cloud storage
This commit is contained in:
78
app/storage/local.py
Normal file
78
app/storage/local.py
Normal file
@@ -0,0 +1,78 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from .base import StorageService
|
||||
|
||||
import aiofiles
|
||||
|
||||
|
||||
class LocalStorageService(StorageService):
|
||||
def __init__(
|
||||
self,
|
||||
storage_path: str,
|
||||
):
|
||||
self.storage_path = Path(storage_path).resolve()
|
||||
self.storage_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def _get_file_path(self, file_path: str) -> Path:
|
||||
clean_path = file_path.lstrip("/")
|
||||
full_path = self.storage_path / clean_path
|
||||
|
||||
try:
|
||||
full_path.resolve().relative_to(self.storage_path)
|
||||
except ValueError:
|
||||
raise ValueError(f"Invalid file path: {file_path}")
|
||||
|
||||
return full_path
|
||||
|
||||
async def write_file(
|
||||
self,
|
||||
file_path: str,
|
||||
content: bytes,
|
||||
content_type: str = "application/octet-stream",
|
||||
cache_control: str = "public, max-age=31536000",
|
||||
) -> None:
|
||||
full_path = self._get_file_path(file_path)
|
||||
full_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
try:
|
||||
async with aiofiles.open(full_path, "wb") as f:
|
||||
await f.write(content)
|
||||
except OSError as e:
|
||||
raise RuntimeError(f"Failed to write file: {e}")
|
||||
|
||||
async def read_file(self, file_path: str) -> bytes:
|
||||
full_path = self._get_file_path(file_path)
|
||||
|
||||
if not full_path.exists():
|
||||
raise FileNotFoundError(f"File not found: {file_path}")
|
||||
|
||||
try:
|
||||
async with aiofiles.open(full_path, "rb") as f:
|
||||
return await f.read()
|
||||
except OSError as e:
|
||||
raise RuntimeError(f"Failed to read file: {e}")
|
||||
|
||||
async def delete_file(self, file_path: str) -> None:
|
||||
full_path = self._get_file_path(file_path)
|
||||
|
||||
if not full_path.exists():
|
||||
return
|
||||
|
||||
try:
|
||||
full_path.unlink()
|
||||
|
||||
parent = full_path.parent
|
||||
while parent != self.storage_path and not any(parent.iterdir()):
|
||||
parent.rmdir()
|
||||
parent = parent.parent
|
||||
except OSError as e:
|
||||
raise RuntimeError(f"Failed to delete file: {e}")
|
||||
|
||||
async def is_exists(self, file_path: str) -> bool:
|
||||
full_path = self._get_file_path(file_path)
|
||||
return full_path.exists() and full_path.is_file()
|
||||
|
||||
async def get_file_url(self, file_path: str) -> str:
|
||||
return str(self.storage_path / file_path)
|
||||
Reference in New Issue
Block a user