feat: download - download status aan logs toegevoegd
This commit is contained in:
@@ -387,17 +387,74 @@ class FileOpsService:
|
||||
)
|
||||
|
||||
def prepare_download(self, paths: list[str]) -> dict:
|
||||
history_entry_id: str | None = None
|
||||
history_mode = self._download_mode_from_request_paths(paths)
|
||||
history_path = self._summarize_download_targets(paths)
|
||||
history_download_name: str | None = None
|
||||
if not paths:
|
||||
raise AppError(
|
||||
error = AppError(
|
||||
code="invalid_request",
|
||||
message="At least one path is required",
|
||||
status_code=400,
|
||||
)
|
||||
self._record_download_failure(
|
||||
mode=history_mode,
|
||||
path_summary=history_path,
|
||||
download_name=None,
|
||||
error=error,
|
||||
history_entry_id=None,
|
||||
)
|
||||
raise error
|
||||
|
||||
resolved_targets = [self._path_guard.resolve_existing_path(path) for path in paths]
|
||||
if len(resolved_targets) == 1 and resolved_targets[0].absolute.is_file():
|
||||
return self._prepare_single_file_download(resolved_targets[0])
|
||||
return self._prepare_zip_download(resolved_targets)
|
||||
try:
|
||||
resolved_targets = [self._path_guard.resolve_existing_path(path) for path in paths]
|
||||
history_mode = self._download_mode_from_resolved_targets(resolved_targets)
|
||||
history_path = self._summarize_download_targets([target.relative for target in resolved_targets])
|
||||
history_download_name = self._download_name_for_targets(resolved_targets)
|
||||
history_entry_id = self._record_download_status(
|
||||
status="requested",
|
||||
mode=history_mode,
|
||||
path_summary=history_path,
|
||||
download_name=history_download_name,
|
||||
)
|
||||
|
||||
if len(resolved_targets) == 1 and resolved_targets[0].absolute.is_file():
|
||||
prepared = self._prepare_single_file_download(resolved_targets[0])
|
||||
else:
|
||||
prepared = self._prepare_zip_download(resolved_targets, history_download_name)
|
||||
|
||||
self._record_download_status(
|
||||
status="ready",
|
||||
mode=history_mode,
|
||||
path_summary=history_path,
|
||||
download_name=history_download_name,
|
||||
history_entry_id=history_entry_id,
|
||||
)
|
||||
return prepared
|
||||
except AppError as error:
|
||||
self._record_download_failure(
|
||||
mode=history_mode,
|
||||
path_summary=history_path,
|
||||
download_name=history_download_name,
|
||||
error=error,
|
||||
history_entry_id=history_entry_id,
|
||||
)
|
||||
raise
|
||||
except OSError as exc:
|
||||
error = AppError(
|
||||
code="io_error",
|
||||
message="Filesystem operation failed",
|
||||
status_code=500,
|
||||
details={"reason": str(exc)},
|
||||
)
|
||||
self._record_download_failure(
|
||||
mode=history_mode,
|
||||
path_summary=history_path,
|
||||
download_name=history_download_name,
|
||||
error=error,
|
||||
history_entry_id=history_entry_id,
|
||||
)
|
||||
raise error
|
||||
|
||||
def save(self, path: str, content: str, expected_modified: str) -> SaveResponse:
|
||||
resolved_target = self._path_guard.resolve_existing_path(path)
|
||||
@@ -699,7 +756,7 @@ class FileOpsService:
|
||||
"content_type": self._content_type_for(resolved_target.absolute) or "application/octet-stream",
|
||||
}
|
||||
|
||||
def _prepare_zip_download(self, resolved_targets: list) -> dict:
|
||||
def _prepare_zip_download(self, resolved_targets: list, download_name: str) -> dict:
|
||||
archive_names: set[str] = set()
|
||||
for resolved_target in resolved_targets:
|
||||
archive_name = resolved_target.absolute.name
|
||||
@@ -712,11 +769,6 @@ class FileOpsService:
|
||||
archive_names.add(archive_name)
|
||||
self._run_zip_download_preflight(resolved_targets)
|
||||
|
||||
if len(resolved_targets) == 1 and resolved_targets[0].absolute.is_dir():
|
||||
download_name = f"{resolved_targets[0].absolute.name}.zip"
|
||||
else:
|
||||
download_name = f"kodidownload-{datetime.now(timezone.utc).strftime('%Y%m%d-%H%M%S')}.zip"
|
||||
|
||||
buffer = BytesIO()
|
||||
with zipfile.ZipFile(buffer, "w", compression=zipfile.ZIP_DEFLATED) as archive:
|
||||
for resolved_target in resolved_targets:
|
||||
@@ -734,6 +786,97 @@ class FileOpsService:
|
||||
"content_type": "application/zip",
|
||||
}
|
||||
|
||||
def _download_name_for_targets(self, resolved_targets: list) -> str:
|
||||
if len(resolved_targets) == 1 and resolved_targets[0].absolute.is_file():
|
||||
return resolved_targets[0].absolute.name
|
||||
if len(resolved_targets) == 1 and resolved_targets[0].absolute.is_dir():
|
||||
return f"{resolved_targets[0].absolute.name}.zip"
|
||||
return f"kodidownload-{datetime.now(timezone.utc).strftime('%Y%m%d-%H%M%S')}.zip"
|
||||
|
||||
@staticmethod
|
||||
def _download_mode_from_request_paths(paths: list[str]) -> str:
|
||||
return "multi_zip" if len(paths) > 1 else "single_file"
|
||||
|
||||
@staticmethod
|
||||
def _download_mode_from_resolved_targets(resolved_targets: list) -> str:
|
||||
if len(resolved_targets) == 1 and resolved_targets[0].absolute.is_file():
|
||||
return "single_file"
|
||||
if len(resolved_targets) == 1 and resolved_targets[0].absolute.is_dir():
|
||||
return "single_directory_zip"
|
||||
return "multi_zip"
|
||||
|
||||
@staticmethod
|
||||
def _summarize_download_targets(paths: list[str]) -> str:
|
||||
if not paths:
|
||||
return "-"
|
||||
if len(paths) == 1:
|
||||
return paths[0]
|
||||
if len(paths) == 2:
|
||||
return f"{paths[0]}, {paths[1]}"
|
||||
return f"{paths[0]}, {paths[1]}, +{len(paths) - 2} more"
|
||||
|
||||
def _record_download_status(
|
||||
self,
|
||||
*,
|
||||
status: str,
|
||||
mode: str,
|
||||
path_summary: str,
|
||||
download_name: str | None,
|
||||
history_entry_id: str | None = None,
|
||||
) -> str | None:
|
||||
if not self._history_repository:
|
||||
return history_entry_id
|
||||
if history_entry_id:
|
||||
self._history_repository.update_entry(
|
||||
entry_id=history_entry_id,
|
||||
status=status,
|
||||
error_code=None,
|
||||
error_message=None,
|
||||
finished_at=self._now_iso(),
|
||||
)
|
||||
return history_entry_id
|
||||
created = self._history_repository.create_entry(
|
||||
operation="download",
|
||||
status=status,
|
||||
source=mode,
|
||||
destination=download_name,
|
||||
path=path_summary,
|
||||
finished_at=self._now_iso() if status != "requested" else None,
|
||||
)
|
||||
return created["id"]
|
||||
|
||||
def _record_download_failure(
|
||||
self,
|
||||
*,
|
||||
mode: str,
|
||||
path_summary: str,
|
||||
download_name: str | None,
|
||||
error: AppError,
|
||||
history_entry_id: str | None,
|
||||
) -> None:
|
||||
if not self._history_repository:
|
||||
return
|
||||
failure_status = "preflight_failed" if error.code == "download_preflight_failed" else "failed"
|
||||
if history_entry_id:
|
||||
self._history_repository.update_entry(
|
||||
entry_id=history_entry_id,
|
||||
status=failure_status,
|
||||
error_code=error.code,
|
||||
error_message=error.message,
|
||||
finished_at=self._now_iso(),
|
||||
)
|
||||
return
|
||||
self._history_repository.create_entry(
|
||||
operation="download",
|
||||
status=failure_status,
|
||||
source=mode,
|
||||
destination=download_name,
|
||||
path=path_summary,
|
||||
error_code=error.code,
|
||||
error_message=error.message,
|
||||
finished_at=self._now_iso(),
|
||||
)
|
||||
|
||||
def _run_zip_download_preflight(self, resolved_targets: list) -> None:
|
||||
started_at = self._monotonic()
|
||||
state = ZipDownloadPreflightState()
|
||||
|
||||
Reference in New Issue
Block a user