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 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)
|
||||||
|
|||||||
@@ -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)">
|
||||||
|
|||||||
Reference in New Issue
Block a user