diff --git a/README.md b/README.md index e33bef1..079e52c 100644 --- a/README.md +++ b/README.md @@ -72,4 +72,8 @@ ssh sam@192.168.20.13 'cd /home/sam/Docker/Containers/dsh && docker compose buil pi host (see `deploy/pi-dash-sync.sh`; run it on a cron to keep it fresh). - Kids' accounts get a reduced view (no AI/chat, no admin-only items). -See `docs/` for the website inventory, the audio plan, and the dsh plugin plan. \ No newline at end of file +See `docs/` for the website inventory, the audio plan, and the dsh plugin plan. +## How to get files into the tools (e.g. GIMP) +Upload from your browser via the console → **Files** (top nav). Files appear in +GIMP (and video/audio) under `/media//`. GIMP runs on `.13`, so +use the upload page rather than a local file path. diff --git a/docker-compose.yml b/docker-compose.yml index 52b7aa3..f30dd18 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -89,6 +89,7 @@ services: PI_DASHBOARD_DIR: /pi-dashboard volumes: - /mnt/data/family-home-lab/pi-dashboard:/pi-dashboard:ro + - /mnt/data/family-home-lab/shared-media:/shared-media:rw depends_on: - postgres - redis @@ -185,7 +186,7 @@ services: - "8087:3000" volumes: - gimp-config:/config - - /mnt/data/family-home-lab/shared-media:/media:ro + - /mnt/data/family-home-lab/shared-media:/media:rw - /home/sam:/home/sam:ro - /home/sam/Downloads:/home/abc/Downloads - /home/sam/Pictures:/home/abc/Pictures diff --git a/portal/main.py b/portal/main.py index 88ddd9c..afacfcb 100644 --- a/portal/main.py +++ b/portal/main.py @@ -223,6 +223,67 @@ async def tool_embed(request: Request, tool_id: str) -> HTMLResponse: return templates.TemplateResponse(request, "embed.html", ctx) + +SHARED_DIR = Path(os.getenv("SHARED_DIR", "/shared-media")) + + +def _shared_files(username: str) -> list: + """List this user's uploaded files in the shared-media folder (created on demand).""" + if not SHARED_DIR.is_dir(): + return [] + user_dir = SHARED_DIR / username + if not user_dir.is_dir(): + return [] + out = [] + for f in sorted(user_dir.iterdir()): + if f.is_file(): + out.append({"name": f.name, "size": f.stat().st_size}) + return out + + +@app.get("/files", response_class=HTMLResponse) +async def files_page(request: Request) -> HTMLResponse: + user = await _current_user(request) + if user is None: + return RedirectResponse("/login", status_code=303) + return templates.TemplateResponse( + request, "files.html", {"user": user, "files": _shared_files(user.username)} + ) + + +@app.post("/files/upload") +async def files_upload(request: Request, file: Annotated[UploadFile, File()]) -> HTMLResponse: + user = await _current_user(request) + if user is None: + return RedirectResponse("/login", status_code=303) + error = None + if not file or not file.filename: + error = "No file chosen." + else: + try: + name = Path(file.filename).name.lower() + # allow images + common docs (photos are the main use case) + if not name.endswith((".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".svg", ".psd", ".xcf", ".pdf", ".txt", ".md")): + error = f"File type not allowed: {file.filename}" + else: + SHARED_DIR.mkdir(exist_ok=True) + user_dir = SHARED_DIR / user.username + user_dir.mkdir(exist_ok=True) + data = await file.read() + if len(data) > 50 * 1024 * 1024: + error = "File too large (max 50 MB)." + else: + (user_dir / file.filename).write_bytes(data) + except Exception as exc: # noqa: BLE001 + logger.warning("files upload failed", exc_info=True) + error = f"Upload failed: {exc}" + return templates.TemplateResponse( + request, "files.html", + {"user": user, "files": _shared_files(user.username), + "error": error, "ok": error is None and file and file.filename}, + ) + + @app.get("/admin", response_class=HTMLResponse) async def admin_panel(request: Request) -> HTMLResponse: user = await _current_user(request) diff --git a/portal/templates/base.html b/portal/templates/base.html index f355792..0a6982a 100644 --- a/portal/templates/base.html +++ b/portal/templates/base.html @@ -15,6 +15,7 @@ Home Lab