Files page: thumbnails + view + download (email access to finished work)

This commit is contained in:
2026-08-26 15:30:15 +10:00
parent 1cc159ef80
commit 188e9a0dd6
2 changed files with 58 additions and 3 deletions

View File

@@ -23,7 +23,7 @@ from typing import Annotated
from fastapi import Depends, FastAPI, File, Form, HTTPException, Request, UploadFile from fastapi import Depends, FastAPI, File, Form, HTTPException, Request, UploadFile
from fastapi.concurrency import run_in_threadpool from fastapi.concurrency import run_in_threadpool
from fastapi.responses import HTMLResponse, RedirectResponse, Response from fastapi.responses import HTMLResponse, RedirectResponse, Response, Response
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from sqlalchemy import select from sqlalchemy import select
@@ -237,10 +237,55 @@ def _shared_files(username: str) -> list:
out = [] out = []
for f in sorted(user_dir.iterdir()): for f in sorted(user_dir.iterdir()):
if f.is_file(): if f.is_file():
out.append({"name": f.name, "size": f.stat().st_size}) out.append({"name": f.name, "size": f.stat().st_size,
"img": f.suffix.lower() in _IMAGE_MIME})
return out return out
_IMAGE_MIME = {".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg",
".gif": "image/gif", ".webp": "image/webp", ".bmp": "image/bmp",
".svg": "image/svg+xml"}
def _resolve_shared(username: str, name: str):
"""Return a validated path inside this user's shared folder, or None."""
if Path(name).name != name: # block path traversal
return None
p = (SHARED_DIR / username / name).resolve()
base = (SHARED_DIR / username).resolve()
if not str(p).startswith(str(base)):
return None
return p if p.is_file() else None
@app.get("/files/raw/{name}")
async def files_raw(request: Request, name: str) -> Response:
"""Inline the file (images render as a preview)."""
user = await _current_user(request)
if user is None:
return RedirectResponse("/login", status_code=303)
p = _resolve_shared(user.username, name)
if p is None:
return Response(b"Not found", status_code=404)
media = _IMAGE_MIME.get(p.suffix.lower(), "application/octet-stream")
return Response(content=p.read_bytes(), media_type=media)
@app.get("/files/dl/{name}")
async def files_dl(request: Request, name: str) -> Response:
"""Download (forced attachment) a file so it can be saved/emailed."""
user = await _current_user(request)
if user is None:
return RedirectResponse("/login", status_code=303)
p = _resolve_shared(user.username, name)
if p is None:
return Response(b"Not found", status_code=404)
media = _IMAGE_MIME.get(p.suffix.lower(), "application/octet-stream")
return Response(content=p.read_bytes(), media_type=media,
headers={"Content-Disposition": f'attachment; filename="{p.name}"'})
@app.get("/files", response_class=HTMLResponse) @app.get("/files", response_class=HTMLResponse)
async def files_page(request: Request) -> HTMLResponse: async def files_page(request: Request) -> HTMLResponse:
user = await _current_user(request) user = await _current_user(request)

View File

@@ -27,7 +27,17 @@
{% if files %} {% if files %}
<ul class="file-list"> <ul class="file-list">
{% for f in files %} {% for f in files %}
<li><span class="file-name" title="{{ f.name }}">{{ f.name }}</span><span class="faint">{{ (f.size/1024)|round(1) }} KB</span></li> <li style="align-items:center;gap:12px">
{% if f.img %}
<img src="/files/raw/{{ f.name }}" alt="{{ f.name }}" style="width:56px;height:56px;object-fit:cover;border-radius:8px;border:1px solid var(--hairline,#e6e6e6)">
{% else %}
<span style="width:56px;height:56px;display:inline-flex;align-items:center;justify-content:center;border:1px solid var(--hairline,#e6e6e6);border-radius:8px;color:#999;font-size:11px">file</span>
{% endif %}
<span class="file-name" title="{{ f.name }}" style="flex:1">{{ f.name }}</span>
<span class="faint">{{ (f.size/1024)|round(1) }} KB</span>
<a class="button button-utility" href="/files/raw/{{ f.name }}" target="_blank" title="View">View</a>
<a class="button button-utility" href="/files/dl/{{ f.name }}" title="Download">Download ↓</a>
</li>
{% endfor %} {% endfor %}
</ul> </ul>
<p class="faint margin-top" style="font: var(--type-caption)"> <p class="faint margin-top" style="font: var(--type-caption)">