Skip to content

Commit

Permalink
Merge pull request #1911 from anyproto/go-4270-add-virtual-file-blocks
Browse files Browse the repository at this point in the history
GO-4270 - Add virtual file blocks
  • Loading branch information
requilence authored Dec 11, 2024
2 parents 854d11f + 3dd6948 commit 555d266
Show file tree
Hide file tree
Showing 7 changed files with 316 additions and 97 deletions.
33 changes: 32 additions & 1 deletion core/block/editor/files.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand Down Expand Up @@ -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)
},
}
}
Expand Down Expand Up @@ -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()
}
18 changes: 17 additions & 1 deletion core/block/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
}
Expand Down
157 changes: 157 additions & 0 deletions core/files/fileobject/fileblocks/fileblocks.go
Original file line number Diff line number Diff line change
@@ -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(),
},
},
}
}
104 changes: 104 additions & 0 deletions core/files/fileobject/fileblocks/fileblocks_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
Loading

0 comments on commit 555d266

Please sign in to comment.