/api/files is the library's home: list a user's done files (by folder, searched, sorted, or the ones shared with them), rename / move / keep / delete a file, make, rename and delete own folders (a deleted folder's files go back to their source folders), and set how long any folder keeps a file — done files shift by the difference, never closer than the GC's warning window. An upload can name its folder (an own folder or Uploads), checked before the body is read and again before the file lands; it is hashed while it streams, and any file can be hashed on first ask. The download says the hash (Repr-Digest) once it is known; a new file at an asset's path forgets it. Uploads, deletes, keeps, moves and folder changes are audited (ids only). The fileless "expired" rows of the old retention are deleted by migration 0103: an expired file now leaves the library. The shared route helpers moved to routes/files.py; the old /api/downloads library routes delegate to the new ones until the client moves over.
63 lines
2.4 KiB
Python
63 lines
2.4 KiB
Python
"""Migration 0103's data step, DB-lane: the fileless "expired" rows of the old retention go, every
|
|
other job stays.
|
|
|
|
An expired file now leaves the library (`downloads.gc`); before, it stayed as a done job with no
|
|
asset. Left in place, those rows would sit unseen in no listing forever.
|
|
"""
|
|
|
|
from alembic import command
|
|
from alembic.config import Config
|
|
from sqlalchemy import text
|
|
|
|
from app.db import engine
|
|
|
|
|
|
def test_the_fileless_expired_rows_go_and_every_other_job_stays(db, make_user):
|
|
uid = make_user().id
|
|
cfg = Config("alembic.ini")
|
|
command.downgrade(cfg, "0102_file_folders")
|
|
try:
|
|
with engine.begin() as conn:
|
|
folder = conn.execute(
|
|
text(
|
|
"INSERT INTO file_folders (user_id, kind, source_key, retention_days)"
|
|
" VALUES (:u, 'source', 'youtube', 30) RETURNING id"
|
|
),
|
|
{"u": uid},
|
|
).scalar_one()
|
|
asset = conn.execute(
|
|
text(
|
|
"INSERT INTO media_assets (source_kind, source_ref, format_sig, status, ref_count)"
|
|
" VALUES ('youtube', 'abcdefghijk', 'sig', 'ready', 1) RETURNING id"
|
|
)
|
|
).scalar_one()
|
|
|
|
def job(status, asset_id):
|
|
return conn.execute(
|
|
text(
|
|
"INSERT INTO download_jobs (user_id, asset_id, source_kind, source_ref,"
|
|
" profile_snapshot, status, folder_id) VALUES (:u, :a, 'youtube',"
|
|
" 'abcdefghijk', '{}', :s, :f) RETURNING id"
|
|
),
|
|
{"u": uid, "a": asset_id, "s": status, "f": folder},
|
|
).scalar_one()
|
|
|
|
expired = job("done", None)
|
|
alive = job("done", asset)
|
|
failed = job("error", None) # a failed job has no file either, and is not expired
|
|
queued = job("queued", None)
|
|
finally:
|
|
command.upgrade(cfg, "head")
|
|
|
|
with engine.connect() as conn:
|
|
left = set(conn.execute(text("SELECT id FROM download_jobs")).scalars())
|
|
has_hash = conn.execute(
|
|
text(
|
|
"SELECT count(*) FROM information_schema.columns"
|
|
" WHERE table_name = 'media_assets' AND column_name = 'sha256'"
|
|
)
|
|
).scalar()
|
|
assert left == {alive, failed, queued}
|
|
assert expired not in left
|
|
assert has_hash == 1
|