feat: feedback verbetering

This commit is contained in:
kodi
2026-03-15 13:28:11 +01:00
parent a52493459a
commit 66abf991d8
15 changed files with 585 additions and 180 deletions
+77 -10
View File
@@ -45,17 +45,14 @@ class CopyTaskService:
)
if item["kind"] == "directory":
self._runner.enqueue_copy_directory(
task_id=task["id"],
source=item["source_absolute"],
destination=item["destination_absolute"],
)
self._runner.enqueue_copy_directory(task_id=task["id"], item=item)
else:
self._runner.enqueue_copy_file(
task_id=task["id"],
source=item["source_absolute"],
destination=item["destination_absolute"],
total_bytes=item["total_bytes"],
current_item=item["files"][0]["label"],
)
return TaskCreateResponse(task_id=task["id"], status=task["status"])
@@ -94,6 +91,7 @@ class CopyTaskService:
destination=destination,
resolved_destination=resolved_destination_base,
destination_base=destination_base,
include_root_prefix=True,
)
items.append(item)
@@ -118,6 +116,8 @@ class CopyTaskService:
"source": item["source_absolute"],
"destination": item["destination_absolute"],
"kind": item["kind"],
"files": item["files"],
"directories": item["directories"],
}
for item in items
],
@@ -130,6 +130,7 @@ class CopyTaskService:
destination: str,
resolved_destination: ResolvedPath | None = None,
destination_base: str | None = None,
include_root_prefix: bool = False,
) -> dict:
resolved_source = self._path_guard.resolve_existing_path(source)
_, _, lexical_source, _ = self._path_guard.resolve_lexical_path(source)
@@ -151,9 +152,6 @@ class CopyTaskService:
details={"path": source},
)
if source_is_directory:
self._validate_directory_tree(resolved_source)
resolved_destination = resolved_destination or self._path_guard.resolve_path(destination)
destination_absolute = (
resolved_destination.absolute / resolved_source.absolute.name
@@ -189,6 +187,22 @@ class CopyTaskService:
details={"path": source, "destination": destination_relative},
)
if source_is_directory:
directories, files = self._build_directory_plan(
resolved_source=resolved_source,
destination_root=destination_absolute,
include_root_prefix=include_root_prefix,
)
else:
files = [
{
"source": str(resolved_source.absolute),
"destination": str(destination_absolute),
"label": resolved_source.absolute.name,
}
]
directories = []
return {
"source_relative": resolved_source.relative,
"destination_relative": destination_relative,
@@ -196,6 +210,8 @@ class CopyTaskService:
"destination_absolute": str(destination_absolute),
"kind": "directory" if source_is_directory else "file",
"total_bytes": int(resolved_source.absolute.stat().st_size) if source_is_file else None,
"files": files,
"directories": directories,
}
def _map_directory_validation(self, relative_path: str) -> None:
@@ -211,10 +227,25 @@ class CopyTaskService:
)
raise
def _validate_directory_tree(self, resolved_source: ResolvedPath) -> None:
def _build_directory_plan(
self,
*,
resolved_source: ResolvedPath,
destination_root: Path,
include_root_prefix: bool,
) -> tuple[list[dict[str, str]], list[dict[str, str]]]:
directories: list[dict[str, str]] = [
{
"source": str(resolved_source.absolute),
"destination": str(destination_root),
}
]
files: list[dict[str, str]] = []
for root, dirnames, filenames in os.walk(resolved_source.absolute, followlinks=False):
root_path = Path(root)
for name in [*dirnames, *filenames]:
dirnames.sort(key=str.lower)
filenames.sort(key=str.lower)
for name in dirnames:
entry = root_path / name
if entry.is_symlink():
raise AppError(
@@ -223,6 +254,42 @@ class CopyTaskService:
status_code=409,
details={"path": resolved_source.relative},
)
relative = entry.relative_to(resolved_source.absolute)
directories.append(
{
"source": str(entry),
"destination": str(destination_root / relative),
}
)
for name in filenames:
entry = root_path / name
if entry.is_symlink():
raise AppError(
code="type_conflict",
message="Source directory must not contain symlinks",
status_code=409,
details={"path": resolved_source.relative},
)
relative = entry.relative_to(resolved_source.absolute)
files.append(
{
"source": str(entry),
"destination": str(destination_root / relative),
"label": self._progress_label(
top_level_name=resolved_source.absolute.name,
relative_path=relative,
include_root_prefix=include_root_prefix,
),
}
)
return directories, files
@staticmethod
def _progress_label(*, top_level_name: str, relative_path: Path, include_root_prefix: bool) -> str:
relative_value = relative_path.as_posix()
if not relative_value:
return top_level_name
return f"{top_level_name}/{relative_value}" if include_root_prefix else relative_value
@staticmethod
def _join_destination_base(destination_base: str, name: str) -> str: