1
0
Fork 0
mirror of https://github.com/anyproto/anytype-heart.git synced 2025-06-11 10:18:28 +09:00

Merge pull request #1911 from anyproto/go-4270-add-virtual-file-blocks

GO-4270 - Add virtual file blocks
This commit is contained in:
Roman Khafizianov 2024-12-11 20:46:40 +01:00 committed by GitHub
commit 555d2665f9
Signed by: github
GPG key ID: B5690EEEBB952194
7 changed files with 316 additions and 97 deletions

View file

@ -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()
}

View file

@ -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
}

View file

@ -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(),
},
},
}
}

View file

@ -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)
}

View file

@ -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(),
},
},
}
}

View file

@ -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)

View file

@ -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{