diff --git a/core/block/editor/files.go b/core/block/editor/files.go index 7068a5ad05..8a9e97a989 100644 --- a/core/block/editor/files.go +++ b/core/block/editor/files.go @@ -4,6 +4,8 @@ import ( "context" "fmt" + "github.com/gogo/protobuf/types" + "github.com/anyproto/anytype-heart/core/block/editor/basic" "github.com/anyproto/anytype-heart/core/block/editor/smartblock" "github.com/anyproto/anytype-heart/core/block/editor/state" @@ -12,10 +14,12 @@ import ( "github.com/anyproto/anytype-heart/core/block/source" "github.com/anyproto/anytype-heart/core/domain" "github.com/anyproto/anytype-heart/core/files/fileobject" + "github.com/anyproto/anytype-heart/core/files/fileobject/fileblocks" "github.com/anyproto/anytype-heart/core/files/reconciler" "github.com/anyproto/anytype-heart/core/filestorage" "github.com/anyproto/anytype-heart/pkg/lib/bundle" coresb "github.com/anyproto/anytype-heart/pkg/lib/core/smartblock" + "github.com/anyproto/anytype-heart/pkg/lib/pb/model" ) // required relations for files beside the bundle.RequiredInternalRelations @@ -59,7 +63,7 @@ func (f *File) CreationStateMigration(ctx *smartblock.InitContext) migration.Mig // - In background metadata indexer, if we use asynchronous metadata indexing mode // // See fileobject.Service - f.fileObjectService.InitEmptyFileState(ctx.State) + fileblocks.InitEmptyFileState(ctx.State) }, } } @@ -98,3 +102,30 @@ func (f *File) Init(ctx *smartblock.InitContext) error { } return nil } + +func (f *File) InjectVirtualBlocks(objectId string, view *model.ObjectView) { + if view.Type != model.SmartBlockType_FileObject { + return + } + + var details *types.Struct + for _, det := range view.Details { + if det.Id == objectId { + details = det.Details + break + } + } + if details == nil { + return + } + + st := state.NewDoc(objectId, nil).NewState() + st.SetDetails(details) + fileblocks.InitEmptyFileState(st) + if err := fileblocks.AddFileBlocks(st, details, objectId); err != nil { + log.Errorf("failed to inject virtual file blocks: %v", err) + return + } + + view.Blocks = st.Blocks() +} diff --git a/core/block/service.go b/core/block/service.go index d722bd398e..f471ac44dd 100644 --- a/core/block/service.go +++ b/core/block/service.go @@ -59,6 +59,10 @@ import ( const CName = "block-service" +type withVirtualBlocks interface { + InjectVirtualBlocks(objectId string, view *model.ObjectView) +} + var ErrUnknownObjectType = fmt.Errorf("unknown object type") var log = logging.Logger("anytype-mw-service") @@ -193,6 +197,10 @@ func (s *Service) OpenBlock(sctx session.Context, id domain.FullID, includeRelat log.Errorf("failed to watch status for object %s: %s", id, err) } + if v, ok := ob.(withVirtualBlocks); ok { + v.InjectVirtualBlocks(id.ObjectID, obj) + } + afterHashesTime := time.Now() metrics.Service.Send(&metrics.OpenBlockEvent{ ObjectId: id.ObjectID, @@ -241,7 +249,15 @@ func (s *Service) ShowBlock(id domain.FullID, includeRelationsAsDependentObjects b.EnabledRelationAsDependentObjects() } obj, err = b.Show() - return err + if err != nil { + return err + } + + if v, ok := b.(withVirtualBlocks); ok { + v.InjectVirtualBlocks(id.ObjectID, obj) + } + + return nil }) return obj, err } diff --git a/core/files/fileobject/fileblocks/fileblocks.go b/core/files/fileobject/fileblocks/fileblocks.go new file mode 100644 index 0000000000..c1340fc633 --- /dev/null +++ b/core/files/fileobject/fileblocks/fileblocks.go @@ -0,0 +1,157 @@ +package fileblocks + +import ( + "fmt" + + "github.com/gogo/protobuf/types" + + "github.com/anyproto/anytype-heart/core/block/editor/state" + "github.com/anyproto/anytype-heart/core/block/editor/template" + "github.com/anyproto/anytype-heart/core/block/simple" + fileblock "github.com/anyproto/anytype-heart/core/block/simple/file" + "github.com/anyproto/anytype-heart/core/domain" + "github.com/anyproto/anytype-heart/pkg/lib/bundle" + "github.com/anyproto/anytype-heart/pkg/lib/pb/model" + "github.com/anyproto/anytype-heart/util/pbtypes" +) + +func InitEmptyFileState(st *state.State) { + template.InitTemplate(st, + template.WithEmpty, + template.WithTitle, + template.WithDefaultFeaturedRelations, + template.WithFeaturedRelations, + template.WithAllBlocksEditsRestricted, + ) +} + +func AddFileBlocks(st *state.State, details *types.Struct, objectId string) error { + fname := pbtypes.GetString(details, bundle.RelationKeyName.String()) + fileType := fileblock.DetectTypeByMIME(fname, pbtypes.GetString(details, bundle.RelationKeyFileMimeType.String())) + + if fileType == model.BlockContentFile_Image { + st.SetDetailAndBundledRelation(bundle.RelationKeyIconImage, pbtypes.String(objectId)) + } + + blocks := buildFileBlocks(details, objectId, fname, fileType) + + for _, b := range blocks { + if st.Exists(b.Id) { + st.Set(simple.New(b)) + } else { + st.Add(simple.New(b)) + err := st.InsertTo(st.RootId(), model.Block_Inner, b.Id) + if err != nil { + return fmt.Errorf("failed to insert file block: %w", err) + } + } + } + template.WithAllBlocksEditsRestricted(st) + return nil +} + +func buildFileBlocks(details *types.Struct, objectId, fname string, fileType model.BlockContentFileType) []*model.Block { + var blocks []*model.Block + blocks = append(blocks, &model.Block{ + Id: "file", + Content: &model.BlockContentOfFile{ + File: &model.BlockContentFile{ + Name: fname, + Mime: pbtypes.GetString(details, bundle.RelationKeyFileMimeType.String()), + TargetObjectId: objectId, + Type: fileType, + Size_: int64(pbtypes.GetFloat64(details, bundle.RelationKeySizeInBytes.String())), + State: model.BlockContentFile_Done, + AddedAt: int64(pbtypes.GetFloat64(details, bundle.RelationKeyAddedDate.String())), + }, + }}, makeFileInfoBlock(), makeRelationBlock(bundle.RelationKeyFileExt)) + + switch fileType { + case model.BlockContentFile_Image: + for _, relKey := range []domain.RelationKey{ + bundle.RelationKeyWidthInPixels, + bundle.RelationKeyHeightInPixels, + bundle.RelationKeyCamera, + bundle.RelationKeyMediaArtistName, + bundle.RelationKeyMediaArtistURL, + } { + if notEmpty(details, relKey) { + blocks = append(blocks, makeRelationBlock(relKey)) + } + } + case model.BlockContentFile_Audio: + for _, relKey := range []domain.RelationKey{ + bundle.RelationKeyArtist, + bundle.RelationKeyAudioAlbum, + bundle.RelationKeyAudioAlbumTrackNumber, + bundle.RelationKeyAudioGenre, + bundle.RelationKeyAudioLyrics, + bundle.RelationKeyReleasedYear, + } { + if notEmpty(details, relKey) { + blocks = append(blocks, makeRelationBlock(relKey)) + } + } + case model.BlockContentFile_Video: + for _, relKey := range []domain.RelationKey{ + bundle.RelationKeyWidthInPixels, + bundle.RelationKeyHeightInPixels, + bundle.RelationKeyCamera, + bundle.RelationKeyCameraIso, + bundle.RelationKeyAperture, + bundle.RelationKeyExposure, + } { + if notEmpty(details, relKey) { + blocks = append(blocks, makeRelationBlock(relKey)) + } + } + } + + for _, relKey := range []domain.RelationKey{ + bundle.RelationKeySizeInBytes, + bundle.RelationKeyOrigin, + bundle.RelationKeyImportType, + bundle.RelationKeyAddedDate, + } { + if pbtypes.GetInt64(details, relKey.String()) != 0 { + blocks = append(blocks, makeRelationBlock(relKey)) + } + } + + return blocks +} + +func makeFileInfoBlock() *model.Block { + return &model.Block{ + Id: "info", + Content: &model.BlockContentOfText{ + Text: &model.BlockContentText{ + Text: "File Information", + Marks: &model.BlockContentTextMarks{ + Marks: []*model.BlockContentTextMark{{ + Range: &model.Range{ + From: 0, + To: 16, + }, + Type: model.BlockContentTextMark_Bold, + }}, + }, + }, + }, + } +} + +func notEmpty(details *types.Struct, relKey domain.RelationKey) bool { + return pbtypes.GetInt64(details, relKey.String()) != 0 || pbtypes.GetString(details, relKey.String()) != "" +} + +func makeRelationBlock(relationKey domain.RelationKey) *model.Block { + return &model.Block{ + Id: relationKey.String(), + Content: &model.BlockContentOfRelation{ + Relation: &model.BlockContentRelation{ + Key: relationKey.String(), + }, + }, + } +} diff --git a/core/files/fileobject/fileblocks/fileblocks_test.go b/core/files/fileobject/fileblocks/fileblocks_test.go new file mode 100644 index 0000000000..144ad5abb1 --- /dev/null +++ b/core/files/fileobject/fileblocks/fileblocks_test.go @@ -0,0 +1,104 @@ +package fileblocks + +import ( + "fmt" + "testing" + "time" + + "github.com/gogo/protobuf/types" + "github.com/stretchr/testify/assert" + + "github.com/anyproto/anytype-heart/core/block/editor/state" + "github.com/anyproto/anytype-heart/core/block/simple" + "github.com/anyproto/anytype-heart/core/domain" + "github.com/anyproto/anytype-heart/pkg/lib/bundle" + "github.com/anyproto/anytype-heart/pkg/lib/pb/model" + "github.com/anyproto/anytype-heart/util/pbtypes" +) + +func TestAddFileBlocks(t *testing.T) { + id := "some_file" + + for _, tc := range []struct { + name string + details *types.Struct + expectedRelations []domain.RelationKey + }{ + { + "image", + &types.Struct{Fields: map[string]*types.Value{ + bundle.RelationKeyName.String(): pbtypes.String("photo.jpeg"), + bundle.RelationKeyFileMimeType.String(): pbtypes.String("image/jpeg"), + bundle.RelationKeyWidthInPixels.String(): pbtypes.Int64(400), + bundle.RelationKeyHeightInPixels.String(): pbtypes.Int64(600), + bundle.RelationKeyAddedDate.String(): pbtypes.Int64(time.Now().Unix()), + }}, + []domain.RelationKey{bundle.RelationKeyFileExt, bundle.RelationKeyWidthInPixels, bundle.RelationKeyHeightInPixels, bundle.RelationKeyAddedDate}, + }, + { + "plain file", + &types.Struct{Fields: map[string]*types.Value{ + bundle.RelationKeyName.String(): pbtypes.String("txt.txt"), + bundle.RelationKeySizeInBytes.String(): pbtypes.Int64(24000), + bundle.RelationKeyOrigin.String(): pbtypes.Int64(int64(model.ObjectOrigin_dragAndDrop)), + bundle.RelationKeyAddedDate.String(): pbtypes.Int64(time.Now().Unix()), + }}, + []domain.RelationKey{bundle.RelationKeyFileExt, bundle.RelationKeySizeInBytes, bundle.RelationKeyOrigin, bundle.RelationKeyAddedDate}, + }, + { + "audio", + &types.Struct{Fields: map[string]*types.Value{ + bundle.RelationKeyName.String(): pbtypes.String("song.mp3"), + bundle.RelationKeyFileMimeType.String(): pbtypes.String("audio/mp3"), + bundle.RelationKeySizeInBytes.String(): pbtypes.Int64(2400000), + bundle.RelationKeyAudioAlbum.String(): pbtypes.String("Never mind"), + bundle.RelationKeyAudioAlbumTrackNumber.String(): pbtypes.Int64(13), + bundle.RelationKeyOrigin.String(): pbtypes.Int64(int64(model.ObjectOrigin_clipboard)), + bundle.RelationKeyImportType.String(): pbtypes.Int64(2), + }}, + []domain.RelationKey{bundle.RelationKeyFileExt, bundle.RelationKeySizeInBytes, bundle.RelationKeyAudioAlbum, bundle.RelationKeyAudioAlbumTrackNumber, bundle.RelationKeyOrigin, bundle.RelationKeyImportType}, + }, + } { + t.Run(fmt.Sprintf("add file blocks: %s", tc.name), func(t *testing.T) { + // given + st := state.NewDoc(id, map[string]simple.Block{ + id: simple.New(&model.Block{Id: id}), + }).NewState() + + // when + err := AddFileBlocks(st, tc.details, id) + + // then + assert.NoError(t, err) + assertBlocks(t, st.Blocks(), tc.expectedRelations) + }) + } +} + +func assertBlocks(t *testing.T, blocks []*model.Block, relations []domain.RelationKey) { + counter := 0 + var txtFound, fileFound bool + for _, block := range blocks { + rb := block.GetRelation() + if rb != nil { + assert.Contains(t, relations, domain.RelationKey(rb.Key)) + counter++ + continue + } + + txt := block.GetText() + if txt != nil { + assert.Equal(t, "File Information", txt.GetText()) + txtFound = true + continue + } + + file := block.GetFile() + if file != nil { + fileFound = true + } + } + assert.Equal(t, counter, len(relations)) + assert.True(t, txtFound) + assert.True(t, fileFound) +} diff --git a/core/files/fileobject/fileindex.go b/core/files/fileobject/fileindex.go index c6c280c812..7c47a1859a 100644 --- a/core/files/fileobject/fileindex.go +++ b/core/files/fileobject/fileindex.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "strings" "sync" "time" @@ -15,11 +14,9 @@ import ( "github.com/anyproto/anytype-heart/core/block/editor/smartblock" "github.com/anyproto/anytype-heart/core/block/editor/state" - "github.com/anyproto/anytype-heart/core/block/editor/template" - "github.com/anyproto/anytype-heart/core/block/simple" - fileblock "github.com/anyproto/anytype-heart/core/block/simple/file" "github.com/anyproto/anytype-heart/core/domain" "github.com/anyproto/anytype-heart/core/files" + "github.com/anyproto/anytype-heart/core/files/fileobject/fileblocks" "github.com/anyproto/anytype-heart/core/filestorage/rpcstore" "github.com/anyproto/anytype-heart/pkg/lib/bundle" "github.com/anyproto/anytype-heart/pkg/lib/database" @@ -252,7 +249,7 @@ func (ind *indexer) injectMetadataToState(ctx context.Context, st *state.State, details = pbtypes.StructMerge(prevDetails, details, false) st.SetDetails(details) - err = ind.addBlocks(st, details, id.ObjectID) + err = fileblocks.AddFileBlocks(st, details, id.ObjectID) if err != nil { return fmt.Errorf("add blocks: %w", err) } @@ -291,83 +288,3 @@ func (ind *indexer) buildDetails(ctx context.Context, id domain.FullFileId) (det details.Fields[bundle.RelationKeyFileIndexingStatus.String()] = pbtypes.Int64(int64(model.FileIndexingStatus_Indexed)) return details, typeKey, nil } - -func (ind *indexer) addBlocks(st *state.State, details *types.Struct, objectId string) error { - fname := pbtypes.GetString(details, bundle.RelationKeyName.String()) - fileType := fileblock.DetectTypeByMIME(fname, pbtypes.GetString(details, bundle.RelationKeyFileMimeType.String())) - - ext := pbtypes.GetString(details, bundle.RelationKeyFileExt.String()) - - if ext != "" && !strings.HasSuffix(fname, "."+ext) { - fname = fname + "." + ext - } - - var blocks []*model.Block - blocks = append(blocks, &model.Block{ - Id: "file", - Content: &model.BlockContentOfFile{ - File: &model.BlockContentFile{ - Name: fname, - Mime: pbtypes.GetString(details, bundle.RelationKeyFileMimeType.String()), - TargetObjectId: objectId, - Type: fileType, - Size_: int64(pbtypes.GetFloat64(details, bundle.RelationKeySizeInBytes.String())), - State: model.BlockContentFile_Done, - AddedAt: int64(pbtypes.GetFloat64(details, bundle.RelationKeyFileMimeType.String())), - }, - }}) - - switch fileType { - case model.BlockContentFile_Image: - st.SetDetailAndBundledRelation(bundle.RelationKeyIconImage, pbtypes.String(objectId)) - - if pbtypes.GetInt64(details, bundle.RelationKeyWidthInPixels.String()) != 0 { - blocks = append(blocks, makeRelationBlock(bundle.RelationKeyWidthInPixels)) - } - - if pbtypes.GetInt64(details, bundle.RelationKeyHeightInPixels.String()) != 0 { - blocks = append(blocks, makeRelationBlock(bundle.RelationKeyHeightInPixels)) - } - - if pbtypes.GetString(details, bundle.RelationKeyCamera.String()) != "" { - blocks = append(blocks, makeRelationBlock(bundle.RelationKeyCamera)) - } - - if pbtypes.GetInt64(details, bundle.RelationKeySizeInBytes.String()) != 0 { - blocks = append(blocks, makeRelationBlock(bundle.RelationKeySizeInBytes)) - } - if pbtypes.GetString(details, bundle.RelationKeyMediaArtistName.String()) != "" { - blocks = append(blocks, makeRelationBlock(bundle.RelationKeyMediaArtistName)) - } - if pbtypes.GetString(details, bundle.RelationKeyMediaArtistURL.String()) != "" { - blocks = append(blocks, makeRelationBlock(bundle.RelationKeyMediaArtistURL)) - } - default: - blocks = append(blocks, makeRelationBlock(bundle.RelationKeySizeInBytes)) - } - - for _, b := range blocks { - if st.Exists(b.Id) { - st.Set(simple.New(b)) - } else { - st.Add(simple.New(b)) - err := st.InsertTo(st.RootId(), model.Block_Inner, b.Id) - if err != nil { - return fmt.Errorf("failed to insert file block: %w", err) - } - } - } - template.WithAllBlocksEditsRestricted(st) - return nil -} - -func makeRelationBlock(relationKey domain.RelationKey) *model.Block { - return &model.Block{ - Id: relationKey.String(), - Content: &model.BlockContentOfRelation{ - Relation: &model.BlockContentRelation{ - Key: relationKey.String(), - }, - }, - } -} diff --git a/core/files/fileobject/service.go b/core/files/fileobject/service.go index 5422164cd3..ea1d247549 100644 --- a/core/files/fileobject/service.go +++ b/core/files/fileobject/service.go @@ -15,7 +15,6 @@ import ( "github.com/anyproto/anytype-heart/core/anytype/config" "github.com/anyproto/anytype-heart/core/block/editor/smartblock" "github.com/anyproto/anytype-heart/core/block/editor/state" - "github.com/anyproto/anytype-heart/core/block/editor/template" "github.com/anyproto/anytype-heart/core/block/object/idresolver" "github.com/anyproto/anytype-heart/core/block/object/objectcreator" "github.com/anyproto/anytype-heart/core/block/object/payloadcreator" @@ -23,6 +22,7 @@ import ( "github.com/anyproto/anytype-heart/core/domain" "github.com/anyproto/anytype-heart/core/domain/objectorigin" "github.com/anyproto/anytype-heart/core/files" + "github.com/anyproto/anytype-heart/core/files/fileobject/fileblocks" "github.com/anyproto/anytype-heart/core/files/fileobject/filemodels" "github.com/anyproto/anytype-heart/core/files/fileoffloader" "github.com/anyproto/anytype-heart/core/filestorage/filesync" @@ -260,13 +260,7 @@ func (s *service) Close(ctx context.Context) error { } func (s *service) InitEmptyFileState(st *state.State) { - template.InitTemplate(st, - template.WithEmpty, - template.WithTitle, - template.WithDefaultFeaturedRelations, - template.WithFeaturedRelations, - template.WithAllBlocksEditsRestricted, - ) + fileblocks.InitEmptyFileState(st) } func (s *service) Create(ctx context.Context, spaceId string, req filemodels.CreateRequest) (id string, object *types.Struct, err error) { @@ -309,7 +303,7 @@ func (s *service) createInSpace(ctx context.Context, space clientspace.Space, re EncryptionKeys: req.EncryptionKeys, }) if !req.AsyncMetadataIndexing { - s.InitEmptyFileState(createState) + fileblocks.InitEmptyFileState(createState) fullFileId := domain.FullFileId{SpaceId: space.Id(), FileId: req.FileId} fullObjectId := domain.FullID{SpaceID: space.Id(), ObjectID: payload.RootRawChange.Id} err := s.indexer.injectMetadataToState(ctx, createState, fullFileId, fullObjectId) diff --git a/core/subscription/service_test.go b/core/subscription/service_test.go index 00f12753c8..3a0172fb17 100644 --- a/core/subscription/service_test.go +++ b/core/subscription/service_test.go @@ -82,7 +82,7 @@ func TestService_Search(t *testing.T) { require.NoError(t, err) // Wait enough time to flush pending updates to subscriptions handler - time.Sleep(batchTime + time.Millisecond) + time.Sleep(batchTime + 3*time.Millisecond) spaceSub.onChange([]*entry{ newEntry("1", &types.Struct{Fields: map[string]*types.Value{