fix(downloads): a produced file is held to the space left by its real size
yt-dlp's `max_filesize` only stops a plain HTTP file that announces its size: YouTube's fragmented formats never meet it — measured on localdev, a 718 MB download ran to the end with 1 MB left and put its owner four times over the cap. The download now re-reads what is left once its file is made (after the phone-safe pass) and refuses one larger than that as the owner's refusal; an editor clip, which nothing checked, is held the same way, as a player clip already was.
This commit is contained in:
@@ -344,3 +344,51 @@ def test_a_download_over_the_space_left_says_so_and_releases_the_shared_asset(
|
||||
assert (job.status, job.error_code) == ("error", "quota_exceeded")
|
||||
assert job.error == dlerrors.DOWNLOAD_TOO_BIG_MESSAGE
|
||||
assert db.get(MediaAsset, shared.id).status == "pending"
|
||||
|
||||
|
||||
def test_a_download_that_outgrows_the_space_left_unannounced_is_refused_and_not_placed(
|
||||
db, monkeypatch, tmp_path
|
||||
):
|
||||
# yt-dlp's `max_filesize` only stops a plain HTTP file that announces its size: YouTube's
|
||||
# fragmented formats never meet it (measured on localdev: 718 MB fetched with 1 MB left). The
|
||||
# file it produced is held to what is left by its real size, as a clip is.
|
||||
monkeypatch.setattr(settings, "download_root", str(tmp_path))
|
||||
u = User(email="unannounced@example.com")
|
||||
db.add(u)
|
||||
db.flush()
|
||||
db.add(
|
||||
DownloadQuota(user_id=u.id, max_bytes=1000, max_concurrent=2, max_jobs=100, unlimited=False)
|
||||
)
|
||||
held = _sized_asset(db, 400, "unannounced-held")
|
||||
_job(db, u.id, queue_pos=0, status="done", asset_id=held.id)
|
||||
shared = MediaAsset(
|
||||
source_kind="youtube",
|
||||
source_ref="z" * 11,
|
||||
format_sig="s",
|
||||
status="downloading",
|
||||
ref_count=1,
|
||||
)
|
||||
db.add(shared)
|
||||
db.flush()
|
||||
mine = _job(db, u.id, queue_pos=1, status="running", asset_id=shared.id)
|
||||
db.commit()
|
||||
|
||||
def fetched_past_the_cap(job_id, asset_id, url, spec, staging, adapter, **kw):
|
||||
f = staging / "zzzzzzzzzzz.mp4"
|
||||
f.write_bytes(b"v" * 2000) # 600 left
|
||||
return {"id": "z" * 11, "requested_downloads": [{"filepath": str(f)}]}
|
||||
|
||||
monkeypatch.setattr(pipeline, "_extract_with_fallback", fetched_past_the_cap)
|
||||
monkeypatch.setattr(pipeline, "_ensure_phone_safe", lambda _j, produced, *_a: produced)
|
||||
pipeline._download(mine.id, shared.id, "youtube", "z" * 11, {}, u.id)
|
||||
|
||||
db.expire_all()
|
||||
job = db.get(DownloadJob, mine.id)
|
||||
assert (job.status, job.error_code, job.error) == (
|
||||
"error",
|
||||
"quota_exceeded",
|
||||
dlerrors.DOWNLOAD_TOO_BIG_MESSAGE,
|
||||
)
|
||||
asset = db.get(MediaAsset, shared.id)
|
||||
assert (asset.status, asset.rel_path) == ("pending", None)
|
||||
assert not any(p.suffix == ".mp4" for p in tmp_path.rglob("*") if "staging" not in p.parts)
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
"""An editor clip is held to its owner's storage cap by its real size, as a download and a player
|
||||
clip are: its file is new storage on the owner's footprint, and nothing else checked it.
|
||||
|
||||
Real ffmpeg — the size the check reads is the encoder's.
|
||||
"""
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
from app.config import settings
|
||||
from app.downloads import errors as dlerrors
|
||||
from app.downloads import pipeline, quota, service
|
||||
from app.downloads import upload as uploadmod
|
||||
from app.models import DownloadJob, DownloadQuota, MediaAsset
|
||||
|
||||
pytestmark = pytest.mark.skipif(shutil.which("ffmpeg") is None, reason="ffmpeg not available")
|
||||
|
||||
|
||||
def test_an_editor_clip_larger_than_what_is_left_is_refused_and_not_placed(
|
||||
db, make_user, tmp_path, monkeypatch
|
||||
):
|
||||
monkeypatch.setattr(settings, "download_root", str(tmp_path))
|
||||
src = tmp_path / "in.mp4"
|
||||
subprocess.run(
|
||||
[
|
||||
"ffmpeg", "-y", "-loglevel", "error",
|
||||
"-f", "lavfi", "-i", "testsrc2=size=160x120:rate=10:duration=2",
|
||||
"-c:v", "libx264", "-pix_fmt", "yuv420p", str(src),
|
||||
],
|
||||
check=True,
|
||||
) # fmt: skip
|
||||
u = make_user()
|
||||
source = uploadmod.ingest_upload(
|
||||
db, u.id, filename="clip.mp4", src_path=src, size=src.stat().st_size
|
||||
)
|
||||
job = service.enqueue_edit(db, u.id, source, {"trim": {"start_s": 0, "end_s": 1.5}})
|
||||
asset = db.get(MediaAsset, job.asset_id)
|
||||
job.status, asset.status = "running", "downloading" # the worker's claim
|
||||
# Ten bytes left: no clip of a real video fits.
|
||||
db.add(
|
||||
DownloadQuota(
|
||||
user_id=u.id,
|
||||
max_bytes=quota.footprint(db, u.id) + 10,
|
||||
max_concurrent=2,
|
||||
max_jobs=100,
|
||||
unlimited=False,
|
||||
)
|
||||
)
|
||||
db.commit()
|
||||
|
||||
pipeline._process_edit(job.id, asset.id)
|
||||
|
||||
db.expire_all()
|
||||
job, asset = db.get(DownloadJob, job.id), db.get(MediaAsset, job.asset_id)
|
||||
assert (job.status, job.error_code, job.error) == (
|
||||
"error",
|
||||
"quota_exceeded",
|
||||
dlerrors.CLIP_TOO_BIG_MESSAGE,
|
||||
)
|
||||
assert asset.rel_path is None
|
||||
assert not list(tmp_path.glob(".edits/**/*.mp4"))
|
||||
Reference in New Issue
Block a user