|
|
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"}
|
|
|
|
|
|
|
|
|
if path.is_file():
|
|
|
return {
|
|
|
"name": path.name,
|
|
|
"type": "file"
|
|
|
}
|
|
|
|
|
|
|
|
|
children = []
|
|
|
try:
|
|
|
entries = sorted(path.iterdir(), key=lambda p: p.name.lower())
|
|
|
except Exception:
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
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'")
|
|
|
|
|
|
|
|
|
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"
|
|
|
)
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
path = Path(target).resolve()
|
|
|
|
|
|
|
|
|
if not str(path).startswith(str(DATA_ROOT)):
|
|
|
raise HTTPException(status_code=400, detail="Path must be inside /data/")
|
|
|
|
|
|
|
|
|
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"
|
|
|
)
|
|
|
|
|
|
|
|
|
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)
|
|
|
} |