import os import io import json import shutil import sqlite3 from pathlib import Path from fastapi import APIRouter, UploadFile, File, Query, HTTPException from fastapi.responses import FileResponse, JSONResponse from storage.files.file_manager import FileManager from storage.common import validate_token router = APIRouter(prefix="/data", tags=["Data Manager"]) DATA_ROOT = Path("/data") HF_TOKEN = os.getenv("HF_TOKEN") @router.get("/data_tree", tags=["Data Manager"]) def get_data_tree( token: str = Query(..., description="Token required for authorization") ): """ Return a formatted tree of folders and files under /data. Behavior: - Validate token. - Walk the /data directory and build a recursive tree. - Each entry includes: name, type (file/directory), and children if directory. """ validate_token(token) def build_tree(path: Path): """ Build a tree representation of directories and files. Prevents errors by checking if the path is a directory before iterating. """ if not path.exists(): return {"name": path.name, "type": "missing"} # Si es un fichero → devolvemos un nodo simple if path.is_file(): return { "name": path.name, "type": "file" } # Si es un directorio → construimos sus hijos children = [] try: entries = sorted(path.iterdir(), key=lambda p: p.name.lower()) except Exception: # Si por cualquier razón no podemos listar, lo tratamos como file return { "name": path.name, "type": "file" } for child in entries: children.append(build_tree(child)) return { "name": path.name, "type": "directory", "children": children } if not DATA_ROOT.exists(): return {"error": "/data does not exist"} return build_tree(DATA_ROOT) @router.post("/create_data_item", tags=["Data Manager"]) def create_data_item( path: str = Query(..., description="Full path starting with /data/"), item_type: str = Query(..., description="directory or file"), token: str = Query(..., description="Token required for authorization") ): """ Create a directory or file under /data. Restrictions: - Path must start with /data/. - Writing to /data/db or /data/media (or any of their subpaths) is forbidden. - item_type must be 'directory' or 'file'. Behavior: - Validate token. - Check path validity and protection. - Create directory or empty file. - Raise error if path already exists. """ validate_token(token) target = Path(path) # Validación básica if not path.startswith("/data/"): raise HTTPException(status_code=400, detail="Path must start with /data/") if item_type not in ("directory", "file"): raise HTTPException(status_code=400, detail="item_type must be 'directory' or 'file'") # Protección de carpetas sensibles protected = ["/data/db", "/data/media"] for p in protected: if path == p or path.startswith(p + "/"): raise HTTPException( status_code=403, detail=f"Access to protected path '{p}' is not allowed" ) # No permitir sobrescritura if target.exists(): raise HTTPException(status_code=409, detail="Path already exists") try: if item_type == "directory": target.mkdir(parents=True, exist_ok=False) else: target.parent.mkdir(parents=True, exist_ok=True) with open(target, "wb") as f: f.write(b"") except Exception as exc: raise HTTPException(status_code=500, detail=f"Failed to create item: {exc}") return { "status": "ok", "created": str(target), "type": item_type } @router.delete("/delete_path", tags=["Data Manager"]) def delete_path( target: str = Query(..., description="Ruta absoluta dentro de /data a borrar"), token: str = Query(..., description="Token requerido para autorización") ): """ Delete a file or directory recursively inside /data, except protected folders. Behavior: - Validate token. - Validate path starts with /data/. - Deny deletion of /data/db and /data/media (and anything inside them). - If target is a file → delete file. - If target is a directory → delete recursively. """ validate_token(token) # Normalizar ruta path = Path(target).resolve() # Debe estar dentro de /data/ if not str(path).startswith(str(DATA_ROOT)): raise HTTPException(status_code=400, detail="Path must be inside /data/") # Carpetas protegidas protected = [ DATA_ROOT / "db", DATA_ROOT / "media", ] for p in protected: if path == p or str(path).startswith(str(p) + "/"): raise HTTPException( status_code=403, detail=f"Deletion forbidden: {p} is protected" ) # Verificar existencia if not path.exists(): raise HTTPException(status_code=404, detail="Target path does not exist") try: if path.is_file(): path.unlink() else: shutil.rmtree(path) except Exception as exc: raise HTTPException(status_code=500, detail=f"Failed to delete: {exc}") return { "status": "ok", "deleted": str(path) }