Files page: thumbnails + view + download (email access to finished work)
This commit is contained in:
@@ -23,7 +23,7 @@ from typing import Annotated
|
||||
|
||||
from fastapi import Depends, FastAPI, File, Form, HTTPException, Request, UploadFile
|
||||
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.templating import Jinja2Templates
|
||||
from sqlalchemy import select
|
||||
@@ -237,10 +237,55 @@ def _shared_files(username: str) -> list:
|
||||
out = []
|
||||
for f in sorted(user_dir.iterdir()):
|
||||
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
|
||||
|
||||
|
||||
|
||||
_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)
|
||||
async def files_page(request: Request) -> HTMLResponse:
|
||||
user = await _current_user(request)
|
||||
|
||||
@@ -27,7 +27,17 @@
|
||||
{% if files %}
|
||||
<ul class="file-list">
|
||||
{% 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 %}
|
||||
</ul>
|
||||
<p class="faint margin-top" style="font: var(--type-caption)">
|
||||
|
||||
Reference in New Issue
Block a user