fix(downloads): a conversion's commit, a stopped upload, the fast join and a clip's copy hold up
- The fast join gives each ffconcat entry its own media-only format whitelist, so a library file
can no longer be opened as a nested concat list (the list's whitelist was handed down to it).
- A file the guarded probe cannot read is a download-only file, not a guess from yt-dlp's info.
- A conversion's or an ingest's commit that raises is re-read in a fresh session before any file
is removed: a commit that landed keeps the file its row now names.
- An original that cannot be removed after conversion is logged; the upload stays ready.
- An upload whose conversion is cancelled or deleted midway is freed once nothing holds it; a
paused one is kept.
- A shared link says "preparing" only while its video's job is queued or running.
- The admin storage view counts every file on disk ("Files on disk"), not only ready ones.
- A clip's failure title, unknown failure and cancel dialog, and a clip's and an upload's delete
dialog speak of what the job made; an upload's quota refusal explains that the original is kept.
- /watch announces "preparing" as an alert and moves focus to the new Retry after an action.
This commit is contained in:
@@ -83,9 +83,10 @@ def ingest_upload(
|
||||
rel = storage.upload_rel_path(user_id, asset.id, filename)
|
||||
storage.place_file(src_path, settings.download_root, rel)
|
||||
# From here the file is on disk under `.uploads/`. If the transaction fails before it commits, the
|
||||
# asset/job rows roll back but the placed file would be ORPHANED — nothing references it, the
|
||||
# per-user delete path keys off asset.rel_path (which is gone), and the GC orphan sweep skips the
|
||||
# dot-prefixed `.uploads/` tree. So roll back the on-disk side too on any failure.
|
||||
# asset/job rows roll back and nothing names the placed file: the per-user delete path keys off
|
||||
# asset.rel_path (which is gone), and the GC's `.uploads` sweep reaches it only an hour later. So
|
||||
# roll back the on-disk side too — unless the commit itself raised after it landed (see below).
|
||||
asset_id = asset.id
|
||||
try:
|
||||
final = storage.abs_path(settings.download_root, rel)
|
||||
actual_size = final.stat().st_size if final.exists() else size
|
||||
@@ -126,6 +127,11 @@ def ingest_upload(
|
||||
db.commit()
|
||||
except Exception:
|
||||
db.rollback()
|
||||
if _row_landed(db, asset_id) is not False:
|
||||
# The commit reached the server and lost its answer, or the row cannot be read: the file
|
||||
# may be the asset's now, so it is never removed blind — the sweep takes it if no row
|
||||
# names it.
|
||||
raise
|
||||
# The rows are gone; remove the now-unreferenced file so no bytes leak. Clean the poster by its
|
||||
# DETERMINISTIC path (rel with a .jpg suffix, the same path ensure_poster writes to) rather than
|
||||
# ensure_poster's return value: it writes the frame directly to that path and returns None on an
|
||||
@@ -137,3 +143,13 @@ def ingest_upload(
|
||||
raise
|
||||
db.refresh(job)
|
||||
return job
|
||||
|
||||
|
||||
def _row_landed(db: Session, asset_id: int) -> bool | None:
|
||||
"""After the ingest transaction raised: is the asset row there anyway? A commit can reach the
|
||||
server and lose its answer (the connection drops after COMMIT). None when it cannot be read."""
|
||||
try:
|
||||
return db.get(MediaAsset, asset_id) is not None
|
||||
except Exception: # noqa: BLE001 — "cannot tell" is the answer, the caller acts on it
|
||||
# silent-ok: the caller re-raises the ingest's own error.
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user