{"record":{"id":"d9c0cdba9e60b65e","repo":"harry0703/MoneyPrinterTurbo","slug":"background-music-upload-is-not-seekable","errorCode":null,"errorMessage":"background music upload is not seekable","messagePattern":"background music upload is not seekable","errorType":"validation","errorClass":"BgmUploadError","httpStatus":400,"severity":"error","filePath":"app/services/bgm.py","lineNumber":189,"sourceCode":"    将上传流写入同目录临时文件，并返回安全文件名、临时路径和字节数。\n\n    WebUI 的上传预检和最终持久化必须使用完全相同的分块读取、大小限制与文件名\n    规则，否则可能出现界面显示可用、点击生成后却被服务端拒绝的状态分裂。\n    临时文件由调用方在完成音频探测后删除或原子替换。\n    \"\"\"\n    safe_name = sanitize_upload_filename(filename)\n    try:\n        target_dir = uploaded_bgm_dir(create=True)\n    except OSError as exc:\n        raise BgmServiceError(\"failed to prepare background music storage\") from exc\n    temp_path = \"\"\n    total_bytes = 0\n\n    try:\n        try:\n            source.seek(0)\n        except (AttributeError, OSError) as exc:\n            raise BgmUploadError(\"background music upload is not seekable\") from exc\n\n        # 保留原始扩展名便于 FFmpeg 针对无容器头的 AAC 等格式选择正确的\n        # demuxer；临时文件仍放在目标目录，以保证最终 os.replace 是原子操作。\n        descriptor, temp_path = tempfile.mkstemp(\n            prefix=_INTERNAL_UPLOAD_PREFIX,\n            suffix=Path(safe_name).suffix.lower(),\n            dir=target_dir,\n        )\n        with os.fdopen(descriptor, \"wb\") as output:\n            while True:\n                chunk = source.read(_COPY_CHUNK_BYTES)\n                if not chunk:\n                    break\n                if not isinstance(chunk, (bytes, bytearray, memoryview)):\n                    raise BgmUploadError(\"background music upload must be binary\")\n                total_bytes += len(chunk)\n                if total_bytes > MAX_BGM_UPLOAD_BYTES:\n                    raise BgmUploadError(\"background music file exceeds the 30 MB limit\")","sourceCodeStart":171,"sourceCodeEnd":207,"githubUrl":"https://github.com/harry0703/MoneyPrinterTurbo/blob/1f9f19c2021a68d04df228f33e9099a0c947f6f8/app/services/bgm.py#L171-L207","documentation":"Raised in _stage_bgm_upload when source.seek(0) raises AttributeError (the stream object has no seek) or OSError (seek exists but fails, e.g. a non-regular file descriptor). The seek is mandatory: both the WebUI's upload preflight and the final persistence must read from byte 0 with identical chunking and size limits, otherwise the UI could show a file as valid while the server later rejects it — the state-split the staging function is designed to prevent.","triggerScenarios":"Passing a non-seekable stream to the upload API: an HTTP request body iterator, a pipe (e.g. subprocess stdout), an open('file') on a stream-only pseudo-file, or a wrapper class without a seek method; also a SpooledTemporaryFile already rolled to disk and closed by a prior error.","commonSituations":"Clients streaming uploads directly from network objects instead of buffering; middleware wrapping UploadFile.file in a forward-only reader; testing with io.RawIOBase subclasses that don't implement seek.","solutions":["Buffer the bytes first: data = source.read() then pass io.BytesIO(data) to the upload.","If wrapping streams, ensure the wrapper delegates seek/tell and that the underlying object supports random access.","For framework code, use the framework's spooled upload file object directly rather than an iterator adapter."],"exampleFix":"# before\ndef upload(source):  # source is a forward-only stream\n    safe = save_bgm_upload(name, source)\n\n# after\nimport io\ndef upload(source):\n    source.seek(0) if hasattr(source, \"seek\") else None\n    data = source.read()\n    safe = save_bgm_upload(name, io.BytesIO(data))","handlingStrategy":"validation","validationCode":"import io\ndef ensure_seekable(source):\n    if not (hasattr(source, \"seek\") and source.seekable()):\n        return io.BytesIO(source.read())\n    source.seek(0)\n    return source","typeGuard":"import io\ndef is_seekable_stream(source) -> bool:\n    return isinstance(source, (io.BytesIO, io.BufferedIOBase)) and source.seekable()","tryCatchPattern":"try:\n    safe_name = save_bgm_upload(name, source)\nexcept BgmUploadError as e:\n    if \"not seekable\" in str(e):\n        source = io.BytesIO(original_bytes)  # buffer, then retry once\n        safe_name = save_bgm_upload(name, source)\n    else:\n        raise","preventionTips":["Buffer streamed bodies into BytesIO before handing them to upload APIs.","Don't wrap upload files in forward-only iterators.","seek(0) before any retry that reuses the same file object."],"tags":["file-io","seekable-stream","upload","bgm"],"backgroundTag":null,"analyzedSha":"1f9f19c2021a68d04df228f33e9099a0c947f6f8","analyzedAt":"2026-08-14T19:41:05.568Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}