feat: menu contextuel Déplacer/Dupliquer sidebar + réparation drag & drop interne #201 #BUG-117
CI / lint (push) Successful in 3m13s
CI / security (push) Successful in 1m59s
CI / test (push) Successful in 5m16s
CI / build (push) Successful in 1m52s
CI / e2e (push) Successful in 18m2s

This commit is contained in:
2026-10-15 10:02:48 -04:00
parent ed9029d760
commit 411667fd88
19 changed files with 314 additions and 20 deletions
+74
View File
@@ -1534,6 +1534,80 @@ def move_path(vault_name: str, source_path: str, destination_dir: str = "") -> d
"item_type": item_type,
}
def duplicate_path(vault_name: str, source_path: str, destination_dir: str = "") -> dict[str, Any]:
"""Duplicate a file or directory (copy) into *destination_dir* (same vault).
The copy is named ``<stem> copie<ext>`` (then ``copie 2``, ``copie 3``… on
collision). Directories are copied recursively.
Raises:
ServiceError: ``not_found`` (404), ``read_only`` (403),
``permission_denied`` (403), ``invalid_arguments`` (400) when the
destination is inside the source directory.
"""
root = get_vault_root(vault_name)
_ensure_writable(root)
source = resolve_safe_path(root, source_path)
if not source.exists():
raise ServiceError(
f"Source not found: {source_path}",
code="not_found",
status=404,
details={"vault": vault_name, "path": source_path},
)
is_directory = source.is_dir()
dest_clean = (destination_dir or "").strip("/")
if dest_clean:
dest_parent = resolve_safe_path(root, dest_clean)
if not dest_parent.exists() or not dest_parent.is_dir():
raise ServiceError(
f"Destination directory not found: {destination_dir}",
code="not_found",
status=404,
details={"vault": vault_name, "path": destination_dir},
)
else:
dest_parent = root
if is_directory and str(dest_parent.resolve()) in (str(source.resolve()), str(source.resolve()) + os.sep, str(source.resolve()) + "/"):
raise ServiceError(
"Cannot duplicate a directory inside itself",
code="invalid_arguments",
status=400,
details={"vault": vault_name, "path": source_path},
)
ext = "" if is_directory else source.suffix
base = dest_parent / (source.stem + " copie" + ext)
target = base
n = 2
while target.exists():
target = base.parent / (source.stem + " copie " + str(n) + ext)
n += 1
try:
if is_directory:
shutil.copytree(source, target)
else:
shutil.copy2(source, target)
except PermissionError as e:
raise ServiceError("Permission denied: cannot duplicate item", code="permission_denied", status=403) from e
new_rel = _rel(root, target)
item_type = "directory" if is_directory else "file"
logger.info(f"Item duplicated: {vault_name}/{source_path} -> {new_rel}")
return {
"success": True,
"vault": vault_name,
"source_path": _rel(root, source),
"new_path": new_rel,
"item_type": item_type,
}
# ── D3. Find & replace ─────────────────────────────────────────────────────