fix(dump): stream files during restore to avoid memory pressure

Use a temporary file instead of io.ReadAll when restoring attachments
from a dump. This prevents loading entire files into memory, which could
cause OOM errors for large attachments during restore.
This commit is contained in:
kolaente
2026-02-04 19:33:54 +01:00
parent 82933a0836
commit ab705d7d21

View File

@@ -174,21 +174,8 @@ func Restore(filename string, overrideConfig bool) error {
return fmt.Errorf("could not parse file id %s: %w", i, err)
}
f := &files.File{ID: id}
fc, err := file.Open()
if err != nil {
return fmt.Errorf("could not open file %s: %w", i, err)
}
content, err := io.ReadAll(fc)
_ = fc.Close()
if err != nil {
return fmt.Errorf("could not read file %s: %w", i, err)
}
if err := f.Save(bytes.NewReader(content)); err != nil {
return fmt.Errorf("could not save file: %w", err)
if err := restoreFile(id, file); err != nil {
return fmt.Errorf("could not restore file %s: %w", i, err)
}
log.Infof("Restored file %s", i)
}
@@ -204,6 +191,38 @@ func Restore(filename string, overrideConfig bool) error {
return nil
}
func restoreFile(id int64, zipFile *zip.File) error {
f := &files.File{ID: id}
fc, err := zipFile.Open()
if err != nil {
return fmt.Errorf("could not open zip entry: %w", err)
}
defer fc.Close()
// Create a temporary file to make the content seekable without loading
// it all into memory. zip.File.Open() returns io.ReadCloser which is not
// seekable, but f.Save requires io.ReadSeeker.
tmpFile, err := os.CreateTemp("", "vikunja-restore-*")
if err != nil {
return fmt.Errorf("could not create temp file: %w", err)
}
defer func() {
_ = tmpFile.Close()
_ = os.Remove(tmpFile.Name())
}()
if _, err := io.Copy(tmpFile, fc); err != nil {
return fmt.Errorf("could not copy to temp file: %w", err)
}
if _, err := tmpFile.Seek(0, io.SeekStart); err != nil {
return fmt.Errorf("could not seek temp file: %w", err)
}
return f.Save(tmpFile)
}
func convertFieldValue(fieldName string, value interface{}, isFloat bool) (interface{}, error) {
// Check if this is a float field and the value is already a number
if isFloat {