mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-12 01:37:06 +02:00
106 lines
3.4 KiB
Python
106 lines
3.4 KiB
Python
"""Routes du workspace : arborescence, contenu, téléchargement, suppression.
|
|
|
|
Toutes les routes acceptent un paramètre optionnel ``project`` : la requête
|
|
est alors re-racinée sur ``workspace/<projet>`` (même confinement).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import shutil
|
|
|
|
from fastapi import APIRouter, HTTPException
|
|
from fastapi.responses import FileResponse
|
|
|
|
from .. import tools as tools_mod
|
|
from ..tools import ToolError, _safe_path
|
|
|
|
router = APIRouter(prefix="/api/files", tags=["files"])
|
|
|
|
|
|
def _activate(project: str | None) -> None:
|
|
"""Pose le projet actif pour la requête (400 si nom invalide)."""
|
|
try:
|
|
tools_mod.set_project(project or None)
|
|
except ToolError as exc:
|
|
raise HTTPException(400, str(exc)) from exc
|
|
|
|
|
|
def _tree(path: str, root: str) -> list[dict]:
|
|
"""Arborescence triée (dossiers d'abord) de la racine active."""
|
|
items = []
|
|
for name in sorted(os.listdir(path)):
|
|
if name.startswith("."):
|
|
continue
|
|
full = os.path.join(path, name)
|
|
rel = os.path.relpath(full, root)
|
|
if os.path.isdir(full):
|
|
items.append({"name": name, "path": rel, "type": "dir",
|
|
"children": _tree(full, root)})
|
|
else:
|
|
items.append({"name": name, "path": rel, "type": "file",
|
|
"size": os.path.getsize(full)})
|
|
# Dossiers avant fichiers
|
|
items.sort(key=lambda x: (x["type"] != "dir", x["name"]))
|
|
return items
|
|
|
|
|
|
@router.get("")
|
|
async def list_files(project: str | None = None) -> dict:
|
|
_activate(project)
|
|
root = tools_mod.active_root()
|
|
return {"tree": _tree(root, root)}
|
|
|
|
|
|
@router.get("/content")
|
|
async def file_content(path: str, project: str | None = None) -> dict:
|
|
_activate(project)
|
|
try:
|
|
target = _safe_path(path)
|
|
except ToolError as exc:
|
|
raise HTTPException(400, str(exc))
|
|
if not os.path.isfile(target):
|
|
raise HTTPException(404, "fichier introuvable")
|
|
with open(target, "r", encoding="utf-8", errors="replace") as f:
|
|
return {"path": path, "content": f.read()}
|
|
|
|
|
|
@router.get("/download")
|
|
async def download_file(path: str, project: str | None = None) -> FileResponse:
|
|
"""Télécharge un fichier en conservant le confinement au workspace."""
|
|
_activate(project)
|
|
try:
|
|
target = _safe_path(path)
|
|
except ToolError as exc:
|
|
raise HTTPException(400, str(exc)) from exc
|
|
if not os.path.isfile(target):
|
|
raise HTTPException(404, "fichier introuvable")
|
|
return FileResponse(
|
|
target,
|
|
filename=os.path.basename(target),
|
|
media_type="application/octet-stream",
|
|
)
|
|
|
|
|
|
@router.delete("")
|
|
async def delete_file(path: str, project: str | None = None) -> dict:
|
|
"""Supprime un fichier ou un dossier (récursif) de la racine active.
|
|
|
|
Même confinement que le téléchargement (`_safe_path`) ; la racine active
|
|
est refusée. Les dotfiles (.git…) ne sont jamais listés par
|
|
l'arborescence, donc inaccessibles depuis l'UI.
|
|
"""
|
|
_activate(project)
|
|
try:
|
|
target = _safe_path(path)
|
|
except ToolError as exc:
|
|
raise HTTPException(400, str(exc)) from exc
|
|
if os.path.abspath(target) == tools_mod.active_root():
|
|
raise HTTPException(400, "suppression de la racine refusée")
|
|
if os.path.isdir(target):
|
|
shutil.rmtree(target)
|
|
elif os.path.isfile(target):
|
|
os.remove(target)
|
|
else:
|
|
raise HTTPException(404, "fichier introuvable")
|
|
return {"deleted": path}
|