diff --git a/cli/command_restore.go b/cli/command_restore.go
index ceb1e7740..94392aebf 100644
--- a/cli/command_restore.go
+++ b/cli/command_restore.go
@@ -105,6 +105,7 @@ type commandRestore struct {
restoreMode string
restoreParallel int
restoreIgnorePermissionErrors bool
+ restoreWriteFilesAtomically bool
restoreSkipTimes bool
restoreSkipOwners bool
restoreSkipPermissions bool
@@ -131,6 +132,7 @@ func (c *commandRestore) setup(svc appServices, parent commandParent) {
cmd.Flag("skip-permissions", "Skip permissions during restore").BoolVar(&c.restoreSkipPermissions)
cmd.Flag("skip-times", "Skip times during restore").BoolVar(&c.restoreSkipTimes)
cmd.Flag("ignore-permission-errors", "Ignore permission errors").Default("true").BoolVar(&c.restoreIgnorePermissionErrors)
+ cmd.Flag("write-files-atomically", "Write files atomically to disk, ensuring they are either fully committed, or not written at all, preventing partially written files").Default("false").BoolVar(&c.restoreWriteFilesAtomically)
cmd.Flag("ignore-errors", "Ignore all errors").BoolVar(&c.restoreIgnoreErrors)
cmd.Flag("skip-existing", "Skip files and symlinks that exist in the output").BoolVar(&c.restoreIncremental)
cmd.Flag("shallow", "Shallow restore the directory hierarchy starting at this level (default is to deep restore the entire hierarchy.)").Int32Var(&c.restoreShallowAtDepth)
@@ -214,6 +216,7 @@ func (c *commandRestore) restoreOutput(ctx context.Context) (restore.Output, err
OverwriteFiles: c.restoreOverwriteFiles,
OverwriteSymlinks: c.restoreOverwriteSymlinks,
IgnorePermissionErrors: c.restoreIgnorePermissionErrors,
+ WriteFilesAtomically: c.restoreWriteFilesAtomically,
SkipOwners: c.restoreSkipOwners,
SkipPermissions: c.restoreSkipPermissions,
SkipTimes: c.restoreSkipTimes,
diff --git a/htmlui/src/BeginRestore.js b/htmlui/src/BeginRestore.js
index 3f65fac80..2e3f582ab 100644
--- a/htmlui/src/BeginRestore.js
+++ b/htmlui/src/BeginRestore.js
@@ -23,6 +23,7 @@ export class BeginRestore extends Component {
overwriteDirectories: false,
overwriteSymlinks: false,
ignorePermissionErrors: true,
+ writeFilesAtomically: false,
restoreDirEntryAtDepth: 1000,
minSizeForPlaceholder: 0,
restoreTask: "",
@@ -67,6 +68,7 @@ export class BeginRestore extends Component {
overwriteFiles: this.state.overwriteFiles,
overwriteDirectories: this.state.overwriteDirectories,
overwriteSymlinks: this.state.overwriteSymlinks,
+ writeFilesAtomically: this.state.writeFilesAtomically
}
}
@@ -123,6 +125,9 @@ export class BeginRestore extends Component {
{RequiredBoolean(this, "Overwrite Symbolic Links", "overwriteSymlinks")}
+
+ {RequiredBoolean(this, "Write files atomically", "writeFilesAtomically")}
+
diff --git a/snapshot/restore/local_fs_output.go b/snapshot/restore/local_fs_output.go
index ad653891e..35addddaa 100644
--- a/snapshot/restore/local_fs_output.go
+++ b/snapshot/restore/local_fs_output.go
@@ -14,6 +14,7 @@
"github.com/kopia/kopia/fs"
"github.com/kopia/kopia/fs/localfs"
"github.com/kopia/kopia/internal/atomicfile"
+ "github.com/kopia/kopia/internal/iocopy"
"github.com/kopia/kopia/snapshot"
)
@@ -44,6 +45,9 @@ type FilesystemOutput struct {
// IgnorePermissionErrors causes restore to ignore errors due to invalid permissions.
IgnorePermissionErrors bool `json:"ignorePermissionErrors"`
+ // When set to true, first write to a temp file and rename it, to ensure there are no partially written files in case of a crash.
+ WriteFilesAtomically bool `json:"writeFilesAtomically"`
+
// SkipOwners when set to true causes restore to skip restoring owner information.
SkipOwners bool `json:"skipOwners"`
@@ -299,6 +303,29 @@ func (o *FilesystemOutput) createDirectory(ctx context.Context, path string) err
}
}
+func write(targetPath string, r fs.Reader) error {
+ f, 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 f.Close() //nolint:errcheck,gosec
+
+ name := f.Name()
+
+ if err := iocopy.JustCopy(f, r); err != nil {
+ return errors.Wrap(err, "cannot write data to file %q "+name)
+ }
+
+ if err := f.Close(); err != nil {
+ return err //nolint:wrapcheck
+ }
+
+ return nil
+}
+
func (o *FilesystemOutput) copyFileContent(ctx context.Context, targetPath string, f fs.File) error {
switch _, err := os.Stat(targetPath); {
case os.IsNotExist(err): // copy file below
@@ -320,8 +347,12 @@ func (o *FilesystemOutput) copyFileContent(ctx context.Context, targetPath strin
log(ctx).Debugf("copying file contents to: %v", targetPath)
- // nolint:wrapcheck
- return atomicfile.Write(targetPath, r)
+ if o.WriteFilesAtomically {
+ // nolint:wrapcheck
+ return atomicfile.Write(targetPath, r)
+ }
+
+ return write(atomicfile.MaybePrefixLongFilenameOnWindows(targetPath), r)
}
func isEmptyDirectory(name string) (bool, error) {