forked from TrueCloudLab/restic
Refactor file handing for self-update.
* Write new file payload to a temp file before touching the original binary. Minimizes the possibility of failing mid-write and corrupting the binary. * On Windows, move the original binary out to a temp file rather than removing it as the running binary is locked. Fixes issue #2248.
This commit is contained in:
parent
7d55b4f95e
commit
0ba9d4ced7
4 changed files with 69 additions and 24 deletions
8
changelog/unreleased/issue-2248
Normal file
8
changelog/unreleased/issue-2248
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
Bugfix: Support self-update on Windows
|
||||||
|
|
||||||
|
Restic self-update would fail in situations where the operating system
|
||||||
|
locks running binaries, including Windows. The new behavior works around
|
||||||
|
this by renaming the running file and swapping the updated file in place.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/2248
|
||||||
|
https://github.com/restic/restic/pull/3675
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
@ -40,14 +41,6 @@ func findHash(buf []byte, filename string) (hash []byte, err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractToFile(buf []byte, filename, target string, printf func(string, ...interface{})) error {
|
func extractToFile(buf []byte, filename, target string, printf func(string, ...interface{})) error {
|
||||||
var mode = os.FileMode(0755)
|
|
||||||
|
|
||||||
// get information about the target file
|
|
||||||
fi, err := os.Lstat(target)
|
|
||||||
if err == nil {
|
|
||||||
mode = fi.Mode()
|
|
||||||
}
|
|
||||||
|
|
||||||
var rd io.Reader = bytes.NewReader(buf)
|
var rd io.Reader = bytes.NewReader(buf)
|
||||||
switch filepath.Ext(filename) {
|
switch filepath.Ext(filename) {
|
||||||
case ".bz2":
|
case ".bz2":
|
||||||
|
@ -74,33 +67,44 @@ func extractToFile(buf []byte, filename, target string, printf func(string, ...i
|
||||||
rd = file
|
rd = file
|
||||||
}
|
}
|
||||||
|
|
||||||
err = os.Remove(target)
|
// Write everything to a temp file
|
||||||
if os.IsNotExist(err) {
|
dir := filepath.Dir(target)
|
||||||
err = nil
|
new, err := ioutil.TempFile(dir, "restic")
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to remove target file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
dest, err := os.OpenFile(target, os.O_CREATE|os.O_EXCL|os.O_WRONLY, mode)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
n, err := io.Copy(dest, rd)
|
n, err := io.Copy(new, rd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = dest.Close()
|
_ = new.Close()
|
||||||
_ = os.Remove(dest.Name())
|
_ = os.Remove(new.Name())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = new.Sync(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = new.Close(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = dest.Close()
|
mode := os.FileMode(0755)
|
||||||
if err != nil {
|
// attempt to find the original mode
|
||||||
|
if fi, err := os.Lstat(target); err == nil {
|
||||||
|
mode = fi.Mode()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the original binary.
|
||||||
|
if err := removeResticBinary(dir, target); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
printf("saved %d bytes in %v\n", n, dest.Name())
|
// Rename the temp file to the final location atomically.
|
||||||
return nil
|
if err := os.Rename(new.Name(), target); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("saved %d bytes in %v\n", n, target)
|
||||||
|
return os.Chmod(target, mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DownloadLatestStableRelease downloads the latest stable released version of
|
// DownloadLatestStableRelease downloads the latest stable released version of
|
||||||
|
|
10
internal/selfupdate/download_unix.go
Normal file
10
internal/selfupdate/download_unix.go
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
//go:build !windows
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
|
package selfupdate
|
||||||
|
|
||||||
|
// Remove the target binary.
|
||||||
|
func removeResticBinary(dir, target string) error {
|
||||||
|
// removed on rename on this platform
|
||||||
|
return nil
|
||||||
|
}
|
23
internal/selfupdate/download_windows.go
Normal file
23
internal/selfupdate/download_windows.go
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
//go:build windows
|
||||||
|
// +build windows
|
||||||
|
|
||||||
|
package selfupdate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Rename (rather than remove) the running version. The running binary will be locked
|
||||||
|
// on Windows and cannot be removed while still executing.
|
||||||
|
func removeResticBinary(dir, target string) error {
|
||||||
|
backup := filepath.Join(dir, filepath.Base(target)+".bak")
|
||||||
|
if _, err := os.Stat(backup); err == nil {
|
||||||
|
_ = os.Remove(backup)
|
||||||
|
}
|
||||||
|
if err := os.Rename(target, backup); err != nil {
|
||||||
|
return fmt.Errorf("unable to rename target file: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
Loading…
Reference in a new issue