Files
kopia/internal/server/api_restore.go
2025-05-29 06:07:49 +00:00

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) (any, *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, requestError(serverapi.ErrorMalformedRequest, "invalid root entry")
}
var (
out restore.Output
description string
)
switch {
case req.Filesystem != nil:
if err := req.Filesystem.Init(ctx); err != nil {
return nil, internalServerError(err)
}
out = req.Filesystem
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(_ 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.New("task not found"))
}
return task, nil
}