diff --git a/src/internal/kopia/model_store.go b/src/internal/kopia/model_store.go index 3978a5d9f..b75eddf09 100644 --- a/src/internal/kopia/model_store.go +++ b/src/internal/kopia/model_store.go @@ -15,6 +15,7 @@ const stableIDKey = "stableID" var ( errNoModelStoreID = errors.New("model has no ModelStoreID") + errNoStableID = errors.New("model has no StableID") errBadTagKey = errors.New("tag key overlaps with required key") ) @@ -67,7 +68,7 @@ func tagsForModelWithID( tags map[string]string, ) (map[string]string, error) { if len(id) == 0 { - return nil, errors.New("missing ID for model") + return nil, errors.WithStack(errNoStableID) } res, err := tagsForModel(t, tags) @@ -149,6 +150,31 @@ func (ms *ModelStore) GetIDsForType( return nil, nil } +// getModelStoreID gets the ModelStoreID of the model with the given +// StableID. Returns github.com/kopia/kopia/repo/manifest.ErrNotFound if no +// model was found. Returns an error if the given StableID is empty or more than +// one model has the same StableID. +func (ms *ModelStore) getModelStoreID(ctx context.Context, id model.ID) (manifest.ID, error) { + if len(id) == 0 { + return "", errors.WithStack(errNoStableID) + } + + tags := map[string]string{stableIDKey: string(id)} + metadata, err := ms.wrapper.rep.FindManifests(ctx, tags) + if err != nil { + return "", errors.Wrap(err, "getting ModelStoreID") + } + + if len(metadata) == 0 { + return "", errors.Wrap(manifest.ErrNotFound, "getting ModelStoreID") + } + if len(metadata) != 1 { + return "", errors.New("multiple models with same StableID") + } + + return metadata[0].ID, nil +} + // Get deserializes the model with the given ID into data. func (ms *ModelStore) Get(ctx context.Context, id model.ID, data any) error { return nil @@ -195,7 +221,37 @@ func (ms *ModelStore) Update( return "", nil } -// Delete deletes the model with the given ID from the model store. +// Delete deletes the model with the given StableID. Turns into a noop if id is +// not empty but the model does not exist. func (ms *ModelStore) Delete(ctx context.Context, id model.ID) error { - return nil + latest, err := ms.getModelStoreID(ctx, id) + if err != nil { + if errors.Is(err, manifest.ErrNotFound) { + return nil + } + + return err + } + + return ms.DeleteWithModelStoreID(ctx, latest) +} + +// DeletWithModelStoreID deletes the model with the given ModelStoreID from the +// model store. Turns into a noop if id is not empty but the model does not +// exist. +func (ms *ModelStore) DeleteWithModelStoreID(ctx context.Context, id manifest.ID) error { + if len(id) == 0 { + return errors.WithStack(errNoModelStoreID) + } + + err := repo.WriteSession( + ctx, + ms.wrapper.rep, + repo.WriteSessionOptions{Purpose: "ModelStoreDelete"}, + func(innerCtx context.Context, w repo.RepositoryWriter) error { + return w.DeleteManifest(innerCtx, id) + }, + ) + + return errors.Wrap(err, "deleting model") } diff --git a/src/internal/kopia/model_store_test.go b/src/internal/kopia/model_store_test.go index 428278bee..7418051cc 100644 --- a/src/internal/kopia/model_store_test.go +++ b/src/internal/kopia/model_store_test.go @@ -96,14 +96,17 @@ func (suite *ModelStoreIntegrationSuite) TestNoIDsErrors() { }() noStableID := &fooModel{Bar: uuid.NewString()} - noStableID.Base().StableID = "" - noStableID.Base().ModelStoreID = manifest.ID(uuid.NewString()) + noStableID.StableID = "" + noStableID.ModelStoreID = manifest.ID(uuid.NewString()) noModelStoreID := &fooModel{Bar: uuid.NewString()} - noModelStoreID.Base().StableID = model.ID(uuid.NewString()) - noModelStoreID.Base().ModelStoreID = "" + noModelStoreID.StableID = model.ID(uuid.NewString()) + noModelStoreID.ModelStoreID = "" assert.Error(t, m.GetWithModelStoreID(ctx, "", nil)) + + assert.Error(t, m.Delete(ctx, "")) + assert.Error(t, m.DeleteWithModelStoreID(ctx, "")) } func (suite *ModelStoreIntegrationSuite) TestPutGet() { @@ -155,11 +158,11 @@ func (suite *ModelStoreIntegrationSuite) TestPutGet() { return } - require.NotEmpty(t, foo.Base().ModelStoreID) - require.NotEmpty(t, foo.Base().StableID) + require.NotEmpty(t, foo.ModelStoreID) + require.NotEmpty(t, foo.StableID) returned := &fooModel{} - err = m.GetWithModelStoreID(ctx, foo.Base().ModelStoreID, returned) + err = m.GetWithModelStoreID(ctx, foo.ModelStoreID, returned) require.NoError(t, err) assert.Equal(t, foo, returned) }) @@ -182,11 +185,11 @@ func (suite *ModelStoreIntegrationSuite) TestPutGet_WithTags() { require.NoError(t, m.Put(ctx, BackupOpModel, foo)) - require.NotEmpty(t, foo.Base().ModelStoreID) - require.NotEmpty(t, foo.Base().StableID) + require.NotEmpty(t, foo.ModelStoreID) + require.NotEmpty(t, foo.StableID) returned := &fooModel{} - err := m.GetWithModelStoreID(ctx, foo.Base().ModelStoreID, returned) + err := m.GetWithModelStoreID(ctx, foo.ModelStoreID, returned) require.NoError(t, err) assert.Equal(t, foo, returned) } @@ -202,3 +205,36 @@ func (suite *ModelStoreIntegrationSuite) TestGet_NotFoundErrors() { assert.ErrorIs(t, m.GetWithModelStoreID(ctx, "baz", nil), manifest.ErrNotFound) } + +func (suite *ModelStoreIntegrationSuite) TestPutDelete() { + ctx := context.Background() + t := suite.T() + + m := getModelStore(t, ctx) + defer func() { + assert.NoError(t, m.wrapper.Close(ctx)) + }() + + foo := &fooModel{Bar: uuid.NewString()} + + require.NoError(t, m.Put(ctx, BackupOpModel, foo)) + + require.NoError(t, m.Delete(ctx, foo.StableID)) + + returned := &fooModel{} + err := m.GetWithModelStoreID(ctx, foo.ModelStoreID, returned) + assert.ErrorIs(t, err, manifest.ErrNotFound) +} + +func (suite *ModelStoreIntegrationSuite) TestPutDelete_BadIDsNoop() { + ctx := context.Background() + t := suite.T() + + m := getModelStore(t, ctx) + defer func() { + assert.NoError(t, m.wrapper.Close(ctx)) + }() + + assert.NoError(t, m.Delete(ctx, "foo")) + assert.NoError(t, m.DeleteWithModelStoreID(ctx, "foo")) +}