Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GO-3526 drag and drop files to collection add them to collection #1622

66 changes: 59 additions & 7 deletions core/block/editor/file/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/anyproto/anytype-heart/core/block/cache"
"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/process"
"github.com/anyproto/anytype-heart/core/block/simple"
"github.com/anyproto/anytype-heart/core/block/simple/file"
Expand Down Expand Up @@ -220,8 +221,10 @@ func (sf *sfile) updateFile(ctx session.Context, id, groupId string, apply func(
}

func (sf *sfile) DropFiles(req pb.RpcFileDropRequest) (err error) {
if err = sf.Restrictions().Object.Check(model.Restrictions_Blocks); err != nil {
return err
if !isCollection(sf) {
if err = sf.Restrictions().Object.Check(model.Restrictions_Blocks); err != nil {
return err
}
}
proc := &dropFilesProcess{
spaceID: sf.SpaceID(),
Expand All @@ -233,7 +236,7 @@ func (sf *sfile) DropFiles(req pb.RpcFileDropRequest) (err error) {
return
}
var ch = make(chan error)
go proc.Start(sf.RootId(), req.DropTargetId, req.Position, ch)
go proc.Start(sf, req.DropTargetId, req.Position, ch)
err = <-ch
return
}
Expand All @@ -247,7 +250,6 @@ func (sf *sfile) dropFilesCreateStructure(groupId, targetId string, pos model.Bl
for _, entry := range entries {
var blockId, pageId string
if entry.isDir {

if err = sf.Apply(s); err != nil {
return
}
Expand Down Expand Up @@ -303,6 +305,13 @@ func (sf *sfile) dropFilesSetInfo(info dropFileInfo) (err error) {
s.Unlink(info.blockId)
return sf.Apply(s)
}
if isCollection(sf) {
s := sf.NewState()
if !s.HasInStore([]string{info.file.TargetObjectId}) {
s.UpdateStoreSlice(template.CollectionStoreKey, append(s.GetStoreSlice(template.CollectionStoreKey), info.file.TargetObjectId))
}
return sf.Apply(s)
}
return sf.UpdateFile(info.blockId, info.groupId, func(f file.Block) error {
if info.err != nil || info.file == nil || info.file.State == model.BlockContentFile_Error {
if info.err != nil {
Expand Down Expand Up @@ -450,7 +459,7 @@ func (dp *dropFilesProcess) readdir(entry *dropFileEntry, allowSymlinks bool) (o
return true, nil
}

func (dp *dropFilesProcess) Start(rootId, targetId string, pos model.BlockPosition, rootDone chan error) {
func (dp *dropFilesProcess) Start(file smartblock.SmartBlock, targetId string, pos model.BlockPosition, rootDone chan error) {
dp.id = uuid.New().String()
dp.doneCh = make(chan struct{})
dp.cancel = make(chan struct{})
Expand All @@ -469,6 +478,46 @@ func (dp *dropFilesProcess) Start(rootId, targetId string, pos model.BlockPositi
go dp.addFilesWorker(wg, in)
}

if isCollection(file) {
dp.handleDragAndDropInCollection(file.RootId(), dp.root.child, rootDone, in)
} else {
dp.handleDragAndDropInDocument(file.RootId(), targetId, pos, rootDone, in)
}
wg.Wait()
}

func (dp *dropFilesProcess) handleDragAndDropInCollection(rootId string, droppedFiles []*dropFileEntry, rootDone chan error, in chan *dropFileInfo) {
close(rootDone)
filesToUpload := dp.getFilesToUploadFromDirs(droppedFiles)
for _, entry := range filesToUpload {
in <- &dropFileInfo{
pageId: rootId,
path: entry.path,
name: entry.name,
}
}
close(in)
}

func (dp *dropFilesProcess) getFilesToUploadFromDirs(droppedFiles []*dropFileEntry) []*dropFileEntry {
var (
stack []*dropFileEntry
totalFiles []*dropFileEntry
)
stack = append(stack, droppedFiles...)
for len(stack) > 0 {
entry := stack[len(stack)-1]
stack = stack[:len(stack)-1]
if entry.isDir {
stack = append(stack, entry.child...)
} else {
totalFiles = append(totalFiles, entry)
}
}
return totalFiles
}

func (dp *dropFilesProcess) handleDragAndDropInDocument(rootId, targetId string, pos model.BlockPosition, rootDone chan error, in chan *dropFileInfo) {
var flatEntries = [][]*dropFileEntry{dp.root.child}
var smartBlockIds = []string{rootId}
var handleLevel = func(idx int) (isContinue bool, err error) {
Expand Down Expand Up @@ -533,8 +582,6 @@ func (dp *dropFilesProcess) Start(rootId, targetId string, pos model.BlockPositi
idx++
}
close(in)
wg.Wait()
return
}

func (dp *dropFilesProcess) addFilesWorker(wg *sync.WaitGroup, in chan *dropFileInfo) {
Expand Down Expand Up @@ -590,3 +637,8 @@ func (dp *dropFilesProcess) apply(f *dropFileInfo) (err error) {
return sbHandler.dropFilesSetInfo(*f)
})
}

func isCollection(smartBlock smartblock.SmartBlock) bool {
layout, ok := smartBlock.Layout()
return ok && layout == model.ObjectType_collection
}
209 changes: 203 additions & 6 deletions core/block/editor/file/file_test.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,42 @@
package file

import (
"context"
"errors"
"os"
"path/filepath"
"testing"

"github.com/anyproto/any-sync/accountservice/mock_accountservice"
"github.com/anyproto/any-sync/app"
"github.com/anyproto/any-sync/commonfile/fileservice"
"github.com/gogo/protobuf/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"

"github.com/anyproto/anytype-heart/core/anytype/config"
"github.com/anyproto/anytype-heart/core/block/cache/mock_cache"
"github.com/anyproto/anytype-heart/core/block/editor/smartblock/smarttest"
"github.com/anyproto/anytype-heart/core/block/editor/template"
"github.com/anyproto/anytype-heart/core/block/process"
"github.com/anyproto/anytype-heart/core/block/restriction"
"github.com/anyproto/anytype-heart/core/event/mock_event"
"github.com/anyproto/anytype-heart/core/files"
"github.com/anyproto/anytype-heart/core/files/fileobject/mock_fileobject"
"github.com/anyproto/anytype-heart/core/files/fileuploader"
"github.com/anyproto/anytype-heart/core/filestorage"
"github.com/anyproto/anytype-heart/core/filestorage/filesync"
"github.com/anyproto/anytype-heart/core/filestorage/rpcstore"
wallet2 "github.com/anyproto/anytype-heart/core/wallet"
"github.com/anyproto/anytype-heart/core/wallet/mock_wallet"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
"github.com/anyproto/anytype-heart/pkg/lib/core"
"github.com/anyproto/anytype-heart/pkg/lib/datastore"
"github.com/anyproto/anytype-heart/pkg/lib/localstore/filestore"
"github.com/anyproto/anytype-heart/pkg/lib/localstore/objectstore"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
"github.com/anyproto/anytype-heart/tests/blockbuilder"
"github.com/anyproto/anytype-heart/tests/testutil"
Expand All @@ -21,21 +45,31 @@ import (

type fileFixture struct {
sfile
pickerFx *mock_cache.MockObjectGetter
sb *smarttest.SmartTest
pickerFx *mock_cache.MockObjectGetter
sb *smarttest.SmartTest
mockSender *mock_event.MockSender
}

func newFixture(t *testing.T) *fileFixture {
picker := mock_cache.NewMockObjectGetter(t)
sb := smarttest.New("root")
mockSender := mock_event.NewMockSender(t)
fx := &fileFixture{
pickerFx: picker,
sb: sb,
pickerFx: picker,
sb: sb,
mockSender: mockSender,
}

a := &app.App{}
a.Register(testutil.PrepareMock(context.Background(), a, mockSender))
service := process.New()
err := service.Init(a)
assert.Nil(t, err)

fx.sfile = sfile{
SmartBlock: sb,
picker: picker,
SmartBlock: sb,
picker: picker,
processService: service,
}
return fx
}
Expand Down Expand Up @@ -119,4 +153,167 @@ func TestDropFiles(t *testing.T) {
assert.Error(t, err)
assert.True(t, errors.Is(err, restriction.ErrRestricted))
})
t.Run("drop files in collection - no restriction error", func(t *testing.T) {
// given
dir := t.TempDir()
file, err := os.Create(filepath.Join(dir, "test"))
assert.Nil(t, err)

fx := newFixture(t)
st := fx.sb.Doc.NewState()
st.SetDetail(bundle.RelationKeyLayout.String(), pbtypes.Int64(int64(model.ObjectType_collection)))
fx.sb.Doc = st
fx.pickerFx.EXPECT().GetObject(context.Background(), "root").Return(fx, nil).Maybe()
fx.mockSender.EXPECT().Broadcast(mock.Anything).Return().Maybe()
mockService := mock_fileobject.NewMockService(t)
mockService.EXPECT().Create(mock.Anything, mock.Anything, mock.Anything).Return("fileObjectId", &types.Struct{Fields: map[string]*types.Value{}}, nil).Maybe()
fx.fileUploaderFactory = prepareFileService(t, fx.mockSender, mockService)

// when
err = fx.sfile.DropFiles(pb.RpcFileDropRequest{
ContextId: "root",
LocalFilePaths: []string{file.Name()},
})

// then
assert.Nil(t, err)
})
t.Run("drop dir in collection - no restriction error", func(t *testing.T) {
// given
dir := t.TempDir()
_, err := os.Create(filepath.Join(dir, "test"))
assert.Nil(t, err)

fx := newFixture(t)
st := fx.sb.Doc.NewState()
st.SetDetail(bundle.RelationKeyLayout.String(), pbtypes.Int64(int64(model.ObjectType_collection)))
fx.sb.Doc = st
fx.pickerFx.EXPECT().GetObject(context.Background(), "root").Return(fx, nil).Maybe()
fx.mockSender.EXPECT().Broadcast(mock.Anything).Return().Maybe()
mockService := mock_fileobject.NewMockService(t)
mockService.EXPECT().Create(mock.Anything, mock.Anything, mock.Anything).Return("fileObjectId", &types.Struct{Fields: map[string]*types.Value{}}, nil).Maybe()
fx.fileUploaderFactory = prepareFileService(t, fx.mockSender, mockService)

// when
err = fx.sfile.DropFiles(pb.RpcFileDropRequest{
ContextId: "root",
LocalFilePaths: []string{dir},
})

// then
assert.Nil(t, err)
})
t.Run("drop files in collection - success", func(t *testing.T) {
// given
dir := t.TempDir()
file, err := os.Create(filepath.Join(dir, "test"))
assert.Nil(t, err)

fx := newFixture(t)
st := fx.sb.Doc.NewState()
st.SetDetail(bundle.RelationKeyLayout.String(), pbtypes.Int64(int64(model.ObjectType_collection)))
fx.sb.Doc = st
fx.pickerFx.EXPECT().GetObject(context.Background(), "root").Return(fx, nil)
fx.mockSender.EXPECT().Broadcast(mock.Anything).Return()
mockService := mock_fileobject.NewMockService(t)
mockService.EXPECT().Create(context.Background(), "", mock.Anything).Return("fileObjectId", &types.Struct{Fields: map[string]*types.Value{}}, nil).Maybe()
fx.fileUploaderFactory = prepareFileService(t, fx.mockSender, mockService)

// when
proc := &dropFilesProcess{
spaceID: fx.SpaceID(),
processService: fx.processService,
picker: fx.picker,
fileUploaderFactory: fx.fileUploaderFactory,
}
err = proc.Init([]string{file.Name()})
assert.Nil(t, err)
var ch = make(chan error)
proc.Start(fx, "", model.Block_Bottom, ch)
err = <-ch

// then
assert.Nil(t, err)
storeSlice := fx.NewState().GetStoreSlice(template.CollectionStoreKey)
assert.Len(t, storeSlice, 1)
assert.Equal(t, "fileObjectId", storeSlice[0])
})
t.Run("drop dir with file in collection - success", func(t *testing.T) {
// given
dir := t.TempDir()
_, err := os.Create(filepath.Join(dir, "test"))
assert.Nil(t, err)

fx := newFixture(t)
st := fx.sb.Doc.NewState()
st.SetDetail(bundle.RelationKeyLayout.String(), pbtypes.Int64(int64(model.ObjectType_collection)))
fx.sb.Doc = st
fx.pickerFx.EXPECT().GetObject(context.Background(), "root").Return(fx, nil)
fx.mockSender.EXPECT().Broadcast(mock.Anything).Return()
mockService := mock_fileobject.NewMockService(t)
mockService.EXPECT().Create(context.Background(), "", mock.Anything).Return("fileObjectId", &types.Struct{Fields: map[string]*types.Value{}}, nil).Maybe()
fx.fileUploaderFactory = prepareFileService(t, fx.mockSender, mockService)

// when
proc := &dropFilesProcess{
spaceID: fx.SpaceID(),
processService: fx.processService,
picker: fx.picker,
fileUploaderFactory: fx.fileUploaderFactory,
}
err = proc.Init([]string{dir})
assert.Nil(t, err)
var ch = make(chan error)
proc.Start(fx, "", model.Block_Bottom, ch)
err = <-ch

// then
assert.Nil(t, err)
storeSlice := fx.NewState().GetStoreSlice(template.CollectionStoreKey)
assert.Len(t, storeSlice, 1)
assert.Equal(t, "fileObjectId", storeSlice[0])
})
}

func prepareFileService(t *testing.T, sender *mock_event.MockSender, fileObjectService *mock_fileobject.MockService) fileuploader.Service {
dataStoreProvider, err := datastore.NewInMemory()
assert.Nil(t, err)

blockStorage := filestorage.NewInMemory()

rpcStore := rpcstore.NewInMemoryStore(1024)
rpcStoreService := rpcstore.NewInMemoryService(rpcStore)
commonFileService := fileservice.New()
fileSyncService := filesync.New()
objectStore := objectstore.NewStoreFixture(t)

ctx := context.Background()
ctrl := gomock.NewController(t)
wallet := mock_wallet.NewMockWallet(t)
wallet.EXPECT().Name().Return(wallet2.CName)
wallet.EXPECT().RepoPath().Return("repo/path")

a := new(app.App)
a.Register(dataStoreProvider)
a.Register(filestore.New())
a.Register(commonFileService)
a.Register(fileSyncService)
a.Register(testutil.PrepareMock(ctx, a, sender))
a.Register(blockStorage)
a.Register(objectStore)
a.Register(rpcStoreService)
a.Register(testutil.PrepareMock(ctx, a, mock_accountservice.NewMockService(ctrl)))
a.Register(testutil.PrepareMock(ctx, a, wallet))
a.Register(testutil.PrepareMock(ctx, a, fileObjectService))
a.Register(&config.Config{DisableFileConfig: true, NetworkMode: pb.RpcAccount_DefaultConfig, PeferYamuxTransport: true})
a.Register(core.NewTempDirService())
a.Register(testutil.PrepareMock(ctx, a, mock_cache.NewMockObjectGetterComponent(t)))
a.Register(files.New())
err = a.Start(ctx)
assert.Nil(t, err)

service := fileuploader.New()
err = service.Init(a)
assert.Nil(t, err)
return service
}
Loading