From ab705d7d21a5b3a284a78e7f91d53f2a5af41c7e Mon Sep 17 00:00:00 2001 From: kolaente Date: Wed, 4 Feb 2026 19:33:54 +0100 Subject: [PATCH] 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. --- pkg/modules/dump/restore.go | 49 +++++++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/pkg/modules/dump/restore.go b/pkg/modules/dump/restore.go index 749ae08f7..637a8717e 100644 --- a/pkg/modules/dump/restore.go +++ b/pkg/modules/dump/restore.go @@ -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 {