feat(dragdrop): drag & drop complet de fichiers/dossiers & intégration IA (#89)
CI / lint (push) Successful in 1m20s
CI / security (push) Successful in 50s
CI / test (push) Successful in 2m42s
CI / build (push) Successful in 51s
CI / e2e (push) Successful in 10m29s

This commit is contained in:
2026-09-14 15:31:12 -04:00
parent 150b57d538
commit 1db3e0ad2f
13 changed files with 990 additions and 10 deletions
+100
View File
@@ -105,6 +105,9 @@ from backend.services.backups import list_backup_files as service_list_backup_fi
from backend.services.errors import ServiceError
from backend.services.files import read_raw_file
from backend.services.graph import get_graph as service_get_graph
from backend.services.mutations import (
batch_upload_files as service_batch_upload_files,
)
from backend.services.mutations import (
create_directory as service_create_directory,
)
@@ -436,6 +439,31 @@ class FileCreateResponse(BaseModel):
path: str = Field(description="Path of the created file")
class BatchUploadFileItem(BaseModel):
"""A single file/dir entry in a batch upload request."""
path: str = Field(description="Relative path of the item within the batch")
content: str | None = Field(default=None, description="Base64 encoded or text content for files")
is_dir: bool = Field(default=False, description="True if entry represents an empty directory")
class BatchUploadRequest(BaseModel):
"""Request payload for batch file/directory upload."""
target_dir: str = Field(default="", description="Base directory in vault to upload into (empty for root)")
files: list[BatchUploadFileItem] = Field(description="List of files and directories to upload")
overwrite: bool = Field(default=True, description="Whether to overwrite existing files (creates backups)")
class BatchUploadResponse(BaseModel):
"""Response from batch file/directory upload."""
success: bool = Field(description="True if all files uploaded without error")
vault: str = Field(description="Vault name")
target_dir: str = Field(description="Target directory")
uploaded: list[str] = Field(description="List of created/updated file paths")
created_dirs: list[str] = Field(description="List of created directory paths")
errors: list[dict[str, Any]] = Field(default_factory=list, description="List of items that failed")
total_files: int = Field(description="Total uploaded files count")
class FileRenameRequest(BaseModel):
"""Request to rename a file."""
path: str = Field(description="Current path of the file")
@@ -1905,6 +1933,78 @@ async def api_file_create(
return {"success": True, "path": result["path"]}
@app.post("/api/vault/{vault_name}/batch-upload", response_model=BatchUploadResponse)
async def api_batch_upload(
vault_name: str,
body: BatchUploadRequest,
current_user=Depends(require_auth),
):
"""Upload multiple files and directories (recursively) into a vault.
Accepts base64 encoded or plain text files with relative directory paths.
Creates missing parent folders safely.
Args:
vault_name: Target vault name.
body: BatchUploadRequest with target_dir and files list.
Returns:
BatchUploadResponse with summary of uploaded files and errors.
"""
if not check_vault_access(vault_name, current_user):
raise HTTPException(status_code=403, detail=f"Accès refusé à la vault '{vault_name}'")
import base64
items: list[dict[str, Any]] = []
for f in body.files:
if f.is_dir:
items.append({"path": f.path, "is_dir": True})
continue
raw_bytes = b""
if f.content is not None:
# Check if content is base64 encoded data URI or raw base64
content_str = f.content
if content_str.startswith("data:") and ";base64," in content_str:
content_str = content_str.split(";base64,", 1)[1]
try:
raw_bytes = base64.b64decode(content_str)
except Exception:
# Fallback to utf-8 text encoding
raw_bytes = f.content.encode("utf-8")
items.append({"path": f.path, "content": raw_bytes, "is_dir": False})
result = service_batch_upload_files(
vault_name,
body.target_dir,
items,
overwrite=body.overwrite,
)
# Update index and SSE notifications for uploaded files
for path in result["uploaded"]:
try:
await update_single_file(vault_name, path)
await sse_manager.broadcast("file_created", {
"vault": vault_name,
"path": path,
})
await dispatch_webhooks("file_created", {"vault": vault_name, "path": path})
except Exception as e:
logger.warning(f"Failed to post-process upload of {path}: {e}")
# SSE notification for tree refresh
if result["uploaded"] or result["created_dirs"]:
await sse_manager.broadcast("tree_updated", {
"vault": vault_name,
"target_dir": result["target_dir"],
})
return result
@app.patch("/api/file/{vault_name}", response_model=FileRenameResponse)
async def api_file_rename(
vault_name: str,