feat: feedback verbetering
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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:
|
||||
|
||||
@@ -31,7 +31,11 @@ class DuplicateTaskService:
|
||||
items: list[dict[str, str]] = []
|
||||
reserved_destinations: set[str] = set()
|
||||
for input_path in paths:
|
||||
item = self._build_duplicate_item(input_path, reserved_destinations)
|
||||
item = self._build_duplicate_item(
|
||||
input_path,
|
||||
reserved_destinations,
|
||||
include_root_prefix=len(paths) > 1,
|
||||
)
|
||||
if item is None:
|
||||
continue
|
||||
reserved_destinations.add(item["destination_absolute"])
|
||||
@@ -60,6 +64,8 @@ class DuplicateTaskService:
|
||||
"source": item["source_absolute"],
|
||||
"destination": item["destination_absolute"],
|
||||
"kind": item["kind"],
|
||||
"files": item["files"],
|
||||
"directories": item["directories"],
|
||||
}
|
||||
for item in items
|
||||
],
|
||||
@@ -77,7 +83,13 @@ class DuplicateTaskService:
|
||||
)
|
||||
raise
|
||||
|
||||
def _build_duplicate_item(self, source: str, reserved_destinations: set[str]) -> dict[str, str] | None:
|
||||
def _build_duplicate_item(
|
||||
self,
|
||||
source: str,
|
||||
reserved_destinations: set[str],
|
||||
*,
|
||||
include_root_prefix: bool,
|
||||
) -> dict[str, str] | None:
|
||||
resolved_source = self._path_guard.resolve_existing_path(source)
|
||||
_, _, lexical_source, _ = self._path_guard.resolve_lexical_path(source)
|
||||
if self._should_skip_name(lexical_source.name):
|
||||
@@ -100,9 +112,6 @@ class DuplicateTaskService:
|
||||
details={"path": source},
|
||||
)
|
||||
|
||||
if source_is_directory:
|
||||
self._validate_directory_tree(resolved_source)
|
||||
|
||||
destination_absolute = self._next_duplicate_destination(resolved_source.absolute, reserved_destinations)
|
||||
destination_relative = self._path_guard.entry_relative_path(
|
||||
resolved_source.alias,
|
||||
@@ -110,19 +119,68 @@ class DuplicateTaskService:
|
||||
display_style=resolved_source.display_style,
|
||||
)
|
||||
|
||||
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,
|
||||
"source_absolute": str(resolved_source.absolute),
|
||||
"destination_absolute": str(destination_absolute),
|
||||
"kind": "directory" if source_is_directory else "file",
|
||||
"files": files,
|
||||
"directories": directories,
|
||||
}
|
||||
|
||||
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):
|
||||
dirnames[:] = [name for name in dirnames if not self._should_skip_name(name)]
|
||||
dirnames.sort(key=str.lower)
|
||||
filenames = sorted(filenames, key=str.lower)
|
||||
root_path = Path(root)
|
||||
for name in [*dirnames, *filenames]:
|
||||
for name in dirnames:
|
||||
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)
|
||||
directories.append(
|
||||
{
|
||||
"source": str(entry),
|
||||
"destination": str(destination_root / relative),
|
||||
}
|
||||
)
|
||||
for name in filenames:
|
||||
if self._should_skip_name(name):
|
||||
continue
|
||||
entry = root_path / name
|
||||
@@ -133,6 +191,26 @@ class DuplicateTaskService:
|
||||
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
|
||||
|
||||
@classmethod
|
||||
def _next_duplicate_destination(cls, source: Path, reserved_destinations: set[str]) -> Path:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import uuid
|
||||
@@ -45,11 +46,7 @@ class MoveTaskService:
|
||||
)
|
||||
|
||||
if item["kind"] == "directory":
|
||||
self._runner.enqueue_move_directory(
|
||||
task_id=task["id"],
|
||||
source=item["source_absolute"],
|
||||
destination=item["destination_absolute"],
|
||||
)
|
||||
self._runner.enqueue_move_directory(task_id=task["id"], item=item)
|
||||
else:
|
||||
self._runner.enqueue_move_file(
|
||||
task_id=task["id"],
|
||||
@@ -57,6 +54,7 @@ class MoveTaskService:
|
||||
destination=item["destination_absolute"],
|
||||
total_bytes=item["total_bytes"],
|
||||
same_root=item["same_root"],
|
||||
current_item=item["files"][0]["label"],
|
||||
)
|
||||
|
||||
return TaskCreateResponse(task_id=task["id"], status=task["status"])
|
||||
@@ -113,6 +111,7 @@ class MoveTaskService:
|
||||
destination=destination,
|
||||
resolved_destination=resolved_destination_base,
|
||||
destination_base=destination_base,
|
||||
include_root_prefix=True,
|
||||
)
|
||||
items.append(item)
|
||||
|
||||
@@ -137,6 +136,8 @@ class MoveTaskService:
|
||||
"source": item["source_absolute"],
|
||||
"destination": item["destination_absolute"],
|
||||
"kind": item["kind"],
|
||||
"files": item["files"],
|
||||
"directories": item["directories"],
|
||||
}
|
||||
for item in items
|
||||
],
|
||||
@@ -149,6 +150,7 @@ class MoveTaskService:
|
||||
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)
|
||||
@@ -224,6 +226,22 @@ class MoveTaskService:
|
||||
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,
|
||||
@@ -232,6 +250,8 @@ class MoveTaskService:
|
||||
"kind": "directory" if source_is_directory else "file",
|
||||
"same_root": same_root,
|
||||
"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:
|
||||
@@ -251,6 +271,70 @@ class MoveTaskService:
|
||||
def _join_destination_base(destination_base: str, name: str) -> str:
|
||||
return f"{destination_base.rstrip('/')}/{name}" if destination_base.rstrip("/") else f"/{name}"
|
||||
|
||||
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)
|
||||
dirnames.sort(key=str.lower)
|
||||
filenames.sort(key=str.lower)
|
||||
for name in dirnames:
|
||||
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)
|
||||
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 _is_nested_destination(source: Path, destination: Path) -> bool:
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user