Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 117 additions & 10 deletions core/block/import/common/objectcreator/objectcreator.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ func (oc *ObjectCreator) Create(dataObject *DataObject, sn *common.Snapshot) (*d
newID := oldIDtoNew[sn.Id]

if sn.Snapshot.SbType == coresb.SmartBlockTypeFile {
// Legacy file snapshots resolve to file-object ids via object-id providers.
// Apply imported details directly to the resolved file object so replace/import
// keeps snapshot metadata (for example name and dates).
oc.restoreLegacyFileDetails(snapshot, newID)
return nil, newID, nil
}

Expand Down Expand Up @@ -144,23 +148,40 @@ func (oc *ObjectCreator) Create(dataObject *DataObject, sn *common.Snapshot) (*d
if err != nil {
log.With("objectID", newID).Errorf("failed to install bundled relations and types: %s", err)
}
var respDetails *domain.Details
var (
respDetails *domain.Details
isExisting bool
)
if payload := dataObject.createPayloads[newID]; payload.RootRawChange != nil {
respDetails, err = oc.createNewObject(ctx, spaceID, payload, st, newID, oldIDtoNew)
if err != nil {
log.With("objectID", newID).Errorf("failed to create %s: %s", newID, err)
return nil, "", err
}
} else {
isExisting = true
if canUpdateObject(sn.Snapshot.SbType) {
respDetails = oc.updateExistingObject(st, oldIDtoNew, newID)
}
}
oc.setFavorite(snapshot, newID)

oc.setArchived(ctx, snapshot, newID)
wasUnarchived := oc.setArchived(ctx, snapshot, newID)

syncErr := oc.syncFilesAndLinks(dataObject.newIdsSet, domain.FullID{SpaceID: spaceID, ObjectID: newID}, origin)
if isExisting || wasUnarchived {
// Keep backup timestamp semantics for replace/existing-object import after
// archive/sync side-effects that can bump modified time.
// wasUnarchived covers the create-but-tree-exists path: the payload has
// RootRawChange so isExisting is false, yet the object may already exist
// (ErrTreeExists fallback) and have been archived before this import.
oc.restoreLastModifiedDate(snapshot, newID)
}
if isExisting && sn.Snapshot.SbType == coresb.SmartBlockTypeFileObject {
// File objects can retain runtime metadata despite resetState; re-apply
// imported file details explicitly for replace on existing objects.
oc.restoreLegacyFileDetails(snapshot, newID)
}
if syncErr != nil {
if errors.Is(syncErr, common.ErrFileLoad) {
return respDetails, newID, syncErr
Expand All @@ -172,7 +193,6 @@ func (oc *ObjectCreator) Create(dataObject *DataObject, sn *common.Snapshot) (*d
func canUpdateObject(sbType coresb.SmartBlockType) bool {
return sbType != coresb.SmartBlockTypeRelation &&
sbType != coresb.SmartBlockTypeRelationOption &&
sbType != coresb.SmartBlockTypeFileObject &&
sbType != coresb.SmartBlockTypeParticipant
}

Expand Down Expand Up @@ -403,15 +423,102 @@ func (oc *ObjectCreator) setFavorite(snapshot *common.StateSnapshot, newID strin
}
}

func (oc *ObjectCreator) setArchived(ctx context.Context, snapshot *common.StateSnapshot, newID string) {
isArchive := snapshot.Details.GetBool(bundle.RelationKeyIsArchived)
if isArchive {
err := oc.detailsService.SetIsArchived(ctx, newID, true)
if err != nil {
log.With(zap.String("object id", newID)).
Errorf("failed to set isFavorite when importing object %s: %s", newID, err)
func (oc *ObjectCreator) setArchived(ctx context.Context, snapshot *common.StateSnapshot, newID string) bool {
desiredArchived := snapshot.Details.GetBool(bundle.RelationKeyIsArchived)
currentArchived, err := oc.isArchived(newID)
if err != nil {
log.With(zap.String("object id", newID)).
Errorf("failed to get current archived status during import %s: %s", newID, err)
return false
}
if desiredArchived == currentArchived {
return false
}
err = oc.detailsService.SetIsArchived(ctx, newID, desiredArchived)
if err != nil {
log.With(zap.String("object id", newID)).
Errorf("failed to set isArchived when importing object %s: %s", newID, err)
return false
}
return !desiredArchived && currentArchived
}

func (oc *ObjectCreator) restoreLastModifiedDate(snapshot *common.StateSnapshot, objectID string) {
if !snapshot.Details.Has(bundle.RelationKeyLastModifiedDate) {
return
}
err := cache.Do(oc.objectGetterDeleter, objectID, func(b smartblock.SmartBlock) error {
st := b.NewState()
st.SetLocalDetail(bundle.RelationKeyLastModifiedDate, snapshot.Details.Get(bundle.RelationKeyLastModifiedDate))
return b.Apply(st, smartblock.NoHistory, smartblock.NoEvent, smartblock.NoRestrictions, smartblock.KeepInternalFlags)
})
if err != nil {
log.With(zap.String("object id", objectID)).
Errorf("failed to restore lastModifiedDate after import: %s", err)
}
}

func (oc *ObjectCreator) isArchived(objectID string) (bool, error) {
var archived bool
err := cache.Do(oc.objectGetterDeleter, objectID, func(b smartblock.SmartBlock) error {
archived = b.CombinedDetails().GetBool(bundle.RelationKeyIsArchived)
return nil
})
if err != nil {
return false, fmt.Errorf("get object details: %w", err)
}
return archived, nil
}

func (oc *ObjectCreator) restoreLegacyFileDetails(snapshot *common.StateSnapshot, objectID string) {
keys := []domain.RelationKey{
bundle.RelationKeyName,
bundle.RelationKeyIsHiddenDiscovery,
bundle.RelationKeyCreatedDate,
bundle.RelationKeyLastModifiedDate,
bundle.RelationKeyAddedDate,
bundle.RelationKeyCreator,
bundle.RelationKeyLastModifiedBy,
}
restoredFileName := snapshot.Details.GetString(bundle.RelationKeyName)
if restoredFileName == "" {
restoredFileName = firstSnapshotFileBlockName(snapshot)
}
err := cache.Do(oc.objectGetterDeleter, objectID, func(b smartblock.SmartBlock) error {
st := b.NewState()
for _, key := range keys {
if snapshot.Details.Has(key) {
st.SetLocalDetail(key, snapshot.Details.Get(key))
}
}
if restoredFileName != "" {
// File-object reads can use the file block caption/name; keep it in sync with
// restored details for replace/updateExisting import.
if iterErr := st.Iterate(func(bl simple.Block) (isContinue bool) {
if file := bl.Model().GetFile(); file != nil {
file.Name = restoredFileName
return false
}
return true
}); iterErr != nil {
return iterErr
}
}
return b.Apply(st, smartblock.NoHistory, smartblock.NoEvent, smartblock.NoRestrictions, smartblock.KeepInternalFlags)
})
if err != nil {
log.With(zap.String("object id", objectID)).
Errorf("failed to restore legacy file details after import: %s", err)
}
}

func firstSnapshotFileBlockName(snapshot *common.StateSnapshot) string {
for _, block := range snapshot.Blocks {
if file := block.GetFile(); file != nil && file.Name != "" {
return file.Name
}
}
return ""
}

func (oc *ObjectCreator) syncFilesAndLinks(newIdsSet map[string]struct{}, id domain.FullID, origin objectorigin.ObjectOrigin) error {
Expand Down
89 changes: 89 additions & 0 deletions core/block/import/common/objectcreator/objectcreator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/anyproto/any-sync/commonspace/object/tree/treestorage"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"

"github.com/anyproto/anytype-heart/core/block/detailservice/mock_detailservice"
"github.com/anyproto/anytype-heart/core/block/editor/smartblock"
Expand Down Expand Up @@ -99,6 +100,54 @@ func TestObjectCreator_Create(t *testing.T) {
assert.Equal(t, participantId, id)
assert.Equal(t, testDetails, testParticipant.CombinedDetails())
})
t.Run("legacy file snapshot updates existing file object details", func(t *testing.T) {
spaceID := "spaceId"
fileObjectID := "fileObjectId"
fileName := "imported-file-name.png"
detailsService := mock_detailservice.NewMockService(t)

oldToNew := map[string]string{"oldFile": fileObjectID}
dataObject := NewDataObject(context.Background(), oldToNew, nil, nil, objectorigin.Import(model.Import_Pb), spaceID)
sn := &common.Snapshot{
Id: "oldFile",
Snapshot: &common.SnapshotModel{
SbType: coresb.SmartBlockTypeFile,
Data: &common.StateSnapshot{
Details: domain.NewDetailsFromMap(map[domain.RelationKey]domain.Value{
bundle.RelationKeyName: domain.String(fileName),
bundle.RelationKeyLastModifiedDate: domain.Int64(123),
}),
},
},
}

testFile := smarttest.New(fileObjectID)
st := testFile.NewState()
st.SetDetails(domain.NewDetailsFromMap(map[domain.RelationKey]domain.Value{
bundle.RelationKeyId: domain.String(fileObjectID),
bundle.RelationKeyName: domain.String("old-name.png"),
}))
err := testFile.Apply(st)
require.NoError(t, err)

getter := newDumbObjectGetter(map[string]smartblock.SmartBlock{
fileObjectID: testFile,
})

fetcher := mock_relationutils.NewMockRelationFormatFetcher(t)
service := New(detailsService, nil, nil, nil, nil, objectcreator.NewCreator(), getter, fetcher)

create, id, err := service.Create(dataObject, sn)
require.NoError(t, err)
assert.Nil(t, create)
assert.Equal(t, fileObjectID, id)
assert.Equal(t, fileName, testFile.CombinedDetails().GetString(bundle.RelationKeyName))
assert.Equal(
t,
int64(123),
testFile.CombinedDetails().GetInt64(bundle.RelationKeyLastModifiedDate),
)
})
}

func TestObjectCreator_updateKeys(t *testing.T) {
Expand Down Expand Up @@ -167,6 +216,46 @@ func TestObjectCreator_updateKeys(t *testing.T) {
})
}

func TestCanUpdateObject(t *testing.T) {
tests := []struct {
name string
sbType coresb.SmartBlockType
want bool
}{
{
name: "page can be updated",
sbType: coresb.SmartBlockTypePage,
want: true,
},
{
name: "file object can be updated",
sbType: coresb.SmartBlockTypeFileObject,
want: true,
},
{
name: "relation cannot be updated",
sbType: coresb.SmartBlockTypeRelation,
want: false,
},
{
name: "relation option cannot be updated",
sbType: coresb.SmartBlockTypeRelationOption,
want: false,
},
{
name: "participant cannot be updated",
sbType: coresb.SmartBlockTypeParticipant,
want: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, canUpdateObject(tt.sbType))
})
}
}

type dumbObjectGetter struct {
objects map[string]smartblock.SmartBlock
}
Expand Down
Loading