feat(files): the Files page — the library in one aligned list, its folders in a rail
Files is the one library: every done file, the ones shared with me, or one folder's, picked in a side rail (sources, own folders, the storage meter) and mirrored to ?folder=. One table with every kind of value in its own column; Copy link, Share and Save in sight, every action on a right click: play, cut, rename in the row, move to an own folder or back home, keep, details, re-download, delete. The Downloads page keeps only the queue until the tray replaces it. The old /api/downloads library routes are gone: list shared, upload, the details PATCH, delete, the file and the usage now live under /api/files, and a listed file says which source it came from. The job's "expired" flag is gone with the state it described.
This commit is contained in:
@@ -212,7 +212,7 @@ def test_an_anonymous_upload_is_refused_before_a_byte_of_it_is_read(client):
|
||||
multipart the old route would have spooled, not the raw body the new one expects."""
|
||||
body, pulled = _counting_multipart()
|
||||
r = client.post(
|
||||
"/api/downloads/upload",
|
||||
"/api/files/upload",
|
||||
content=body,
|
||||
headers={"content-type": f"multipart/form-data; boundary={_BOUNDARY}"},
|
||||
)
|
||||
@@ -227,7 +227,7 @@ def test_an_authenticated_upload_still_has_its_body_read(as_user, make_user, _ro
|
||||
body, pulled = _counting_body()
|
||||
c = as_user(make_user())
|
||||
r = c.post(
|
||||
"/api/downloads/upload",
|
||||
"/api/files/upload",
|
||||
content=body,
|
||||
headers={"content-type": "application/octet-stream", "x-upload-filename": "clip.bin"},
|
||||
)
|
||||
@@ -281,7 +281,7 @@ def test_upload_route_generic_forces_download(
|
||||
_probe(monkeypatch, (None, None, None, None, None))
|
||||
c = as_user(make_user())
|
||||
r = c.post(
|
||||
"/api/downloads/upload",
|
||||
"/api/files/upload",
|
||||
content=b"<script>alert(1)</script>",
|
||||
headers=upload_headers("report.html"),
|
||||
)
|
||||
@@ -290,7 +290,7 @@ def test_upload_route_generic_forces_download(
|
||||
assert job["asset"]["media_kind"] == "file"
|
||||
assert job["can_download"] is True
|
||||
# The served file must be a neutral force-download attachment — never inline text/html.
|
||||
fr = c.get(f"/api/downloads/{job['id']}/file")
|
||||
fr = c.get(f"/api/files/{job['id']}/file")
|
||||
assert fr.status_code == 200
|
||||
assert fr.headers["content-type"].startswith("application/octet-stream")
|
||||
assert "attachment" in fr.headers["content-disposition"].lower()
|
||||
@@ -307,7 +307,7 @@ def test_upload_route_media_html_extension_not_served_as_html(
|
||||
_probe(monkeypatch, ("h264", "aac", 320, 240, 5)) # ffprobe says it's video…
|
||||
c = as_user(make_user())
|
||||
r = c.post(
|
||||
"/api/downloads/upload",
|
||||
"/api/files/upload",
|
||||
content=b"<script>alert(1)</script>",
|
||||
headers=upload_headers("evil.html"),
|
||||
)
|
||||
@@ -324,7 +324,7 @@ def test_upload_route_media_html_extension_not_served_as_html(
|
||||
db.commit()
|
||||
|
||||
# Authenticated endpoint: never text/html.
|
||||
fr = c.get(f"/api/downloads/{job['id']}/file")
|
||||
fr = c.get(f"/api/files/{job['id']}/file")
|
||||
assert fr.status_code == 200
|
||||
assert fr.headers["content-type"].startswith("application/octet-stream")
|
||||
assert "attachment" in fr.headers["content-disposition"].lower()
|
||||
@@ -348,7 +348,7 @@ def test_public_watch_file_forces_download_for_generic_file(
|
||||
_probe(monkeypatch, (None, None, None, None, None))
|
||||
c = as_user(make_user())
|
||||
job = c.post(
|
||||
"/api/downloads/upload",
|
||||
"/api/files/upload",
|
||||
content=b"<script>alert(1)</script>",
|
||||
headers=upload_headers("notes.html"),
|
||||
).json()
|
||||
@@ -369,7 +369,7 @@ def test_public_watch_file_serves_media_inline_with_real_type(
|
||||
_probe(monkeypatch, ("h264", "aac", 640, 360, 12))
|
||||
c = as_user(make_user())
|
||||
job = c.post(
|
||||
"/api/downloads/upload",
|
||||
"/api/files/upload",
|
||||
content=b"fake-mp4-bytes",
|
||||
headers=upload_headers("clip.mp4"),
|
||||
).json()
|
||||
@@ -389,7 +389,7 @@ def test_upload_route_enforces_per_file_cap(
|
||||
db.commit()
|
||||
c = as_user(make_user())
|
||||
r = c.post(
|
||||
"/api/downloads/upload",
|
||||
"/api/files/upload",
|
||||
content=b"way over the cap",
|
||||
headers=upload_headers("big.bin"),
|
||||
)
|
||||
@@ -412,14 +412,10 @@ def test_the_cap_admits_a_body_of_EXACTLY_the_cap_and_refuses_one_byte_more(
|
||||
db.commit()
|
||||
c = as_user(make_user())
|
||||
|
||||
at_the_cap = c.post(
|
||||
"/api/downloads/upload", content=payload, headers=upload_headers("exact.bin")
|
||||
)
|
||||
at_the_cap = c.post("/api/files/upload", content=payload, headers=upload_headers("exact.bin"))
|
||||
assert at_the_cap.status_code == 200, at_the_cap.text
|
||||
|
||||
over = c.post(
|
||||
"/api/downloads/upload", content=payload + b"x", headers=upload_headers("over.bin")
|
||||
)
|
||||
over = c.post("/api/files/upload", content=payload + b"x", headers=upload_headers("over.bin"))
|
||||
assert over.status_code == 422
|
||||
assert over.json()["detail"] == {"code": "upload_too_large", "limit": len(payload)}
|
||||
|
||||
@@ -437,9 +433,7 @@ def test_a_non_ascii_filename_survives_the_header_round_trip(
|
||||
"""
|
||||
_probe(monkeypatch, (None, None, None, None, None))
|
||||
c = as_user(make_user())
|
||||
r = c.post(
|
||||
"/api/downloads/upload", content=b"data", headers=upload_headers("nyaralás ősszel.bin")
|
||||
)
|
||||
r = c.post("/api/files/upload", content=b"data", headers=upload_headers("nyaralás ősszel.bin"))
|
||||
assert r.status_code == 200, r.text
|
||||
asset = db.execute(select(MediaAsset).order_by(MediaAsset.id.desc())).scalars().first()
|
||||
assert asset is not None
|
||||
@@ -467,9 +461,7 @@ def test_a_long_display_name_is_truncated_on_EVERY_route_that_takes_one(
|
||||
c = as_user(make_user())
|
||||
|
||||
# 1. the upload route — the name arrives in a request header.
|
||||
up = c.post(
|
||||
"/api/downloads/upload", content=b"video", headers=upload_headers("a.mp4", long_name)
|
||||
)
|
||||
up = c.post("/api/files/upload", content=b"video", headers=upload_headers("a.mp4", long_name))
|
||||
assert up.status_code == 200, up.text
|
||||
assert up.json()["display_name"] == bound
|
||||
|
||||
@@ -494,10 +486,10 @@ def test_a_long_display_name_is_truncated_on_EVERY_route_that_takes_one(
|
||||
assert edit.status_code == 200, edit.text
|
||||
assert edit.json()["display_name"] == bound
|
||||
|
||||
# 4 + 5. the metadata PATCH's two fields.
|
||||
# 4 + 5. the library PATCH's two fields.
|
||||
patched = c.patch(
|
||||
f"/api/downloads/{up.json()['id']}",
|
||||
json={"display_name": long_name, "display_uploader": long_name},
|
||||
f"/api/files/{up.json()['id']}",
|
||||
json={"name": long_name, "uploader": long_name},
|
||||
)
|
||||
assert patched.status_code == 200, patched.text
|
||||
assert patched.json()["display_name"] == bound
|
||||
@@ -610,12 +602,11 @@ DISPLAY_WRITE_SITES = {
|
||||
("routes/downloads.py", "enqueue_download"): {"display_text(body.display_name)"},
|
||||
("routes/downloads.py", "enqueue_edit"): {"display_text(body.display_name)"},
|
||||
("routes/downloads.py", "enqueue_clip"): {"display_text(body.display_name)", "display_name"},
|
||||
("routes/downloads.py", "update_download_meta"): {
|
||||
"display_text(body.display_name)",
|
||||
"display_text(body.display_uploader)",
|
||||
},
|
||||
("routes/files.py", "ingest_spooled"): {"upload.display_name"},
|
||||
("routes/files.py", "update_file"): {"display_text(body.name)"},
|
||||
("routes/files.py", "update_file"): {
|
||||
"display_text(body.name)",
|
||||
"display_text(body.uploader)",
|
||||
},
|
||||
("routes/files.py", "spool_upload"): {
|
||||
"display_text(_header_text(request, UPLOAD_DISPLAY_NAME_HEADER))"
|
||||
},
|
||||
@@ -666,7 +657,7 @@ def test_the_display_name_header_reaches_the_job_and_is_bounded(
|
||||
c = as_user(make_user())
|
||||
|
||||
ok = c.post(
|
||||
"/api/downloads/upload",
|
||||
"/api/files/upload",
|
||||
content=b"data",
|
||||
headers=upload_headers("clip.bin", "Nyári vakáció"),
|
||||
)
|
||||
@@ -675,7 +666,7 @@ def test_the_display_name_header_reaches_the_job_and_is_bounded(
|
||||
|
||||
long_name = "n" * 300
|
||||
bounded = c.post(
|
||||
"/api/downloads/upload", content=b"data2", headers=upload_headers("clip2.bin", long_name)
|
||||
"/api/files/upload", content=b"data2", headers=upload_headers("clip2.bin", long_name)
|
||||
)
|
||||
assert bounded.status_code == 200, bounded.text
|
||||
assert bounded.json()["display_name"] == "n" * DISPLAY_TEXT_MAX
|
||||
@@ -718,7 +709,7 @@ def test_the_upload_holds_no_db_transaction_while_the_body_streams(
|
||||
monkeypatch.setattr(files_routes, "release_read_connection", spy_release)
|
||||
monkeypatch.setattr(files_routes, "capped_stream_to", spy_stream)
|
||||
c = as_user(make_user())
|
||||
r = c.post("/api/downloads/upload", content=b"data", headers=upload_headers("held.bin"))
|
||||
r = c.post("/api/files/upload", content=b"data", headers=upload_headers("held.bin"))
|
||||
assert r.status_code == 200, r.text
|
||||
assert seen["before"] is True, (
|
||||
"nothing had opened a transaction before the release, so this test proves nothing — the "
|
||||
@@ -740,14 +731,14 @@ def test_the_usage_route_reports_the_per_file_upload_cap(as_user, make_user, db)
|
||||
from app import sysconfig
|
||||
|
||||
c = as_user(make_user())
|
||||
r = c.get("/api/downloads/usage")
|
||||
r = c.get("/api/files/usage")
|
||||
assert r.status_code == 200, r.text
|
||||
assert r.json()["upload_max_bytes"] == sysconfig.get_int(db, "download_upload_max_bytes")
|
||||
|
||||
# And it is the KNOB, not a constant: a lowered override must show through.
|
||||
sysconfig.set_value(db, "download_upload_max_bytes", 4096)
|
||||
db.commit()
|
||||
assert c.get("/api/downloads/usage").json()["upload_max_bytes"] == 4096
|
||||
assert c.get("/api/files/usage").json()["upload_max_bytes"] == 4096
|
||||
|
||||
|
||||
def test_a_refused_upload_leaves_nothing_in_the_staging_directory(
|
||||
@@ -765,7 +756,7 @@ def test_a_refused_upload_leaves_nothing_in_the_staging_directory(
|
||||
staging = Path(settings.files_root) / ".staging" / "uploads"
|
||||
c = as_user(make_user())
|
||||
|
||||
r = c.post("/api/downloads/upload", content=b"x" * 4096, headers=upload_headers("leaky.bin"))
|
||||
r = c.post("/api/files/upload", content=b"x" * 4096, headers=upload_headers("leaky.bin"))
|
||||
assert r.status_code == 422
|
||||
assert staging.exists(), "the dependency did not get as far as creating the staging dir"
|
||||
assert list(staging.iterdir()) == [], "a refused upload left its partial file behind"
|
||||
@@ -787,7 +778,7 @@ def test_upload_cap_cannot_be_disabled_with_zero(db):
|
||||
|
||||
def test_upload_route_rejects_empty_file(as_user, make_user, _root, monkeypatch, upload_headers):
|
||||
c = as_user(make_user())
|
||||
r = c.post("/api/downloads/upload", content=b"", headers=upload_headers("empty.bin"))
|
||||
r = c.post("/api/files/upload", content=b"", headers=upload_headers("empty.bin"))
|
||||
assert r.status_code == 400
|
||||
assert r.json()["detail"]["code"] == "upload_empty"
|
||||
|
||||
@@ -795,7 +786,7 @@ def test_upload_route_rejects_empty_file(as_user, make_user, _root, monkeypatch,
|
||||
def test_redownload_rejects_an_upload(as_user, make_user, _root, monkeypatch, upload_headers):
|
||||
_probe(monkeypatch, (None, None, None, None, None))
|
||||
c = as_user(make_user())
|
||||
job = c.post("/api/downloads/upload", content=b"data", headers=upload_headers("a.bin")).json()
|
||||
job = c.post("/api/files/upload", content=b"data", headers=upload_headers("a.bin")).json()
|
||||
r = c.post(f"/api/downloads/{job['id']}/redownload")
|
||||
assert r.status_code == 409
|
||||
|
||||
@@ -805,7 +796,7 @@ def test_edit_rejects_a_generic_upload_source(
|
||||
):
|
||||
_probe(monkeypatch, (None, None, None, None, None))
|
||||
c = as_user(make_user())
|
||||
job = c.post("/api/downloads/upload", content=b"data", headers=upload_headers("a.bin")).json()
|
||||
job = c.post("/api/files/upload", content=b"data", headers=upload_headers("a.bin")).json()
|
||||
r = c.post(
|
||||
"/api/downloads/edit",
|
||||
json={"source_job_id": job["id"], "edit_spec": {"trim": {"start_s": 0, "end_s": 1}}},
|
||||
|
||||
Reference in New Issue
Block a user