Save files under temporary name in local backend

Fixes #3435.
This commit is contained in:
greatroar 2020-11-02 10:34:21 +01:00 committed by Michael Eischer
parent bc97a3d1f9
commit 195a5cf996
2 changed files with 35 additions and 26 deletions

View File

@ -3,6 +3,7 @@ package local
import ( import (
"context" "context"
"io" "io"
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"syscall" "syscall"
@ -88,7 +89,8 @@ func (b *Local) Save(ctx context.Context, h restic.Handle, rd restic.RewindReade
return backoff.Permanent(err) return backoff.Permanent(err)
} }
filename := b.Filename(h) finalname := b.Filename(h)
dir := filepath.Dir(finalname)
defer func() { defer func() {
// Mark non-retriable errors as such // Mark non-retriable errors as such
@ -97,19 +99,20 @@ func (b *Local) Save(ctx context.Context, h restic.Handle, rd restic.RewindReade
} }
}() }()
// create new file // Create new file with a temporary name.
f, err := openFile(filename, os.O_CREATE|os.O_EXCL|os.O_WRONLY, backend.Modes.File) tmpname := filepath.Base(finalname) + "-tmp-"
f, err := tempFile(dir, tmpname)
if b.IsNotExist(err) { if b.IsNotExist(err) {
debug.Log("error %v: creating dir", err) debug.Log("error %v: creating dir", err)
// error is caused by a missing directory, try to create it // error is caused by a missing directory, try to create it
mkdirErr := os.MkdirAll(filepath.Dir(filename), backend.Modes.Dir) mkdirErr := fs.MkdirAll(dir, backend.Modes.Dir)
if mkdirErr != nil { if mkdirErr != nil {
debug.Log("error creating dir %v: %v", filepath.Dir(filename), mkdirErr) debug.Log("error creating dir %v: %v", dir, mkdirErr)
} else { } else {
// try again // try again
f, err = openFile(filename, os.O_CREATE|os.O_EXCL|os.O_WRONLY, backend.Modes.File) f, err = tempFile(dir, tmpname)
} }
} }
@ -117,37 +120,44 @@ func (b *Local) Save(ctx context.Context, h restic.Handle, rd restic.RewindReade
return errors.WithStack(err) return errors.WithStack(err)
} }
defer func(f *os.File) {
if err != nil {
_ = f.Close() // Double Close is harmless.
// Remove after Rename is harmless: we embed the final name in the
// temporary's name and no other goroutine will get the same data to
// Save, so the temporary name should never be reused by another
// goroutine.
_ = fs.Remove(f.Name())
}
}(f)
// save data, then sync // save data, then sync
wbytes, err := io.Copy(f, rd) wbytes, err := io.Copy(f, rd)
if err != nil { if err != nil {
_ = f.Close()
return errors.WithStack(err) return errors.WithStack(err)
} }
// sanity check // sanity check
if wbytes != rd.Length() { if wbytes != rd.Length() {
_ = f.Close()
return errors.Errorf("wrote %d bytes instead of the expected %d bytes", wbytes, rd.Length()) return errors.Errorf("wrote %d bytes instead of the expected %d bytes", wbytes, rd.Length())
} }
if err = f.Sync(); err != nil { // Ignore error if filesystem does not support fsync.
pathErr, ok := err.(*os.PathError) if err = f.Sync(); err != nil && !errors.Is(err, syscall.ENOTSUP) {
isNotSupported := ok && pathErr.Op == "sync" && pathErr.Err == syscall.ENOTSUP return errors.WithStack(err)
// ignore error if filesystem does not support the sync operation
if !isNotSupported {
_ = f.Close()
return errors.WithStack(err)
}
} }
err = f.Close() // Close, then rename. Windows doesn't like the reverse order.
if err != nil { if err = f.Close(); err != nil {
return errors.WithStack(err)
}
if err = os.Rename(f.Name(), finalname); err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
// try to mark file as read-only to avoid accidential modifications // try to mark file as read-only to avoid accidential modifications
// ignore if the operation fails as some filesystems don't allow the chmod call // ignore if the operation fails as some filesystems don't allow the chmod call
// e.g. exfat and network file systems with certain mount options // e.g. exfat and network file systems with certain mount options
err = setFileReadonly(filename, backend.Modes.File) err = setFileReadonly(finalname, backend.Modes.File)
if err != nil && !os.IsPermission(err) { if err != nil && !os.IsPermission(err) {
return errors.WithStack(err) return errors.WithStack(err)
} }
@ -155,7 +165,7 @@ func (b *Local) Save(ctx context.Context, h restic.Handle, rd restic.RewindReade
return nil return nil
} }
var openFile = fs.OpenFile // Overridden by test. var tempFile = ioutil.TempFile // Overridden by test.
// Load runs fn with a reader that yields the contents of the file at h at the // Load runs fn with a reader that yields the contents of the file at h at the
// given offset. // given offset.

View File

@ -3,6 +3,7 @@ package local
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"os" "os"
"syscall" "syscall"
"testing" "testing"
@ -14,15 +15,13 @@ import (
) )
func TestNoSpacePermanent(t *testing.T) { func TestNoSpacePermanent(t *testing.T) {
oldOpenFile := openFile oldTempFile := tempFile
defer func() { defer func() {
openFile = oldOpenFile tempFile = oldTempFile
}() }()
openFile = func(name string, flags int, mode os.FileMode) (*os.File, error) { tempFile = func(_, _ string) (*os.File, error) {
// The actual error from os.OpenFile is *os.PathError. return nil, fmt.Errorf("not creating tempfile, %w", syscall.ENOSPC)
// Other functions called inside Save may return *os.SyscallError.
return nil, os.NewSyscallError("open", syscall.ENOSPC)
} }
dir, cleanup := rtest.TempDir(t) dir, cleanup := rtest.TempDir(t)