diff --git a/go.mod b/go.mod index 32b5d94d54..7ca6133b6e 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,7 @@ module github.com/git-lfs/git-lfs require ( github.com/alexbrainman/sspi v0.0.0-20180125232955-4729b3d4d858 + github.com/avast/retry-go v2.4.2+incompatible github.com/git-lfs/gitobj v1.4.1 github.com/git-lfs/go-netrc v0.0.0-20180525200031-e0e9ca483a18 github.com/git-lfs/go-ntlm v0.0.0-20190401175752-c5056e7fa066 diff --git a/go.sum b/go.sum index cc6d8fc6d2..d8decbbc41 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/alexbrainman/sspi v0.0.0-20180125232955-4729b3d4d858 h1:OZQyEhf4BviydsRdmK4ryeJHotDLd7vL1X8+nnxXkfk= github.com/alexbrainman/sspi v0.0.0-20180125232955-4729b3d4d858/go.mod h1:976q2ETgjT2snVCf2ZaBnyBbVoPERGjUz+0sofzEfro= +github.com/avast/retry-go v2.4.2+incompatible h1:+ZjCypQT/CyP0kyJO2EcU4d/ZEJWSbP8NENI578cPmA= +github.com/avast/retry-go v2.4.2+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/git-lfs/gitobj v1.4.1 h1:6nH5d1QP7GJjZfBqaBXpS7mDzT4plXQLqUjPbcbtRpw= diff --git a/lfs/gitfilter_smudge.go b/lfs/gitfilter_smudge.go index 6fefbe9035..110a26d1ac 100644 --- a/lfs/gitfilter_smudge.go +++ b/lfs/gitfilter_smudge.go @@ -120,7 +120,7 @@ func (f *GitFilter) downloadFile(writer io.Writer, ptr *Pointer, workingfile, me } func (f *GitFilter) readLocalFile(writer io.Writer, ptr *Pointer, mediafile string, workingfile string, cb tools.CopyCallback) (int64, error) { - reader, err := os.Open(mediafile) + reader, err := tools.RobustOpen(mediafile) if err != nil { return 0, errors.Wrapf(err, "error opening media file") } diff --git a/tools/filetools.go b/tools/filetools.go index 03ff855fd8..ad179ef502 100644 --- a/tools/filetools.go +++ b/tools/filetools.go @@ -80,7 +80,7 @@ func RenameFileCopyPermissions(srcfile, destfile string) error { } } - if err := os.Rename(srcfile, destfile); err != nil { + if err := RobustRename(srcfile, destfile); err != nil { return fmt.Errorf("cannot replace %q with %q: %v", destfile, srcfile, err) } return nil diff --git a/tools/robustio.go b/tools/robustio.go new file mode 100644 index 0000000000..da8e84ee92 --- /dev/null +++ b/tools/robustio.go @@ -0,0 +1,13 @@ +// +build !windows + +package tools + +import "os" + +func RobustRename(oldpath, newpath string) error { + return os.Rename(oldpath, newpath) +} + +func RobustOpen(name string) (*os.File, error) { + return os.Open(name) +} diff --git a/tools/robustio_windows.go b/tools/robustio_windows.go new file mode 100644 index 0000000000..aaa961a3b3 --- /dev/null +++ b/tools/robustio_windows.go @@ -0,0 +1,42 @@ +// +build windows + +package tools + +import ( + "github.com/avast/retry-go" + "os" + "syscall" +) + +const ( + // This is private in [go]/src/internal/syscall/windows/syscall_windows.go :( + ERROR_SHARING_VIOLATION syscall.Errno = 32 +) + +// isEphemeralError returns true if err may be resolved by waiting. +func isEphemeralError(err error) bool { + return err == ERROR_SHARING_VIOLATION +} + +func RobustRename(oldpath, newpath string) error { + return retry.Do( + func() error { + return os.Rename(oldpath, newpath) + }, + retry.RetryIf(isEphemeralError), + retry.LastErrorOnly(true), + ) +} + +func RobustOpen(name string) (*os.File, error) { + var result *os.File + return result, retry.Do( + func() error { + f, err := os.Open(name) + result = f + return err + }, + retry.RetryIf(isEphemeralError), + retry.LastErrorOnly(true), + ) +} diff --git a/tq/basic_download.go b/tq/basic_download.go index 0b21559ea4..2450472ec4 100644 --- a/tq/basic_download.go +++ b/tq/basic_download.go @@ -62,7 +62,7 @@ func (a *basicDownloadAdapter) DoTransfer(ctx interface{}, t *Transfer, cb Progr } // Attempt to resume download. No error checking here. If we fail, we'll simply download from the start - os.Rename(a.downloadFilename(t), f.Name()) + tools.RobustRename(a.downloadFilename(t), f.Name()) // Open temp file. It is either empty or partially downloaded f, err = os.OpenFile(f.Name(), os.O_RDWR, 0644) @@ -100,7 +100,7 @@ func (a *basicDownloadAdapter) DoTransfer(ctx interface{}, t *Transfer, cb Progr f.Close() // Rename file so next download can resume from where we stopped. // No error checking here, if rename fails then file will be deleted and there just will be no download resuming - os.Rename(f.Name(), a.downloadFilename(t)) + tools.RobustRename(f.Name(), a.downloadFilename(t)) } return err