mirror of
https://github.com/kopia/kopia.git
synced 2026-01-26 23:38:04 -05:00
269 lines
7.2 KiB
Go
269 lines
7.2 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/url"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/kopia/kopia/internal/serverapi"
|
|
"github.com/kopia/kopia/repo"
|
|
"github.com/kopia/kopia/repo/manifest"
|
|
"github.com/kopia/kopia/snapshot"
|
|
"github.com/kopia/kopia/snapshot/policy"
|
|
)
|
|
|
|
func handleListSnapshots(ctx context.Context, rc requestContext) (interface{}, *apiError) {
|
|
si := getSnapshotSourceFromURL(rc.req.URL)
|
|
|
|
manifestIDs, err := snapshot.ListSnapshotManifests(ctx, rc.rep, &si, nil)
|
|
if err != nil {
|
|
return nil, internalServerError(err)
|
|
}
|
|
|
|
manifests, err := snapshot.LoadSnapshots(ctx, rc.rep, manifestIDs)
|
|
if err != nil {
|
|
return nil, internalServerError(err)
|
|
}
|
|
|
|
manifests = snapshot.SortByTime(manifests, false)
|
|
|
|
resp := &serverapi.SnapshotsResponse{
|
|
Snapshots: []*serverapi.Snapshot{},
|
|
}
|
|
|
|
pol, _, _, err := policy.GetEffectivePolicy(ctx, rc.rep, si)
|
|
if err == nil {
|
|
pol.RetentionPolicy.ComputeRetentionReasons(manifests)
|
|
}
|
|
|
|
for _, m := range manifests {
|
|
resp.Snapshots = append(resp.Snapshots, convertSnapshotManifest(m))
|
|
}
|
|
|
|
resp.UnfilteredCount = len(resp.Snapshots)
|
|
|
|
if rc.queryParam("all") == "" {
|
|
resp.Snapshots = uniqueSnapshots(resp.Snapshots)
|
|
resp.UniqueCount = len(resp.Snapshots)
|
|
} else {
|
|
resp.UniqueCount = len(uniqueSnapshots(resp.Snapshots))
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
func handleDeleteSnapshots(ctx context.Context, rc requestContext) (interface{}, *apiError) {
|
|
var req serverapi.DeleteSnapshotsRequest
|
|
|
|
if err := json.Unmarshal(rc.body, &req); err != nil {
|
|
return nil, requestError(serverapi.ErrorMalformedRequest, "malformed request")
|
|
}
|
|
|
|
sm := rc.srv.snapshotAllSourceManagers()[req.SourceInfo]
|
|
if sm == nil {
|
|
return nil, requestError(serverapi.ErrorNotFound, "unknown source")
|
|
}
|
|
|
|
// stop source manager and remove from map
|
|
if req.DeleteSourceAndPolicy {
|
|
if !rc.srv.deleteSourceManager(ctx, req.SourceInfo) {
|
|
return nil, requestError(serverapi.ErrorNotFound, "unknown source")
|
|
}
|
|
}
|
|
|
|
if err := repo.WriteSession(ctx, rc.rep, repo.WriteSessionOptions{
|
|
Purpose: "DeleteSnapshots",
|
|
}, func(ctx context.Context, w repo.RepositoryWriter) error {
|
|
var manifestIDs []manifest.ID
|
|
|
|
if req.DeleteSourceAndPolicy {
|
|
mans, err := snapshot.ListSnapshotManifests(ctx, w, &req.SourceInfo, nil)
|
|
if err != nil {
|
|
return errors.Wrap(err, "unable to list snapshots")
|
|
}
|
|
|
|
manifestIDs = mans
|
|
} else {
|
|
snaps, err := snapshot.LoadSnapshots(ctx, w, req.SnapshotManifestIDs)
|
|
if err != nil {
|
|
return errors.Wrap(err, "unable to load snapshots")
|
|
}
|
|
|
|
for _, sn := range snaps {
|
|
if sn.Source != req.SourceInfo {
|
|
return errors.New("source info does not match snapshot source")
|
|
}
|
|
}
|
|
|
|
manifestIDs = req.SnapshotManifestIDs
|
|
}
|
|
|
|
for _, m := range manifestIDs {
|
|
if err := w.DeleteManifest(ctx, m); err != nil {
|
|
return errors.Wrap(err, "uanble to delete snapshot")
|
|
}
|
|
}
|
|
|
|
if req.DeleteSourceAndPolicy {
|
|
if err := policy.RemovePolicy(ctx, w, req.SourceInfo); err != nil {
|
|
return errors.Wrap(err, "unable to remove policy")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}); err != nil {
|
|
// if source deletion failed, refresh the repository to rediscover the source
|
|
rc.srv.Refresh()
|
|
|
|
return nil, internalServerError(err)
|
|
}
|
|
|
|
return &serverapi.Empty{}, nil
|
|
}
|
|
|
|
func handleEditSnapshots(ctx context.Context, rc requestContext) (interface{}, *apiError) {
|
|
var req serverapi.EditSnapshotsRequest
|
|
|
|
if err := json.Unmarshal(rc.body, &req); err != nil {
|
|
return nil, requestError(serverapi.ErrorMalformedRequest, "malformed request")
|
|
}
|
|
|
|
var snaps []*serverapi.Snapshot
|
|
|
|
if err := repo.WriteSession(ctx, rc.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 forAllSourceManagersMatchingURLFilter(ctx context.Context, managers map[snapshot.SourceInfo]*sourceManager, c func(s *sourceManager, ctx context.Context) serverapi.SourceActionResponse, values url.Values) (interface{}, *apiError) {
|
|
resp := &serverapi.MultipleSourceActionResponse{
|
|
Sources: map[string]serverapi.SourceActionResponse{},
|
|
}
|
|
|
|
for src, mgr := range managers {
|
|
if mgr.isReadOnly {
|
|
continue
|
|
}
|
|
|
|
if !sourceMatchesURLFilter(src, values) {
|
|
continue
|
|
}
|
|
|
|
resp.Sources[src.String()] = c(mgr, ctx)
|
|
}
|
|
|
|
if len(resp.Sources) == 0 {
|
|
return nil, notFoundError("no source matching the provided filters")
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
func handleUpload(ctx context.Context, rc requestContext) (interface{}, *apiError) {
|
|
return forAllSourceManagersMatchingURLFilter(ctx, rc.srv.snapshotAllSourceManagers(), (*sourceManager).upload, rc.req.URL.Query())
|
|
}
|
|
|
|
func handleCancel(ctx context.Context, rc requestContext) (interface{}, *apiError) {
|
|
return forAllSourceManagersMatchingURLFilter(ctx, rc.srv.snapshotAllSourceManagers(), (*sourceManager).cancel, rc.req.URL.Query())
|
|
}
|
|
|
|
func handlePause(ctx context.Context, rc requestContext) (interface{}, *apiError) {
|
|
return forAllSourceManagersMatchingURLFilter(ctx, rc.srv.snapshotAllSourceManagers(), (*sourceManager).pause, rc.req.URL.Query())
|
|
}
|
|
|
|
func handleResume(ctx context.Context, rc requestContext) (interface{}, *apiError) {
|
|
return forAllSourceManagersMatchingURLFilter(ctx, rc.srv.snapshotAllSourceManagers(), (*sourceManager).resume, rc.req.URL.Query())
|
|
}
|
|
|
|
func uniqueSnapshots(rows []*serverapi.Snapshot) []*serverapi.Snapshot {
|
|
result := []*serverapi.Snapshot{}
|
|
resultByRootEntry := map[string]*serverapi.Snapshot{}
|
|
|
|
for _, r := range rows {
|
|
last := resultByRootEntry[r.RootEntry]
|
|
if last == nil {
|
|
result = append(result, r)
|
|
resultByRootEntry[r.RootEntry] = r
|
|
} else {
|
|
last.RetentionReasons = append(last.RetentionReasons, r.RetentionReasons...)
|
|
last.Pins = append(last.Pins, r.Pins...)
|
|
}
|
|
}
|
|
|
|
for _, r := range result {
|
|
r.RetentionReasons = policy.CompactRetentionReasons(r.RetentionReasons)
|
|
r.Pins = policy.CompactPins(r.Pins)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func sourceMatchesURLFilter(src snapshot.SourceInfo, query url.Values) bool {
|
|
if v := query.Get("host"); v != "" && src.Host != v {
|
|
return false
|
|
}
|
|
|
|
if v := query.Get("userName"); v != "" && src.UserName != v {
|
|
return false
|
|
}
|
|
|
|
if v := query.Get("path"); v != "" && src.Path != v {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func convertSnapshotManifest(m *snapshot.Manifest) *serverapi.Snapshot {
|
|
e := &serverapi.Snapshot{
|
|
ID: m.ID,
|
|
Description: m.Description,
|
|
StartTime: m.StartTime,
|
|
EndTime: m.EndTime,
|
|
IncompleteReason: m.IncompleteReason,
|
|
RootEntry: m.RootObjectID().String(),
|
|
RetentionReasons: append([]string{}, m.RetentionReasons...),
|
|
Pins: append([]string{}, m.Pins...),
|
|
}
|
|
|
|
if re := m.RootEntry; re != nil {
|
|
e.Summary = re.DirSummary
|
|
}
|
|
|
|
return e
|
|
}
|