diff --git a/cli/command_restore.go b/cli/command_restore.go index 8b41ce521..f1760dcb5 100644 --- a/cli/command_restore.go +++ b/cli/command_restore.go @@ -101,6 +101,7 @@ type commandRestore struct { restoreOverwriteDirectories bool restoreOverwriteFiles bool restoreOverwriteSymlinks bool + restoreSparse bool restoreConsistentAttributes bool restoreMode string restoreParallel int @@ -125,6 +126,7 @@ func (c *commandRestore) setup(svc appServices, parent commandParent) { cmd.Flag("overwrite-directories", "Overwrite existing directories").Default("true").BoolVar(&c.restoreOverwriteDirectories) cmd.Flag("overwrite-files", "Specifies whether or not to overwrite already existing files").Default("true").BoolVar(&c.restoreOverwriteFiles) cmd.Flag("overwrite-symlinks", "Specifies whether or not to overwrite already existing symlinks").Default("true").BoolVar(&c.restoreOverwriteSymlinks) + cmd.Flag("sparse", "When doing a restore, attempt to write files sparsely-allocating the minimum amount of disk space needed.").Default("false").BoolVar(&c.restoreSparse) cmd.Flag("consistent-attributes", "When multiple snapshots match, fail if they have inconsistent attributes").Envar("KOPIA_RESTORE_CONSISTENT_ATTRIBUTES").BoolVar(&c.restoreConsistentAttributes) cmd.Flag("mode", "Override restore mode").Default(restoreModeAuto).EnumVar(&c.restoreMode, restoreModeAuto, restoreModeLocal, restoreModeZip, restoreModeZipNoCompress, restoreModeTar, restoreModeTgz) cmd.Flag("parallel", "Restore parallelism (1=disable)").Default("8").IntVar(&c.restoreParallel) @@ -220,6 +222,7 @@ func (c *commandRestore) restoreOutput(ctx context.Context) (restore.Output, err SkipOwners: c.restoreSkipOwners, SkipPermissions: c.restoreSkipPermissions, SkipTimes: c.restoreSkipTimes, + Sparse: c.restoreSparse, }, nil case restoreModeZip, restoreModeZipNoCompress: diff --git a/internal/iocopy/copy.go b/internal/iocopy/copy.go index 9f6679a28..e2304ab7f 100644 --- a/internal/iocopy/copy.go +++ b/internal/iocopy/copy.go @@ -6,7 +6,8 @@ "sync" ) -const bufSize = 65536 +// BufSize is the size (in bytes) of the shared copy buffers Kopia uses to copy data. +const BufSize = 65536 var ( mu sync.Mutex //nolint:gochecknoglobals @@ -21,7 +22,7 @@ func GetBuffer() []byte { defer mu.Unlock() if len(buffers) == 0 { - return make([]byte, bufSize) + return make([]byte, BufSize) } var b []byte diff --git a/internal/sparsefile/sparsefile.go b/internal/sparsefile/sparsefile.go new file mode 100644 index 000000000..25ec58529 --- /dev/null +++ b/internal/sparsefile/sparsefile.go @@ -0,0 +1,107 @@ +// 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 +} diff --git a/internal/sparsefile/sparsefile_test.go b/internal/sparsefile/sparsefile_test.go new file mode 100644 index 000000000..56305fd13 --- /dev/null +++ b/internal/sparsefile/sparsefile_test.go @@ -0,0 +1,98 @@ +package sparsefile + +import ( + "bytes" + "os" + "path/filepath" + "runtime" + "testing" + + "github.com/kopia/kopia/internal/stat" +) + +func TestSparseWrite(t *testing.T) { + t.Parallel() + + if runtime.GOOS == "windows" { + t.Skip("sparse files are not supported on windows") + } + + dir := t.TempDir() + + blk, err := stat.GetBlockSize(dir) + if err != nil { + t.Fatal(err) + } + + type chunk struct { + slice []byte + off uint64 + rep uint64 + } + + cases := []struct { + name string + size uint64 + data []chunk + }{ + { + name: "null", + size: 0, + }, + { + name: "empty", + size: blk, + data: []chunk{ + {slice: []byte{0}, off: 0, rep: blk}, + }, + }, + { + name: "hole", + size: 2 * blk, + data: []chunk{ + {slice: []byte{1}, off: blk, rep: blk}, + }, + }, + { + name: "mix", + size: 2 * blk, + data: []chunk{ + {slice: []byte{1}, off: 3, rep: blk - 10}, + {slice: []byte{1}, off: 2*blk - 10, rep: 10}, + }, + }, + } + + for _, c := range cases { + src := filepath.Join(dir, "src"+c.name) + dst := filepath.Join(dir, "dst"+c.name) + + fd, err := os.Create(src) + if err != nil { + t.Fatal(err) + } + + for _, d := range c.data { + fd.WriteAt(bytes.Repeat(d.slice, int(d.rep)), int64(d.off)) + } + + err = Write(dst, fd, int64(c.size)) + if err != nil { + t.Fatalf("error writing %s: %v", dst, err) + } + + s, err := os.ReadFile(src) + if err != nil { + t.Fatal(err) + } + + d, err := os.ReadFile(dst) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(s, d) { + t.Fatalf("contents of %s and %s are not identical", src, dst) + } + } +} diff --git a/internal/stat/stat_bsd.go b/internal/stat/stat_bsd.go new file mode 100644 index 000000000..52e27e407 --- /dev/null +++ b/internal/stat/stat_bsd.go @@ -0,0 +1,37 @@ +//go:build openbsd +// +build openbsd + +// Package stat provides a cross-platform abstraction for +// common stat commands. +package stat + +import "syscall" + +const ( + diskBlockSize uint64 = 512 +) + +// GetFileAllocSize gets the space allocated on disk for the file. +// 'fname' in bytes. +func GetFileAllocSize(fname string) (uint64, error) { + var st syscall.Stat_t + + err := syscall.Stat(fname, &st) + if err != nil { + return 0, err // nolint:wrapcheck + } + + return uint64(st.Blocks) * diskBlockSize, nil +} + +// GetBlockSize gets the disk block size of the underlying system. +func GetBlockSize(path string) (uint64, error) { + var st syscall.Statfs_t + + err := syscall.Statfs(path, &st) + if err != nil { + return 0, err // nolint:wrapcheck + } + + return uint64(st.F_bsize), nil +} diff --git a/internal/stat/stat_test.go b/internal/stat/stat_test.go new file mode 100644 index 000000000..4b10ca94b --- /dev/null +++ b/internal/stat/stat_test.go @@ -0,0 +1,44 @@ +//go:build !windows +// +build !windows + +package stat + +import ( + "bytes" + "os" + "path/filepath" + "testing" +) + +func TestGetBlockSize(t *testing.T) { + s, err := GetBlockSize(os.DevNull) + if err != nil { + t.Fatal(err) + } + + if s <= 0 { + t.Fatalf("invalid disk block size: %d, must be greater than 0", s) + } +} + +func TestGetFileAllocSize(t *testing.T) { + const size = 4096 + + d := t.TempDir() + f := filepath.Join(d, "test") + data := bytes.Repeat([]byte{1}, size) + + err := os.WriteFile(f, data, os.ModePerm) + if err != nil { + t.Fatal(err) + } + + s, err := GetFileAllocSize(f) + if err != nil { + t.Fatalf("error getting file alloc size for %s: %v", f, err) + } + + if s < size { + t.Fatalf("invalid allocated file size %d, expected at least %d", s, size) + } +} diff --git a/internal/stat/stat_unix.go b/internal/stat/stat_unix.go new file mode 100644 index 000000000..b7543e71e --- /dev/null +++ b/internal/stat/stat_unix.go @@ -0,0 +1,37 @@ +//go:build linux || freebsd || darwin +// +build linux freebsd darwin + +// Package stat provides a cross-platform abstraction for +// common stat commands. +package stat + +import "syscall" + +const ( + diskBlockSize uint64 = 512 +) + +// GetFileAllocSize gets the space allocated on disk for the file +// 'fname' in bytes. +func GetFileAllocSize(fname string) (uint64, error) { + var st syscall.Stat_t + + err := syscall.Stat(fname, &st) + if err != nil { + return 0, err // nolint:wrapcheck + } + + return uint64(st.Blocks) * diskBlockSize, nil +} + +// GetBlockSize gets the disk block size of the underlying system. +func GetBlockSize(path string) (uint64, error) { + var st syscall.Statfs_t + + err := syscall.Statfs(path, &st) + if err != nil { + return 0, err // nolint:wrapcheck + } + + return uint64(st.Bsize), nil // nolint:unconvert,nolintlint +} diff --git a/internal/stat/stat_windows.go b/internal/stat/stat_windows.go new file mode 100644 index 000000000..e438d4ff7 --- /dev/null +++ b/internal/stat/stat_windows.go @@ -0,0 +1,21 @@ +//go:build windows +// +build windows + +// Package stat provides a cross-platform abstraction for +// common stat commands. +package stat + +import "errors" + +var errNotImplemented = errors.New("not implemented") + +// GetFileAllocSize gets the space allocated on disk for the file +// 'fname' in bytes. +func GetFileAllocSize(fname string) (uint64, error) { + return 0, errNotImplemented +} + +// GetBlockSize gets the disk block size of the underlying system. +func GetBlockSize(path string) (uint64, error) { + return 0, errNotImplemented +} diff --git a/internal/testutil/testutil.go b/internal/testutil/testutil.go index f800dd594..0b90647c4 100644 --- a/internal/testutil/testutil.go +++ b/internal/testutil/testutil.go @@ -48,6 +48,15 @@ func TestSkipUnlessCI(tb testing.TB, msg string, args ...interface{}) { } } +// TestSkipUnlessLinux skips the current test if the test environment is not Linux. +func TestSkipUnlessLinux(tb testing.TB) { + tb.Helper() + + if runtime.GOOS != "linux" { + tb.Skip("test not supported in this environment.") + } +} + // TestSkipOnCIUnlessLinuxAMD64 skips the current test if running on CI unless the environment is Linux/AMD64. func TestSkipOnCIUnlessLinuxAMD64(tb testing.TB) { tb.Helper() diff --git a/snapshot/restore/local_fs_output.go b/snapshot/restore/local_fs_output.go index 35addddaa..89d829227 100644 --- a/snapshot/restore/local_fs_output.go +++ b/snapshot/restore/local_fs_output.go @@ -15,6 +15,7 @@ "github.com/kopia/kopia/fs/localfs" "github.com/kopia/kopia/internal/atomicfile" "github.com/kopia/kopia/internal/iocopy" + "github.com/kopia/kopia/internal/sparsefile" "github.com/kopia/kopia/snapshot" ) @@ -56,6 +57,9 @@ type FilesystemOutput struct { // SkipTimes when set to true causes restore to skip restoring modification times. SkipTimes bool `json:"skipTimes"` + + // Sparse when set to true causes restore files sparsely-not writing any holes (zero regions) to disk. + Sparse bool `json:"sparse"` } // Parallelizable implements restore.Output interface. @@ -146,11 +150,11 @@ func (o *FilesystemOutput) CreateSymlink(ctx context.Context, relativePath strin path := filepath.Join(o.TargetPath, filepath.FromSlash(relativePath)) - switch stat, err := os.Lstat(path); { + switch st, err := os.Lstat(path); { case os.IsNotExist(err): // Proceed to symlink creation case err != nil: return errors.Wrap(err, "lstat error at symlink path") - case fileIsSymlink(stat): + case fileIsSymlink(st): // Throw error if we are not overwriting symlinks if !o.OverwriteSymlinks { return errors.Errorf("will not overwrite existing symlink") @@ -175,8 +179,8 @@ func (o *FilesystemOutput) CreateSymlink(ctx context.Context, relativePath strin return nil } -func fileIsSymlink(stat os.FileInfo) bool { - return stat.Mode()&os.ModeSymlink != 0 +func fileIsSymlink(st os.FileInfo) bool { + return st.Mode()&os.ModeSymlink != 0 } // SymlinkExists implements restore.Output interface. @@ -282,13 +286,13 @@ func isWindows() bool { } func (o *FilesystemOutput) createDirectory(ctx context.Context, path string) error { - switch stat, err := os.Stat(path); { + switch st, err := os.Stat(path); { case os.IsNotExist(err): // nolint:wrapcheck return os.MkdirAll(path, outputDirMode) case err != nil: return errors.Wrap(err, "failed to stat path "+path) - case stat.Mode().IsDir(): + case st.Mode().IsDir(): if !o.OverwriteDirectories { if empty, _ := isEmptyDirectory(path); !empty { return errors.Errorf("non-empty directory already exists, not overwriting it: %q", path) @@ -315,7 +319,8 @@ func write(targetPath string, r fs.Reader) error { name := f.Name() - if err := iocopy.JustCopy(f, r); err != nil { + err = iocopy.JustCopy(f, r) + if err != nil { return errors.Wrap(err, "cannot write data to file %q "+name) } @@ -346,13 +351,23 @@ func (o *FilesystemOutput) copyFileContent(ctx context.Context, targetPath strin defer r.Close() //nolint:errcheck log(ctx).Debugf("copying file contents to: %v", targetPath) + targetPath = atomicfile.MaybePrefixLongFilenameOnWindows(targetPath) if o.WriteFilesAtomically { // nolint:wrapcheck return atomicfile.Write(targetPath, r) } - return write(atomicfile.MaybePrefixLongFilenameOnWindows(targetPath), r) + if o.Sparse { + if isWindows() { + log(ctx).Infof("sparse files are not supported on Windows, restoring normally") + } else { + // nolint:wrapcheck + return sparsefile.Write(targetPath, r, f.Size()) + } + } + + return write(targetPath, r) } func isEmptyDirectory(name string) (bool, error) { diff --git a/tests/end_to_end_test/restore_test.go b/tests/end_to_end_test/restore_test.go index 28c4c48dc..b0fb6224d 100644 --- a/tests/end_to_end_test/restore_test.go +++ b/tests/end_to_end_test/restore_test.go @@ -3,6 +3,7 @@ import ( "archive/tar" "archive/zip" + "bytes" "compress/gzip" "errors" "fmt" @@ -21,6 +22,8 @@ "github.com/kopia/kopia/fs/localfs" "github.com/kopia/kopia/internal/diff" "github.com/kopia/kopia/internal/fshasher" + "github.com/kopia/kopia/internal/iocopy" + "github.com/kopia/kopia/internal/stat" "github.com/kopia/kopia/internal/testlogging" "github.com/kopia/kopia/internal/testutil" "github.com/kopia/kopia/tests/clitestutil" @@ -488,6 +491,271 @@ func TestRestoreSnapshotOfSingleFile(t *testing.T) { verifyFileMode(t, filepath.Join(restoreDir, "restored-5"), defaultRestoredFilePermission) } +func TestSnapshotSparseRestore(t *testing.T) { + t.Parallel() + + // The behavior of the Darwin (APFS) is not published, and sparse restores + // are not supported on Windows. As such, we cannot (reliably) test them here. + testutil.TestSkipUnlessLinux(t) + + runner := testenv.NewInProcRunner(t) + e := testenv.NewCLITest(t, testenv.RepoFormatNotImportant, runner) + + e.RunAndExpectSuccess(t, "repo", "create", "filesystem", "--path", e.RepoDir) + + sourceDir := testutil.TempDirectory(t) + restoreDir := testutil.TempDirectory(t) + + bufSize := uint64(iocopy.BufSize) + + blkSize, err := stat.GetBlockSize(sourceDir) + if err != nil { + t.Fatalf("error getting disk block size: %v", err) + } + + type chunk struct { + slice []byte + off uint64 + rep uint64 + } + + cases := []struct { + name string + data []chunk + trunc uint64 // Truncate source file to this size + sLog uint64 // Expected logical size of source file + sPhys uint64 // Expected physical size of source file + rLog uint64 // Expected logical size of restored file + rPhys uint64 // Expected physical size of restored file + }{ + { + name: "null_file", + trunc: 0, + sLog: 0, + sPhys: 0, + rLog: 0, + rPhys: 0, + }, + { + name: "empty_file", + trunc: 3 * bufSize, + sLog: 3 * bufSize, + sPhys: 0, + rLog: 3 * bufSize, + rPhys: 0, + }, + { + name: "blk", + data: []chunk{ + {slice: []byte("1"), off: 0, rep: blkSize}, + }, + sLog: blkSize, + sPhys: blkSize, + rLog: blkSize, + rPhys: blkSize, + }, + { + name: "blk_real_zeros", + data: []chunk{ + {slice: []byte{0}, off: 0, rep: blkSize}, + }, + sLog: blkSize, + sPhys: blkSize, + rLog: blkSize, + rPhys: 0, + }, + { + name: "buf_real_zeros", + data: []chunk{ + {slice: []byte{0}, off: 0, rep: bufSize}, + }, + sLog: bufSize, + sPhys: bufSize, + rLog: bufSize, + rPhys: 0, + }, + { + name: "buf_full", + data: []chunk{ + {slice: []byte("1"), off: 0, rep: bufSize}, + }, + sLog: bufSize, + sPhys: bufSize, + rLog: bufSize, + rPhys: bufSize, + }, + { + name: "buf_trailing_bytes", + data: []chunk{ + {slice: []byte("1"), off: bufSize - blkSize - 1, rep: 1}, + {slice: []byte("1"), off: bufSize - 1, rep: 1}, + }, + trunc: bufSize, + sLog: bufSize, + sPhys: 2 * blkSize, + rLog: bufSize, + rPhys: 2 * blkSize, + }, + { + name: "buf_trailing_hole", + data: []chunk{ + {slice: []byte("1"), off: 0, rep: 1}, + }, + trunc: bufSize, + sLog: bufSize, + sPhys: blkSize, + rLog: bufSize, + rPhys: blkSize, + }, + { + name: "buf_hole_aligned", + data: []chunk{ + {slice: []byte("1"), off: bufSize, rep: blkSize}, + }, + trunc: bufSize + blkSize, + sLog: bufSize + blkSize, + sPhys: blkSize, + rLog: bufSize + blkSize, + rPhys: blkSize, + }, + { + name: "buf_hole_on_buf_boundary", + data: []chunk{ + {slice: []byte("1"), off: bufSize / 2, rep: bufSize}, + }, + sLog: bufSize * 3 / 2, + sPhys: bufSize, + rLog: bufSize * 3 / 2, + rPhys: bufSize, + }, + { + name: "blk_hole_on_blk_boundary", + data: []chunk{ + {slice: []byte("1"), off: blkSize / 2, rep: blkSize}, + }, + sLog: blkSize * 3 / 2, + sPhys: blkSize * 2, + rLog: blkSize * 3 / 2, + rPhys: blkSize * 2, + }, + { + name: "blk_hole_on_buf_boundary", + data: []chunk{ + {slice: []byte("1"), off: 0, rep: bufSize - (blkSize / 2)}, + {slice: []byte("1"), off: bufSize + (blkSize / 2), rep: blkSize / 2}, + }, + sLog: bufSize + blkSize, + sPhys: bufSize + blkSize, + rLog: bufSize + blkSize, + rPhys: bufSize + blkSize, + }, + { + name: "blk_hole_aligned", + data: []chunk{ + {slice: []byte("1"), off: 0, rep: bufSize}, + {slice: []byte("1"), off: bufSize + blkSize, rep: bufSize - blkSize}, + }, + trunc: 2 * bufSize, + sLog: 2 * bufSize, + sPhys: 2*bufSize - blkSize, + rLog: 2 * bufSize, + rPhys: 2*bufSize - blkSize, + }, + { + name: "blk_alternating_empty", + data: []chunk{ + {slice: []byte("1"), off: 0, rep: blkSize}, + {slice: []byte("1"), off: 1 * blkSize, rep: blkSize}, + {slice: []byte("1"), off: 4 * blkSize, rep: blkSize}, + {slice: []byte("1"), off: 6 * blkSize, rep: blkSize}, + {slice: []byte("1"), off: 8 * blkSize, rep: blkSize}, + }, + sLog: 9 * blkSize, + sPhys: 5 * blkSize, + rLog: 9 * blkSize, + rPhys: 5 * blkSize, + }, + { + name: "blk_alternating_zero", + data: []chunk{ + {slice: []byte("1"), off: 0, rep: blkSize}, + {slice: []byte{0}, off: blkSize, rep: blkSize}, + {slice: []byte("1"), off: 2 * blkSize, rep: blkSize}, + {slice: []byte{0}, off: 3 * blkSize, rep: blkSize}, + }, + sLog: 4 * blkSize, + sPhys: 4 * blkSize, + rLog: 4 * blkSize, + rPhys: 2 * blkSize, + }, + } + + for _, c := range cases { + sourceFile := filepath.Join(sourceDir, c.name+"_source") + + fd, err := os.Create(sourceFile) + if err != nil { + t.Fatal(err) + } + + err = fd.Truncate(int64(c.trunc)) + if err != nil { + t.Fatal(err) + } + + for _, d := range c.data { + fd.WriteAt(bytes.Repeat(d.slice, int(d.rep)), int64(d.off)) + } + + verifyFileSize(t, sourceFile, c.sLog, c.sPhys) + e.RunAndExpectSuccess(t, "snapshot", "create", sourceFile) + + si := clitestutil.ListSnapshotsAndExpectSuccess(t, e, sourceFile) + if got, want := len(si), 1; got != want { + t.Fatalf("got %v sources, wanted %v", got, want) + } + + if got, want := len(si[0].Snapshots), 1; got != want { + t.Fatalf("got %v snapshots, wanted %v", got, want) + } + + snapID := si[0].Snapshots[0].SnapshotID + restoreFile := filepath.Join(restoreDir, c.name+"_restore") + + e.RunAndExpectSuccess(t, "snapshot", "restore", snapID, "--sparse", restoreFile) + verifyFileSize(t, restoreFile, c.rLog, c.rPhys) + } +} + +func verifyFileSize(t *testing.T, fname string, logical, physical uint64) { + t.Helper() + + st, err := os.Stat(fname) + if err != nil { + t.Fatalf("error verifying file size: %v", err) + } + + realLogical := uint64(st.Size()) + + if realLogical != logical { + t.Errorf("%s logical file size incorrect: expected %d, got %d", fname, logical, realLogical) + } + + if runtime.GOOS == windowsOSName { + t.Logf("getting physical file size is not supported on windows") + return + } + + realPhysical, err := stat.GetFileAllocSize(fname) + if err != nil { + t.Fatalf("error verifying file size: %v", err) + } + + if realPhysical != physical { + t.Errorf("%s physical file size incorrect: expected %d, got %d", fname, physical, realPhysical) + } +} + func verifyFileMode(t *testing.T, filename string, want os.FileMode) { t.Helper()