diff --git a/go.mod b/go.mod index acc7f795d..6061d3271 100644 --- a/go.mod +++ b/go.mod @@ -88,7 +88,7 @@ require ( github.com/googleapis/gax-go/v2 v2.1.1 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/kopia/htmluibuild v0.0.0-20220211164311-3c58cff936c2 + github.com/kopia/htmluibuild v0.0.0-20220212024129-c944ece3b4d0 github.com/kr/fs v0.1.0 // indirect github.com/mattn/go-ieproxy v0.0.1 // indirect github.com/mattn/go-isatty v0.0.14 // indirect diff --git a/go.sum b/go.sum index 015108c43..6affa4cfc 100644 --- a/go.sum +++ b/go.sum @@ -406,6 +406,10 @@ github.com/kopia/htmluibuild v0.0.0-20220208062920-a1d47d1e3096 h1:rbrPWsO4OTT2c github.com/kopia/htmluibuild v0.0.0-20220208062920-a1d47d1e3096/go.mod h1:eWer4rx9P8lJo2eKc+Q7AZ1dE1x1hJNdkbDFPzMu1Hw= github.com/kopia/htmluibuild v0.0.0-20220211164311-3c58cff936c2 h1:8EzPQNPLdwH3camd8l6/gHpSRS43cKqd38FKebcXbrE= github.com/kopia/htmluibuild v0.0.0-20220211164311-3c58cff936c2/go.mod h1:eWer4rx9P8lJo2eKc+Q7AZ1dE1x1hJNdkbDFPzMu1Hw= +github.com/kopia/htmluibuild v0.0.0-20220212022637-f4a448ee180a h1:FugmgYM3pW92AjfMJsGZArtF4TROg5rDKLalXXk/50E= +github.com/kopia/htmluibuild v0.0.0-20220212022637-f4a448ee180a/go.mod h1:eWer4rx9P8lJo2eKc+Q7AZ1dE1x1hJNdkbDFPzMu1Hw= +github.com/kopia/htmluibuild v0.0.0-20220212024129-c944ece3b4d0 h1:jdyUUH53CP2G30w4endw1eXE3GmI4km2hmK1ZZ75fIg= +github.com/kopia/htmluibuild v0.0.0-20220212024129-c944ece3b4d0/go.mod h1:eWer4rx9P8lJo2eKc+Q7AZ1dE1x1hJNdkbDFPzMu1Hw= github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= diff --git a/internal/server/api_snapshots.go b/internal/server/api_snapshots.go index 70e2dd48d..00cce4fbe 100644 --- a/internal/server/api_snapshots.go +++ b/internal/server/api_snapshots.go @@ -128,6 +128,52 @@ func (s *Server) handleDeleteSnapshots(ctx context.Context, r *http.Request, bod return &serverapi.Empty{}, nil } +func (s *Server) handleEditSnapshots(ctx context.Context, r *http.Request, body []byte) (interface{}, *apiError) { + var req serverapi.EditSnapshotsRequest + + if err := json.Unmarshal(body, &req); err != nil { + return nil, requestError(serverapi.ErrorMalformedRequest, "malformed request") + } + + var snaps []*serverapi.Snapshot + + if err := repo.WriteSession(ctx, s.rep, repo.WriteSessionOptions{ + Purpose: "EditSnapshots", + }, func(ctx context.Context, w repo.RepositoryWriter) error { + for _, id := range req.Snapshots { + snap, err := snapshot.LoadSnapshot(ctx, w, id) + if err != nil { + return errors.Wrap(err, "unable to load snapshot") + } + + changed := false + + if snap.UpdatePins(req.AddPins, req.RemovePins) { + changed = true + } + + if req.NewDescription != nil { + changed = true + snap.Description = *req.NewDescription + } + + if changed { + if err := snapshot.UpdateSnapshot(ctx, w, snap); err != nil { + return errors.Wrap(err, "error updating snapshot") + } + } + + snaps = append(snaps, convertSnapshotManifest(snap)) + } + + return nil + }); err != nil { + return nil, internalServerError(err) + } + + return snaps, nil +} + func uniqueSnapshots(rows []*serverapi.Snapshot) []*serverapi.Snapshot { result := []*serverapi.Snapshot{} resultByRootEntry := map[string]*serverapi.Snapshot{} diff --git a/internal/server/api_snapshots_test.go b/internal/server/api_snapshots_test.go index 95d34c279..2c8d2e637 100644 --- a/internal/server/api_snapshots_test.go +++ b/internal/server/api_snapshots_test.go @@ -177,3 +177,81 @@ func TestListAndDeleteSnapshots(t *testing.T) { require.Empty(t, sourceList.Sources) } + +func TestEditSnapshots(t *testing.T) { + ctx, env := repotesting.NewEnvironment(t, repotesting.FormatNotImportant) + + si1 := localSource(env, "/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.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)) + + resp, err := serverapi.ListSnapshots(ctx, cli, si1, true) + require.NoError(t, err) + + require.Len(t, resp.Snapshots, 1) + + var ( + updated []*serverapi.Snapshot + + newDesc1 = "desc1" + newDesc2 = "desc2" + ) + + require.NoError(t, cli.Post(ctx, "snapshots/edit", &serverapi.EditSnapshotsRequest{ + Snapshots: []manifest.ID{id11}, + AddPins: []string{"pin1", "pin2"}, + NewDescription: &newDesc1, + }, &updated)) + + require.Len(t, updated, 1) + require.EqualValues(t, []string{"pin1", "pin2"}, updated[0].Pins) + require.EqualValues(t, newDesc1, updated[0].Description) + + require.NoError(t, cli.Post(ctx, "snapshots/edit", &serverapi.EditSnapshotsRequest{ + Snapshots: []manifest.ID{updated[0].ID}, + AddPins: []string{"pin3"}, + RemovePins: []string{"pin1"}, + NewDescription: &newDesc2, + }, &updated)) + + require.Len(t, updated, 1) + require.EqualValues(t, []string{"pin2", "pin3"}, updated[0].Pins) + require.EqualValues(t, newDesc2, updated[0].Description) + + require.NoError(t, cli.Post(ctx, "snapshots/edit", &serverapi.EditSnapshotsRequest{ + Snapshots: []manifest.ID{updated[0].ID}, + RemovePins: []string{"pin3"}, + }, &updated)) + + require.Len(t, updated, 1) + require.EqualValues(t, []string{"pin2"}, updated[0].Pins) + require.EqualValues(t, newDesc2, updated[0].Description) +} diff --git a/internal/server/server.go b/internal/server/server.go index 9110d5836..5e9e14876 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -96,6 +96,7 @@ func (s *Server) SetupHTMLUIAPIHandlers(m *mux.Router) { // snapshots m.HandleFunc("/api/v1/snapshots", s.handleUI(s.handleListSnapshots)).Methods(http.MethodGet) m.HandleFunc("/api/v1/snapshots/delete", s.handleUI(s.handleDeleteSnapshots)).Methods(http.MethodPost) + m.HandleFunc("/api/v1/snapshots/edit", s.handleUI(s.handleEditSnapshots)).Methods(http.MethodPost) m.HandleFunc("/api/v1/policy", s.handleUI(s.handlePolicyGet)).Methods(http.MethodGet) m.HandleFunc("/api/v1/policy", s.handleUI(s.handlePolicyPut)).Methods(http.MethodPut) m.HandleFunc("/api/v1/policy", s.handleUI(s.handlePolicyDelete)).Methods(http.MethodDelete) diff --git a/internal/serverapi/serverapi.go b/internal/serverapi/serverapi.go index e5f528505..4568f8784 100644 --- a/internal/serverapi/serverapi.go +++ b/internal/serverapi/serverapi.go @@ -178,6 +178,14 @@ type DeleteSnapshotsRequest struct { DeleteSourceAndPolicy bool `json:"deleteSourceAndPolicy"` } +// EditSnapshotsRequest contains request to edit one or more snapshots. +type EditSnapshotsRequest struct { + Snapshots []manifest.ID `json:"snapshots"` + NewDescription *string `json:"description"` + AddPins []string `json:"addPins"` + RemovePins []string `json:"removePins"` +} + // MountSnapshotRequest contains request to mount a snapshot. type MountSnapshotRequest struct { Root string `json:"root"`