restore: support sparse restores also on windows

This commit is contained in:
Michael Eischer 2022-09-04 11:23:31 +02:00
parent 0f89f443c7
commit 19afad8a09
8 changed files with 129 additions and 63 deletions

View file

@ -1,8 +1,8 @@
Enhancement: Restore files with many zeros as sparse files Enhancement: Restore files with many zeros as sparse files
On all platforms except Windows, the restorer may now write files containing When using `restore --sparse`, the restorer may now write files containing long
long runs of zeros as sparse files (also called files with holes): the zeros runs of zeros as sparse files (also called files with holes): the zeros are not
are not actually written to disk. actually written to disk.
How much space is saved by writing sparse files depends on the operating How much space is saved by writing sparse files depends on the operating
system, file system and the distribution of zeros in the file. system, file system and the distribution of zeros in the file.

View file

@ -1,7 +1,6 @@
package main package main
import ( import (
"runtime"
"strings" "strings"
"time" "time"
@ -60,9 +59,7 @@ func init() {
flags.StringVarP(&restoreOptions.Target, "target", "t", "", "directory to extract data to") flags.StringVarP(&restoreOptions.Target, "target", "t", "", "directory to extract data to")
initSingleSnapshotFilterOptions(flags, &restoreOptions.snapshotFilterOptions) initSingleSnapshotFilterOptions(flags, &restoreOptions.snapshotFilterOptions)
if runtime.GOOS != "windows" { flags.BoolVar(&restoreOptions.Sparse, "sparse", false, "restore files as sparse")
flags.BoolVar(&restoreOptions.Sparse, "sparse", false, "restore files as sparse (not supported on windows)")
}
flags.BoolVar(&restoreOptions.Verify, "verify", false, "verify restored files content") flags.BoolVar(&restoreOptions.Verify, "verify", false, "verify restored files content")
} }

View file

@ -67,7 +67,7 @@ func (w *filesWriter) writeToFile(path string, blob []byte, offset int64, create
if createSize >= 0 { if createSize >= 0 {
if sparse { if sparse {
err = f.Truncate(createSize) err = truncateSparse(f, createSize)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"context" "context"
"io/ioutil" "io/ioutil"
"math"
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
@ -11,6 +12,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/restic/restic/internal/archiver"
"github.com/restic/restic/internal/fs" "github.com/restic/restic/internal/fs"
"github.com/restic/restic/internal/repository" "github.com/restic/restic/internal/repository"
"github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/restic"
@ -849,3 +851,58 @@ func TestVerifyCancel(t *testing.T) {
rtest.Equals(t, 1, len(errs)) rtest.Equals(t, 1, len(errs))
rtest.Assert(t, strings.Contains(errs[0].Error(), "Invalid file size for"), "wrong error %q", errs[0].Error()) rtest.Assert(t, strings.Contains(errs[0].Error(), "Invalid file size for"), "wrong error %q", errs[0].Error())
} }
func TestRestorerSparseFiles(t *testing.T) {
repo, cleanup := repository.TestRepository(t)
defer cleanup()
var zeros [1<<20 + 13]byte
target := &fs.Reader{
Mode: 0600,
Name: "/zeros",
ReadCloser: ioutil.NopCloser(bytes.NewReader(zeros[:])),
}
sc := archiver.NewScanner(target)
err := sc.Scan(context.TODO(), []string{"/zeros"})
rtest.OK(t, err)
arch := archiver.New(repo, target, archiver.Options{})
_, id, err := arch.Snapshot(context.Background(), []string{"/zeros"},
archiver.SnapshotOptions{})
rtest.OK(t, err)
res, err := NewRestorer(context.TODO(), repo, id, true)
rtest.OK(t, err)
tempdir, cleanup := rtest.TempDir(t)
defer cleanup()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
err = res.RestoreTo(ctx, tempdir)
rtest.OK(t, err)
filename := filepath.Join(tempdir, "zeros")
content, err := ioutil.ReadFile(filename)
rtest.OK(t, err)
rtest.Equals(t, len(zeros[:]), len(content))
rtest.Equals(t, zeros[:], content)
blocks := getBlockCount(t, filename)
if blocks < 0 {
return
}
// st.Blocks is the size in 512-byte blocks.
denseBlocks := math.Ceil(float64(len(zeros)) / 512)
sparsity := 1 - float64(blocks)/denseBlocks
// This should report 100% sparse. We don't assert that,
// as the behavior of sparse writes depends on the underlying
// file system as well as the OS.
t.Logf("wrote %d zeros as %d blocks, %.1f%% sparse",
len(zeros), blocks, 100*sparsity)
}

View file

@ -4,17 +4,12 @@
package restorer package restorer
import ( import (
"bytes"
"context" "context"
"io/ioutil"
"math"
"os" "os"
"path/filepath" "path/filepath"
"syscall" "syscall"
"testing" "testing"
"github.com/restic/restic/internal/archiver"
"github.com/restic/restic/internal/fs"
"github.com/restic/restic/internal/repository" "github.com/restic/restic/internal/repository"
"github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/restic"
rtest "github.com/restic/restic/internal/test" rtest "github.com/restic/restic/internal/test"
@ -66,59 +61,12 @@ func TestRestorerRestoreEmptyHardlinkedFileds(t *testing.T) {
} }
} }
func TestRestorerSparseFiles(t *testing.T) { func getBlockCount(t *testing.T, filename string) int64 {
repo, cleanup := repository.TestRepository(t)
defer cleanup()
var zeros [1<<20 + 13]byte
target := &fs.Reader{
Mode: 0600,
Name: "/zeros",
ReadCloser: ioutil.NopCloser(bytes.NewReader(zeros[:])),
}
sc := archiver.NewScanner(target)
err := sc.Scan(context.TODO(), []string{"/zeros"})
rtest.OK(t, err)
arch := archiver.New(repo, target, archiver.Options{})
_, id, err := arch.Snapshot(context.Background(), []string{"/zeros"},
archiver.SnapshotOptions{})
rtest.OK(t, err)
res, err := NewRestorer(context.TODO(), repo, id, true)
rtest.OK(t, err)
tempdir, cleanup := rtest.TempDir(t)
defer cleanup()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
err = res.RestoreTo(ctx, tempdir)
rtest.OK(t, err)
filename := filepath.Join(tempdir, "zeros")
content, err := ioutil.ReadFile(filename)
rtest.OK(t, err)
rtest.Equals(t, len(zeros[:]), len(content))
rtest.Equals(t, zeros[:], content)
fi, err := os.Stat(filename) fi, err := os.Stat(filename)
rtest.OK(t, err) rtest.OK(t, err)
st := fi.Sys().(*syscall.Stat_t) st := fi.Sys().(*syscall.Stat_t)
if st == nil { if st == nil {
return return -1
} }
return st.Blocks
// st.Blocks is the size in 512-byte blocks.
denseBlocks := math.Ceil(float64(len(zeros)) / 512)
sparsity := 1 - float64(st.Blocks)/denseBlocks
// This should report 100% sparse. We don't assert that,
// as the behavior of sparse writes depends on the underlying
// file system as well as the OS.
t.Logf("wrote %d zeros as %d blocks, %.1f%% sparse",
len(zeros), st.Blocks, 100*sparsity)
} }

View file

@ -0,0 +1,35 @@
//go:build windows
// +build windows
package restorer
import (
"math"
"syscall"
"testing"
"unsafe"
rtest "github.com/restic/restic/internal/test"
"golang.org/x/sys/windows"
)
func getBlockCount(t *testing.T, filename string) int64 {
libkernel32 := windows.NewLazySystemDLL("kernel32.dll")
err := libkernel32.Load()
rtest.OK(t, err)
proc := libkernel32.NewProc("GetCompressedFileSizeW")
err = proc.Find()
rtest.OK(t, err)
namePtr, err := syscall.UTF16PtrFromString(filename)
rtest.OK(t, err)
result, _, _ := proc.Call(uintptr(unsafe.Pointer(namePtr)), 0)
const invalidFileSize = uintptr(4294967295)
if result == invalidFileSize {
return -1
}
return int64(math.Ceil(float64(result) / 512))
}

View file

@ -0,0 +1,10 @@
//go:build !windows
// +build !windows
package restorer
import "os"
func truncateSparse(f *os.File, size int64) error {
return f.Truncate(size)
}

View file

@ -0,0 +1,19 @@
package restorer
import (
"os"
"github.com/restic/restic/internal/debug"
"golang.org/x/sys/windows"
)
func truncateSparse(f *os.File, size int64) error {
// try setting the sparse file attribute, but ignore the error if it fails
var t uint32
err := windows.DeviceIoControl(windows.Handle(f.Fd()), windows.FSCTL_SET_SPARSE, nil, 0, nil, 0, &t, nil)
if err != nil {
debug.Log("failed to set sparse attribute for %v: %v", f.Name(), err)
}
return f.Truncate(size)
}