Files
geointel/backend/app/services/storage_service.py
T
Codex 6ea3586a3e
GeoIntel CI / docs-smoke (push) Has been cancelled
GeoIntel CI / contract-smoke (push) Has been cancelled
Initial GeoIntel V1 foundation
2026-06-16 23:36:32 +02:00

137 lines
4.8 KiB
Python

from __future__ import annotations
import hashlib
import shutil
from pathlib import Path
from typing import Any
from app.core.config import get_settings
class StorageService:
@staticmethod
def _base_dir() -> Path:
return Path(get_settings().storage_root).resolve()
@staticmethod
def normalize_dataset_type(dataset_type: str) -> str:
normalized = dataset_type.strip().lower()
if normalized == "geojson":
return "vector"
return normalized
@staticmethod
def _safe_filename(value: str) -> str:
value = value.strip().replace("\\", "/").split("/")[-1]
fallback = "upload"
if not value:
return fallback
allowed = []
for char in value:
if char.isalnum() or char in "-_ .":
allowed.append(char)
else:
allowed.append("_")
cleaned = "".join(allowed)
cleaned = cleaned.strip(" .")
return cleaned or fallback
@staticmethod
def dataset_root(project_id: str, dataset_id: str, dataset_type: str) -> Path:
return StorageService._base_dir() / "uploads" / project_id / dataset_type / dataset_id
@staticmethod
def derived_raster_root(project_id: str, dataset_id: str) -> Path:
return StorageService._base_dir() / "rasters" / "derived" / project_id / dataset_id
@staticmethod
def preview_root(project_id: str, dataset_id: str) -> Path:
return StorageService._base_dir() / "previews" / project_id / dataset_id
@staticmethod
def raster_tiles_root(project_id: str, source_dataset_id: str, tile_set_id: str) -> Path:
return StorageService._base_dir() / "tiles" / project_id / source_dataset_id / tile_set_id
@staticmethod
def dataset_file_path(
project_id: str,
dataset_id: str,
dataset_type: str,
original_filename: str,
) -> str:
safe_original = StorageService._safe_filename(original_filename)
stored_filename = f"{dataset_id}_{safe_original}"
return str(StorageService.dataset_root(project_id, dataset_id, dataset_type) / stored_filename)
@staticmethod
def calculate_checksum_sha256(content: bytes) -> str:
digest = hashlib.sha256()
digest.update(content)
return digest.hexdigest()
@staticmethod
def persist_dataset_file(
project_id: str,
dataset_id: str,
dataset_type: str,
original_filename: str,
content: bytes,
content_type: str | None,
) -> dict[str, Any]:
normalized_type = StorageService.normalize_dataset_type(dataset_type)
file_path = Path(StorageService.dataset_file_path(project_id, dataset_id, normalized_type, original_filename))
file_path.parent.mkdir(parents=True, exist_ok=True)
with file_path.open("wb") as stream:
stream.write(content)
metadata: dict[str, Any] = {
"original_filename": StorageService._safe_filename(original_filename),
"stored_filename": file_path.name,
"content_type": content_type or "application/octet-stream",
"size_bytes": len(content),
"checksum_sha256": StorageService.calculate_checksum_sha256(content),
"storage_path": str(file_path),
}
return metadata
@staticmethod
def persist_file(
storage_path: str,
content: bytes,
original_filename: str,
content_type: str | None,
) -> dict[str, Any]:
target = Path(storage_path)
target.parent.mkdir(parents=True, exist_ok=True)
with target.open("wb") as stream:
stream.write(content)
metadata: dict[str, Any] = {
"original_filename": StorageService._safe_filename(original_filename),
"stored_filename": target.name,
"content_type": content_type or "application/octet-stream",
"size_bytes": len(content),
"checksum_sha256": StorageService.calculate_checksum_sha256(content),
"storage_path": str(target),
}
return metadata
@staticmethod
def remove_dataset_file(path: str) -> None:
target = Path(path)
if target.exists():
target.unlink(missing_ok=True)
dataset_parent = target.parent
if dataset_parent.exists() and dataset_parent.is_dir():
has_files = any(dataset_parent.iterdir())
if not has_files:
shutil.rmtree(dataset_parent, ignore_errors=True)
@staticmethod
def dataset_export_path(project_id: str, dataset_id: str, filename: str) -> str:
output_dir = StorageService._base_dir() / "exports" / project_id / "datasets"
output_dir.mkdir(parents=True, exist_ok=True)
return str(output_dir / f"{dataset_id}_{StorageService._safe_filename(filename)}")