208 lines
8.0 KiB
Python
208 lines
8.0 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import Literal
|
|
from fastapi import APIRouter, Depends, File as FastAPIFile, Form, UploadFile
|
|
from sqlalchemy.orm import Session
|
|
|
|
from govoplan_core.auth import ApiPrincipal, require_scope
|
|
from govoplan_files.backend.schemas import (
|
|
ConflictResolutionRequest,
|
|
FileUploadResponse,
|
|
_conflict_resolutions,
|
|
)
|
|
from govoplan_files.backend.db.models import FileAsset
|
|
from govoplan_core.db.session import get_session
|
|
from govoplan_files.backend.runtime import settings
|
|
from govoplan_files.backend.storage.paths import UnsafeFilePathError
|
|
from govoplan_files.backend.storage.archives import extract_zip_upload
|
|
from govoplan_files.backend.storage.common import FileStorageError
|
|
from govoplan_files.backend.storage.connector_policy import (
|
|
ConnectorPolicyDenied,
|
|
)
|
|
from govoplan_files.backend.storage.files import (
|
|
create_file_asset,
|
|
)
|
|
|
|
|
|
from govoplan_files.backend.route_support import (
|
|
_asset_response,
|
|
_audit_connector_imports,
|
|
_cleanup_temp_file,
|
|
_connector_policy_error,
|
|
_enforce_connector_policy,
|
|
_http_error,
|
|
_is_admin,
|
|
_read_limited_upload,
|
|
_source_metadata_from_form,
|
|
_spool_limited_upload_to_temp,
|
|
)
|
|
|
|
router = APIRouter(prefix="/files", tags=["files"])
|
|
|
|
|
|
@router.post("/upload", response_model=FileUploadResponse)
|
|
def upload_files(
|
|
files: list[UploadFile] = FastAPIFile(...),
|
|
owner_type: Literal["user", "group"] = Form(default="user"),
|
|
owner_id: str | None = Form(default=None),
|
|
path: str = Form(default=""),
|
|
campaign_id: str | None = Form(default=None),
|
|
unpack_zip: bool = Form(default=False),
|
|
conflict_strategy: Literal["reject", "overwrite", "rename"] = Form(
|
|
default="reject"
|
|
),
|
|
conflict_resolutions_json: str | None = Form(default=None),
|
|
source_provenance_json: str | None = Form(default=None),
|
|
source_revision: str | None = Form(default=None),
|
|
connector_policy_json: str | None = Form(default=None),
|
|
session: Session = Depends(get_session),
|
|
principal: ApiPrincipal = Depends(require_scope("files:file:upload")),
|
|
):
|
|
target_owner = owner_id or principal.user.id
|
|
uploaded_assets: list[FileAsset] = []
|
|
try:
|
|
raw_resolutions = (
|
|
json.loads(conflict_resolutions_json) if conflict_resolutions_json else []
|
|
)
|
|
upload_resolutions = _conflict_resolutions(
|
|
[ConflictResolutionRequest(**item) for item in raw_resolutions]
|
|
)
|
|
_enforce_connector_policy(
|
|
source_provenance_json, connector_policy_json, operation="import"
|
|
)
|
|
metadata = _source_metadata_from_form(source_provenance_json, source_revision)
|
|
for upload in files:
|
|
filename = upload.filename or "file"
|
|
content_type = upload.content_type or None
|
|
upload_limit = (
|
|
settings.file_upload_zip_max_bytes
|
|
if unpack_zip and filename.lower().endswith(".zip")
|
|
else settings.file_upload_max_bytes
|
|
)
|
|
if unpack_zip and filename.lower().endswith(".zip"):
|
|
zip_path = _spool_limited_upload_to_temp(
|
|
upload, max_bytes=upload_limit, suffix=".zip"
|
|
)
|
|
try:
|
|
extracted = extract_zip_upload(
|
|
session,
|
|
tenant_id=principal.tenant_id,
|
|
owner_type=owner_type,
|
|
owner_id=target_owner,
|
|
user_id=principal.user.id,
|
|
zip_data=zip_path,
|
|
folder=path,
|
|
campaign_id=campaign_id,
|
|
conflict_strategy=conflict_strategy,
|
|
conflict_resolutions=upload_resolutions,
|
|
metadata=metadata,
|
|
is_admin=_is_admin(principal),
|
|
max_file_bytes=settings.file_upload_max_bytes,
|
|
max_total_bytes=settings.file_upload_zip_max_bytes,
|
|
)
|
|
finally:
|
|
_cleanup_temp_file(zip_path)
|
|
uploaded_assets.extend(item.asset for item in extracted)
|
|
continue
|
|
data = _read_limited_upload(upload, max_bytes=upload_limit)
|
|
stored = create_file_asset(
|
|
session,
|
|
tenant_id=principal.tenant_id,
|
|
owner_type=owner_type,
|
|
owner_id=target_owner,
|
|
user_id=principal.user.id,
|
|
filename=filename,
|
|
data=data,
|
|
folder=path,
|
|
content_type=content_type,
|
|
campaign_id=campaign_id,
|
|
conflict_strategy=conflict_strategy,
|
|
conflict_resolutions=upload_resolutions,
|
|
metadata=metadata,
|
|
is_admin=_is_admin(principal),
|
|
)
|
|
uploaded_assets.append(stored.asset)
|
|
_audit_connector_imports(session, principal, uploaded_assets)
|
|
session.commit()
|
|
except ConnectorPolicyDenied as exc:
|
|
session.rollback()
|
|
raise _connector_policy_error(exc) from exc
|
|
except (FileStorageError, UnsafeFilePathError, ValueError) as exc:
|
|
session.rollback()
|
|
raise _http_error(exc) from exc
|
|
return FileUploadResponse(
|
|
files=[
|
|
_asset_response(session, asset, include_shares=True)
|
|
for asset in uploaded_assets
|
|
]
|
|
)
|
|
|
|
|
|
@router.post("/upload-zip", response_model=FileUploadResponse)
|
|
def upload_zip(
|
|
file: UploadFile = FastAPIFile(...),
|
|
owner_type: Literal["user", "group"] = Form(default="user"),
|
|
owner_id: str | None = Form(default=None),
|
|
path: str = Form(default=""),
|
|
campaign_id: str | None = Form(default=None),
|
|
conflict_strategy: Literal["reject", "overwrite", "rename"] = Form(
|
|
default="reject"
|
|
),
|
|
conflict_resolutions_json: str | None = Form(default=None),
|
|
source_provenance_json: str | None = Form(default=None),
|
|
source_revision: str | None = Form(default=None),
|
|
connector_policy_json: str | None = Form(default=None),
|
|
session: Session = Depends(get_session),
|
|
principal: ApiPrincipal = Depends(require_scope("files:file:upload")),
|
|
):
|
|
target_owner = owner_id or principal.user.id
|
|
zip_path: str | None = None
|
|
try:
|
|
raw_resolutions = (
|
|
json.loads(conflict_resolutions_json) if conflict_resolutions_json else []
|
|
)
|
|
upload_resolutions = _conflict_resolutions(
|
|
[ConflictResolutionRequest(**item) for item in raw_resolutions]
|
|
)
|
|
_enforce_connector_policy(
|
|
source_provenance_json, connector_policy_json, operation="import"
|
|
)
|
|
metadata = _source_metadata_from_form(source_provenance_json, source_revision)
|
|
zip_path = _spool_limited_upload_to_temp(
|
|
file, max_bytes=settings.file_upload_zip_max_bytes, suffix=".zip"
|
|
)
|
|
extracted = extract_zip_upload(
|
|
session,
|
|
tenant_id=principal.tenant_id,
|
|
owner_type=owner_type,
|
|
owner_id=target_owner,
|
|
user_id=principal.user.id,
|
|
zip_data=zip_path,
|
|
folder=path,
|
|
campaign_id=campaign_id,
|
|
conflict_strategy=conflict_strategy,
|
|
conflict_resolutions=upload_resolutions,
|
|
metadata=metadata,
|
|
is_admin=_is_admin(principal),
|
|
max_file_bytes=settings.file_upload_max_bytes,
|
|
max_total_bytes=settings.file_upload_zip_max_bytes,
|
|
)
|
|
_audit_connector_imports(session, principal, [item.asset for item in extracted])
|
|
session.commit()
|
|
except ConnectorPolicyDenied as exc:
|
|
session.rollback()
|
|
raise _connector_policy_error(exc) from exc
|
|
except (FileStorageError, UnsafeFilePathError, ValueError) as exc:
|
|
session.rollback()
|
|
raise _http_error(exc) from exc
|
|
finally:
|
|
if zip_path:
|
|
_cleanup_temp_file(zip_path)
|
|
return FileUploadResponse(
|
|
files=[
|
|
_asset_response(session, item.asset, include_shares=True)
|
|
for item in extracted
|
|
]
|
|
)
|