103 lines
3.7 KiB
Python
103 lines
3.7 KiB
Python
from functools import lru_cache
|
|
from pathlib import Path
|
|
import hashlib
|
|
import mimetypes
|
|
import re
|
|
import uuid
|
|
|
|
from app.core.config import settings
|
|
from app.storage.base import StorageProvider
|
|
from app.storage.exceptions import StorageConfigurationError, StorageValidationError
|
|
from app.storage.local import LocalDiskStorageProvider
|
|
from app.storage.schemas import FileMetadata, StoredFileMetadata
|
|
|
|
ALLOWED_EXTENSIONS = {".pdf", ".jpg", ".jpeg", ".png", ".webp", ".txt", ".zip"}
|
|
ALLOWED_MIME_TYPES = {
|
|
"application/pdf",
|
|
"image/jpeg",
|
|
"image/png",
|
|
"image/webp",
|
|
"text/plain",
|
|
"application/zip",
|
|
"application/x-zip-compressed",
|
|
}
|
|
|
|
|
|
class StorageService:
|
|
def __init__(self, provider: StorageProvider, *, max_upload_mb: int) -> None:
|
|
self.provider = provider
|
|
self.max_upload_mb = max(1, max_upload_mb)
|
|
|
|
def save_file(
|
|
self,
|
|
*,
|
|
namespace: str,
|
|
content: bytes,
|
|
original_filename: str,
|
|
mime_type: str | None = None,
|
|
) -> StoredFileMetadata:
|
|
self.validate_file_size(len(content))
|
|
safe_name = self.safe_filename(original_filename)
|
|
resolved_mime_type = mime_type or mimetypes.guess_type(safe_name)[0] or "application/octet-stream"
|
|
self.validate_file_type(safe_name, resolved_mime_type)
|
|
stored_filename = f"{uuid.uuid4().hex}-{safe_name}"
|
|
return self.provider.save_file(
|
|
namespace=namespace,
|
|
content=content,
|
|
original_filename=original_filename,
|
|
stored_filename=stored_filename,
|
|
mime_type=resolved_mime_type,
|
|
)
|
|
|
|
def open_file(self, storage_key: str) -> Path:
|
|
return self.provider.open_file(storage_key)
|
|
|
|
def delete_file(self, storage_key: str) -> None:
|
|
self.provider.delete_file(storage_key)
|
|
|
|
def file_exists(self, storage_key: str) -> bool:
|
|
return self.provider.file_exists(storage_key)
|
|
|
|
def get_file_metadata(self, storage_key: str) -> FileMetadata:
|
|
return self.provider.get_file_metadata(storage_key)
|
|
|
|
@staticmethod
|
|
def calculate_checksum(content: bytes) -> str:
|
|
return hashlib.sha256(content).hexdigest()
|
|
|
|
@staticmethod
|
|
def safe_filename(file_name: str) -> str:
|
|
name = Path(file_name or "").name.strip()
|
|
if not name:
|
|
raise StorageValidationError("Dateiname fehlt")
|
|
stem = Path(name).stem.strip().lower()
|
|
stem = stem.replace("ä", "ae").replace("ö", "oe").replace("ü", "ue").replace("ß", "ss")
|
|
stem = re.sub(r"[^a-z0-9]+", "-", stem).strip("-")
|
|
suffix = Path(name).suffix.lower()
|
|
safe_stem = stem or uuid.uuid4().hex[:10]
|
|
return f"{safe_stem}{suffix}" if suffix else safe_stem
|
|
|
|
def validate_file_type(self, file_name: str, mime_type: str) -> None:
|
|
extension = Path(file_name).suffix.lower()
|
|
if extension not in ALLOWED_EXTENSIONS:
|
|
raise StorageValidationError("Dateityp ist nicht erlaubt")
|
|
if mime_type not in ALLOWED_MIME_TYPES:
|
|
raise StorageValidationError("MIME-Type ist nicht erlaubt")
|
|
|
|
def validate_file_size(self, size: int) -> None:
|
|
if size <= 0:
|
|
raise StorageValidationError("Upload-Datei ist leer")
|
|
if size > self.max_upload_mb * 1024 * 1024:
|
|
raise StorageValidationError("Upload-Datei ist zu groß")
|
|
|
|
|
|
@lru_cache
|
|
def get_storage_service() -> StorageService:
|
|
if settings.storage_provider != "local":
|
|
raise StorageConfigurationError("Nur STORAGE_PROVIDER=local ist aktuell implementiert")
|
|
|
|
provider = LocalDiskStorageProvider(
|
|
base_path=settings.storage_base_path,
|
|
legacy_base_paths=[settings.knowledge_storage_path],
|
|
)
|
|
return StorageService(provider, max_upload_mb=settings.storage_max_upload_mb)
|