mirror of
https://github.com/kopia/kopia.git
synced 2026-01-26 15:28:06 -05:00
* feat(snapshots): support restoring sparse files This commit implements basic support for restoring sparse files from a snapshot. When specifying "--mode=sparse" in a snapshot restore command, Kopia will make a best effort to make sure the underlying filesystem allocates the minimum amount of blocks needed to persist restored files. In other words, enabling this feature will "force" all restored files to be sparse-blocks of zero bytes in the source file should not be allocated. * Address review comments - Separate sparse option into its own bool flag - Implement sparsefile packagewith copySparse method - Truncate once before writing sparse file - Check error from Truncate - Add unit test for copySparse - Invoke GetBlockSize once per file copy - Remove support for Windows and explain why - Add unit test for stat package Co-authored-by: Dave Smith-Uchida <dave@kasten.io>
108 lines
2.0 KiB
Go
108 lines
2.0 KiB
Go
// Package sparsefile provides wrappers for handling the writing of sparse files (files with holes).
|
|
package sparsefile
|
|
|
|
import (
|
|
"io"
|
|
"os"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/kopia/kopia/internal/iocopy"
|
|
"github.com/kopia/kopia/internal/stat"
|
|
)
|
|
|
|
// Write writes the contents of src to the given targetPath, omitting any holes.
|
|
func Write(targetPath string, src io.Reader, size int64) error {
|
|
dst, err := os.OpenFile(targetPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600) //nolint:gosec,gomnd
|
|
if err != nil {
|
|
return err //nolint:wrapcheck
|
|
}
|
|
|
|
// ensure we always close f. Note that this does not conflict with the
|
|
// close below, as close is idempotent.
|
|
defer dst.Close() //nolint:errcheck,gosec
|
|
|
|
if err = dst.Truncate(size); err != nil {
|
|
return errors.Wrap(err, "error writing sparse file")
|
|
}
|
|
|
|
s, err := stat.GetBlockSize(targetPath)
|
|
if err != nil {
|
|
return errors.Wrap(err, "error writing sparse file")
|
|
}
|
|
|
|
buf := iocopy.GetBuffer()
|
|
defer iocopy.ReleaseBuffer(buf)
|
|
|
|
w, err := copySparse(dst, src, buf[0:s])
|
|
if err != nil {
|
|
return errors.Wrap(err, "error writing sparse file")
|
|
}
|
|
|
|
if w != size {
|
|
return errors.Errorf("")
|
|
}
|
|
|
|
if err := dst.Close(); err != nil {
|
|
return err //nolint:wrapcheck
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func copySparse(dst io.WriteSeeker, src io.Reader, buf []byte) (written int64, err error) {
|
|
for {
|
|
nr, er := src.Read(buf)
|
|
if nr > 0 { // nolint:nestif
|
|
// If non-zero data is read, write it. Otherwise, skip forwards.
|
|
if isAllZero(buf) {
|
|
dst.Seek(int64(nr), os.SEEK_CUR) // nolint:errcheck
|
|
written += int64(nr)
|
|
|
|
continue
|
|
}
|
|
|
|
nw, ew := dst.Write(buf[0:nr])
|
|
if nw < 0 || nr < nw {
|
|
nw = 0
|
|
|
|
if ew == nil {
|
|
ew = errors.New("invalid write result")
|
|
}
|
|
}
|
|
|
|
written += int64(nw)
|
|
|
|
if ew != nil {
|
|
err = ew
|
|
break
|
|
}
|
|
|
|
if nr != nw {
|
|
err = io.ErrShortWrite
|
|
break
|
|
}
|
|
}
|
|
|
|
if er != nil {
|
|
if er != io.EOF {
|
|
err = er
|
|
}
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
return written, err
|
|
}
|
|
|
|
func isAllZero(buf []byte) bool {
|
|
for _, b := range buf {
|
|
if b != 0 {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|