feat: menu contextuel Déplacer/Dupliquer sidebar + réparation drag & drop interne #201 #BUG-117
This commit is contained in:
@@ -34,6 +34,8 @@ from backend.schemas import (
|
||||
FileCreateRequest,
|
||||
FileCreateResponse,
|
||||
FileDeleteResponse,
|
||||
FileDuplicateRequest,
|
||||
FileDuplicateResponse,
|
||||
FileMoveRequest,
|
||||
FileMoveResponse,
|
||||
FileRenameRequest,
|
||||
@@ -55,6 +57,9 @@ from backend.services.mutations import (
|
||||
from backend.services.mutations import (
|
||||
delete_file as service_delete_file,
|
||||
)
|
||||
from backend.services.mutations import (
|
||||
duplicate_path as service_duplicate_path,
|
||||
)
|
||||
from backend.services.mutations import (
|
||||
edit_file as service_edit_file,
|
||||
)
|
||||
@@ -743,3 +748,41 @@ async def api_file_move(
|
||||
await dispatch_webhooks("item_moved", {"vault": vault_name, "old_path": old_path_str, "new_path": new_path_str, "item_type": item_type})
|
||||
|
||||
return {"success": True, "old_path": old_path_str, "new_path": new_path_str, "item_type": item_type}
|
||||
|
||||
@router.post("/api/duplicate/{vault_name}", response_model=FileDuplicateResponse)
|
||||
async def api_file_duplicate(
|
||||
vault_name: str,
|
||||
body: FileDuplicateRequest,
|
||||
current_user=Depends(require_auth),
|
||||
):
|
||||
"""Duplicate (copy) a file or directory within the same vault."""
|
||||
if not check_vault_access(vault_name, current_user):
|
||||
raise HTTPException(status_code=403, detail=f"Accès refusé à la vault '{vault_name}'")
|
||||
|
||||
result = service_duplicate_path(vault_name, body.source_path, body.destination_dir)
|
||||
new_path_str = result["new_path"]
|
||||
item_type = result["item_type"]
|
||||
|
||||
# Index: incrémental pour chaque fichier copié (évite un reload complet).
|
||||
from backend.services.vaults import get_vault_root
|
||||
root = get_vault_root(vault_name)
|
||||
target = root / new_path_str
|
||||
if item_type == "directory":
|
||||
for fp in sorted(target.rglob("*")):
|
||||
if fp.is_file():
|
||||
await update_single_file(vault_name, str(fp.relative_to(root)).replace("\\", "/"))
|
||||
else:
|
||||
await update_single_file(vault_name, new_path_str)
|
||||
|
||||
await sse_manager.broadcast("item_duplicated", {
|
||||
"vault": vault_name,
|
||||
"source_path": result["source_path"],
|
||||
"new_path": new_path_str,
|
||||
"item_type": item_type,
|
||||
})
|
||||
await dispatch_webhooks(
|
||||
"file_created" if item_type == "file" else "directory_created",
|
||||
{"vault": vault_name, "path": new_path_str},
|
||||
)
|
||||
|
||||
return {"success": True, "source_path": result["source_path"], "new_path": new_path_str, "item_type": item_type}
|
||||
|
||||
@@ -580,6 +580,20 @@ class FileMoveResponse(BaseModel):
|
||||
success: bool = Field(description="Whether move succeeded")
|
||||
old_path: str = Field(description="Original path")
|
||||
new_path: str = Field(description="New path after move")
|
||||
|
||||
class FileDuplicateRequest(BaseModel):
|
||||
"""Request to duplicate (copy) a file or directory."""
|
||||
|
||||
source_path: str = Field(description="Relative path of the item to duplicate")
|
||||
destination_dir: str = Field(default="", description="Target directory relative path (empty = source parent)")
|
||||
|
||||
|
||||
class FileDuplicateResponse(BaseModel):
|
||||
"""Response after duplicating a file or directory."""
|
||||
|
||||
success: bool = Field(description="Whether duplication succeeded")
|
||||
source_path: str = Field(description="Original path")
|
||||
new_path: str = Field(description="Path of the created copy")
|
||||
item_type: str = Field(description="Type of item moved: 'file' or 'directory'")
|
||||
|
||||
|
||||
|
||||
@@ -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 ─────────────────────────────────────────────────────
|
||||
|
||||
|
||||
Reference in New Issue
Block a user