Skip to content

Commit

Permalink
fix(ui): fixed filesysystem restores triggered from UI (#2163)
Browse files Browse the repository at this point in the history
Added comprehensive test for restore API which was previously completely
uncovered.

Fixes #2162
  • Loading branch information
jkowalski committed Jul 9, 2022
1 parent afab59c commit a621cd3
Show file tree
Hide file tree
Showing 5 changed files with 235 additions and 6 deletions.
6 changes: 3 additions & 3 deletions internal/server/api_restore.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func handleRestore(ctx context.Context, rc requestContext) (interface{}, *apiErr

rootEntry, err := snapshotfs.FilesystemEntryFromIDWithPath(ctx, rep, req.Root, false)
if err != nil {
return nil, internalServerError(err)
return nil, requestError(serverapi.ErrorMalformedRequest, "invalid root entry")
}

var (
Expand All @@ -51,11 +51,11 @@ func handleRestore(ctx context.Context, rc requestContext) (interface{}, *apiErr

switch {
case req.Filesystem != nil:
out := req.Filesystem
if err := out.Init(); err != nil {
if err := req.Filesystem.Init(); err != nil {
return nil, internalServerError(err)
}

out = req.Filesystem
description = "Destination: " + req.Filesystem.TargetPath

case req.ZipFile != "":
Expand Down
219 changes: 219 additions & 0 deletions internal/server/api_restore_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
package server_test

import (
"context"
"math"
"path/filepath"
"testing"
"time"

"github.com/google/uuid"
"github.com/stretchr/testify/require"

"github.com/kopia/kopia/internal/apiclient"
"github.com/kopia/kopia/internal/mockfs"
"github.com/kopia/kopia/internal/repotesting"
"github.com/kopia/kopia/internal/serverapi"
"github.com/kopia/kopia/internal/testutil"
"github.com/kopia/kopia/internal/uitask"
"github.com/kopia/kopia/repo"
"github.com/kopia/kopia/repo/manifest"
"github.com/kopia/kopia/snapshot"
"github.com/kopia/kopia/snapshot/restore"
"github.com/kopia/kopia/snapshot/snapshotfs"
)

func TestRestoreSnapshots(t *testing.T) {
ctx, env := repotesting.NewEnvironment(t, repotesting.FormatNotImportant)

si1 := env.LocalPathSourceInfo("/dummy/path")

var id11 manifest.ID

require.NoError(t, repo.WriteSession(ctx, env.Repository, repo.WriteSessionOptions{Purpose: "Test"}, func(ctx context.Context, w repo.RepositoryWriter) error {
u := snapshotfs.NewUploader(w)

dir1 := mockfs.NewDirectory()

dir1.AddFile("file1", []byte{1, 2, 3}, 0o644)
dir1.AddDir("dir1", 0o644).AddFile("file2", []byte{1, 2, 4}, 0o644)

man11, err := u.Upload(ctx, dir1, nil, si1)
require.NoError(t, err)
id11, err = snapshot.SaveSnapshot(ctx, w, man11)
require.NoError(t, err)

return nil
}))

srvInfo := startServer(t, env, false)

cli, err := apiclient.NewKopiaAPIClient(apiclient.Options{
BaseURL: srvInfo.BaseURL,
TrustedServerCertificateFingerprint: srvInfo.TrustedServerCertificateFingerprint,
Username: testUIUsername,
Password: testUIPassword,
})

require.NoError(t, err)
require.NoError(t, cli.FetchCSRFTokenForTesting(ctx))

t.Run("Filesystem", func(t *testing.T) {
targetPath1 := testutil.TempDirectory(t)
restoreTask1, err := serverapi.Restore(ctx, cli, &serverapi.RestoreRequest{
Root: string(id11),
Options: restore.Options{
RestoreDirEntryAtDepth: math.MaxInt32,
},
Filesystem: &restore.FilesystemOutput{
TargetPath: targetPath1,
SkipOwners: true,
SkipPermissions: true,
},
})

require.NoError(t, err)
waitForTask(t, cli, restoreTask1.TaskID, 30*time.Second)
require.FileExists(t, filepath.Join(targetPath1, "file1"))
require.FileExists(t, filepath.Join(targetPath1, "dir1", "file2"))
})

t.Run("FilesystemSubdir", func(t *testing.T) {
targetPath1 := testutil.TempDirectory(t)
restoreTask1, err := serverapi.Restore(ctx, cli, &serverapi.RestoreRequest{
Root: string(id11) + "/dir1",
Options: restore.Options{
RestoreDirEntryAtDepth: math.MaxInt32,
},
Filesystem: &restore.FilesystemOutput{
TargetPath: targetPath1,
SkipOwners: true,
SkipPermissions: true,
},
})

require.NoError(t, err)
require.Equal(t, uitask.StatusSuccess, waitForTask(t, cli, restoreTask1.TaskID, 30*time.Second).Status)

require.FileExists(t, filepath.Join(targetPath1, "file2"))
})

t.Run("FilesystemFullShallowRestore", func(t *testing.T) {
targetPath1 := testutil.TempDirectory(t)
restoreTask1, err := serverapi.Restore(ctx, cli, &serverapi.RestoreRequest{
Root: string(id11),
Options: restore.Options{},
Filesystem: &restore.FilesystemOutput{
TargetPath: targetPath1,
SkipOwners: true,
SkipPermissions: true,
},
})

require.NoError(t, err)
require.Equal(t, uitask.StatusSuccess, waitForTask(t, cli, restoreTask1.TaskID, 30*time.Second).Status)
require.FileExists(t, filepath.Join(targetPath1, "file1.kopia-entry"))
require.DirExists(t, filepath.Join(targetPath1, "dir1.kopia-entry"))
require.FileExists(t, filepath.Join(targetPath1, "dir1.kopia-entry", ".kopia-entry"))
})

t.Run("FilesystemPartialShallowRestore", func(t *testing.T) {
targetPath1 := testutil.TempDirectory(t)
restoreTask1, err := serverapi.Restore(ctx, cli, &serverapi.RestoreRequest{
Root: string(id11),
Options: restore.Options{
RestoreDirEntryAtDepth: 1,
},
Filesystem: &restore.FilesystemOutput{
TargetPath: targetPath1,
SkipOwners: true,
SkipPermissions: true,
},
})

require.NoError(t, err)
require.Equal(t, uitask.StatusSuccess, waitForTask(t, cli, restoreTask1.TaskID, 30*time.Second).Status)
require.FileExists(t, filepath.Join(targetPath1, "file1"))
require.DirExists(t, filepath.Join(targetPath1, "dir1"))
require.FileExists(t, filepath.Join(targetPath1, "dir1", "file2.kopia-entry"))
})

t.Run("ZipFile", func(t *testing.T) {
outputZipFile := filepath.Join(testutil.TempDirectory(t), "test1.zip")
restoreTask1, err := serverapi.Restore(ctx, cli, &serverapi.RestoreRequest{
Root: string(id11),
Options: restore.Options{
RestoreDirEntryAtDepth: math.MaxInt32,
},
ZipFile: outputZipFile,
})

require.NoError(t, err)
require.Equal(t, uitask.StatusSuccess, waitForTask(t, cli, restoreTask1.TaskID, 30*time.Second).Status)
require.FileExists(t, outputZipFile)
})

t.Run("UncompressedZipFile", func(t *testing.T) {
outputZipFile := filepath.Join(testutil.TempDirectory(t), "test1.zip")
restoreTask1, err := serverapi.Restore(ctx, cli, &serverapi.RestoreRequest{
Root: string(id11),
Options: restore.Options{
RestoreDirEntryAtDepth: math.MaxInt32,
},
ZipFile: outputZipFile,
UncompressedZip: true,
})

require.NoError(t, err)
require.Equal(t, uitask.StatusSuccess, waitForTask(t, cli, restoreTask1.TaskID, 30*time.Second).Status)
require.FileExists(t, outputZipFile)
})

t.Run("TarFile", func(t *testing.T) {
outputTarFile := filepath.Join(testutil.TempDirectory(t), "test1.tar")
restoreTask1, err := serverapi.Restore(ctx, cli, &serverapi.RestoreRequest{
Root: string(id11),
Options: restore.Options{
RestoreDirEntryAtDepth: math.MaxInt32,
},
TarFile: outputTarFile,
})

require.NoError(t, err)
require.Equal(t, uitask.StatusSuccess, waitForTask(t, cli, restoreTask1.TaskID, 30*time.Second).Status)
require.FileExists(t, outputTarFile)
})

t.Run("InvalidRequest", func(t *testing.T) {
requests := []*serverapi.RestoreRequest{
{
Root: string(id11),
// no output
},
{
// no root
ZipFile: filepath.Join(testutil.TempDirectory(t), "test1.zip"),
},
{
Root: string(id11 + "bad"),
ZipFile: filepath.Join(testutil.TempDirectory(t), "test1.zip"),
},
{
Root: string(id11),
ZipFile: "/no/such/directory/" + uuid.NewString() + "/test1.zip",
},
{
Root: string(id11),
TarFile: "/no/such/directory/" + uuid.NewString() + "/test1.tar",
},
}

for _, req := range requests {
_, err := serverapi.Restore(ctx, cli, req)

var se apiclient.HTTPStatusError

require.ErrorAs(t, err, &se)
}
})
}
10 changes: 10 additions & 0 deletions internal/serverapi/client_wrappers.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@ func Estimate(ctx context.Context, c *apiclient.KopiaAPIClient, req *EstimateReq
return resp, nil
}

// Restore starts snapshot restore task for a given directory.
func Restore(ctx context.Context, c *apiclient.KopiaAPIClient, req *RestoreRequest) (*uitask.Info, error) {
resp := &uitask.Info{}
if err := c.Post(ctx, "restore", req, resp); err != nil {
return nil, errors.Wrap(err, "Restore")
}

return resp, nil
}

// GetTask starts snapshot estimation task for a given directory.
func GetTask(ctx context.Context, c *apiclient.KopiaAPIClient, taskID string) (*uitask.Info, error) {
resp := &uitask.Info{}
Expand Down
2 changes: 1 addition & 1 deletion snapshot/restore/local_fs_output.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ type FilesystemOutput struct {

// copier is the StreamCopier to use for copying the actual bit stream to output.
// It is assigned at runtime based on the target filesystem and restore options.
copier streamCopier
copier streamCopier `json:"-"`
}

// Init initializes the internal members of the filesystem writer output.
Expand Down
4 changes: 2 additions & 2 deletions snapshot/restore/restore.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ type Options struct {
RestoreDirEntryAtDepth int32 `json:"restoreDirEntryAtDepth"`
MinSizeForPlaceholder int32 `json:"minSizeForPlaceholder"`

ProgressCallback func(ctx context.Context, s Stats)
Cancel chan struct{} // channel that can be externally closed to signal cancelation
ProgressCallback func(ctx context.Context, s Stats) `json:"-"`
Cancel chan struct{} `json:"-"` // channel that can be externally closed to signal cancelation
}

// Entry walks a snapshot root with given root entry and restores it to the provided output.
Expand Down

0 comments on commit a621cd3

Please sign in to comment.