mirror of
https://github.com/kopia/kopia.git
synced 2026-01-24 22:38:00 -05:00
* Unify sparse and normal IO output This commit refactors the code paths that excercise normal and sparse writing of restored content. The goal is to expose sparsefile.Copy() and iocopy.Copy() to be interchangeable, thereby allowing us to wrap or transform their behavior more easily in the future. * Introduce getStreamCopier() * Pull ioCopy() into getStreamCopier() * Fix small nit in E2E test We should be getting the block size of the destination file, not the source file. * Call stat.GetBlockSize() once per FilesystemOutput A tiny refactor to pull this call out of the generated stream copier, as the block size should not change from one file to the next within a restore entry. NOTE: as a side effect, if block size could not be found (an error is returned), we will return the default stream copier instead of letting the sparse copier fail. A warning will be logged, but this error will not cause the restore to fail; it will proceed silently.
126 lines
3.2 KiB
Go
126 lines
3.2 KiB
Go
package server
|
|
|
|
import (
|
|
"archive/zip"
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/kopia/kopia/internal/serverapi"
|
|
"github.com/kopia/kopia/internal/uitask"
|
|
"github.com/kopia/kopia/snapshot/restore"
|
|
"github.com/kopia/kopia/snapshot/snapshotfs"
|
|
)
|
|
|
|
func restoreCounters(s restore.Stats) map[string]uitask.CounterValue {
|
|
return map[string]uitask.CounterValue{
|
|
"Restored Files": uitask.SimpleCounter(int64(s.RestoredFileCount)),
|
|
"Restored Directories": uitask.SimpleCounter(int64(s.RestoredDirCount)),
|
|
"Restored Symlinks": uitask.SimpleCounter(int64(s.RestoredSymlinkCount)),
|
|
"Restored Bytes": uitask.BytesCounter(s.RestoredTotalFileSize),
|
|
"Ignored Errors": uitask.SimpleCounter(int64(s.IgnoredErrorCount)),
|
|
"Skipped Files": uitask.SimpleCounter(int64(s.SkippedCount)),
|
|
"Skipped Bytes": uitask.BytesCounter(s.SkippedTotalFileSize),
|
|
}
|
|
}
|
|
|
|
func handleRestore(ctx context.Context, rc requestContext) (interface{}, *apiError) {
|
|
var req serverapi.RestoreRequest
|
|
|
|
if err := json.Unmarshal(rc.body, &req); err != nil {
|
|
return nil, requestError(serverapi.ErrorMalformedRequest, "malformed request body")
|
|
}
|
|
|
|
rep := rc.rep
|
|
|
|
if req.Root == "" {
|
|
return nil, requestError(serverapi.ErrorMalformedRequest, "root not specified")
|
|
}
|
|
|
|
rootEntry, err := snapshotfs.FilesystemEntryFromIDWithPath(ctx, rep, req.Root, false)
|
|
if err != nil {
|
|
return nil, internalServerError(err)
|
|
}
|
|
|
|
var (
|
|
out restore.Output
|
|
description string
|
|
)
|
|
|
|
switch {
|
|
case req.Filesystem != nil:
|
|
out := req.Filesystem
|
|
if err := out.Init(); err != nil {
|
|
return nil, internalServerError(err)
|
|
}
|
|
|
|
description = "Destination: " + req.Filesystem.TargetPath
|
|
|
|
case req.ZipFile != "":
|
|
f, err := os.Create(req.ZipFile)
|
|
if err != nil {
|
|
return nil, internalServerError(err)
|
|
}
|
|
|
|
if req.UncompressedZip {
|
|
out = restore.NewZipOutput(f, zip.Store)
|
|
description = "Uncompressed ZIP File: " + req.ZipFile
|
|
} else {
|
|
out = restore.NewZipOutput(f, zip.Deflate)
|
|
description = "ZIP File: " + req.ZipFile
|
|
}
|
|
|
|
case req.TarFile != "":
|
|
f, err := os.Create(req.TarFile)
|
|
if err != nil {
|
|
return nil, internalServerError(err)
|
|
}
|
|
|
|
out = restore.NewTarOutput(f)
|
|
description = "TAR File: " + req.TarFile
|
|
|
|
default:
|
|
return nil, requestError(serverapi.ErrorMalformedRequest, "output not specified")
|
|
}
|
|
|
|
taskIDChan := make(chan string)
|
|
|
|
// launch a goroutine that will continue the restore and can be observed in the Tasks UI.
|
|
|
|
// nolint:errcheck
|
|
go rc.srv.taskManager().Run(ctx, "Restore", description, func(ctx context.Context, ctrl uitask.Controller) error {
|
|
taskIDChan <- ctrl.CurrentTaskID()
|
|
|
|
opt := req.Options
|
|
|
|
opt.ProgressCallback = func(ctx context.Context, s restore.Stats) {
|
|
ctrl.ReportCounters(restoreCounters(s))
|
|
}
|
|
|
|
cancelChan := make(chan struct{})
|
|
opt.Cancel = cancelChan
|
|
|
|
ctrl.OnCancel(func() {
|
|
close(opt.Cancel)
|
|
})
|
|
|
|
st, err := restore.Entry(ctx, rep, out, rootEntry, opt)
|
|
if err == nil {
|
|
ctrl.ReportCounters(restoreCounters(st))
|
|
}
|
|
|
|
return errors.Wrap(err, "error restoring")
|
|
})
|
|
|
|
taskID := <-taskIDChan
|
|
|
|
task, ok := rc.srv.taskManager().GetTask(taskID)
|
|
if !ok {
|
|
return nil, internalServerError(errors.Errorf("task not found"))
|
|
}
|
|
|
|
return task, nil
|
|
}
|