Skip to content

Backup restore chokes on large backups: API recover loads whole upload into RAM (+ synchronous-extraction timeout) #3142

Description

@vpetersson

Symptom

Backup restore fails on devices with a real content library (multi-GB backups) even though the same operation works on a small demo device. Because the failure mode is an OOM kill / request timeout, it shows up as "restore doesn't work" reports rather than a clean Sentry stack.

Root cause 1 — restore loads the whole upload into RAM (fixed here)

RecoverViewMixin.post (the /api/v1|v2/recover endpoint) staged the uploaded archive with:

with open(location, 'wb') as f:
    f.write(file_upload.read())   # entire archive into memory

A backup is every image + video asset on the device, so file_upload.read() allocates a multi-GB bytes object and OOM-kills the worker on a 1 GB Pi. The HTML recover view (settings_recover) was already fixed to stream via file_upload.chunks(); the API path was left behind. PR streams the API path the same way.

Root cause 2 — restore is synchronous and can time out (follow-up)

Even with streaming, recover() extracts the whole archive inside the request handler. Extracting a multi-GB library to a slow SD card can exceed the request/proxy timeout — the same failure the download side hit and solved by streaming (#2987) / async iteration (#3073). The restore equivalent would be to move extraction to a background Celery task with progress, or at least confirm the request timeout is generous enough. Not addressed here.

Root cause 3 — no ENOSPC handling on restore (follow-up)

A large restore that fills the SD card (staging the upload, or during extraction) raises an uncaught OSError → 500. Reusing the is_disk_full helper (added in #3133) to return an actionable "disk full" message would make this a clean operator error instead of a crash.

Note on the download/generate side

Already hardened: stream_backup / astream_backup stream the tar.gz with a flat memory footprint and no SD staging (#2987, #3073), and gzip level was dropped to 1 to keep generation within the browser timeout. The remaining API create_backup path still stages the whole archive to disk before returning a filename, which needs free SD space — worth revisiting but lower impact than the restore path.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions