add repo snapshot operations

Signed-off-by: Lyndon-Li <lyonghui@vmware.com>
This commit is contained in:
Lyndon-Li
2026-04-23 18:30:59 +08:00
parent 2d6865d6e5
commit 4befbc0afe
3 changed files with 350 additions and 13 deletions

View File

@@ -25,12 +25,14 @@ import (
"sync/atomic"
"time"
"github.com/kopia/kopia/fs"
"github.com/kopia/kopia/repo"
"github.com/kopia/kopia/repo/compression"
"github.com/kopia/kopia/repo/content/index"
"github.com/kopia/kopia/repo/maintenance"
"github.com/kopia/kopia/repo/manifest"
"github.com/kopia/kopia/repo/object"
"github.com/kopia/kopia/snapshot"
"github.com/kopia/kopia/snapshot/snapshotmaintenance"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
@@ -446,19 +448,83 @@ func (kr *kopiaRepository) DeleteManifest(ctx context.Context, id udmrepo.ID) er
return nil
}
// TODO add implementation in following PRs
func (kr *kopiaRepository) SaveSnapshot(ctx context.Context, snap udmrepo.Snapshot) (udmrepo.ID, error) {
return "", errors.New("not supported")
if kr.rawWriter == nil {
return "", errors.New("repo writer is closed or not open")
}
if snap.Source == "" {
return "", errors.New("invalid snapshot source")
}
rootObj, err := object.ParseID(string(snap.RootObject.ID))
if err != nil {
return "", errors.Wrapf(err, "error parsing root object ID %v", snap.RootObject.ID)
}
manifest := snapshot.Manifest{
Source: snapshot.SourceInfo{
UserName: udmrepo.GetRepoUser(),
Host: udmrepo.GetRepoDomain(),
Path: snap.Source,
},
Description: snap.Description,
StartTime: fs.UTCTimestampFromTime(snap.StartTime),
EndTime: fs.UTCTimestampFromTime(snap.EndTime),
RootEntry: &snapshot.DirEntry{
Type: snapshot.EntryTypeDirectory,
ObjectID: rootObj,
ModTime: fs.UTCTimestampFromTime(snap.RootObject.ModTime),
Permissions: snapshot.Permissions(snap.RootObject.Permissions),
FileSize: snap.RootObject.Size,
UserID: snap.RootObject.UserID,
GroupID: snap.RootObject.GroupID,
},
Tags: snap.Tags,
}
id, err := snapshot.SaveSnapshot(ctx, kr.rawWriter, &manifest)
if err != nil {
return "", errors.Wrap(err, "error saving snapshot")
}
return udmrepo.ID(id), nil
}
// TODO add implementation in following PRs
func (kr *kopiaRepository) GetSnapshot(ctx context.Context, id udmrepo.ID) (udmrepo.Snapshot, error) {
return udmrepo.Snapshot{}, errors.New("not supported")
snap, err := snapshot.LoadSnapshot(ctx, kr.rawRepo, manifest.ID(id))
if err != nil {
return udmrepo.Snapshot{}, errors.Wrap(err, "error getting snapshot manifest")
}
if snap.RootEntry == nil {
return udmrepo.Snapshot{}, errors.Wrap(err, "invalid snapshot root entry")
}
return udmrepo.Snapshot{
Source: snap.Source.Path,
Description: snap.Description,
StartTime: snap.StartTime.ToTime(),
EndTime: snap.EndTime.ToTime(),
Tags: snap.Tags,
RootObject: udmrepo.ObjectMetadata{
ID: udmrepo.ID(snap.RootEntry.ObjectID.String()),
Type: udmrepo.ObjectDataTypeMetadata,
Size: snap.RootEntry.FileSize,
ModTime: snap.RootEntry.ModTime.ToTime(),
Permissions: int(snap.RootEntry.Permissions),
UserID: snap.RootEntry.UserID,
GroupID: snap.RootEntry.GroupID,
},
}, nil
}
// TODO add implementation in following PRs
func (kr *kopiaRepository) DeleteSnapshot(ctx context.Context, id udmrepo.ID) error {
return errors.New("not supported")
if _, err := kr.GetSnapshot(ctx, id); err != nil {
return errors.Wrap(err, "error getting snapshot")
}
return kr.DeleteManifest(ctx, id)
}
func (kr *kopiaRepository) Flush(ctx context.Context) error {

View File

@@ -18,14 +18,17 @@ package kopialib
import (
"context"
"encoding/json"
"math"
"os"
"testing"
"time"
"github.com/kopia/kopia/fs"
"github.com/kopia/kopia/repo"
"github.com/kopia/kopia/repo/manifest"
"github.com/kopia/kopia/repo/object"
"github.com/kopia/kopia/snapshot"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
@@ -1282,3 +1285,268 @@ func TestIsReady(t *testing.T) {
})
}
}
func TestSaveSnapshot(t *testing.T) {
testCases := []struct {
name string
rawWriter *repomocks.MockRepositoryWriter
snap udmrepo.Snapshot
rawWriterRetErr error
rawWriterRetID manifest.ID
setWriterMock bool
expectedErr string
expectedID udmrepo.ID
}{
{
name: "raw writer is nil",
expectedErr: "repo writer is closed or not open",
},
{
name: "invalid snapshot source",
rawWriter: repomocks.NewMockRepositoryWriter(t),
snap: udmrepo.Snapshot{
Source: "",
},
expectedErr: "invalid snapshot source",
},
{
name: "invalid root object id",
rawWriter: repomocks.NewMockRepositoryWriter(t),
snap: udmrepo.Snapshot{
Source: "fake-source",
RootObject: udmrepo.ObjectMetadata{ID: "fake-id"},
},
expectedErr: "error parsing root object ID fake-id: malformed content ID: \"fake-id\": invalid content prefix",
},
{
name: "save snapshot fail",
rawWriter: repomocks.NewMockRepositoryWriter(t),
snap: udmrepo.Snapshot{
Source: "fake-source",
RootObject: udmrepo.ObjectMetadata{ID: "I123456"},
},
rawWriterRetErr: errors.New("fake-save-error"),
setWriterMock: true,
expectedErr: "error saving snapshot: error putting manifest: fake-save-error",
},
{
name: "succeed",
rawWriter: repomocks.NewMockRepositoryWriter(t),
snap: udmrepo.Snapshot{
Source: "fake-source",
RootObject: udmrepo.ObjectMetadata{ID: "I123456"},
},
rawWriterRetID: manifest.ID("fake-manifest-id"),
setWriterMock: true,
expectedID: udmrepo.ID("fake-manifest-id"),
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
kr := &kopiaRepository{}
if tc.rawWriter != nil {
if tc.setWriterMock {
tc.rawWriter.On("PutManifest", mock.Anything, mock.Anything, mock.Anything).Return(tc.rawWriterRetID, tc.rawWriterRetErr)
}
kr.rawWriter = tc.rawWriter
}
id, err := kr.SaveSnapshot(t.Context(), tc.snap)
if tc.expectedErr == "" {
require.NoError(t, err)
assert.Equal(t, tc.expectedID, id)
} else {
assert.EqualError(t, err, tc.expectedErr)
}
})
}
}
func TestGetSnapshot(t *testing.T) {
expectedTime := time.Now()
rawObjID, _ := object.ParseID("I123456")
mockMani := &snapshot.Manifest{
Source: snapshot.SourceInfo{Path: "fake-source"},
Description: "fake-desc",
StartTime: fs.UTCTimestampFromTime(expectedTime),
EndTime: fs.UTCTimestampFromTime(expectedTime.Add(time.Minute)),
RootEntry: &snapshot.DirEntry{
ObjectID: rawObjID,
},
Tags: map[string]string{"tag1": "val1"},
}
testCases := []struct {
name string
rawRepo *repomocks.MockRepository
snapshotID udmrepo.ID
rawRepoRetErr error
setRepoMock bool
expectedErr string
expectedSnap udmrepo.Snapshot
}{
{
name: "get snapshot fail",
rawRepo: repomocks.NewMockRepository(t),
snapshotID: udmrepo.ID("fake-id"),
rawRepoRetErr: errors.New("fake-get-error"),
setRepoMock: true,
expectedErr: "error getting snapshot manifest: unable to find manifest entries: fake-get-error",
},
{
name: "succeed",
rawRepo: repomocks.NewMockRepository(t),
snapshotID: udmrepo.ID("fake-id"),
setRepoMock: true,
expectedSnap: udmrepo.Snapshot{
Source: "fake-source",
Description: "fake-desc",
StartTime: mockMani.StartTime.ToTime(),
EndTime: mockMani.EndTime.ToTime(),
RootObject: udmrepo.ObjectMetadata{
ID: udmrepo.ID("I123456"),
Type: udmrepo.ObjectDataTypeMetadata,
},
Tags: map[string]string{"tag1": "val1"},
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
kr := &kopiaRepository{}
if tc.rawRepo != nil {
if tc.setRepoMock {
tc.rawRepo.On("GetManifest", mock.Anything, mock.Anything, mock.Anything).Return(&manifest.EntryMetadata{
Labels: map[string]string{
manifest.TypeLabelKey: snapshot.ManifestType,
},
}, tc.rawRepoRetErr).Run(func(args mock.Arguments) {
if tc.rawRepoRetErr == nil {
payload := args.Get(2)
if ptr, ok := payload.(*snapshot.Manifest); ok {
*ptr = *mockMani
} else {
b, _ := json.Marshal(mockMani)
json.Unmarshal(b, payload)
}
}
})
}
kr.rawRepo = tc.rawRepo
}
snap, err := kr.GetSnapshot(t.Context(), tc.snapshotID)
if tc.expectedErr == "" {
require.NoError(t, err)
assert.Equal(t, tc.expectedSnap, snap)
} else {
assert.EqualError(t, err, tc.expectedErr)
}
})
}
}
func TestDeleteSnapshot(t *testing.T) {
expectedTime := time.Now()
rawObjID, _ := object.ParseID("I123456")
mockMani := &snapshot.Manifest{
Source: snapshot.SourceInfo{Path: "fake-source"},
Description: "fake-desc",
StartTime: fs.UTCTimestampFromTime(expectedTime),
EndTime: fs.UTCTimestampFromTime(expectedTime.Add(time.Minute)),
RootEntry: &snapshot.DirEntry{
ObjectID: rawObjID,
},
Tags: map[string]string{"tag1": "val1"},
}
testCases := []struct {
name string
rawRepo *repomocks.MockRepository
rawWriter *repomocks.MockRepositoryWriter
snapshotID udmrepo.ID
rawRepoRetErr error
rawWriterRetErr error
setRepoMock bool
setWriterMock bool
expectedErr string
}{
{
name: "get snapshot fail",
rawRepo: repomocks.NewMockRepository(t),
snapshotID: udmrepo.ID("fake-id"),
rawRepoRetErr: errors.New("fake-get-error"),
setRepoMock: true,
expectedErr: "error getting snapshot: error getting snapshot manifest: unable to find manifest entries: fake-get-error",
},
{
name: "delete manifest fail",
rawRepo: repomocks.NewMockRepository(t),
rawWriter: repomocks.NewMockRepositoryWriter(t),
snapshotID: udmrepo.ID("fake-id"),
rawWriterRetErr: errors.New("fake-delete-error"),
setRepoMock: true,
setWriterMock: true,
expectedErr: "error to delete manifest: fake-delete-error",
},
{
name: "succeed",
rawRepo: repomocks.NewMockRepository(t),
rawWriter: repomocks.NewMockRepositoryWriter(t),
snapshotID: udmrepo.ID("fake-id"),
setRepoMock: true,
setWriterMock: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
kr := &kopiaRepository{}
if tc.rawRepo != nil {
if tc.setRepoMock {
tc.rawRepo.On("GetManifest", mock.Anything, mock.Anything, mock.Anything).Return(&manifest.EntryMetadata{
Labels: map[string]string{
manifest.TypeLabelKey: snapshot.ManifestType,
},
}, tc.rawRepoRetErr).Run(func(args mock.Arguments) {
if tc.rawRepoRetErr == nil {
payload := args.Get(2)
if ptr, ok := payload.(*snapshot.Manifest); ok {
*ptr = *mockMani
} else {
b, _ := json.Marshal(mockMani)
json.Unmarshal(b, payload)
}
}
})
}
kr.rawRepo = tc.rawRepo
}
if tc.rawWriter != nil {
if tc.setWriterMock {
tc.rawWriter.On("DeleteManifest", mock.Anything, mock.Anything).Return(tc.rawWriterRetErr)
}
kr.rawWriter = tc.rawWriter
}
err := kr.DeleteSnapshot(t.Context(), tc.snapshotID)
if tc.expectedErr == "" {
require.NoError(t, err)
} else {
assert.EqualError(t, err, tc.expectedErr)
}
})
}
}

View File

@@ -77,15 +77,18 @@ type AdvancedFeatureInfo struct {
}
type ObjectMetadata struct {
ID ID
Type int // OBJECT_DATA_TYPE_*
Size int64
ID ID
Type int // OBJECT_DATA_TYPE_*
Size int64
ModTime time.Time
Permissions int
UserID uint32
GroupID uint32
}
type Metadata struct {
SubObjects []ObjectMetadata // For dir metadata only, the sub objects in this dir.
ExtraDataLen int // Extra data associated to this metadata.
ExtraData []byte
SubObjects []ObjectMetadata
Summary string
}
type Snapshot struct {
@@ -94,7 +97,7 @@ type Snapshot struct {
StartTime time.Time
EndTime time.Time
Tags map[string]string
RootObject ID
RootObject ObjectMetadata
}
// BackupRepoService is used to initialize, open or maintain a backup repository