diff options
Diffstat (limited to 'backend/app/main.py')
| -rw-r--r-- | backend/app/main.py | 100 |
1 files changed, 100 insertions, 0 deletions
diff --git a/backend/app/main.py b/backend/app/main.py index 261b45a..886bd9e 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,6 +1,7 @@ from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import StreamingResponse, FileResponse +from fastapi import UploadFile, File from pydantic import BaseModel from app.schemas import NodeRunRequest, NodeRunResponse, MergeStrategy, Role, Message, Context, LLMConfig, ModelProvider, ReasoningEffort from app.services.llm import llm_streamer, generate_title @@ -9,6 +10,7 @@ import os import json import shutil from typing import List, Literal, Optional +from uuid import uuid4 load_dotenv() @@ -49,6 +51,12 @@ def archive_root(user: str) -> str: return os.path.join(ensure_user_root(user), "archive") +def files_root(user: str) -> str: + root = os.path.join(ensure_user_root(user), "files") + os.makedirs(root, exist_ok=True) + return root + + def migrate_legacy_layout(user: str): """ Migrate from legacy ./projects/<user> and legacy archive folders to the new data/<user>/ structure. @@ -136,6 +144,15 @@ class RenameRequest(BaseModel): new_name: Optional[str] = None new_path: Optional[str] = None +class FileMeta(BaseModel): + id: str + name: str + size: int + mime: str + created_at: float + provider: Optional[str] = None + provider_file_id: Optional[str] = None + class FolderRequest(BaseModel): user: str = DEFAULT_USER path: str # relative folder path @@ -375,6 +392,27 @@ def archived_path(user: str) -> str: root = archive_root(user) return os.path.join(root, ARCHIVE_FILENAME) +# ---------------- Files (uploads) ---------------- +def files_index_path(user: str) -> str: + return os.path.join(files_root(user), "index.json") + + +def load_files_index(user: str) -> List[FileMeta]: + path = files_index_path(user) + if not os.path.exists(path): + return [] + with open(path, "r", encoding="utf-8") as f: + data = json.load(f) + return [FileMeta(**item) for item in data] + + +def save_files_index(user: str, items: List[FileMeta]): + path = files_index_path(user) + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, "w", encoding="utf-8") as f: + json.dump([item.model_dump() for item in items], f, ensure_ascii=False, indent=2) + +# ------------------------------------------------- @app.get("/api/projects/archived") def get_archived_nodes(user: str = DEFAULT_USER): @@ -401,3 +439,65 @@ def save_archived_nodes(payload: dict): return {"ok": True} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/api/files") +def list_files(user: str = DEFAULT_USER): + migrate_legacy_layout(user) + items = load_files_index(user) + return {"files": [item.model_dump() for item in items]} + + +@app.post("/api/files/upload") +async def upload_file(user: str = DEFAULT_USER, file: UploadFile = File(...)): + migrate_legacy_layout(user) + items = load_files_index(user) + file_id = str(uuid4()) + dest_root = files_root(user) + dest_path = os.path.join(dest_root, file_id) + try: + content = await file.read() + size = len(content) + with open(dest_path, "wb") as f: + f.write(content) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + meta = FileMeta( + id=file_id, + name=file.filename, + size=size, + mime=file.content_type or "application/octet-stream", + created_at=os.path.getmtime(dest_path), + ) + items.append(meta) + save_files_index(user, items) + return {"file": meta} + + +@app.get("/api/files/download") +def download_file(user: str = DEFAULT_USER, file_id: str = ""): + migrate_legacy_layout(user) + items = load_files_index(user) + meta = next((i for i in items if i.id == file_id), None) + if not meta: + raise HTTPException(status_code=404, detail="file not found") + path = os.path.join(files_root(user), file_id) + if not os.path.exists(path): + raise HTTPException(status_code=404, detail="file missing on disk") + return FileResponse(path, filename=meta.name, media_type=meta.mime) + + +@app.post("/api/files/delete") +def delete_file(user: str = DEFAULT_USER, file_id: str = ""): + migrate_legacy_layout(user) + items = load_files_index(user) + meta = next((i for i in items if i.id == file_id), None) + if not meta: + raise HTTPException(status_code=404, detail="file not found") + path = os.path.join(files_root(user), file_id) + if os.path.exists(path): + os.remove(path) + items = [i for i in items if i.id != file_id] + save_files_index(user, items) + return {"ok": True} |
