2
2
mirror of https://github.com/octoleo/restic.git synced 2024-12-23 11:28:54 +00:00

Merge pull request #3436 from greatroar/local-save-tmp

Save files under temporary name in local backend
This commit is contained in:
MichaelEischer 2021-08-04 22:01:53 +02:00 committed by GitHub
commit 1b1a2115fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 82 additions and 26 deletions

View File

@ -0,0 +1,12 @@
Enhancement: Improve local backend's resilience to (system) crashes
Restic now ensures that files stored using the `local` backend are created
atomically (that is, files are either stored completely or not at all). This
ensures that no incomplete files are left behind even if restic is terminated
while writing a file.
In addition, restic now tries to ensure that the directory in the repository
which contains a newly uploaded file is also written to disk. This can prevent
missing files if the system crashes or the disk is not properly unmounted.
https://github.com/restic/restic/pull/3436

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,54 @@ 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) err = f.Sync()
isNotSupported := ok && pathErr.Op == "sync" && pathErr.Err == syscall.ENOTSUP syncNotSup := errors.Is(err, syscall.ENOTSUP)
// ignore error if filesystem does not support the sync operation if err != nil && !syncNotSup {
if !isNotSupported { return errors.WithStack(err)
_ = 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) return errors.WithStack(err)
} }
if err = os.Rename(f.Name(), finalname); err != nil {
return errors.WithStack(err)
}
// Now sync the directory to commit the Rename.
if !syncNotSup {
err = fsyncDir(dir)
if err != nil {
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 +175,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)

View File

@ -3,11 +3,33 @@
package local package local
import ( import (
"errors"
"os" "os"
"syscall"
"github.com/restic/restic/internal/fs" "github.com/restic/restic/internal/fs"
) )
// fsyncDir flushes changes to the directory dir.
func fsyncDir(dir string) error {
d, err := os.Open(dir)
if err != nil {
return err
}
err = d.Sync()
if errors.Is(err, syscall.ENOTSUP) {
err = nil
}
cerr := d.Close()
if err == nil {
err = cerr
}
return err
}
// set file to readonly // set file to readonly
func setFileReadonly(f string, mode os.FileMode) error { func setFileReadonly(f string, mode os.FileMode) error {
return fs.Chmod(f, mode&^0222) return fs.Chmod(f, mode&^0222)

View File

@ -4,6 +4,9 @@ import (
"os" "os"
) )
// Can't explicitly flush directory changes on Windows.
func fsyncDir(dir string) error { return nil }
// We don't modify read-only on windows, // We don't modify read-only on windows,
// since it will make us unable to delete the file, // since it will make us unable to delete the file,
// and this isn't common practice on this platform. // and this isn't common practice on this platform.