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

GO-3490 Merge branch 'go-3170-check-objects-and-files-are-synced-with-responsible-peers-in' of github.com:anyproto/anytype-heart into go-3490-add-relations-with-sync-status-to-objects-to-show-object

# Conflicts:
#	.mockery.yaml
#	core/syncstatus/objectsyncstatus/syncstatus.go
#	core/syncstatus/objectsyncstatus/syncstatus_test.go
#	core/syncstatus/spacesyncstatus/filestate.go
#	core/syncstatus/spacesyncstatus/objectstate.go
#	core/syncstatus/spacesyncstatus/spacestatus.go
This commit is contained in:
AnastasiaShemyakinskaya 2024-06-07 19:39:29 +02:00
commit 1a583b0567
No known key found for this signature in database
GPG key ID: CCD60ED83B103281
86 changed files with 4417 additions and 2079 deletions

View file

@ -165,6 +165,9 @@ packages:
github.com/anyproto/anytype-heart/core/block/object/idresolver:
interfaces:
Resolver:
github.com/anyproto/anytype-heart/util/linkpreview:
interfaces:
LinkPreview:
github.com/anyproto/anytype-heart/core/block/object/treesyncer:
interfaces:
Updater:
@ -172,7 +175,7 @@ packages:
SyncDetailsUpdater:
github.com/anyproto/anytype-heart/core/syncstatus/objectsyncstatus:
interfaces:
Updater:
SpaceStatusUpdater:
UpdateReceiver:
config:
dir: "{{.InterfaceDir}}"

View file

@ -13,6 +13,7 @@ import (
"time"
"github.com/anyproto/any-sync/app"
"github.com/globalsign/mgo/bson"
"github.com/gogo/protobuf/types"
"github.com/anyproto/anytype-heart/core/block/editor/state"
@ -240,7 +241,7 @@ func (s *service) ContentUpdaters(spaceID string, url string, parseBlock bool) (
updaters := make(chan func(contentBookmark *bookmark.ObjectContent), 1)
data, body, err := s.linkPreview.Fetch(ctx, url)
data, body, isFile, err := s.linkPreview.Fetch(ctx, url)
if err != nil {
updaters <- func(c *bookmark.ObjectContent) {
if c.BookmarkContent == nil {
@ -312,6 +313,10 @@ func (s *service) ContentUpdaters(spaceID string, url string, parseBlock bool) (
go func() {
defer wg.Done()
updaters <- func(c *bookmark.ObjectContent) {
if isFile {
s.handleFileBlock(c, url)
return
}
blocks, _, err := anymark.HTMLToBlocks(body, url)
if err != nil {
log.Errorf("parse blocks: %s", err)
@ -328,6 +333,19 @@ func (s *service) ContentUpdaters(spaceID string, url string, parseBlock bool) (
return updaters, nil
}
func (s *service) handleFileBlock(c *bookmark.ObjectContent, url string) {
c.Blocks = append(
c.Blocks,
&model.Block{
Id: bson.NewObjectId().Hex(),
Content: &model.BlockContentOfFile{
File: &model.BlockContentFile{
Name: url,
}},
},
)
}
func (s *service) fetcher(spaceID string, blockID string, params bookmark.FetchParams) error {
updaters, err := s.ContentUpdaters(spaceID, params.Url, false)
if err != nil {

View file

@ -0,0 +1,67 @@
package bookmark
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
"github.com/anyproto/anytype-heart/util/linkpreview/mock_linkpreview"
)
func TestService_FetchBookmarkContent(t *testing.T) {
t.Run("link to html page - create blocks", func(t *testing.T) {
// given
preview := mock_linkpreview.NewMockLinkPreview(t)
preview.EXPECT().Fetch(mock.Anything, "http://test.com").Return(model.LinkPreview{}, []byte(testHtml), false, nil)
s := &service{linkPreview: preview}
// when
updaters := s.FetchBookmarkContent("space", "http://test.com", true)
// then
content := updaters()
assert.Len(t, content.Blocks, 2)
})
t.Run("link to file - create one block with file", func(t *testing.T) {
// given
preview := mock_linkpreview.NewMockLinkPreview(t)
preview.EXPECT().Fetch(mock.Anything, "http://test.com").Return(model.LinkPreview{}, nil, true, nil)
s := &service{linkPreview: preview}
// when
updaters := s.FetchBookmarkContent("space", "http://test.com", true)
// then
content := updaters()
assert.Len(t, content.Blocks, 1)
assert.NotNil(t, content.Blocks[0].GetFile())
assert.Equal(t, "http://test.com", content.Blocks[0].GetFile().GetName())
})
t.Run("link to file - create one block with file, image is base64", func(t *testing.T) {
// given
preview := mock_linkpreview.NewMockLinkPreview(t)
preview.EXPECT().Fetch(mock.Anything, "http://test.com").Return(model.LinkPreview{}, []byte(testHtmlBase64), false, nil)
s := &service{linkPreview: preview}
// when
updaters := s.FetchBookmarkContent("space", "http://test.com", true)
// then
content := updaters()
assert.Len(t, content.Blocks, 1)
assert.NotNil(t, content.Blocks[0].GetFile())
})
}
const testHtml = `<html><head>
<title>Title</title>
Test
</head></html>`
const testHtmlBase64 = "<img src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAApgAAAKYB3X3/OAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAANCSURBVEiJtZZPbBtFFMZ/M7ubXdtdb1xSFyeilBapySVU8h8OoFaooFSqiihIVIpQBKci6KEg9Q6H9kovIHoCIVQJJCKE1ENFjnAgcaSGC6rEnxBwA04Tx43t2FnvDAfjkNibxgHxnWb2e/u992bee7tCa00YFsffekFY+nUzFtjW0LrvjRXrCDIAaPLlW0nHL0SsZtVoaF98mLrx3pdhOqLtYPHChahZcYYO7KvPFxvRl5XPp1sN3adWiD1ZAqD6XYK1b/dvE5IWryTt2udLFedwc1+9kLp+vbbpoDh+6TklxBeAi9TL0taeWpdmZzQDry0AcO+jQ12RyohqqoYoo8RDwJrU+qXkjWtfi8Xxt58BdQuwQs9qC/afLwCw8tnQbqYAPsgxE1S6F3EAIXux2oQFKm0ihMsOF71dHYx+f3NND68ghCu1YIoePPQN1pGRABkJ6Bus96CutRZMydTl+TvuiRW1m3n0eDl0vRPcEysqdXn+jsQPsrHMquGeXEaY4Yk4wxWcY5V/9scqOMOVUFthatyTy8QyqwZ+kDURKoMWxNKr2EeqVKcTNOajqKoBgOE28U4tdQl5p5bwCw7BWquaZSzAPlwjlithJtp3pTImSqQRrb2Z8PHGigD4RZuNX6JYj6wj7O4TFLbCO/Mn/m8R+h6rYSUb3ekokRY6f/YukArN979jcW+V/S8g0eT/N3VN3kTqWbQ428m9/8k0P/1aIhF36PccEl6EhOcAUCrXKZXXWS3XKd2vc/TRBG9O5ELC17MmWubD2nKhUKZa26Ba2+D3P+4/MNCFwg59oWVeYhkzgN/JDR8deKBoD7Y+ljEjGZ0sosXVTvbc6RHirr2reNy1OXd6pJsQ+gqjk8VWFYmHrwBzW/n+uMPFiRwHB2I7ih8ciHFxIkd/3Omk5tCDV1t+2nNu5sxxpDFNx+huNhVT3/zMDz8usXC3ddaHBj1GHj/As08fwTS7Kt1HBTmyN29vdwAw+/wbwLVOJ3uAD1wi/dUH7Qei66PfyuRj4Ik9is+hglfbkbfR3cnZm7chlUWLdwmprtCohX4HUtlOcQjLYCu+fzGJH2QRKvP3UNz8bWk1qMxjGTOMThZ3kvgLI5AzFfo379UAAAAASUVORK5CYII=\">"

View file

@ -239,7 +239,7 @@ func (s *Service) DeleteDataviewView(ctx session.Context, req pb.RpcBlockDatavie
func (s *Service) SetDataviewActiveView(ctx session.Context, req pb.RpcBlockDataviewViewSetActiveRequest) error {
return cache.Do(s, req.ContextId, func(b dataview.Dataview) error {
return b.SetActiveView(ctx, req.BlockId, req.ViewId, int(req.Limit), int(req.Offset))
return b.SetActiveView(ctx, req.BlockId, req.ViewId)
})
}

View file

@ -21,6 +21,7 @@ import (
"github.com/anyproto/anytype-heart/pkg/lib/localstore/objectstore"
"github.com/anyproto/anytype-heart/pkg/lib/logging"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
"github.com/anyproto/anytype-heart/util/badgerhelper"
"github.com/anyproto/anytype-heart/util/pbtypes"
"github.com/anyproto/anytype-heart/util/slice"
)
@ -35,7 +36,7 @@ type Dataview interface {
GetDataview(blockID string) (*model.BlockContentDataview, error)
DeleteView(ctx session.Context, blockId string, viewId string, showEvent bool) error
SetActiveView(ctx session.Context, blockId string, activeViewId string, limit int, offset int) error
SetActiveView(ctx session.Context, blockId string, activeViewId string) error
CreateView(ctx session.Context, blockID string,
view model.BlockContentDataviewView, source []string) (*model.BlockContentDataviewView, error)
SetViewPosition(ctx session.Context, blockId string, viewId string, position uint32) error
@ -55,6 +56,7 @@ func NewDataview(sb smartblock.SmartBlock, objectStore objectstore.ObjectStore)
objectStore: objectStore,
}
sb.AddHook(dv.checkDVBlocks, smartblock.HookBeforeApply)
sb.AddHook(dv.injectActiveViews, smartblock.HookBeforeApply)
return dv
}
@ -201,7 +203,7 @@ func (d *sdataview) UpdateView(ctx session.Context, blockID string, viewID strin
return d.Apply(s, smartblock.NoEvent)
}
func (d *sdataview) SetActiveView(ctx session.Context, id string, activeViewId string, limit int, offset int) error {
func (d *sdataview) SetActiveView(ctx session.Context, id string, activeViewId string) error {
s := d.NewStateCtx(ctx)
dvBlock, err := getDataviewBlock(s, id)
@ -214,8 +216,12 @@ func (d *sdataview) SetActiveView(ctx session.Context, id string, activeViewId s
}
dvBlock.SetActiveView(activeViewId)
if err = d.objectStore.SetActiveView(d.Id(), id, activeViewId); err != nil {
return err
}
d.SmartBlock.CheckSubscriptions()
return d.Apply(s)
return d.Apply(s, smartblock.NoHooks)
}
func (d *sdataview) SetViewPosition(ctx session.Context, blockId string, viewId string, position uint32) (err error) {
@ -419,6 +425,34 @@ func (d *sdataview) checkDVBlocks(info smartblock.ApplyInfo) (err error) {
return
}
func (d *sdataview) injectActiveViews(info smartblock.ApplyInfo) (err error) {
s := info.State
views, err := d.objectStore.GetActiveViews(d.Id())
if badgerhelper.IsNotFound(err) {
return nil
}
if err != nil {
log.With("objectId", s.RootId()).Warnf("failed to get list of active views from store: %v", err)
return
}
for blockId, viewId := range views {
b := s.Pick(blockId)
if b == nil {
log.With("objectId", s.RootId()).Warnf("failed to get block '%s' to inject active view", blockId)
continue
}
dv := b.Model().GetDataview()
if dv == nil {
log.With("objectId", s.RootId()).Warnf("block '%s' is not dataview, so cannot inject active view", blockId)
continue
}
dv.ActiveView = viewId
}
return nil
}
func getDataviewBlock(s *state.State, id string) (dataview.Block, error) {
b := s.Get(id)
if b == nil {

View file

@ -1,24 +1,52 @@
package dataview
import (
"errors"
"fmt"
"testing"
"github.com/dgraph-io/badger/v4"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/anyproto/anytype-heart/core/block/editor/smartblock"
"github.com/anyproto/anytype-heart/core/block/editor/smartblock/smarttest"
"github.com/anyproto/anytype-heart/core/block/editor/state"
"github.com/anyproto/anytype-heart/core/block/simple"
"github.com/anyproto/anytype-heart/core/block/simple/dataview"
"github.com/anyproto/anytype-heart/core/session"
"github.com/anyproto/anytype-heart/pkg/lib/localstore/objectstore/mock_objectstore"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
)
const objId = "root"
type fixture struct {
store *mock_objectstore.MockObjectStore
sb *smarttest.SmartTest
*sdataview
}
func newFixture(t *testing.T) *fixture {
store := mock_objectstore.NewMockObjectStore(t)
sb := smarttest.New(objId)
dv := NewDataview(sb, store).(*sdataview)
return &fixture{
store: store,
sb: sb,
sdataview: dv,
}
}
func TestDataviewCollectionImpl_SetViewPosition(t *testing.T) {
newTestDv := func() (Dataview, *smarttest.SmartTest) {
sb := smarttest.New("root")
sbs := sb.Doc.(*state.State)
sbs.Add(simple.New(&model.Block{Id: "root", ChildrenIds: []string{"dv"}}))
fx := newFixture(t)
sbs := fx.sb.Doc.(*state.State)
sbs.Add(simple.New(&model.Block{Id: objId, ChildrenIds: []string{"dv"}}))
sbs.Add(simple.New(&model.Block{Id: "dv", Content: &model.BlockContentOfDataview{
Dataview: &model.BlockContentDataview{
Views: []*model.BlockContentDataviewView{
@ -28,8 +56,8 @@ func TestDataviewCollectionImpl_SetViewPosition(t *testing.T) {
},
},
}}))
return NewDataview(sb, nil), sb
fx.store.EXPECT().GetActiveViews(mock.Anything).Return(nil, nil).Maybe()
return fx.sdataview, fx.sb
}
assertViewPositions := func(viewId string, pos uint32, exp []string) {
dv, sb := newTestDv()
@ -49,3 +77,81 @@ func TestDataviewCollectionImpl_SetViewPosition(t *testing.T) {
assertViewPositions("1", 0, []string{"1", "2", "3"})
assertViewPositions("1", 42, []string{"2", "3", "1"})
}
func TestInjectActiveView(t *testing.T) {
dv1 := "dataview1"
dv2 := "dataview2"
dv3 := "dataview3"
getInfo := func() smartblock.ApplyInfo {
st := state.NewDoc(objId, map[string]simple.Block{
objId: simple.New(&model.Block{Id: objId, ChildrenIds: []string{dv1, dv2, dv3}}),
dv1: dataview.NewDataview(&model.Block{
Id: dv1,
Content: &model.BlockContentOfDataview{Dataview: &model.BlockContentDataview{}},
}),
dv2: dataview.NewDataview(&model.Block{
Id: dv2,
Content: &model.BlockContentOfDataview{Dataview: &model.BlockContentDataview{}},
}),
dv3: dataview.NewDataview(&model.Block{
Id: dv3,
Content: &model.BlockContentOfDataview{Dataview: &model.BlockContentDataview{}},
}),
}).(*state.State)
return smartblock.ApplyInfo{State: st}
}
t.Run("inject active views to dataview blocks", func(t *testing.T) {
// given
blocksToView := map[string]string{dv1: "view1", dv2: "view2"}
fx := newFixture(t)
fx.store.EXPECT().GetActiveViews(mock.Anything).RunAndReturn(func(id string) (map[string]string, error) {
assert.Equal(t, objId, id)
return blocksToView, nil
})
info := getInfo()
// when
err := fx.injectActiveViews(info)
st := info.State
// then
assert.NoError(t, err)
assert.Equal(t, blocksToView[dv1], st.Pick(dv1).Model().GetDataview().ActiveView)
assert.Equal(t, blocksToView[dv2], st.Pick(dv2).Model().GetDataview().ActiveView)
assert.Empty(t, st.Pick(dv3).Model().GetDataview().ActiveView)
})
t.Run("do nothing if active views are not found in DB", func(t *testing.T) {
// given
fx := newFixture(t)
fx.store.EXPECT().GetActiveViews(mock.Anything).RunAndReturn(func(id string) (map[string]string, error) {
assert.Equal(t, objId, id)
return nil, badger.ErrKeyNotFound
})
info := getInfo()
// when
err := fx.injectActiveViews(info)
// then
assert.NoError(t, err)
})
t.Run("fail on other DB error", func(t *testing.T) {
// given
fx := newFixture(t)
fx.store.EXPECT().GetActiveViews(mock.Anything).RunAndReturn(func(id string) (map[string]string, error) {
assert.Equal(t, objId, id)
return nil, errors.New("badger was stolen by UFO")
})
info := getInfo()
// when
err := fx.injectActiveViews(info)
// then
assert.Error(t, err)
})
}

View file

@ -688,32 +688,33 @@ func (e *export) cleanupFile(wr writer) {
}
func (e *export) getRelatedDerivedObjects(objects map[string]*types.Struct) ([]database.Record, error) {
derivedObjects, err := e.iterateObjects(objects)
derivedObjects, typesAndTemplates, err := e.iterateObjects(objects)
if err != nil {
return nil, err
}
if len(derivedObjects) > 0 {
// get derived objects only from types and templates,
// because relations currently have only system relations and object type
if len(typesAndTemplates) > 0 {
derivedObjectsMap := make(map[string]*types.Struct, 0)
for _, object := range derivedObjects {
for _, object := range typesAndTemplates {
id := object.Get(bundle.RelationKeyId.String()).GetStringValue()
derivedObjectsMap[id] = object.Details
}
iteratedObjects, err := e.iterateObjects(derivedObjectsMap)
iteratedObjects, typesAndTemplates, err := e.iterateObjects(derivedObjectsMap)
if err != nil {
return nil, err
}
derivedObjects = append(derivedObjects, iteratedObjects...)
derivedObjects = append(derivedObjects, typesAndTemplates...)
}
return derivedObjects, nil
}
func (e *export) iterateObjects(objects map[string]*types.Struct) ([]database.Record, error) {
var (
derivedObjects []database.Record
relations []string
)
func (e *export) iterateObjects(objects map[string]*types.Struct,
) (allObjects []database.Record, typesAndTemplates []database.Record, err error) {
var relations []string
for id, object := range objects {
err := cache.Do(e.picker, id, func(b sb.SmartBlock) error {
err = cache.Do(e.picker, id, func(b sb.SmartBlock) error {
state := b.NewState()
relations = e.getObjectRelations(state, relations)
details := state.Details()
@ -727,14 +728,14 @@ func (e *export) iterateObjects(objects map[string]*types.Struct) ([]database.Re
return nil
})
if err != nil {
return nil, err
return nil, nil, err
}
derivedObjects, err = e.processObject(object, derivedObjects, relations)
allObjects, typesAndTemplates, err = e.processObject(object, allObjects, typesAndTemplates, relations)
if err != nil {
return nil, err
return nil, nil, err
}
}
return derivedObjects, nil
return allObjects, typesAndTemplates, nil
}
func (e *export) getDataviewRelations(state *state.State) ([]string, error) {
@ -767,60 +768,69 @@ func (e *export) isObjectWithDataview(details *types.Struct) bool {
func (e *export) processObject(object *types.Struct,
derivedObjects []database.Record,
typesAndTemplates []database.Record,
relations []string,
) ([]database.Record, error) {
) ([]database.Record, []database.Record, error) {
for _, relation := range relations {
storeRelation, err := e.getRelation(relation)
if err != nil {
return nil, err
return nil, nil, err
}
if storeRelation != nil {
derivedObjects, err = e.addRelationAndOptions(storeRelation, derivedObjects, relation)
if err != nil {
return nil, err
return nil, nil, err
}
}
}
objectTypeId := pbtypes.GetString(object, bundle.RelationKeyType.String())
derivedObjects, err := e.addObjectType(objectTypeId, derivedObjects)
var err error
derivedObjects, typesAndTemplates, err = e.addObjectType(objectTypeId, derivedObjects, typesAndTemplates)
if err != nil {
return nil, err
return nil, nil, err
}
derivedObjects, err = e.addTemplates(objectTypeId, derivedObjects)
derivedObjects, typesAndTemplates, err = e.addTemplates(objectTypeId, derivedObjects, typesAndTemplates)
if err != nil {
return nil, err
return nil, nil, err
}
return e.handleSetOfRelation(object, derivedObjects)
derivedObjects, err = e.handleSetOfRelation(object, derivedObjects)
if err != nil {
return nil, nil, err
}
return derivedObjects, typesAndTemplates, nil
}
func (e *export) addObjectType(objectTypeId string, derivedObjects []database.Record) ([]database.Record, error) {
func (e *export) addObjectType(objectTypeId string, derivedObjects []database.Record, typesAndTemplates []database.Record) ([]database.Record, []database.Record, error) {
objectTypeDetails, err := e.objectStore.GetDetails(objectTypeId)
if err != nil {
return nil, err
return nil, nil, err
}
if objectTypeDetails == nil || objectTypeDetails.Details == nil || len(objectTypeDetails.Details.Fields) == 0 {
return derivedObjects, nil
return derivedObjects, typesAndTemplates, nil
}
uniqueKey := pbtypes.GetString(objectTypeDetails.Details, bundle.RelationKeyUniqueKey.String())
key, err := domain.GetTypeKeyFromRawUniqueKey(uniqueKey)
if err != nil {
return nil, err
return nil, nil, err
}
if bundle.IsInternalType(key) {
return derivedObjects, nil
return derivedObjects, typesAndTemplates, nil
}
recommendedRelations := pbtypes.GetStringList(objectTypeDetails.Details, bundle.RelationKeyRecommendedRelations.String())
for _, relation := range recommendedRelations {
if relation == addr.MissingObject {
continue
}
details, err := e.objectStore.GetDetails(relation)
if err != nil {
return nil, err
return nil, nil, err
}
relationKey := pbtypes.GetString(details.Details, bundle.RelationKeyUniqueKey.String())
uniqueKey, err := domain.UnmarshalUniqueKey(relationKey)
if err != nil {
return nil, err
return nil, nil, err
}
if bundle.IsSystemRelation(domain.RelationKey(uniqueKey.InternalKey())) {
continue
@ -828,7 +838,8 @@ func (e *export) addObjectType(objectTypeId string, derivedObjects []database.Re
derivedObjects = append(derivedObjects, database.Record{Details: details.Details})
}
derivedObjects = append(derivedObjects, database.Record{Details: objectTypeDetails.Details})
return derivedObjects, nil
typesAndTemplates = append(typesAndTemplates, database.Record{Details: objectTypeDetails.Details})
return derivedObjects, typesAndTemplates, nil
}
func (e *export) getRelation(key string) (*database.Record, error) {
@ -918,7 +929,7 @@ func (e *export) getRelationOptions(relationKey string) ([]database.Record, erro
return relationOptionsDetails, nil
}
func (e *export) addTemplates(id string, derivedObjects []database.Record) ([]database.Record, error) {
func (e *export) addTemplates(id string, derivedObjects []database.Record, typesAndTemplates []database.Record) ([]database.Record, []database.Record, error) {
templates, err := e.objectStore.Query(database.Query{
Filters: []*model.BlockContentDataviewFilter{
{
@ -939,10 +950,11 @@ func (e *export) addTemplates(id string, derivedObjects []database.Record) ([]da
},
})
if err != nil {
return nil, err
return nil, nil, err
}
derivedObjects = append(derivedObjects, templates...)
return derivedObjects, nil
typesAndTemplates = append(typesAndTemplates, templates...)
return derivedObjects, typesAndTemplates, nil
}
func (e *export) handleSetOfRelation(object *types.Struct, derivedObjects []database.Record) ([]database.Record, error) {

View file

@ -15,6 +15,7 @@ import (
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
"github.com/anyproto/anytype-heart/pkg/lib/core/smartblock"
"github.com/anyproto/anytype-heart/pkg/lib/localstore/addr"
"github.com/anyproto/anytype-heart/pkg/lib/localstore/objectstore"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
"github.com/anyproto/anytype-heart/space/spacecore/typeprovider/mock_typeprovider"
@ -184,7 +185,6 @@ func Test_docsForExport(t *testing.T) {
objectGetter := mock_cache.NewMockObjectGetter(t)
smartBlockTest := smarttest.New("id")
smartBlockRelation := smarttest.New("key")
doc := smartBlockTest.NewState().SetDetails(&types.Struct{
Fields: map[string]*types.Value{
bundle.RelationKeyId.String(): pbtypes.String("id"),
@ -201,7 +201,6 @@ func Test_docsForExport(t *testing.T) {
smartBlockTest.Doc = doc
objectGetter.EXPECT().GetObject(context.Background(), "id").Return(smartBlockTest, nil)
objectGetter.EXPECT().GetObject(context.Background(), "key").Return(smartBlockRelation, nil)
e := &export{
objectStore: storeFixture,
@ -244,7 +243,6 @@ func Test_docsForExport(t *testing.T) {
objectGetter := mock_cache.NewMockObjectGetter(t)
smartBlockTest := smarttest.New("id")
smartBlockRelation := smarttest.New(relationKey)
doc := smartBlockTest.NewState().SetDetails(&types.Struct{
Fields: map[string]*types.Value{
bundle.RelationKeyId.String(): pbtypes.String("id"),
@ -261,7 +259,6 @@ func Test_docsForExport(t *testing.T) {
smartBlockTest.Doc = doc
objectGetter.EXPECT().GetObject(context.Background(), "id").Return(smartBlockTest, nil)
objectGetter.EXPECT().GetObject(context.Background(), relationKey).Return(smartBlockRelation, nil)
e := &export{
objectStore: storeFixture,
@ -313,8 +310,6 @@ func Test_docsForExport(t *testing.T) {
objectGetter := mock_cache.NewMockObjectGetter(t)
smartBlockTest := smarttest.New("id")
smartBlockRelation := smarttest.New(relationKey)
smartBlockRelationOption := smarttest.New(optionId)
doc := smartBlockTest.NewState().SetDetails(&types.Struct{
Fields: map[string]*types.Value{
bundle.RelationKeyId.String(): pbtypes.String("id"),
@ -331,8 +326,6 @@ func Test_docsForExport(t *testing.T) {
smartBlockTest.Doc = doc
objectGetter.EXPECT().GetObject(context.Background(), "id").Return(smartBlockTest, nil)
objectGetter.EXPECT().GetObject(context.Background(), relationKey).Return(smartBlockRelation, nil)
objectGetter.EXPECT().GetObject(context.Background(), optionId).Return(smartBlockRelationOption, nil)
e := &export{
objectStore: storeFixture,
@ -355,4 +348,147 @@ func Test_docsForExport(t *testing.T) {
}
assert.Contains(t, objectIds, optionId)
})
t.Run("get derived objects - relation, object type with recommended relations, template with link", func(t *testing.T) {
// given
storeFixture := objectstore.NewStoreFixture(t)
relationKey := "key"
objectTypeKey := "customObjectType"
objectTypeUniqueKey, err := domain.NewUniqueKey(smartblock.SmartBlockTypeObjectType, objectTypeKey)
assert.Nil(t, err)
uniqueKey, err := domain.NewUniqueKey(smartblock.SmartBlockTypeRelation, relationKey)
assert.Nil(t, err)
recommendedRelationKey := "recommendedRelationKey"
recommendedRelationUniqueKey, err := domain.NewUniqueKey(smartblock.SmartBlockTypeRelation, recommendedRelationKey)
assert.Nil(t, err)
templateId := "templateId"
linkedObjectId := "linkedObjectId"
storeFixture.AddObjects(t, []objectstore.TestObject{
{
bundle.RelationKeyId: pbtypes.String("id"),
domain.RelationKey(relationKey): pbtypes.String("test"),
bundle.RelationKeyType: pbtypes.String(objectTypeKey),
},
{
bundle.RelationKeyId: pbtypes.String(relationKey),
bundle.RelationKeyRelationKey: pbtypes.String(relationKey),
bundle.RelationKeyUniqueKey: pbtypes.String(uniqueKey.Marshal()),
bundle.RelationKeyLayout: pbtypes.Int64(int64(model.ObjectType_relation)),
},
{
bundle.RelationKeyId: pbtypes.String(objectTypeKey),
bundle.RelationKeyUniqueKey: pbtypes.String(objectTypeUniqueKey.Marshal()),
bundle.RelationKeyLayout: pbtypes.Int64(int64(model.ObjectType_objectType)),
bundle.RelationKeyRecommendedRelations: pbtypes.StringList([]string{recommendedRelationKey}),
},
{
bundle.RelationKeyId: pbtypes.String(recommendedRelationKey),
bundle.RelationKeyRelationKey: pbtypes.String(recommendedRelationKey),
bundle.RelationKeyUniqueKey: pbtypes.String(recommendedRelationUniqueKey.Marshal()),
bundle.RelationKeyLayout: pbtypes.Int64(int64(model.ObjectType_relation)),
},
{
bundle.RelationKeyId: pbtypes.String(templateId),
bundle.RelationKeyTargetObjectType: pbtypes.String(objectTypeKey),
},
{
bundle.RelationKeyId: pbtypes.String(linkedObjectId),
bundle.RelationKeyType: pbtypes.String(objectTypeKey),
},
})
err = storeFixture.UpdateObjectLinks(templateId, []string{linkedObjectId})
assert.Nil(t, err)
objectGetter := mock_cache.NewMockObjectGetter(t)
smartBlockTest := smarttest.New("id")
smartBlockTemplate := smarttest.New(templateId)
smartBlockObjectType := smarttest.New(objectTypeKey)
doc := smartBlockTest.NewState().SetDetails(&types.Struct{
Fields: map[string]*types.Value{
bundle.RelationKeyId.String(): pbtypes.String("id"),
relationKey: pbtypes.String("value"),
bundle.RelationKeyType.String(): pbtypes.String("objectType"),
}})
doc.AddRelationLinks(&model.RelationLink{
Key: bundle.RelationKeyId.String(),
Format: model.RelationFormat_longtext,
}, &model.RelationLink{
Key: relationKey,
Format: model.RelationFormat_tag,
})
smartBlockTest.Doc = doc
objectGetter.EXPECT().GetObject(context.Background(), "id").Return(smartBlockTest, nil)
objectGetter.EXPECT().GetObject(context.Background(), templateId).Return(smartBlockTemplate, nil)
objectGetter.EXPECT().GetObject(context.Background(), objectTypeKey).Return(smartBlockObjectType, nil)
provider := mock_typeprovider.NewMockSmartBlockTypeProvider(t)
provider.EXPECT().Type("spaceId", linkedObjectId).Return(smartblock.SmartBlockTypePage, nil)
e := &export{
objectStore: storeFixture,
picker: objectGetter,
sbtProvider: provider,
}
// when
docsForExport, err := e.docsForExport("spaceId", pb.RpcObjectListExportRequest{
SpaceId: "spaceId",
ObjectIds: []string{"id"},
Format: model.Export_Protobuf,
IncludeNested: true,
})
// then
assert.Nil(t, err)
assert.Equal(t, 6, len(docsForExport))
})
t.Run("get derived objects, object type have missing relations - return only object and its type", func(t *testing.T) {
// given
storeFixture := objectstore.NewStoreFixture(t)
objectTypeKey := "customObjectType"
objectTypeUniqueKey, err := domain.NewUniqueKey(smartblock.SmartBlockTypeObjectType, objectTypeKey)
assert.Nil(t, err)
storeFixture.AddObjects(t, []objectstore.TestObject{
{
bundle.RelationKeyId: pbtypes.String("id"),
bundle.RelationKeyType: pbtypes.String(objectTypeKey),
},
{
bundle.RelationKeyId: pbtypes.String(objectTypeKey),
bundle.RelationKeyUniqueKey: pbtypes.String(objectTypeUniqueKey.Marshal()),
bundle.RelationKeyLayout: pbtypes.Int64(int64(model.ObjectType_objectType)),
bundle.RelationKeyRecommendedRelations: pbtypes.StringList([]string{addr.MissingObject}),
},
})
objectGetter := mock_cache.NewMockObjectGetter(t)
smartBlockTest := smarttest.New("id")
smartBlockObjectType := smarttest.New(objectTypeKey)
objectGetter.EXPECT().GetObject(context.Background(), "id").Return(smartBlockTest, nil)
objectGetter.EXPECT().GetObject(context.Background(), objectTypeKey).Return(smartBlockObjectType, nil)
e := &export{
objectStore: storeFixture,
picker: objectGetter,
}
// when
docsForExport, err := e.docsForExport("spaceId", pb.RpcObjectListExportRequest{
SpaceId: "spaceId",
ObjectIds: []string{"id"},
Format: model.Export_Protobuf,
})
// then
assert.Nil(t, err)
assert.Equal(t, 2, len(docsForExport))
})
}

View file

@ -7,6 +7,8 @@ import (
"github.com/globalsign/mgo/bson"
"github.com/gogo/protobuf/types"
"github.com/stretchr/testify/assert"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/text"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
"github.com/anyproto/anytype-heart/util/pbtypes"
@ -235,3 +237,54 @@ func TestCloseTextBlock(t *testing.T) {
assert.Equal(t, "id2", renderer.blocks[1].ChildrenIds[0])
})
}
func Test_renderCodeBloc(t *testing.T) {
t.Run("simple case", func(t *testing.T) {
// given
r := NewRenderer(newBlocksRenderer("", nil, false))
node := ast.NewCodeBlock()
segments := text.NewSegments()
segments.Append(text.Segment{
Start: 0,
Stop: 4,
})
node.SetLines(segments)
// when
_, err := r.renderCodeBlock(nil, []byte("test"), node, true)
assert.Nil(t, err)
_, err = r.renderCodeBlock(nil, []byte("test"), node, false)
// then
assert.Nil(t, err)
assert.Len(t, r.blocks, 1)
assert.Equal(t, "test", r.blocks[0].GetText().GetText())
assert.Equal(t, r.blocks[0].GetText().GetStyle(), model.BlockContentText_Code)
})
t.Run("2 lines", func(t *testing.T) {
// given
r := NewRenderer(newBlocksRenderer("", nil, false))
node := ast.NewCodeBlock()
segments := text.NewSegments()
segments.Append(text.Segment{
Start: 0,
Stop: 5,
})
segments.Append(text.Segment{
Start: 5,
Stop: 8,
})
node.SetLines(segments)
// when
_, err := r.renderCodeBlock(nil, []byte("testtest"), node, true)
assert.Nil(t, err)
_, err = r.renderCodeBlock(nil, []byte("testtest"), node, false)
// then
assert.Nil(t, err)
assert.Len(t, r.blocks, 1)
assert.Equal(t, "testtest", r.blocks[0].GetText().GetText())
assert.Equal(t, model.BlockContentText_Code, r.blocks[0].GetText().GetStyle())
})
}

View file

@ -192,11 +192,8 @@ func getCustomHTMLRules() []html2md.Rule {
img := html2md.Rule{
Filter: []string{"img"},
Replacement: func(content string, selec *goquery.Selection, options *html2md.Options) *string {
var (
src, title string
ok bool
)
if src, ok = selec.Attr("src"); !ok {
var src, title string
if src = extractImageSource(selec); src == "" {
return nil
}
@ -233,6 +230,19 @@ func getCustomHTMLRules() []html2md.Rule {
simpleText, blockquote, italic, code, bdo, div, img, table}
}
func extractImageSource(selec *goquery.Selection) string {
var (
src string
ok bool
)
if src, ok = selec.Attr("src"); !ok || src == "" {
if src, ok = selec.Attr("data-src"); !ok || src == "" {
return ""
}
}
return src
}
func addHeaderRow(content string, numberOfCells int, numberOfRows int) string {
numberOfColumns := numberOfCells / numberOfRows

View file

@ -121,6 +121,9 @@ func (r *Renderer) renderCodeBlock(_ util.BufWriter,
n ast.Node,
entering bool) (ast.WalkStatus, error) {
r.openTextBlockWithStyle(entering, model.BlockContentText_Code, nil)
if entering {
r.writeLines(source, n)
}
return ast.WalkContinue, nil
}

View file

@ -217,8 +217,23 @@
"blocks": [{"id":"1","Content":{"file":{"name":"http://static.com/image.png","type":2}}}]
},
{
"desc": "section linm",
"desc": "section link",
"html": "<img class=\"logo-icon\" src=\"#description\" alt=\"\">",
"blocks": [{"id":"1","Content":{"file":{"name":"http://test.com/test#description","type":2}}}]
},
{
"desc": "image with data-src atr",
"html": "<img class=\"logo-icon\" data-src=\"/static/image.png\" alt=\"\">",
"blocks": [{"id":"1","Content":{"file":{"name":"http://test.com/static/image.png","type":2}}}]
},
{
"desc": "image without src",
"html": "<img class=\"logo-icon\" alt=\"\">",
"blocks": null
},
{
"desc": "image with empty src",
"html": "<img class=\"logo-icon\" src=\"\" data-src=\"\" alt=\"\">",
"blocks": null
}
]

View file

@ -104,8 +104,6 @@ func (s *service) InstallBundledObjects(
objects = append(objects, newDetails)
}
}
s.reviseSystemObjects(space, existingObjectMap)
return
}

View file

@ -388,7 +388,7 @@ func (s *service) addFileNode(ctx context.Context, spaceID string, mill m.Mill,
return newExistingFileResult(variant)
}
res, err := mill.Mill(conf.Reader, conf.Name, conf.checksum)
res, err := mill.Mill(conf.Reader, conf.Name)
if err != nil {
return nil, fmt.Errorf("%w: %w", m.ErrProcessing, err)
}
@ -401,7 +401,12 @@ func (s *service) addFileNode(ctx context.Context, spaceID string, mill m.Mill,
}
if variant, err := s.fileStore.GetFileVariantByChecksum(mill.ID(), variantChecksum); err == nil {
return newExistingFileResult(variant)
if variant.Source == conf.checksum {
// we may have same variant checksum for different files
// e.g. empty image exif with the same resolution
// reuse the whole file only in case the checksum of the original file is the same
return newExistingFileResult(variant)
}
}
_, err = conf.Reader.Seek(0, io.SeekStart)
@ -409,8 +414,7 @@ func (s *service) addFileNode(ctx context.Context, spaceID string, mill m.Mill,
return nil, err
}
// because mill result reader doesn't support seek we need to do the mill again
res, err = mill.Mill(conf.Reader, conf.Name, conf.checksum)
_, err = res.File.Seek(0, io.SeekStart)
if err != nil {
return nil, err
}
@ -465,6 +469,10 @@ func (s *service) addFileNode(ctx context.Context, spaceID string, mill m.Mill,
fileInfo.MetaHash = metaNode.Cid().String()
pairNode, err := s.addFilePairNode(ctx, spaceID, fileInfo)
err = res.File.Close()
if err != nil {
log.Warnf("failed to close file: %s", err)
}
if err != nil {
return nil, fmt.Errorf("add file pair node: %w", err)
}

View file

@ -1,7 +1,9 @@
package files
import (
"bytes"
"context"
"io"
"os"
"testing"
"time"
@ -102,6 +104,52 @@ func TestImageAddWithCustomEncryptionKeys(t *testing.T) {
assertCustomEncryptionKeys(t, fx, got, customKeys)
}
func TestImageAddReuse(t *testing.T) {
fx := newFixture(t)
f, err := os.Open("../../pkg/lib/mill/testdata/image.jpeg")
require.NoError(t, err)
defer f.Close()
fileName := "myFile"
lastModifiedDate := time.Now()
opts := []AddOption{
WithName(fileName),
WithLastModifiedDate(lastModifiedDate.Unix()),
WithReader(f),
}
got1, err := fx.ImageAdd(context.Background(), spaceId, opts...)
require.NoError(t, err)
got1.Commit()
f.Seek(0, 0)
got2, err := fx.ImageAdd(context.Background(), spaceId, opts...)
require.NoError(t, err)
got2.Commit()
require.True(t, got2.IsExisting)
require.Equal(t, got1.FileId.String(), got2.FileId.String())
require.Equal(t, got1.EncryptionKeys.EncryptionKeys, got2.EncryptionKeys.EncryptionKeys)
b, err := io.ReadAll(f)
require.NoError(t, err)
b[10000] = 0x00
// patch the original image so it will have the different source hash, but the same(empty) exif
patchedReader := bytes.NewReader(b)
opts = []AddOption{
WithName(fileName),
WithLastModifiedDate(lastModifiedDate.Unix()),
WithReader(patchedReader),
}
// exif will be the same but images are different
got3, err := fx.ImageAdd(context.Background(), spaceId, opts...)
require.NoError(t, err)
got3.Commit()
fileId3 := got3.FileId.String()
require.NotEqual(t, got1.FileId.String(), fileId3)
require.False(t, got3.IsExisting)
}
func assertCustomEncryptionKeys(t *testing.T, fx *fixture, got *AddResult, customKeys map[string]string) {
encKeys, err := fx.fileStore.GetFileKeys(got.FileId)
require.NoError(t, err)

View file

@ -32,6 +32,7 @@ var log = logger.NewNamed(CName)
var loopTimeout = time.Minute
type StatusCallback func(fileObjectId string) error
type DeleteCallback func(fileObjectId domain.FullFileId)
type FileSync interface {
AddFile(fileObjectId string, fileId domain.FullFileId, uploadedByUser, imported bool) (err error)
@ -39,6 +40,7 @@ type FileSync interface {
OnUploadStarted(StatusCallback)
OnUploaded(StatusCallback)
OnLimited(StatusCallback)
OnDelete(DeleteCallback)
DeleteFile(objectId string, fileId domain.FullFileId) (err error)
DeleteFileSynchronously(fileId domain.FullFileId) (err error)
UpdateNodeUsage(ctx context.Context) error
@ -74,6 +76,7 @@ type fileSync struct {
onUploaded StatusCallback
onUploadStarted StatusCallback
onLimited StatusCallback
onDelete DeleteCallback
uploadingQueue *persistentqueue.Queue[*QueueItem]
retryUploadingQueue *persistentqueue.Queue[*QueueItem]
@ -121,6 +124,10 @@ func (s *fileSync) OnLimited(callback StatusCallback) {
s.onLimited = callback
}
func (s *fileSync) OnDelete(callback DeleteCallback) {
s.onDelete = callback
}
func (s *fileSync) Name() (name string) {
return CName
}

View file

@ -514,6 +514,39 @@ func (_c *MockFileSync_NodeUsage_Call) RunAndReturn(run func(context.Context) (f
return _c
}
// OnDelete provides a mock function with given fields: _a0
func (_m *MockFileSync) OnDelete(_a0 filesync.DeleteCallback) {
_m.Called(_a0)
}
// MockFileSync_OnDelete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OnDelete'
type MockFileSync_OnDelete_Call struct {
*mock.Call
}
// OnDelete is a helper method to define mock.On call
// - _a0 filesync.DeleteCallback
func (_e *MockFileSync_Expecter) OnDelete(_a0 interface{}) *MockFileSync_OnDelete_Call {
return &MockFileSync_OnDelete_Call{Call: _e.mock.On("OnDelete", _a0)}
}
func (_c *MockFileSync_OnDelete_Call) Run(run func(_a0 filesync.DeleteCallback)) *MockFileSync_OnDelete_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(filesync.DeleteCallback))
})
return _c
}
func (_c *MockFileSync_OnDelete_Call) Return() *MockFileSync_OnDelete_Call {
_c.Call.Return()
return _c
}
func (_c *MockFileSync_OnDelete_Call) RunAndReturn(run func(filesync.DeleteCallback)) *MockFileSync_OnDelete_Call {
_c.Call.Return(run)
return _c
}
// OnLimited provides a mock function with given fields: _a0
func (_m *MockFileSync) OnLimited(_a0 filesync.StatusCallback) {
_m.Called(_a0)

View file

@ -46,6 +46,9 @@ func (s *fileSync) deletionHandler(ctx context.Context, it *QueueItem) (persiste
if err != nil {
log.Error("remove from deletion queues", zap.String("fileId", it.FileId.String()), zap.Error(err))
}
if s.onDelete != nil {
s.onDelete(fileId)
}
return persistentqueue.ActionDone, nil
}
@ -63,6 +66,9 @@ func (s *fileSync) retryDeletionHandler(ctx context.Context, it *QueueItem) (per
if err != nil {
log.Error("remove from deletion queues", zap.String("fileId", it.FileId.String()), zap.Error(err))
}
if s.onDelete != nil {
s.onDelete(fileId)
}
return persistentqueue.ActionDone, nil
}

View file

@ -133,7 +133,7 @@ func (s *ownProfileSubscription) run(ctx context.Context) (err error) {
s.handleOwnProfileDetails(records[0].Details)
}
s.fetchGlobalName()
go s.fetchGlobalName(s.componentCtx, s.namingService)
go func() {
for {
@ -205,8 +205,12 @@ func (s *ownProfileSubscription) handleOwnProfileDetails(profileDetails *types.S
s.enqueuePush()
}
func (s *ownProfileSubscription) fetchGlobalName() {
response, err := s.namingService.GetNameByAnyId(s.componentCtx, &nameserviceproto.NameByAnyIdRequest{AnyAddress: s.myIdentity})
func (s *ownProfileSubscription) fetchGlobalName(ctx context.Context, ns nameserviceclient.AnyNsClientService) {
if ns == nil {
log.Error("error fetching global name of our own identity from Naming Service as the service is not initialized")
return
}
response, err := ns.GetNameByAnyId(ctx, &nameserviceproto.NameByAnyIdRequest{AnyAddress: s.myIdentity})
if err != nil || response == nil {
log.Error("error fetching global name of our own identity from Naming Service", zap.Error(err))
return
@ -215,11 +219,16 @@ func (s *ownProfileSubscription) fetchGlobalName() {
log.Debug("globalName was not found for our own identity in Naming Service")
return
}
s.handleGlobalNameUpdate(response.Name)
s.updateGlobalName(response.Name)
}
func (s *ownProfileSubscription) updateGlobalName(globalName string) {
s.globalNameUpdatedCh <- globalName
select {
case <-s.componentCtx.Done():
return
case s.globalNameUpdatedCh <- globalName:
return
}
}
func (s *ownProfileSubscription) handleGlobalNameUpdate(globalName string) {

View file

@ -135,6 +135,8 @@ func TestOwnProfileSubscription(t *testing.T) {
err := fx.run(context.Background())
require.NoError(t, err)
time.Sleep(testBatchTimeout / 4)
fx.objectStoreFixture.AddObjects(t, []objectstore.TestObject{
{
bundle.RelationKeyId: pbtypes.String(testProfileObjectId),
@ -201,6 +203,8 @@ func TestOwnProfileSubscription(t *testing.T) {
err := fx.run(context.Background())
require.NoError(t, err)
time.Sleep(testBatchTimeout / 4)
fx.updateGlobalName(newName)
time.Sleep(2 * testBatchTimeout)
@ -254,6 +258,8 @@ func TestOwnProfileSubscription(t *testing.T) {
err := fx.run(context.Background())
require.NoError(t, err)
time.Sleep(testBatchTimeout / 4)
fx.objectStoreFixture.AddObjects(t, []objectstore.TestObject{
{
bundle.RelationKeyId: pbtypes.String(testProfileObjectId),

View file

@ -33,7 +33,7 @@ func (mw *Middleware) LinkPreview(cctx context.Context, req *pb.RpcLinkPreviewRe
}
}
lp := mw.applicationService.GetApp().MustComponent(linkpreview.CName).(linkpreview.LinkPreview)
data, _, err := lp.Fetch(ctx, u.String())
data, _, _, err := lp.Fetch(ctx, u.String())
if err != nil {
// trim the actual url from the error
errTrimmed := strings.Replace(err.Error(), u.String(), "<url>", -1)

View file

@ -181,6 +181,7 @@ func (mw *Middleware) ObjectSearchWithMeta(cctx context.Context, req *pb.RpcObje
rec.Details = pbtypes.StructFilterKeys(rec.Details, req.Keys)
}
resultsModels = append(resultsModels, &model.SearchResult{
ObjectId: pbtypes.GetString(rec.Details, database.RecordIDField),
Details: rec.Details,
Meta: []*model.SearchMeta{&(results[i].Meta)},

View file

@ -12,6 +12,13 @@ import (
"github.com/anyproto/anytype-heart/pb"
)
// Semantics in case of NO INTERNET:
//
// If called with req.NoCache -> returns error
// If called without req.NoCache:
//
// has no fresh data -> returns error
// has fresh data -> returns data
func (mw *Middleware) MembershipGetStatus(ctx context.Context, req *pb.RpcMembershipGetStatusRequest) *pb.RpcMembershipGetStatusResponse {
log.Info("payments - client asked to get a subscription status", zap.Any("req", req))

View file

@ -31,9 +31,11 @@ var (
// once you change the cache format, you need to update this variable
// it will cause cache to be dropped and recreated
const LAST_CACHE_VERSION = 5
const cacheLastVersion = 6
var dbKey = "payments/subscription/v" + strconv.Itoa(LAST_CACHE_VERSION)
const cacheLifetimeDur = 24 * time.Hour
var dbKey = "payments/subscription/v" + strconv.Itoa(cacheLastVersion)
type StorageStruct struct {
// not to migrate old storage to new format, but just to check the validity of the cache
@ -44,7 +46,7 @@ type StorageStruct struct {
// this variable is just for info
LastUpdated time.Time
// depending on the type of the subscription the cache will have different lifetime
// depending on the type of the membership the cache will have different lifetime
// if current time is >= ExpireTime -> cache is expired
ExpireTime time.Time
@ -58,7 +60,7 @@ type StorageStruct struct {
func newStorageStruct() *StorageStruct {
return &StorageStruct{
CurrentVersion: LAST_CACHE_VERSION,
CurrentVersion: cacheLastVersion,
LastUpdated: time.Now().UTC(),
ExpireTime: time.Time{},
DisableUntilTime: time.Time{},
@ -79,7 +81,7 @@ type CacheService interface {
// if cache is disabled -> will return no error
// if cache is expired -> will return no error
// status or tiers can be nil depending on what you want to update
CacheSet(status *pb.RpcMembershipGetStatusResponse, tiers *pb.RpcMembershipGetTiersResponse, ExpireTime time.Time) (err error)
CacheSet(status *pb.RpcMembershipGetStatusResponse, tiers *pb.RpcMembershipGetTiersResponse, subscriptionEnds time.Time) (err error)
IsCacheEnabled() (enabled bool)
@ -134,11 +136,11 @@ func (s *cacheservice) CacheGet() (status *pb.RpcMembershipGetStatusResponse, ti
// 1 - check in storage
ss, err := s.get()
if err != nil {
log.Error("can not get subscription status from cache", zap.Error(err))
log.Error("can not get membership status from cache", zap.Error(err))
return nil, nil, ErrCacheDbError
}
if ss.CurrentVersion != LAST_CACHE_VERSION {
if ss.CurrentVersion != cacheLastVersion {
// currently we have only one version, but in future we can have more
// this error can happen if you "downgrade" the app
log.Error("unsupported cache version", zap.Uint16("version", ss.CurrentVersion))
@ -161,7 +163,31 @@ func (s *cacheservice) CacheGet() (status *pb.RpcMembershipGetStatusResponse, ti
return &ss.SubscriptionStatus, &ss.TiersData, nil
}
func (s *cacheservice) CacheSet(status *pb.RpcMembershipGetStatusResponse, tiers *pb.RpcMembershipGetTiersResponse, expireTime time.Time) (err error) {
func getCacheExpireTime(dateEnds time.Time) time.Time {
// dateEnds can be 0
isExpired := time.Now().UTC().After(dateEnds)
timeNow := time.Now().UTC()
timeNext := timeNow.Add(cacheLifetimeDur)
// sub end < now OR no sub end provided (unlimited)
if isExpired {
log.Debug("incrementing cache lifetime because membership is isExpired")
return timeNext
}
// sub end >= now
// return min(sub end, now + 24h)
if dateEnds.Before(timeNext) {
log.Debug("incrementing cache lifetime because membership ends soon")
return dateEnds
}
return timeNext
}
func (s *cacheservice) CacheSet(status *pb.RpcMembershipGetStatusResponse, tiers *pb.RpcMembershipGetTiersResponse, subscriptionEnds time.Time) (err error) {
expireTime := getCacheExpireTime(subscriptionEnds)
// 1 - get existing storage
ss, err := s.get()
if err != nil {

View file

@ -381,6 +381,6 @@ func TestPayments_CacheSetSubscriptionStatus(t *testing.T) {
require.Equal(t, nil, err)
_, _, err = fx.CacheGet()
require.Equal(t, ErrCacheExpired, err)
require.Equal(t, nil, err)
})
}

View file

@ -170,8 +170,8 @@ func (s *service) Close(_ context.Context) (err error) {
}
func (s *service) getPeriodicStatus(ctx context.Context) error {
// get subscription status (from cache or from PP node)
// if status is changed -> it will send an event
// get subscription status (from cache or from the PP node)
// if status has changed -> it will send an event
log.Debug("periodic: getting subscription status from cache/PP node")
_, err := s.GetSubscriptionStatus(ctx, &pb.RpcMembershipGetStatusRequest{})
@ -192,6 +192,18 @@ func (s *service) sendEvent(status *pb.RpcMembershipGetStatusResponse) {
})
}
// Logic:
//
// 1. Check in cache. if req.NoCache -> do not check in cache.
// 2. If found in cache -> return it
// 3. Ask PP node
// 4a. If PP node didn't answer and we have membership -> return it
// 4b. If PP node didn't answer -> create empty response
// 5. Save to cache. Lifetime - min(subscription ends, 24h)
// 6. If tier or status has changed -> send event
// 7. If name has changed -> update global name
// 8. UpdateLimits
// 9. Enable cache again if status is active
func (s *service) GetSubscriptionStatus(ctx context.Context, req *pb.RpcMembershipGetStatusRequest) (*pb.RpcMembershipGetStatusResponse, error) {
s.mx.Lock()
defer s.mx.Unlock()
@ -200,16 +212,18 @@ func (s *service) GetSubscriptionStatus(ctx context.Context, req *pb.RpcMembersh
privKey := s.wallet.GetAccountPrivkey()
// 1 - check in cache
// tiers var. is unused here
cachedStatus, tiers, err := s.cache.CacheGet()
// if cache is disabled -> will return objects and ErrCacheDisabled
// if cache is expired -> will return objects and ErrCacheExpired
cachedStatus, _, err := s.cache.CacheGet()
// if NoCache -> skip returning from cache
// if NoCache flag -> skip returning from cache
if !req.NoCache && (err == nil) && (cachedStatus != nil) && (cachedStatus.Data != nil) {
// 2. If found in cache -> return it
log.Debug("returning subscription status from cache", zap.Error(err), zap.Any("cachedStatus", cachedStatus))
return cachedStatus, nil
}
// 2 - send request to PP node
// 3 - send request to PP node
gsr := proto.GetSubscriptionRequest{
// payment node will check if signature matches with this OwnerAnyID
OwnerAnyID: ownerID,
@ -236,18 +250,14 @@ func (s *service) GetSubscriptionStatus(ctx context.Context, req *pb.RpcMembersh
status, err := s.ppclient.GetSubscriptionStatus(ctx, &reqSigned)
if err != nil {
// if we have non-standard tiers already -> then try not to overwrite cache please
// but just return error
if tiers != nil && tiers.Tiers != nil && len(tiers.Tiers) > 0 {
if tiers.Tiers[0].Id != uint32(proto.SubscriptionTier_TierExplorer) {
// return error
log.Error("returning error in get status", zap.Error(err))
return nil, err
}
// 4a. try reading from cache again
if (cachedStatus != nil) && (cachedStatus.Data != nil) {
log.Debug("returning subscription status from cache again", zap.Error(err), zap.Any("cachedStatus", cachedStatus))
return cachedStatus, nil
}
// if we have no tiers or standard tier -> overwrite cache and return no error please
log.Info("creating empty subscription in cache because can not get subscription status from payment node")
// 4b. If PP node didn't answer -> create empty response
log.Info("creating empty subscription in cache because can not get subscription status from the payment node")
// eat error and create empty status ("no tier") so that we will then save it to the cache
status = &proto.GetSubscriptionResponse{
@ -273,22 +283,10 @@ func (s *service) GetSubscriptionStatus(ctx context.Context, req *pb.RpcMembersh
out.Data.UserEmail = status.UserEmail
out.Data.SubscribeToNewsletter = status.SubscribeToNewsletter
// 3 - save into cache
// truncate nseconds here
var cacheExpireTime time.Time = time.Unix(int64(status.DateEnds), 0)
isExpired := time.Now().UTC().After(cacheExpireTime)
// if subscription DateEns is null - then default expire time is in 10 days
// or until user clicks on a “Pay by card/crypto” or “Manage” button
if status.DateEnds == 0 || isExpired {
log.Debug("setting cache to +1 day because subscription is isExpired")
timeNow := time.Now().UTC()
cacheExpireTime = timeNow.Add(1 * 24 * time.Hour)
}
// 5. Save to cache. Lifetime - min(subscription ends, now + 24h)
// update only status, not tiers
err = s.cache.CacheSet(&out, nil, cacheExpireTime)
// truncate nseconds here
err = s.cache.CacheSet(&out, nil, time.Unix(int64(status.DateEnds), 0))
if err != nil {
log.Error("can not save subscription status to cache", zap.Error(err))
return nil, ErrCacheProblem
@ -300,7 +298,7 @@ func (s *service) GetSubscriptionStatus(ctx context.Context, req *pb.RpcMembersh
log.Debug("subscription status", zap.Any("from server", status), zap.Any("cached", cachedStatus), zap.Bool("isEmailDiff", isEmailDiff))
// 4 - return, if cache was enabled and nothing is changed
// 6. If tier or status has changed -> send event
if cachedStatus != nil && !isDiffTier && !isDiffStatus && !isEmailDiff {
log.Debug("subscription status has NOT changed",
zap.Bool("cache was empty", cachedStatus == nil),
@ -316,11 +314,9 @@ func (s *service) GetSubscriptionStatus(ctx context.Context, req *pb.RpcMembersh
zap.Bool("isDiffStatus", isDiffStatus),
zap.Bool("isEmailDiff", isEmailDiff),
)
// 4.1 - send the event
s.sendEvent(&out)
// 4.2 - update globalName of our own identity
// 7. If name has changed -> update global name or own identity
if status.RequestedAnyName != "" {
log.Debug("update global name",
zap.String("requestedAnyName", status.RequestedAnyName),
@ -329,14 +325,14 @@ func (s *service) GetSubscriptionStatus(ctx context.Context, req *pb.RpcMembersh
s.profileUpdater.UpdateOwnGlobalName(status.RequestedAnyName)
}
// 8. UpdateLimits
err = s.updateLimits(ctx)
if err != nil {
log.Error("update limits", zap.Error(err))
}
// 4.3 - enable cache again (only when status is active)
// 9. Enable cache again if status is active
isFinished := status.Status == proto.SubscriptionStatus_StatusActive
if isFinished {
log.Info("enabling cache again")
@ -860,17 +856,12 @@ func (s *service) getAllTiers(ctx context.Context, req *pb.RpcMembershipGetTiers
}
// 3 - update tiers, not status
var cacheExpireTime time.Time
var ends time.Time = time.Unix(0, 0)
if (cachedStatus != nil) && (cachedStatus.Data != nil) {
cacheExpireTime = time.Unix(int64(cachedStatus.Data.DateEnds), 0)
} else {
log.Debug("setting tiers cache to +1 day")
timeNow := time.Now().UTC()
cacheExpireTime = timeNow.Add(1 * 24 * time.Hour)
ends = time.Unix(int64(cachedStatus.Data.DateEnds), 0)
}
err = s.cache.CacheSet(nil, &out, cacheExpireTime)
err = s.cache.CacheSet(nil, &out, ends)
if err != nil {
log.Error("can not save tiers to cache", zap.Error(err))
return nil, ErrCacheProblem

View file

@ -174,6 +174,71 @@ func TestGetStatus(t *testing.T) {
assert.Equal(t, model.Membership_StatusUnknown, resp.Data.Status)
})
t.Run("success if NoCache flag is passed, but no connectivity", func(t *testing.T) {
fx := newFixture(t)
defer fx.finish(t)
fx.ppclient.EXPECT().GetSubscriptionStatus(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx interface{}, in *psp.GetSubscriptionRequestSigned) (*psp.GetSubscriptionResponse, error) {
// >>> here
return nil, ErrNoConnection
}).MinTimes(1)
psgsr := pb.RpcMembershipGetStatusResponse{
Data: &model.Membership{
Tier: uint32(psp.SubscriptionTier_TierExplorer),
Status: model.Membership_StatusActive,
DateStarted: uint64(timeNow.Unix()),
DateEnds: uint64(subsExpire.Unix()),
IsAutoRenew: true,
PaymentMethod: model.Membership_MethodCrypto,
NsName: "something",
NsNameType: model.NameserviceNameType_AnyName,
},
}
fx.cache.EXPECT().CacheGet().Return(&psgsr, nil, nil)
// Call the function being tested
req := pb.RpcMembershipGetStatusRequest{
// / >>> here:
NoCache: true,
}
resp, err := fx.GetSubscriptionStatus(ctx, &req)
assert.NoError(t, err)
assert.Equal(t, uint32(psp.SubscriptionTier_TierExplorer), resp.Data.Tier)
assert.Equal(t, model.Membership_StatusActive, resp.Data.Status)
})
t.Run("fail if NoCache flag is passed, no cache, no connectivity", func(t *testing.T) {
fx := newFixture(t)
defer fx.finish(t)
fx.ppclient.EXPECT().GetSubscriptionStatus(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx interface{}, in *psp.GetSubscriptionRequestSigned) (*psp.GetSubscriptionResponse, error) {
// >>> here
return nil, ErrNoConnection
}).MinTimes(1)
// >>> here:
fx.cache.EXPECT().CacheGet().Return(nil, nil, nil)
fx.cache.EXPECT().CacheSet(mock.AnythingOfType("*pb.RpcMembershipGetStatusResponse"), mock.AnythingOfType("*pb.RpcMembershipGetTiersResponse"), mock.AnythingOfType("time.Time")).RunAndReturn(func(in *pb.RpcMembershipGetStatusResponse, tiers *pb.RpcMembershipGetTiersResponse, expire time.Time) (err error) {
return nil
})
fx.expectLimitsUpdated()
// Call the function being tested
req := pb.RpcMembershipGetStatusRequest{
// / >>> here:
NoCache: true,
}
resp, err := fx.GetSubscriptionStatus(ctx, &req)
assert.NoError(t, err)
// default values
assert.Equal(t, uint32(psp.SubscriptionTier_TierUnknown), resp.Data.Tier)
assert.Equal(t, model.Membership_StatusUnknown, resp.Data.Status)
})
t.Run("fail if no cache, GetSubscriptionStatus returns error, and default tiers", func(t *testing.T) {
fx := newFixture(t)
defer fx.finish(t)
@ -219,22 +284,13 @@ func TestGetStatus(t *testing.T) {
}).MinTimes(1)
fx.cache.EXPECT().CacheGet().Return(&psgsr, &tgr, cache.ErrCacheExpired)
fx.cache.EXPECT().CacheSet(mock.AnythingOfType("*pb.RpcMembershipGetStatusResponse"), mock.AnythingOfType("*pb.RpcMembershipGetTiersResponse"), mock.AnythingOfType("time.Time")).RunAndReturn(func(in *pb.RpcMembershipGetStatusResponse, tiers *pb.RpcMembershipGetTiersResponse, expire time.Time) (err error) {
return nil
})
fx.expectLimitsUpdated()
// Call the function being tested
_, err := fx.GetSubscriptionStatus(ctx, &pb.RpcMembershipGetStatusRequest{})
assert.NoError(t, err)
// resp object is nil in case of error
// assert.Equal(t, pb.RpcPaymentsSubscriptionGetStatusResponseErrorCode(pb.RpcPaymentsSubscriptionGetStatusResponseError_UNKNOWN_ERROR), resp.Error.Code)
// assert.Equal(t, "can not write to cache!", resp.Error.Description)
})
t.Run("fail if no cache, GetSubscriptionStatus returns error, and NOT default tiers", func(t *testing.T) {
t.Run("success if no cache, GetSubscriptionStatus returns error and data", func(t *testing.T) {
fx := newFixture(t)
defer fx.finish(t)
@ -279,11 +335,13 @@ func TestGetStatus(t *testing.T) {
return nil, errors.New("no internet")
}).MinTimes(1)
// TODO: refactor - bad method semantics:
// returns error, but also returns data...
fx.cache.EXPECT().CacheGet().Return(&psgsr, &tgr, cache.ErrCacheExpired)
// Call the function being tested
_, err := fx.GetSubscriptionStatus(ctx, &pb.RpcMembershipGetStatusRequest{})
assert.Error(t, err)
assert.NoError(t, err)
})
t.Run("success if cache is expired and GetSubscriptionStatus returns no error", func(t *testing.T) {
@ -318,7 +376,7 @@ func TestGetStatus(t *testing.T) {
}).MinTimes(1)
fx.cache.EXPECT().CacheGet().Return(&psgsr, nil, cache.ErrCacheExpired)
fx.cache.EXPECT().CacheSet(mock.AnythingOfType("*pb.RpcMembershipGetStatusResponse"), mock.AnythingOfType("*pb.RpcMembershipGetTiersResponse"), cacheExpireTime).RunAndReturn(func(in *pb.RpcMembershipGetStatusResponse, tiers *pb.RpcMembershipGetTiersResponse, expire time.Time) (err error) {
fx.cache.EXPECT().CacheSet(mock.AnythingOfType("*pb.RpcMembershipGetStatusResponse"), mock.AnythingOfType("*pb.RpcMembershipGetTiersResponse"), mock.AnythingOfType("time.Time")).RunAndReturn(func(in *pb.RpcMembershipGetStatusResponse, tiers *pb.RpcMembershipGetTiersResponse, expire time.Time) (err error) {
return nil
})
// fx.cache.EXPECT().CacheEnable().Return(nil)
@ -422,7 +480,7 @@ func TestGetStatus(t *testing.T) {
}).MinTimes(1)
fx.cache.EXPECT().CacheGet().Return(&psgsr, nil, cache.ErrCacheExpired)
fx.cache.EXPECT().CacheSet(mock.AnythingOfType("*pb.RpcMembershipGetStatusResponse"), mock.AnythingOfType("*pb.RpcMembershipGetTiersResponse"), cacheExpireTime).RunAndReturn(func(in *pb.RpcMembershipGetStatusResponse, tiers *pb.RpcMembershipGetTiersResponse, expire time.Time) (err error) {
fx.cache.EXPECT().CacheSet(mock.AnythingOfType("*pb.RpcMembershipGetStatusResponse"), mock.AnythingOfType("*pb.RpcMembershipGetTiersResponse"), mock.AnythingOfType("time.Time")).RunAndReturn(func(in *pb.RpcMembershipGetStatusResponse, tiers *pb.RpcMembershipGetTiersResponse, expire time.Time) (err error) {
return errors.New("can not write to cache!")
})
@ -500,7 +558,7 @@ func TestGetStatus(t *testing.T) {
}).MinTimes(1)
fx.cache.EXPECT().CacheGet().Return(nil, nil, cache.ErrCacheExpired)
fx.cache.EXPECT().CacheSet(&psgsr, mock.AnythingOfType("*pb.RpcMembershipGetTiersResponse"), cacheExpireTime).RunAndReturn(func(in *pb.RpcMembershipGetStatusResponse, tiers *pb.RpcMembershipGetTiersResponse, expire time.Time) (err error) {
fx.cache.EXPECT().CacheSet(&psgsr, mock.AnythingOfType("*pb.RpcMembershipGetTiersResponse"), mock.AnythingOfType("time.Time")).RunAndReturn(func(in *pb.RpcMembershipGetStatusResponse, tiers *pb.RpcMembershipGetTiersResponse, expire time.Time) (err error) {
return nil
})
fx.cache.EXPECT().CacheEnable().Return(nil)
@ -559,7 +617,7 @@ func TestGetStatus(t *testing.T) {
// return real struct and error
fx.cache.EXPECT().CacheGet().Return(nil, nil, cache.ErrCacheDisabled)
fx.cache.EXPECT().CacheSet(&psgsr2, mock.AnythingOfType("*pb.RpcMembershipGetTiersResponse"), cacheExpireTime).RunAndReturn(func(in *pb.RpcMembershipGetStatusResponse, tiers *pb.RpcMembershipGetTiersResponse, expire time.Time) (err error) {
fx.cache.EXPECT().CacheSet(&psgsr2, mock.AnythingOfType("*pb.RpcMembershipGetTiersResponse"), mock.AnythingOfType("time.Time")).RunAndReturn(func(in *pb.RpcMembershipGetStatusResponse, tiers *pb.RpcMembershipGetTiersResponse, expire time.Time) (err error) {
return nil
})

View file

@ -21,7 +21,7 @@ func RelationFromStruct(st *types.Struct) *Relation {
DefaultValue: pbtypes.Get(st, bundle.RelationKeyRelationDefaultValue.String()),
DataSource: model.Relation_details,
Hidden: pbtypes.GetBool(st, bundle.RelationKeyIsHidden.String()),
ReadOnly: pbtypes.GetBool(st, bundle.RelationKeyIsReadonly.String()),
ReadOnly: pbtypes.GetBool(st, bundle.RelationKeyRelationReadonlyValue.String()),
ReadOnlyRelation: false,
Multi: maxCount > 1,
ObjectTypes: pbtypes.GetStringList(st, bundle.RelationKeyRelationFormatObjectTypes.String()),

View file

@ -27,6 +27,10 @@ func (s *service) OnFileLimited(objectId string) error {
return s.indexFileSyncStatus(objectId, filesyncstatus.Limited)
}
func (s *service) OnFileDelete(fileId domain.FullFileId) {
s.sendSpaceStatusUpdate(filesyncstatus.Synced, fileId.SpaceId)
}
func (s *service) indexFileSyncStatus(fileObjectId string, status filesyncstatus.Status) error {
var spaceId string
err := cache.Do(s.objectGetter, fileObjectId, func(sb smartblock.SmartBlock) (err error) {

View file

@ -0,0 +1,256 @@
// Code generated by mockery. DO NOT EDIT.
package mock_objectsyncstatus
import (
context "context"
app "github.com/anyproto/any-sync/app"
domain "github.com/anyproto/anytype-heart/core/domain"
mock "github.com/stretchr/testify/mock"
)
// MockSpaceStatusUpdater is an autogenerated mock type for the SpaceStatusUpdater type
type MockSpaceStatusUpdater struct {
mock.Mock
}
type MockSpaceStatusUpdater_Expecter struct {
mock *mock.Mock
}
func (_m *MockSpaceStatusUpdater) EXPECT() *MockSpaceStatusUpdater_Expecter {
return &MockSpaceStatusUpdater_Expecter{mock: &_m.Mock}
}
// Close provides a mock function with given fields: ctx
func (_m *MockSpaceStatusUpdater) Close(ctx context.Context) error {
ret := _m.Called(ctx)
if len(ret) == 0 {
panic("no return value specified for Close")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context) error); ok {
r0 = rf(ctx)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockSpaceStatusUpdater_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
type MockSpaceStatusUpdater_Close_Call struct {
*mock.Call
}
// Close is a helper method to define mock.On call
// - ctx context.Context
func (_e *MockSpaceStatusUpdater_Expecter) Close(ctx interface{}) *MockSpaceStatusUpdater_Close_Call {
return &MockSpaceStatusUpdater_Close_Call{Call: _e.mock.On("Close", ctx)}
}
func (_c *MockSpaceStatusUpdater_Close_Call) Run(run func(ctx context.Context)) *MockSpaceStatusUpdater_Close_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context))
})
return _c
}
func (_c *MockSpaceStatusUpdater_Close_Call) Return(err error) *MockSpaceStatusUpdater_Close_Call {
_c.Call.Return(err)
return _c
}
func (_c *MockSpaceStatusUpdater_Close_Call) RunAndReturn(run func(context.Context) error) *MockSpaceStatusUpdater_Close_Call {
_c.Call.Return(run)
return _c
}
// Init provides a mock function with given fields: a
func (_m *MockSpaceStatusUpdater) Init(a *app.App) error {
ret := _m.Called(a)
if len(ret) == 0 {
panic("no return value specified for Init")
}
var r0 error
if rf, ok := ret.Get(0).(func(*app.App) error); ok {
r0 = rf(a)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockSpaceStatusUpdater_Init_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Init'
type MockSpaceStatusUpdater_Init_Call struct {
*mock.Call
}
// Init is a helper method to define mock.On call
// - a *app.App
func (_e *MockSpaceStatusUpdater_Expecter) Init(a interface{}) *MockSpaceStatusUpdater_Init_Call {
return &MockSpaceStatusUpdater_Init_Call{Call: _e.mock.On("Init", a)}
}
func (_c *MockSpaceStatusUpdater_Init_Call) Run(run func(a *app.App)) *MockSpaceStatusUpdater_Init_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(*app.App))
})
return _c
}
func (_c *MockSpaceStatusUpdater_Init_Call) Return(err error) *MockSpaceStatusUpdater_Init_Call {
_c.Call.Return(err)
return _c
}
func (_c *MockSpaceStatusUpdater_Init_Call) RunAndReturn(run func(*app.App) error) *MockSpaceStatusUpdater_Init_Call {
_c.Call.Return(run)
return _c
}
// Name provides a mock function with given fields:
func (_m *MockSpaceStatusUpdater) Name() string {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Name")
}
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// MockSpaceStatusUpdater_Name_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Name'
type MockSpaceStatusUpdater_Name_Call struct {
*mock.Call
}
// Name is a helper method to define mock.On call
func (_e *MockSpaceStatusUpdater_Expecter) Name() *MockSpaceStatusUpdater_Name_Call {
return &MockSpaceStatusUpdater_Name_Call{Call: _e.mock.On("Name")}
}
func (_c *MockSpaceStatusUpdater_Name_Call) Run(run func()) *MockSpaceStatusUpdater_Name_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockSpaceStatusUpdater_Name_Call) Return(name string) *MockSpaceStatusUpdater_Name_Call {
_c.Call.Return(name)
return _c
}
func (_c *MockSpaceStatusUpdater_Name_Call) RunAndReturn(run func() string) *MockSpaceStatusUpdater_Name_Call {
_c.Call.Return(run)
return _c
}
// Run provides a mock function with given fields: ctx
func (_m *MockSpaceStatusUpdater) Run(ctx context.Context) error {
ret := _m.Called(ctx)
if len(ret) == 0 {
panic("no return value specified for Run")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context) error); ok {
r0 = rf(ctx)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockSpaceStatusUpdater_Run_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Run'
type MockSpaceStatusUpdater_Run_Call struct {
*mock.Call
}
// Run is a helper method to define mock.On call
// - ctx context.Context
func (_e *MockSpaceStatusUpdater_Expecter) Run(ctx interface{}) *MockSpaceStatusUpdater_Run_Call {
return &MockSpaceStatusUpdater_Run_Call{Call: _e.mock.On("Run", ctx)}
}
func (_c *MockSpaceStatusUpdater_Run_Call) Run(run func(ctx context.Context)) *MockSpaceStatusUpdater_Run_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context))
})
return _c
}
func (_c *MockSpaceStatusUpdater_Run_Call) Return(err error) *MockSpaceStatusUpdater_Run_Call {
_c.Call.Return(err)
return _c
}
func (_c *MockSpaceStatusUpdater_Run_Call) RunAndReturn(run func(context.Context) error) *MockSpaceStatusUpdater_Run_Call {
_c.Call.Return(run)
return _c
}
// SendUpdate provides a mock function with given fields: spaceSync
func (_m *MockSpaceStatusUpdater) SendUpdate(spaceSync *domain.SpaceSync) {
_m.Called(spaceSync)
}
// MockSpaceStatusUpdater_SendUpdate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendUpdate'
type MockSpaceStatusUpdater_SendUpdate_Call struct {
*mock.Call
}
// SendUpdate is a helper method to define mock.On call
// - spaceSync *domain.SpaceSync
func (_e *MockSpaceStatusUpdater_Expecter) SendUpdate(spaceSync interface{}) *MockSpaceStatusUpdater_SendUpdate_Call {
return &MockSpaceStatusUpdater_SendUpdate_Call{Call: _e.mock.On("SendUpdate", spaceSync)}
}
func (_c *MockSpaceStatusUpdater_SendUpdate_Call) Run(run func(spaceSync *domain.SpaceSync)) *MockSpaceStatusUpdater_SendUpdate_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(*domain.SpaceSync))
})
return _c
}
func (_c *MockSpaceStatusUpdater_SendUpdate_Call) Return() *MockSpaceStatusUpdater_SendUpdate_Call {
_c.Call.Return()
return _c
}
func (_c *MockSpaceStatusUpdater_SendUpdate_Call) RunAndReturn(run func(*domain.SpaceSync)) *MockSpaceStatusUpdater_SendUpdate_Call {
_c.Call.Return(run)
return _c
}
// NewMockSpaceStatusUpdater creates a new instance of MockSpaceStatusUpdater. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockSpaceStatusUpdater(t interface {
mock.TestingT
Cleanup(func())
}) *MockSpaceStatusUpdater {
mock := &MockSpaceStatusUpdater{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View file

@ -30,6 +30,11 @@ const (
var log = logger.NewNamed(syncstatus.CName)
type SpaceStatusUpdater interface {
app.ComponentRunnable
SendUpdate(spaceSync *domain.SpaceSync)
}
type UpdateReceiver interface {
UpdateTree(ctx context.Context, treeId string, status SyncStatus) (err error)
UpdateNodeStatus()
@ -97,6 +102,7 @@ type syncStatusService struct {
syncDetailsUpdater Updater
nodeStatus nodestatus.NodeStatus
config *config.Config
spaceSyncStatus SpaceStatusUpdater
}
func NewSyncStatusService() StatusService {
@ -113,6 +119,7 @@ func (s *syncStatusService) Init(a *app.App) (err error) {
s.spaceId = sharedState.SpaceId
s.configuration = app.MustComponent[nodeconf.NodeConf](a)
s.storage = app.MustComponent[spacestorage.SpaceStorage](a)
s.spaceSyncStatus = app.MustComponent[SpaceStatusUpdater](a)
s.periodicSync = periodicsync.NewPeriodicSync(
s.updateIntervalSecs,
s.updateTimeout,
@ -154,6 +161,7 @@ func (s *syncStatusService) HeadsChange(treeId string, heads []string) {
}
s.stateCounter++
s.updateDetails(treeId, domain.Syncing)
s.spaceSyncStatus.SendUpdate(domain.MakeSyncStatus(s.spaceId, domain.Syncing, 1, domain.Null, domain.Objects))
}
func (s *syncStatusService) update(ctx context.Context) (err error) {

View file

@ -20,6 +20,8 @@ import (
"github.com/anyproto/anytype-heart/core/syncstatus/nodestatus"
"github.com/anyproto/anytype-heart/core/syncstatus/objectsyncstatus/mock_objectsyncstatus"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/core/domain"
"github.com/anyproto/anytype-heart/core/syncstatus/objectsyncstatus/mock_objectsyncstatus"
"github.com/anyproto/anytype-heart/tests/testutil"
)
@ -27,7 +29,7 @@ func Test_HeadsChange(t *testing.T) {
t.Run("HeadsChange: new object", func(t *testing.T) {
// given
s := newFixture(t)
s.config.NetworkMode = pb.RpcAccount_LocalOnly
s.spaceStatusUpdater.EXPECT().SendUpdate(domain.MakeSyncStatus(s.spaceId, domain.Syncing, 1, domain.Null, domain.Objects)).Return()
s.detailsUpdater.EXPECT().UpdateDetails("id", domain.Offline, domain.Null, "spaceId")
// when
@ -40,6 +42,7 @@ func Test_HeadsChange(t *testing.T) {
t.Run("HeadsChange: update existing object", func(t *testing.T) {
// given
s := newFixture(t)
s.spaceStatusUpdater.EXPECT().SendUpdate(domain.MakeSyncStatus(s.spaceId, domain.Syncing, 1, domain.Null, domain.Objects)).Return()
s.config.NetworkMode = pb.RpcAccount_DefaultConfig
s.detailsUpdater.EXPECT().UpdateDetails("id", domain.Syncing, domain.Null, "spaceId")
@ -101,6 +104,7 @@ func TestSyncStatusService_HeadsReceive(t *testing.T) {
s.detailsUpdater.EXPECT().UpdateDetails("id", domain.Syncing, domain.Null, "spaceId")
// when
s.spaceStatusUpdater.EXPECT().SendUpdate(domain.MakeSyncStatus(s.spaceId, domain.Syncing, 1, domain.Null, domain.Objects)).Return()
s.HeadsChange("id", []string{"head1"})
s.HeadsReceive("peerId", "id", []string{"head2"})
@ -116,6 +120,7 @@ func TestSyncStatusService_HeadsReceive(t *testing.T) {
s.detailsUpdater.EXPECT().UpdateDetails("id", domain.Synced, domain.Null, "spaceId")
// when
s.spaceStatusUpdater.EXPECT().SendUpdate(domain.MakeSyncStatus(s.spaceId, domain.Syncing, 1, domain.Null, domain.Objects)).Return()
s.HeadsChange("id", []string{"head1"})
s.HeadsReceive("peerId", "id", []string{"head1"})
@ -131,6 +136,7 @@ func TestSyncStatusService_Watch(t *testing.T) {
s := newFixture(t)
// when
s.spaceStatusUpdater.EXPECT().SendUpdate(domain.MakeSyncStatus(s.spaceId, domain.Syncing, 1, domain.Null, domain.Objects)).Return()
s.detailsUpdater.EXPECT().UpdateDetails("id", domain.Syncing, domain.Null, "spaceId")
s.HeadsChange("id", []string{"head1"})
err := s.Watch("id")
@ -179,6 +185,7 @@ func TestSyncStatusService_Unwatch(t *testing.T) {
// when
s.detailsUpdater.EXPECT().UpdateDetails("id", domain.Syncing, domain.Null, "spaceId")
s.spaceStatusUpdater.EXPECT().SendUpdate(domain.MakeSyncStatus(s.spaceId, domain.Syncing, 1, domain.Null, domain.Objects)).Return()
s.HeadsChange("id", []string{"head1"})
err := s.Watch("id")
assert.Nil(t, err)
@ -202,6 +209,7 @@ func TestSyncStatusService_update(t *testing.T) {
// when
s.detailsUpdater.EXPECT().UpdateDetails("id", domain.Syncing, domain.Null, "spaceId")
s.spaceStatusUpdater.EXPECT().SendUpdate(domain.MakeSyncStatus(s.spaceId, domain.Syncing, 1, domain.Null, domain.Objects)).Return()
s.HeadsChange("id", []string{"head1"})
err := s.Watch("id")
assert.Nil(t, err)
@ -219,6 +227,7 @@ func TestSyncStatusService_update(t *testing.T) {
// when
s.detailsUpdater.EXPECT().UpdateDetails("id", domain.Syncing, domain.Null, "spaceId")
s.spaceStatusUpdater.EXPECT().SendUpdate(domain.MakeSyncStatus(s.spaceId, domain.Syncing, 1, domain.Null, domain.Objects)).Return()
s.HeadsChange("id", []string{"head1"})
err := s.Watch("id")
assert.Nil(t, err)
@ -316,6 +325,7 @@ type fixture struct {
config *config.Config
detailsUpdater *mock_objectsyncstatus.MockUpdater
nodeStatus nodestatus.NodeStatus
spaceStatusUpdater *mock_objectsyncstatus.MockSpaceStatusUpdater
}
func newFixture(t *testing.T) *fixture {
@ -326,6 +336,7 @@ func newFixture(t *testing.T) *fixture {
config := &config.Config{}
detailsUpdater := mock_objectsyncstatus.NewMockUpdater(t)
nodeStatus := nodestatus.NewNodeStatus()
spaceStatusUpdater := mock_objectsyncstatus.NewMockSpaceStatusUpdater(t)
a := &app.App{}
a.Register(testutil.PrepareMock(context.Background(), a, service)).
@ -351,5 +362,6 @@ func newFixture(t *testing.T) *fixture {
config: config,
detailsUpdater: detailsUpdater,
nodeStatus: nodeStatus,
spaceStatusUpdater: spaceStatusUpdater,
}
}

View file

@ -74,6 +74,7 @@ func (s *service) Init(a *app.App) (err error) {
s.fileSyncService.OnUploaded(s.OnFileUploaded)
s.fileSyncService.OnUploadStarted(s.OnFileUploadStarted)
s.fileSyncService.OnLimited(s.OnFileLimited)
s.fileSyncService.OnDelete(s.OnFileDelete)
s.spaceSyncStatus = app.MustComponent[spacesyncstatus.Updater](a)
return nil

View file

@ -13,6 +13,7 @@ import (
type FileState struct {
fileSyncCountBySpace map[string]int
fileSyncStatusBySpace map[string]domain.SyncStatus
filesErrorBySpace map[string]domain.SyncError
store objectstore.ObjectStore
}
@ -21,6 +22,7 @@ func NewFileState(store objectstore.ObjectStore) *FileState {
return &FileState{
fileSyncCountBySpace: make(map[string]int, 0),
fileSyncStatusBySpace: make(map[string]domain.SyncStatus, 0),
filesErrorBySpace: make(map[string]domain.SyncError, 0),
store: store,
}
@ -47,18 +49,29 @@ func (f *FileState) SetObjectsNumber(status *domain.SpaceSync) {
f.fileSyncCountBySpace[status.SpaceId] = len(records)
}
func (f *FileState) SetSyncStatus(status *domain.SpaceSync) {
func (f *FileState) SetSyncStatusAndErr(status *domain.SpaceSync) {
switch status.Status {
case domain.Synced:
f.fileSyncStatusBySpace[status.SpaceId] = domain.Synced
if number := f.fileSyncCountBySpace[status.SpaceId]; number > 0 {
f.fileSyncStatusBySpace[status.SpaceId] = domain.Syncing
f.setError(status.SpaceId, status.SyncError)
return
}
if fileLimitedCount := f.getFileLimitedCount(status.SpaceId); fileLimitedCount > 0 {
f.fileSyncStatusBySpace[status.SpaceId] = domain.Error
f.setError(status.SpaceId, domain.StorageLimitExceed)
}
case domain.Error, domain.Syncing, domain.Offline:
f.fileSyncStatusBySpace[status.SpaceId] = status.Status
f.setError(status.SpaceId, status.SyncError)
}
}
func (f *FileState) setError(spaceId string, syncErr domain.SyncError) {
f.filesErrorBySpace[spaceId] = syncErr
}
func (f *FileState) GetSyncStatus(spaceId string) domain.SyncStatus {
return f.fileSyncStatusBySpace[spaceId]
}
@ -67,11 +80,27 @@ func (f *FileState) GetSyncObjectCount(spaceId string) int {
return f.fileSyncCountBySpace[spaceId]
}
func (f *FileState) IsSyncFinished(spaceId string) bool {
if _, ok := f.fileSyncStatusBySpace[spaceId]; !ok {
return false
}
status := f.fileSyncStatusBySpace[spaceId]
count := f.fileSyncCountBySpace[spaceId]
return count == 0 && status == domain.Synced
func (f *FileState) GetSyncErr(spaceId string) domain.SyncError {
return f.filesErrorBySpace[spaceId]
}
func (f *FileState) getFileLimitedCount(spaceId string) int {
records, err := f.store.Query(database.Query{
Filters: []*model.BlockContentDataviewFilter{
{
RelationKey: bundle.RelationKeyFileBackupStatus.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.Int64(int64(filesyncstatus.Limited)),
},
{
RelationKey: bundle.RelationKeySpaceId.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.String(spaceId),
},
},
})
if err != nil {
log.Errorf("failed to query file status: %s", err)
}
return len(records)
}

View file

@ -104,56 +104,19 @@ func TestFileState_SetObjectsNumber(t *testing.T) {
})
}
func TestFileState_IsSyncFinished(t *testing.T) {
t.Run("IsSyncFinished, sync is not finished", func(t *testing.T) {
// given
fileState := NewFileState(nil)
// when
finished := fileState.IsSyncFinished("spaceId")
// then
assert.False(t, finished)
})
t.Run("IsSyncFinished, sync is finished", func(t *testing.T) {
// given
fileState := NewFileState(nil)
// when
syncStatus := domain.MakeSyncStatus("spaceId", domain.Synced, 0, domain.Null, domain.Files)
fileState.SetSyncStatus(syncStatus)
finished := fileState.IsSyncFinished("spaceId")
// then
assert.True(t, finished)
})
t.Run("IsSyncFinished, sync is not finished", func(t *testing.T) {
// given
fileState := NewFileState(nil)
// when
syncStatus := domain.MakeSyncStatus("spaceId", domain.Offline, 3, domain.Null, domain.Files)
fileState.SetSyncStatus(syncStatus)
finished := fileState.IsSyncFinished("spaceId")
// then
assert.False(t, finished)
})
}
func TestFileState_SetSyncStatus(t *testing.T) {
t.Run("SetSyncStatus, status synced", func(t *testing.T) {
t.Run("SetSyncStatusAndErr, status synced", func(t *testing.T) {
// given
fileState := NewFileState(nil)
fileState := NewFileState(objectstore.NewStoreFixture(t))
// when
syncStatus := domain.MakeSyncStatus("spaceId", domain.Synced, 0, domain.Null, domain.Files)
fileState.SetSyncStatus(syncStatus)
fileState.SetSyncStatusAndErr(syncStatus)
// then
assert.Equal(t, domain.Synced, fileState.GetSyncStatus("spaceId"))
})
t.Run("SetSyncStatus, received status synced, but there are syncing files in store", func(t *testing.T) {
t.Run("SetSyncStatusAndErr, received status synced, but there are syncing files in store", func(t *testing.T) {
// given
storeFixture := objectstore.NewStoreFixture(t)
storeFixture.AddObjects(t, []objectstore.TestObject{
@ -178,40 +141,40 @@ func TestFileState_SetSyncStatus(t *testing.T) {
// when
syncStatus := domain.MakeSyncStatus("spaceId", domain.Synced, 0, domain.Null, domain.Files)
fileState.SetObjectsNumber(syncStatus)
fileState.SetSyncStatus(syncStatus)
fileState.SetSyncStatusAndErr(syncStatus)
// then
assert.Equal(t, domain.Syncing, fileState.GetSyncStatus("spaceId"))
})
t.Run("SetSyncStatus, sync in progress", func(t *testing.T) {
t.Run("SetSyncStatusAndErr, sync in progress", func(t *testing.T) {
// given
fileState := NewFileState(nil)
fileState := NewFileState(objectstore.NewStoreFixture(t))
// when
syncStatus := domain.MakeSyncStatus("spaceId", domain.Syncing, 0, domain.Null, domain.Files)
fileState.SetSyncStatus(syncStatus)
fileState.SetSyncStatusAndErr(syncStatus)
// then
assert.Equal(t, domain.Syncing, fileState.GetSyncStatus("spaceId"))
})
t.Run("SetSyncStatus, sync is finished with error", func(t *testing.T) {
t.Run("SetSyncStatusAndErr, sync is finished with error", func(t *testing.T) {
// given
fileState := NewFileState(nil)
fileState := NewFileState(objectstore.NewStoreFixture(t))
// when
syncStatus := domain.MakeSyncStatus("spaceId", domain.Error, 3, domain.Null, domain.Files)
fileState.SetSyncStatus(syncStatus)
fileState.SetSyncStatusAndErr(syncStatus)
// then
assert.Equal(t, domain.Error, fileState.GetSyncStatus("spaceId"))
})
t.Run("SetSyncStatus, offline", func(t *testing.T) {
t.Run("SetSyncStatusAndErr, offline", func(t *testing.T) {
// given
fileState := NewFileState(nil)
fileState := NewFileState(objectstore.NewStoreFixture(t))
// when
syncStatus := domain.MakeSyncStatus("spaceId", domain.Offline, 3, domain.Null, domain.Files)
fileState.SetSyncStatus(syncStatus)
fileState.SetSyncStatusAndErr(syncStatus)
// then
assert.Equal(t, domain.Offline, fileState.GetSyncStatus("spaceId"))

View file

@ -7,12 +7,14 @@ import (
type ObjectState struct {
objectSyncStatusBySpace map[string]domain.SyncStatus
objectSyncCountBySpace map[string]int
objectSyncErrBySpace map[string]domain.SyncError
}
func NewObjectState() *ObjectState {
return &ObjectState{
objectSyncCountBySpace: make(map[string]int, 0),
objectSyncStatusBySpace: make(map[string]domain.SyncStatus, 0),
objectSyncErrBySpace: make(map[string]domain.SyncError, 0),
}
}
@ -25,8 +27,9 @@ func (o *ObjectState) SetObjectsNumber(status *domain.SpaceSync) {
}
}
func (o *ObjectState) SetSyncStatus(status *domain.SpaceSync) {
func (o *ObjectState) SetSyncStatusAndErr(status *domain.SpaceSync) {
o.objectSyncStatusBySpace[status.SpaceId] = status.Status
o.objectSyncErrBySpace[status.SpaceId] = status.SyncError
}
func (o *ObjectState) GetSyncStatus(spaceId string) domain.SyncStatus {
@ -37,11 +40,6 @@ func (o *ObjectState) GetSyncObjectCount(spaceId string) int {
return o.objectSyncCountBySpace[spaceId]
}
func (o *ObjectState) IsSyncFinished(spaceId string) bool {
if _, ok := o.objectSyncStatusBySpace[spaceId]; !ok {
return false
}
status := o.objectSyncStatusBySpace[spaceId]
count := o.objectSyncCountBySpace[spaceId]
return count == 0 && status == domain.Synced
func (o *ObjectState) GetSyncErr(spaceId string) domain.SyncError {
return o.objectSyncErrBySpace[spaceId]
}

View file

@ -81,84 +81,47 @@ func TestObjectState_SetObjectsNumber(t *testing.T) {
})
}
func TestObjectState_IsSyncFinished(t *testing.T) {
t.Run("IsSyncFinished, sync is not finished", func(t *testing.T) {
// given
objectState := NewObjectState()
// when
finished := objectState.IsSyncFinished("spaceId")
// then
assert.False(t, finished)
})
t.Run("IsSyncFinished, sync is finished", func(t *testing.T) {
// given
objectState := NewObjectState()
// when
syncStatus := domain.MakeSyncStatus("spaceId", domain.Synced, 0, domain.Null, domain.Objects)
objectState.SetSyncStatus(syncStatus)
finished := objectState.IsSyncFinished("spaceId")
// then
assert.True(t, finished)
})
t.Run("IsSyncFinished, sync is not finished", func(t *testing.T) {
// given
objectState := NewObjectState()
// when
syncStatus := domain.MakeSyncStatus("spaceId", domain.Offline, 3, domain.Null, domain.Objects)
objectState.SetSyncStatus(syncStatus)
finished := objectState.IsSyncFinished("spaceId")
// then
assert.False(t, finished)
})
}
func TestObjectState_SetSyncStatus(t *testing.T) {
t.Run("SetSyncStatus, status synced", func(t *testing.T) {
t.Run("SetSyncStatusAndErr, status synced", func(t *testing.T) {
// given
objectState := NewObjectState()
// when
syncStatus := domain.MakeSyncStatus("spaceId", domain.Synced, 0, domain.Null, domain.Objects)
objectState.SetSyncStatus(syncStatus)
objectState.SetSyncStatusAndErr(syncStatus)
// then
assert.Equal(t, domain.Synced, objectState.GetSyncStatus("spaceId"))
})
t.Run("SetSyncStatus, sync in progress", func(t *testing.T) {
t.Run("SetSyncStatusAndErr, sync in progress", func(t *testing.T) {
// given
objectState := NewObjectState()
// when
syncStatus := domain.MakeSyncStatus("spaceId", domain.Syncing, 1, domain.Null, domain.Objects)
objectState.SetSyncStatus(syncStatus)
objectState.SetSyncStatusAndErr(syncStatus)
// then
assert.Equal(t, domain.Syncing, objectState.GetSyncStatus("spaceId"))
})
t.Run("SetSyncStatus, sync is finished with error", func(t *testing.T) {
t.Run("SetSyncStatusAndErr, sync is finished with error", func(t *testing.T) {
// given
objectState := NewObjectState()
// when
syncStatus := domain.MakeSyncStatus("spaceId", domain.Error, 3, domain.Null, domain.Objects)
objectState.SetSyncStatus(syncStatus)
objectState.SetSyncStatusAndErr(syncStatus)
// then
assert.Equal(t, domain.Error, objectState.GetSyncStatus("spaceId"))
})
t.Run("SetSyncStatus, offline", func(t *testing.T) {
t.Run("SetSyncStatusAndErr, offline", func(t *testing.T) {
// given
objectState := NewObjectState()
// when
syncStatus := domain.MakeSyncStatus("spaceId", domain.Offline, 3, domain.Null, domain.Objects)
objectState.SetSyncStatus(syncStatus)
objectState.SetSyncStatusAndErr(syncStatus)
// then
assert.Equal(t, domain.Offline, objectState.GetSyncStatus("spaceId"))

View file

@ -28,10 +28,10 @@ type TechSpaceIdGetter interface {
type State interface {
SetObjectsNumber(status *domain.SpaceSync)
SetSyncStatus(status *domain.SpaceSync)
SetSyncStatusAndErr(status *domain.SpaceSync)
GetSyncStatus(spaceId string) domain.SyncStatus
GetSyncObjectCount(spaceId string) int
IsSyncFinished(spaceId string) bool
GetSyncErr(spaceId string) domain.SyncError
}
type NetworkConfig interface {
@ -118,14 +118,9 @@ func (s *spaceSyncStatus) processEvents() {
}
func (s *spaceSyncStatus) updateSpaceSyncStatus(status *domain.SpaceSync) {
// don't send unnecessary event
if s.isSyncFinished(status) {
return
}
state := s.getCurrentState(status)
state.SetObjectsNumber(status)
state.SetSyncStatus(status)
state.SetSyncStatusAndErr(status)
// send synced event only if files and objects are all synced
if !s.needToSendEvent(status) {
@ -156,16 +151,12 @@ func (s *spaceSyncStatus) Close(ctx context.Context) (err error) {
return s.batcher.Close()
}
func (s *spaceSyncStatus) isSyncFinished(status *domain.SpaceSync) bool {
return status.Status == domain.Synced && s.filesState.IsSyncFinished(status.SpaceId) && s.objectsState.IsSyncFinished(status.SpaceId)
}
func (s *spaceSyncStatus) makeSpaceSyncEvent(status *domain.SpaceSync) *pb.EventSpaceSyncStatusUpdate {
return &pb.EventSpaceSyncStatusUpdate{
Id: status.SpaceId,
Status: mapStatus(s.getSpaceSyncStatus(status)),
Network: mapNetworkMode(s.networkConfig.GetNetworkMode()),
Error: mapError(status.SyncError),
Error: s.getError(status.SpaceId),
SyncingObjectsCounter: int64(s.filesState.GetSyncObjectCount(status.SpaceId) + s.objectsState.GetSyncObjectCount(status.SpaceId)),
}
}
@ -215,6 +206,20 @@ func (s *spaceSyncStatus) getCurrentState(status *domain.SpaceSync) State {
return s.objectsState
}
func (s *spaceSyncStatus) getError(spaceId string) pb.EventSpaceSyncError {
syncErr := s.filesState.GetSyncErr(spaceId)
if syncErr != domain.Null {
return mapError(syncErr)
}
syncErr = s.objectsState.GetSyncErr(spaceId)
if syncErr != domain.Null {
return mapError(syncErr)
}
return pb.EventSpace_Null
}
func mapNetworkMode(mode pb.RpcAccountNetworkMode) pb.EventSpaceNetwork {
switch mode {
case pb.RpcAccount_LocalOnly:

View file

@ -81,28 +81,6 @@ func TestSpaceSyncStatus_Init(t *testing.T) {
}
func TestSpaceSyncStatus_updateSpaceSyncStatus(t *testing.T) {
t.Run("don't send not needed synced event", func(t *testing.T) {
// given
eventSender := mock_event.NewMockSender(t)
status := spaceSyncStatus{
eventSender: eventSender,
networkConfig: &config.Config{NetworkMode: pb.RpcAccount_DefaultConfig},
batcher: mb.New[*domain.SpaceSync](0),
filesState: NewFileState(objectstore.NewStoreFixture(t)),
objectsState: NewObjectState(),
}
syncStatus := domain.MakeSyncStatus("spaceId", domain.Synced, 0, domain.Null, domain.Files)
status.filesState.SetSyncStatus(syncStatus)
status.filesState.SetObjectsNumber(syncStatus)
status.objectsState.SetSyncStatus(syncStatus)
status.objectsState.SetObjectsNumber(syncStatus)
// then
status.updateSpaceSyncStatus(syncStatus)
// when
eventSender.AssertNotCalled(t, "Broadcast")
})
t.Run("syncing event for objects", func(t *testing.T) {
// given
eventSender := mock_event.NewMockSender(t)
@ -199,7 +177,7 @@ func TestSpaceSyncStatus_updateSpaceSyncStatus(t *testing.T) {
objectsState: NewObjectState(),
}
objectsSyncStatus := domain.MakeSyncStatus("spaceId", domain.Syncing, 2, domain.Null, domain.Objects)
status.objectsState.SetSyncStatus(objectsSyncStatus)
status.objectsState.SetSyncStatusAndErr(objectsSyncStatus)
// then
syncStatus := domain.MakeSyncStatus("spaceId", domain.Synced, 0, domain.Null, domain.Files)
@ -299,7 +277,7 @@ func TestSpaceSyncStatus_updateSpaceSyncStatus(t *testing.T) {
}
syncStatus := domain.MakeSyncStatus("spaceId", domain.Syncing, 2, domain.Null, domain.Objects)
status.objectsState.SetObjectsNumber(syncStatus)
status.objectsState.SetSyncStatus(syncStatus)
status.objectsState.SetSyncStatusAndErr(syncStatus)
// then
syncStatus = domain.MakeSyncStatus("spaceId", domain.Synced, 0, domain.Null, domain.Objects)

View file

@ -6456,7 +6456,7 @@ Makes blocks copy by given ids and paste it to shown place
<a name="anytype-Rpc-BlockDataview-View-SetActive"></a>
### Rpc.BlockDataview.View.SetActive
set the current active view (persisted only within a session)
set the current active view locally
@ -6474,8 +6474,6 @@ set the current active view (persisted only within a session)
| contextId | [string](#string) | | |
| blockId | [string](#string) | | id of dataview block |
| viewId | [string](#string) | | id of active view |
| offset | [uint32](#uint32) | | |
| limit | [uint32](#uint32) | | |
@ -19491,7 +19489,7 @@ Middleware-to-front-end response, that can contain a NULL error or a non-NULL er
| ---- | ------ | ----------- |
| NULL | 0 | |
| UNKNOWN_ERROR | 1 | |
| BAD_INPUT | 2 | ... |
| BAD_INPUT | 2 | |
@ -25910,8 +25908,8 @@ Bookmark is to keep a web-link and to preview a content.
| ----- | ---- | ----- | ----------- |
| source | [string](#string) | repeated | |
| views | [Block.Content.Dataview.View](#anytype-model-Block-Content-Dataview-View) | repeated | |
| activeView | [string](#string) | | do not generate changes for this field |
| relations | [Relation](#anytype-model-Relation) | repeated | deprecated |
| activeView | [string](#string) | | saved within a session |
| groupOrders | [Block.Content.Dataview.GroupOrder](#anytype-model-Block-Content-Dataview-GroupOrder) | repeated | |
| objectOrders | [Block.Content.Dataview.ObjectOrder](#anytype-model-Block-Content-Dataview-ObjectOrder) | repeated | |
| relationLinks | [RelationLink](#anytype-model-RelationLink) | repeated | |

16
go.mod
View file

@ -91,11 +91,11 @@ require (
go.uber.org/multierr v1.11.0
go.uber.org/zap v1.27.0
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
golang.org/x/image v0.15.0
golang.org/x/image v0.16.0
golang.org/x/mobile v0.0.0-20240404231514-09dbf07665ed
golang.org/x/net v0.25.0
golang.org/x/oauth2 v0.19.0
golang.org/x/text v0.15.0
golang.org/x/net v0.26.0
golang.org/x/oauth2 v0.20.0
golang.org/x/text v0.16.0
google.golang.org/grpc v1.64.0
gopkg.in/Graylog2/go-gelf.v2 v2.0.0-20180125164251-1832d8546a9f
gopkg.in/yaml.v3 v3.0.1
@ -260,13 +260,13 @@ require (
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/otel v1.14.0 // indirect
go.opentelemetry.io/otel/trace v1.14.0 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/crypto v0.24.0 // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/term v0.20.0 // indirect
golang.org/x/sys v0.21.0 // indirect
golang.org/x/term v0.21.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.21.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect
google.golang.org/protobuf v1.33.0 // indirect

32
go.sum
View file

@ -1527,8 +1527,8 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@ -1549,8 +1549,8 @@ golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86h
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
golang.org/x/image v0.16.0 h1:9kloLAKhUufZhA12l5fwnx2NZW39/we1UhBesW433jw=
golang.org/x/image v0.16.0/go.mod h1:ugSZItdV4nOxyqp56HmXwH0Ry0nBCpjnZdpDaIHdoPs=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@ -1642,8 +1642,8 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -1653,8 +1653,8 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg=
golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8=
golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo=
golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -1763,8 +1763,8 @@ golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@ -1772,8 +1772,8 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -1785,8 +1785,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@ -1855,8 +1855,8 @@ golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw=
golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

File diff suppressed because it is too large Load diff

View file

@ -5483,14 +5483,12 @@ message Rpc {
}
}
}
// set the current active view (persisted only within a session)
// set the current active view locally
message SetActive {
message Request {
string contextId = 1;
string blockId = 2; // id of dataview block
string viewId = 3; // id of active view
uint32 offset = 4;
uint32 limit = 5;
}
message Response {
Error error = 1;
@ -5504,7 +5502,6 @@ message Rpc {
NULL = 0;
UNKNOWN_ERROR = 1;
BAD_INPUT = 2;
// ...
}
}
}

View file

@ -0,0 +1,65 @@
package objectstore
import (
"errors"
"strings"
"github.com/anyproto/anytype-heart/util/badgerhelper"
)
const (
blockViewSeparator = ":"
viewsSeparator = ","
)
var ErrParseView = errors.New("failed to parse view")
// SetActiveViews accepts map of active views by blocks, as objects can handle multiple dataview blocks
func (s *dsObjectStore) SetActiveViews(objectId string, views map[string]string) error {
return badgerhelper.SetValue(s.db, pagesActiveViewBase.ChildString(objectId).Bytes(), viewsMapToString(views))
}
func (s *dsObjectStore) SetActiveView(objectId, blockId, viewId string) error {
views, err := s.GetActiveViews(objectId)
// if active views are not found in BD, or we could not parse them, then we need to rewrite them
if err != nil && !badgerhelper.IsNotFound(err) && !errors.Is(err, ErrParseView) {
return err
}
if views == nil {
views = make(map[string]string, 1)
}
views[blockId] = viewId
return s.SetActiveViews(objectId, views)
}
// GetActiveViews returns a map of activeViews by block ids
func (s *dsObjectStore) GetActiveViews(objectId string) (views map[string]string, err error) {
raw, err := badgerhelper.GetValue(s.db, pagesActiveViewBase.ChildString(objectId).Bytes(), bytesToString)
if err != nil {
return nil, err
}
return parseViewsMap(raw)
}
func viewsMapToString(views map[string]string) (result string) {
for block, view := range views {
result = result + viewsSeparator + block + blockViewSeparator + view
}
if len(views) != 0 {
result = result[1:]
}
return result
}
func parseViewsMap(s string) (viewsMap map[string]string, err error) {
viewsMap = make(map[string]string)
views := strings.Split(s, viewsSeparator)
for _, view := range views {
parts := strings.Split(view, blockViewSeparator)
if len(parts) != 2 {
return nil, ErrParseView
}
viewsMap[parts[0]] = parts[1]
}
return viewsMap, nil
}

View file

@ -0,0 +1,39 @@
package objectstore
import (
"errors"
"reflect"
"testing"
"github.com/stretchr/testify/assert"
)
func TestViewsMapToString(t *testing.T) {
assert.Contains(t, []string{"block1:view1,block2:view2", "block2:view2,block1:view1"}, viewsMapToString(map[string]string{"block1": "view1", "block2": "view2"}))
assert.Equal(t, "", viewsMapToString(nil))
assert.Equal(t, "", viewsMapToString(map[string]string{}))
assert.Contains(t, []string{":view,block:", "block:,:view"}, viewsMapToString(map[string]string{"": "view", "block": ""}))
}
func TestParseViewsMap(t *testing.T) {
for _, tc := range []struct {
name, str string
expectedErr error
expectedMap map[string]string
}{
{"success", "block1:view1,block2:view2", nil, map[string]string{"block1": "view1", "block2": "view2"}},
{"empty", "", nil, nil},
{"invalid", "invalid", ErrParseView, nil},
{"empty ids", ":view,block:", nil, map[string]string{"": "view", "block": ""}},
} {
t.Run(tc.name, func(t *testing.T) {
views, err := parseViewsMap(tc.str)
if tc.expectedErr != nil {
assert.Error(t, err)
assert.True(t, errors.Is(err, tc.expectedErr))
} else {
assert.True(t, reflect.DeepEqual(tc.expectedMap, views))
}
})
}
}

View file

@ -684,6 +684,64 @@ func (_c *MockObjectStore_GetAccountStatus_Call) RunAndReturn(run func() (*coord
return _c
}
// GetActiveViews provides a mock function with given fields: objectId
func (_m *MockObjectStore) GetActiveViews(objectId string) (map[string]string, error) {
ret := _m.Called(objectId)
if len(ret) == 0 {
panic("no return value specified for GetActiveViews")
}
var r0 map[string]string
var r1 error
if rf, ok := ret.Get(0).(func(string) (map[string]string, error)); ok {
return rf(objectId)
}
if rf, ok := ret.Get(0).(func(string) map[string]string); ok {
r0 = rf(objectId)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(map[string]string)
}
}
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(objectId)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockObjectStore_GetActiveViews_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetActiveViews'
type MockObjectStore_GetActiveViews_Call struct {
*mock.Call
}
// GetActiveViews is a helper method to define mock.On call
// - objectId string
func (_e *MockObjectStore_Expecter) GetActiveViews(objectId interface{}) *MockObjectStore_GetActiveViews_Call {
return &MockObjectStore_GetActiveViews_Call{Call: _e.mock.On("GetActiveViews", objectId)}
}
func (_c *MockObjectStore_GetActiveViews_Call) Run(run func(objectId string)) *MockObjectStore_GetActiveViews_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string))
})
return _c
}
func (_c *MockObjectStore_GetActiveViews_Call) Return(_a0 map[string]string, _a1 error) *MockObjectStore_GetActiveViews_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockObjectStore_GetActiveViews_Call) RunAndReturn(run func(string) (map[string]string, error)) *MockObjectStore_GetActiveViews_Call {
_c.Call.Return(run)
return _c
}
// GetByIDs provides a mock function with given fields: spaceID, ids
func (_m *MockObjectStore) GetByIDs(spaceID string, ids []string) ([]*model.ObjectInfo, error) {
ret := _m.Called(spaceID, ids)
@ -2672,6 +2730,101 @@ func (_c *MockObjectStore_SaveVirtualSpace_Call) RunAndReturn(run func(string) e
return _c
}
// SetActiveView provides a mock function with given fields: objectId, blockId, viewId
func (_m *MockObjectStore) SetActiveView(objectId string, blockId string, viewId string) error {
ret := _m.Called(objectId, blockId, viewId)
if len(ret) == 0 {
panic("no return value specified for SetActiveView")
}
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string) error); ok {
r0 = rf(objectId, blockId, viewId)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockObjectStore_SetActiveView_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetActiveView'
type MockObjectStore_SetActiveView_Call struct {
*mock.Call
}
// SetActiveView is a helper method to define mock.On call
// - objectId string
// - blockId string
// - viewId string
func (_e *MockObjectStore_Expecter) SetActiveView(objectId interface{}, blockId interface{}, viewId interface{}) *MockObjectStore_SetActiveView_Call {
return &MockObjectStore_SetActiveView_Call{Call: _e.mock.On("SetActiveView", objectId, blockId, viewId)}
}
func (_c *MockObjectStore_SetActiveView_Call) Run(run func(objectId string, blockId string, viewId string)) *MockObjectStore_SetActiveView_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string), args[1].(string), args[2].(string))
})
return _c
}
func (_c *MockObjectStore_SetActiveView_Call) Return(_a0 error) *MockObjectStore_SetActiveView_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockObjectStore_SetActiveView_Call) RunAndReturn(run func(string, string, string) error) *MockObjectStore_SetActiveView_Call {
_c.Call.Return(run)
return _c
}
// SetActiveViews provides a mock function with given fields: objectId, views
func (_m *MockObjectStore) SetActiveViews(objectId string, views map[string]string) error {
ret := _m.Called(objectId, views)
if len(ret) == 0 {
panic("no return value specified for SetActiveViews")
}
var r0 error
if rf, ok := ret.Get(0).(func(string, map[string]string) error); ok {
r0 = rf(objectId, views)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockObjectStore_SetActiveViews_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetActiveViews'
type MockObjectStore_SetActiveViews_Call struct {
*mock.Call
}
// SetActiveViews is a helper method to define mock.On call
// - objectId string
// - views map[string]string
func (_e *MockObjectStore_Expecter) SetActiveViews(objectId interface{}, views interface{}) *MockObjectStore_SetActiveViews_Call {
return &MockObjectStore_SetActiveViews_Call{Call: _e.mock.On("SetActiveViews", objectId, views)}
}
func (_c *MockObjectStore_SetActiveViews_Call) Run(run func(objectId string, views map[string]string)) *MockObjectStore_SetActiveViews_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string), args[1].(map[string]string))
})
return _c
}
func (_c *MockObjectStore_SetActiveViews_Call) Return(_a0 error) *MockObjectStore_SetActiveViews_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockObjectStore_SetActiveViews_Call) RunAndReturn(run func(string, map[string]string) error) *MockObjectStore_SetActiveViews_Call {
_c.Call.Return(run)
return _c
}
// SubscribeForAll provides a mock function with given fields: callback
func (_m *MockObjectStore) SubscribeForAll(callback func(database.Record)) {
_m.Called(callback)

View file

@ -35,9 +35,10 @@ const CName = "objectstore"
var (
// ObjectInfo is stored in db key pattern:
pagesPrefix = "pages"
pagesDetailsBase = ds.NewKey("/" + pagesPrefix + "/details")
pendingDetailsBase = ds.NewKey("/" + pagesPrefix + "/pending")
pagesPrefix = "pages"
pagesDetailsBase = ds.NewKey("/" + pagesPrefix + "/details")
pendingDetailsBase = ds.NewKey("/" + pagesPrefix + "/pending")
pagesActiveViewBase = ds.NewKey("/" + pagesPrefix + "/activeView")
pagesSnippetBase = ds.NewKey("/" + pagesPrefix + "/snippet")
pagesInboundLinksBase = ds.NewKey("/" + pagesPrefix + "/inbound")
@ -145,6 +146,10 @@ type ObjectStore interface {
GetOutboundLinksByID(id string) ([]string, error)
GetWithLinksInfoByID(spaceID string, id string) (*model.ObjectInfoWithLinks, error)
SetActiveView(objectId, blockId, viewId string) error
SetActiveViews(objectId string, views map[string]string) error
GetActiveViews(objectId string) (map[string]string, error)
GetRelationLink(spaceID string, key string) (*model.RelationLink, error)
FetchRelationByKey(spaceID string, key string) (relation *relationutils.Relation, err error)
FetchRelationByKeys(spaceId string, keys ...string) (relations relationutils.Relations, err error)

View file

@ -22,6 +22,6 @@ func (m *Blob) Options(add map[string]interface{}) (string, error) {
return hashOpts(make(map[string]string), add)
}
func (m *Blob) Mill(r io.ReadSeeker, name string, sourceChecksum string) (*Result, error) {
return &Result{File: r}, nil
func (m *Blob) Mill(r io.ReadSeeker, name string) (*Result, error) {
return &Result{File: noopCloser(r)}, nil
}

View file

@ -12,7 +12,9 @@ func TestBlob_Mill(t *testing.T) {
input := make([]byte, 512)
rand.Read(input)
if _, err := m.Mill(bytes.NewReader(input), "test", ""); err != nil {
if r, err := m.Mill(bytes.NewReader(input), "test"); err != nil {
t.Fatal(err)
} else {
_ = r.File.Close()
}
}

View file

@ -14,18 +14,17 @@ import (
)
type ImageExifSchema struct {
SourceChecksum string `json:"source_checksum"`
Created time.Time `json:"created,omitempty"`
Name string `json:"name"`
Description string `json:"description"`
Ext string `json:"extension"`
Width int `json:"width"`
Height int `json:"height"`
Format string `json:"format"`
CameraModel string `json:"model,omitempty"`
ISO int `json:"iso"`
ExposureTime string `json:"exposure_time"`
FNumber float64 `json:"f_number"`
Created time.Time `json:"created,omitempty"`
Name string `json:"name"`
Description string `json:"description"`
Ext string `json:"extension"`
Width int `json:"width"`
Height int `json:"height"`
Format string `json:"format"`
CameraModel string `json:"model,omitempty"`
ISO int `json:"iso"`
ExposureTime string `json:"exposure_time"`
FNumber float64 `json:"f_number"`
Latitude float64 `json:"latitude,omitempty"`
Longitude float64 `json:"longitude,omitempty"`
@ -57,7 +56,7 @@ func (m *ImageExif) Options(add map[string]interface{}) (string, error) {
return hashOpts(make(map[string]string), add)
}
func (m *ImageExif) Mill(r io.ReadSeeker, name string, sourceChecksum string) (*Result, error) {
func (m *ImageExif) Mill(r io.ReadSeeker, name string) (*Result, error) {
conf, formatStr, err := image.DecodeConfig(r)
if err != nil {
return nil, err
@ -124,21 +123,20 @@ func (m *ImageExif) Mill(r io.ReadSeeker, name string, sourceChecksum string) (*
}
res := &ImageExifSchema{
SourceChecksum: sourceChecksum,
Created: created,
Name: name,
Ext: strings.ToLower(filepath.Ext(name)),
Format: string(format),
CameraModel: model,
ISO: iso,
ExposureTime: exposureTime,
FNumber: fNumber,
Width: conf.Width,
Height: conf.Height,
Latitude: lat,
Longitude: lon,
Artist: artist,
Description: description,
Created: created,
Name: name,
Ext: strings.ToLower(filepath.Ext(name)),
Format: string(format),
CameraModel: model,
ISO: iso,
ExposureTime: exposureTime,
FNumber: fNumber,
Width: conf.Width,
Height: conf.Height,
Latitude: lat,
Longitude: lon,
Artist: artist,
Description: description,
}
b, err := jsonutil.MarshalSafely(res)
@ -146,5 +144,5 @@ func (m *ImageExif) Mill(r io.ReadSeeker, name string, sourceChecksum string) (*
return nil, err
}
return &Result{File: bytes.NewReader(b)}, nil
return &Result{File: noopCloser(bytes.NewReader(b))}, nil
}

View file

@ -2,12 +2,9 @@ package mill
import (
"encoding/json"
"io"
"os"
"testing"
"github.com/stretchr/testify/require"
"github.com/anyproto/anytype-heart/pkg/lib/mill/testdata"
)
@ -20,7 +17,7 @@ func TestImageExif_Mill(t *testing.T) {
t.Fatal(err)
}
res, err := m.Mill(file, "test", "")
res, err := m.Mill(file, "test")
if err != nil {
t.Fatal(err)
}
@ -42,26 +39,3 @@ func TestImageExif_Mill(t *testing.T) {
}
}
}
func TestImageExif_Mill_Checksum(t *testing.T) {
m := &ImageExif{}
file, err := os.Open("testdata/Landscape_8.jpg")
require.NoError(t, err)
defer file.Close()
res, err := m.Mill(file, "test", "FOO")
require.NoError(t, err)
raw1, err := io.ReadAll(res.File)
require.NoError(t, err)
res, err = m.Mill(file, "test", "BAR")
require.NoError(t, err)
raw2, err := io.ReadAll(res.File)
require.NoError(t, err)
// Different checksums produce different results
require.NotEqual(t, raw1, raw2)
}

View file

@ -1,7 +1,6 @@
package mill
import (
"bytes"
"errors"
"fmt"
"image"
@ -99,13 +98,12 @@ func (m *ImageResize) Options(add map[string]interface{}) (string, error) {
return hashOpts(m.Opts, add)
}
func (m *ImageResize) Mill(r io.ReadSeeker, name string, sourceChecksum string) (*Result, error) {
func (m *ImageResize) Mill(r io.ReadSeeker, name string) (*Result, error) {
imgConfig, formatStr, err := image.DecodeConfig(r)
if err != nil {
return nil, err
}
format := Format(formatStr)
_, err = r.Seek(0, io.SeekStart)
if err != nil {
return nil, err
@ -179,7 +177,7 @@ func (m *ImageResize) resizeJPEG(imgConfig *image.Config, r io.ReadSeeker) (*Res
}
if orientation <= 1 && width == imgConfig.Width {
var r2 io.Reader
var r2 io.ReadSeekCloser
r2, err = patchReaderRemoveExif(r)
if err != nil {
return nil, err
@ -187,7 +185,7 @@ func (m *ImageResize) resizeJPEG(imgConfig *image.Config, r io.ReadSeeker) (*Res
// here is an optimization
// lets return the original picture in case it has not been resized or normalized
return &Result{
File: r2,
File: noopCloser(r2),
Meta: map[string]interface{}{
"width": imgConfig.Width,
"height": imgConfig.Height,
@ -204,13 +202,21 @@ func (m *ImageResize) resizeJPEG(imgConfig *image.Config, r io.ReadSeeker) (*Res
resized := imaging.Resize(img, width, 0, imaging.Lanczos)
width, height = resized.Rect.Max.X, resized.Rect.Max.Y
buff := &bytes.Buffer{}
buff := pool.Get()
defer func() {
_ = buff.Close()
}()
if err = jpeg.Encode(buff, resized, &jpeg.Options{Quality: quality}); err != nil {
return nil, err
}
readCloser, err := buff.GetReadSeekCloser()
if err != nil {
return nil, err
}
return &Result{
File: buff,
File: readCloser,
Meta: map[string]interface{}{
"width": width,
"height": height,
@ -234,7 +240,7 @@ func (m *ImageResize) resizePNG(imgConfig *image.Config, r io.ReadSeeker) (*Resu
// here is an optimization
// lets return the original picture in case it has not been resized or normalized
return &Result{
File: r,
File: noopCloser(r),
Meta: map[string]interface{}{
"width": imgConfig.Width,
"height": imgConfig.Height,
@ -250,13 +256,20 @@ func (m *ImageResize) resizePNG(imgConfig *image.Config, r io.ReadSeeker) (*Resu
resized := imaging.Resize(img, width, 0, imaging.Lanczos)
width, height = resized.Rect.Max.X, resized.Rect.Max.Y
buff := &bytes.Buffer{}
if err = png.Encode(buff, resized); err != nil {
buf := pool.Get()
defer func() {
_ = buf.Close()
}()
if err = png.Encode(buf, resized); err != nil {
return nil, err
}
readSeekCloser, err := buf.GetReadSeekCloser()
if err != nil {
return nil, err
}
return &Result{
File: buff,
File: readSeekCloser,
Meta: map[string]interface{}{
"width": width,
"height": height,
@ -279,7 +292,7 @@ func (m *ImageResize) resizeGIF(imgConfig *image.Config, r io.ReadSeeker) (*Resu
// here is an optimization
// lets return the original picture in case it has not been resized or normalized
return &Result{
File: r,
File: noopCloser(r),
Meta: map[string]interface{}{
"width": imgConfig.Width,
"height": imgConfig.Height,
@ -302,13 +315,20 @@ func (m *ImageResize) resizeGIF(imgConfig *image.Config, r io.ReadSeeker) (*Resu
}
gifImg.Config.Width, gifImg.Config.Height = gifImg.Image[0].Bounds().Dx(), gifImg.Image[0].Bounds().Dy()
buff := bytes.NewBuffer(make([]byte, 0))
if err = gif.EncodeAll(buff, gifImg); err != nil {
buf := pool.Get()
defer func() {
_ = buf.Close()
}()
if err = gif.EncodeAll(buf, gifImg); err != nil {
return nil, err
}
readSeekCloser, err := buf.GetReadSeekCloser()
if err != nil {
return nil, err
}
return &Result{
File: buff,
File: readSeekCloser,
Meta: map[string]interface{}{
"width": gifImg.Config.Width,
"height": gifImg.Config.Height,
@ -379,7 +399,7 @@ func imageToPaletted(img image.Image) *image.Paletted {
return pm
}
func patchReaderRemoveExif(r io.ReadSeeker) (io.Reader, error) {
func patchReaderRemoveExif(r io.ReadSeeker) (io.ReadSeekCloser, error) {
jmp := jpegstructure.NewJpegMediaParser()
size, err := r.Seek(0, io.SeekEnd)
if err != nil {
@ -387,7 +407,10 @@ func patchReaderRemoveExif(r io.ReadSeeker) (io.Reader, error) {
}
_, _ = r.Seek(0, io.SeekStart)
buff := bytes.NewBuffer(make([]byte, 0, size))
buff := pool.Get()
defer func() {
_ = buff.Close()
}()
intfc, err := jmp.Parse(r, int(size))
if err != nil {
return nil, fmt.Errorf("failed to open file to read exif: %w", err)
@ -404,5 +427,5 @@ func patchReaderRemoveExif(r io.ReadSeeker) (io.Reader, error) {
return nil, err
}
return buff, nil
return buff.GetReadSeekCloser()
}

View file

@ -3,7 +3,6 @@
package mill
import (
"bytes"
"fmt"
"image"
"image/jpeg"
@ -35,14 +34,20 @@ func (m *ImageResize) resizeHEIC(imgConfig *image.Config, r io.ReadSeeker) (*Res
return nil, fmt.Errorf("invalid quality: " + m.Opts.Quality)
}
buff := &bytes.Buffer{}
buf := pool.Get()
defer func() {
_ = buf.Close()
}()
if err = jpeg.Encode(buff, resized, &jpeg.Options{Quality: quality}); err != nil {
if err = jpeg.Encode(buf, resized, &jpeg.Options{Quality: quality}); err != nil {
return nil, err
}
readSeekCloser, err := buf.GetReadSeekCloser()
if err != nil {
return nil, err
}
return &Result{
File: buff,
File: readSeekCloser,
Meta: map[string]interface{}{
"width": width,
"height": height,

View file

@ -56,7 +56,7 @@ func TestImageResize_Mill_ShouldRotateAndRemoveExif(t *testing.T) {
file.Seek(0, io.SeekStart)
res, err := cfg.Mill(file, "test", "")
res, err := cfg.Mill(file, "test")
if err != nil {
t.Fatal(err)
}
@ -118,7 +118,7 @@ func TestImageResize_Mill_ShouldNotBeReencoded(t *testing.T) {
file.Seek(0, io.SeekStart)
res, err := cfg.Mill(file, "test", "")
res, err := cfg.Mill(file, "test")
if err != nil {
t.Fatal(err)
}
@ -128,6 +128,9 @@ func TestImageResize_Mill_ShouldNotBeReencoded(t *testing.T) {
t.Fatal(err)
}
err = res.File.Close()
require.NoError(t, err)
img, err := jpeg.Decode(bytes.NewReader(b))
require.NoError(t, err)
require.Equal(t, 680, img.Bounds().Max.X)
@ -154,7 +157,7 @@ func TestImageResize_Mill(t *testing.T) {
t.Fatal(err)
}
res, err := m.Mill(file, "test", "")
res, err := m.Mill(file, "test")
if err != nil {
t.Fatal(err)
}
@ -169,6 +172,8 @@ func TestImageResize_Mill(t *testing.T) {
t.Errorf("exif data was not removed")
}
file.Close()
err = res.File.Close()
require.NoError(t, err)
}
}

View file

@ -3,7 +3,6 @@
package mill
import (
"bytes"
"fmt"
"image"
"io"
@ -34,7 +33,7 @@ func (m *ImageResize) resizeWEBP(imgConfig *image.Config, r io.ReadSeeker) (*Res
// here is an optimization
// lets return the original picture in case it has not been resized or normalized
return &Result{
File: r,
File: noopCloser(r),
Meta: map[string]interface{}{
"width": imgConfig.Width,
"height": imgConfig.Height,
@ -50,13 +49,21 @@ func (m *ImageResize) resizeWEBP(imgConfig *image.Config, r io.ReadSeeker) (*Res
resized := imaging.Resize(img, width, 0, imaging.Lanczos)
width, height = resized.Rect.Max.X, resized.Rect.Max.Y
buff := &bytes.Buffer{}
if webp.Encode(buff, resized, &webp.Options{Quality: float32(quality)}) != nil {
buf := pool.Get()
defer func() {
_ = buf.Close()
}()
if webp.Encode(buf, resized, &webp.Options{Quality: float32(quality)}) != nil {
return nil, err
}
readSeekCloser, err := buf.GetReadSeekCloser()
if err != nil {
return nil, err
}
return &Result{
File: buff,
File: readSeekCloser,
Meta: map[string]interface{}{
"width": width,
"height": height,

View file

@ -9,14 +9,17 @@ import (
"github.com/mr-tron/base58/base58"
"github.com/anyproto/anytype-heart/pkg/lib/logging"
"github.com/anyproto/anytype-heart/util/bufferpool"
)
var log = logging.Logger("tex-mill")
var pool = bufferpool.NewPool()
var ErrMediaTypeNotSupported = fmt.Errorf("media type not supported")
type Result struct {
File io.Reader
File io.ReadSeekCloser
Meta map[string]interface{}
}
@ -25,7 +28,7 @@ type Mill interface {
Pin() bool // pin by default
AcceptMedia(media string) error
Options(add map[string]interface{}) (string, error)
Mill(r io.ReadSeeker, name string, sourceChecksum string) (*Result, error)
Mill(r io.ReadSeeker, name string) (*Result, error)
}
func accepts(list []string, media string) error {

View file

@ -0,0 +1,15 @@
package mill
import "io"
type noopCloserWrapper struct {
io.ReadSeeker
}
func (n *noopCloserWrapper) Close() error {
return nil
}
func noopCloser(r io.ReadSeeker) io.ReadSeekCloser {
return &noopCloserWrapper{r}
}

View file

@ -3591,11 +3591,11 @@ func (m *BlockContentSmartblock) XXX_DiscardUnknown() {
var xxx_messageInfo_BlockContentSmartblock proto.InternalMessageInfo
type BlockContentDataview struct {
Source []string `protobuf:"bytes,1,rep,name=source,proto3" json:"source,omitempty"`
Views []*BlockContentDataviewView `protobuf:"bytes,2,rep,name=views,proto3" json:"views,omitempty"`
Source []string `protobuf:"bytes,1,rep,name=source,proto3" json:"source,omitempty"`
Views []*BlockContentDataviewView `protobuf:"bytes,2,rep,name=views,proto3" json:"views,omitempty"`
ActiveView string `protobuf:"bytes,3,opt,name=activeView,proto3" json:"activeView,omitempty"`
// deprecated
Relations []*Relation `protobuf:"bytes,4,rep,name=relations,proto3" json:"relations,omitempty"`
ActiveView string `protobuf:"bytes,3,opt,name=activeView,proto3" json:"activeView,omitempty"`
GroupOrders []*BlockContentDataviewGroupOrder `protobuf:"bytes,12,rep,name=groupOrders,proto3" json:"groupOrders,omitempty"`
ObjectOrders []*BlockContentDataviewObjectOrder `protobuf:"bytes,13,rep,name=objectOrders,proto3" json:"objectOrders,omitempty"`
RelationLinks []*RelationLink `protobuf:"bytes,5,rep,name=relationLinks,proto3" json:"relationLinks,omitempty"`
@ -3650,13 +3650,6 @@ func (m *BlockContentDataview) GetViews() []*BlockContentDataviewView {
return nil
}
func (m *BlockContentDataview) GetRelations() []*Relation {
if m != nil {
return m.Relations
}
return nil
}
func (m *BlockContentDataview) GetActiveView() string {
if m != nil {
return m.ActiveView
@ -3664,6 +3657,13 @@ func (m *BlockContentDataview) GetActiveView() string {
return ""
}
func (m *BlockContentDataview) GetRelations() []*Relation {
if m != nil {
return m.Relations
}
return nil
}
func (m *BlockContentDataview) GetGroupOrders() []*BlockContentDataviewGroupOrder {
if m != nil {
return m.GroupOrders
@ -9036,7 +9036,7 @@ func init() {
}
var fileDescriptor_98a910b73321e591 = []byte{
// 8233 bytes of a gzipped FileDescriptorProto
// 8232 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xcc, 0x7c, 0x5b, 0x8c, 0x24, 0xd9,
0x95, 0x50, 0xe5, 0x3b, 0xf3, 0x64, 0x3d, 0x6e, 0xdd, 0x7e, 0xa5, 0x73, 0x7a, 0x7b, 0xdb, 0xb1,
0xf6, 0xb8, 0xdd, 0x1e, 0x57, 0xcf, 0xf4, 0xbc, 0x67, 0x3d, 0x33, 0xae, 0x67, 0x57, 0xce, 0xd4,
@ -9169,389 +9169,389 @@ var fileDescriptor_98a910b73321e591 = []byte{
0x8d, 0xc7, 0xd2, 0x61, 0x79, 0xeb, 0x9d, 0x19, 0x6a, 0x76, 0x09, 0x6a, 0x8f, 0x87, 0x5e, 0x20,
0x9c, 0x2b, 0xf4, 0xec, 0x22, 0x40, 0xea, 0x1c, 0x37, 0x7f, 0xff, 0x57, 0xd3, 0xe3, 0x1c, 0x6d,
0xd1, 0x28, 0x18, 0x85, 0x5d, 0x49, 0x2a, 0xa4, 0x66, 0x9b, 0x16, 0xff, 0x36, 0x94, 0xf0, 0x7d,
0x1c, 0x8d, 0xb9, 0x3f, 0x97, 0x4b, 0xb6, 0xf6, 0xc4, 0x95, 0xcf, 0x6d, 0x4d, 0xc8, 0xdf, 0xce,
0x9a, 0x27, 0x57, 0x87, 0x0b, 0x33, 0x76, 0x0b, 0xbf, 0x03, 0x20, 0xba, 0xca, 0x3d, 0x93, 0xc8,
0xcb, 0xe8, 0x88, 0x0c, 0x84, 0xdb, 0x50, 0xc7, 0xad, 0x3b, 0x3c, 0x0c, 0x71, 0xb7, 0x37, 0x16,
0x89, 0xf1, 0xeb, 0xf3, 0x75, 0xef, 0x51, 0x42, 0x68, 0x67, 0x99, 0xf0, 0xc7, 0xb0, 0xa8, 0x43,
0x63, 0x86, 0xe9, 0x12, 0x31, 0x7d, 0x63, 0x3e, 0xa6, 0x87, 0x29, 0xa5, 0x3d, 0xc6, 0x66, 0x3a,
0xba, 0x58, 0x7a, 0xe9, 0xe8, 0xe2, 0xab, 0xb0, 0xdc, 0x19, 0xdf, 0x05, 0xfa, 0xa8, 0x98, 0x80,
0x72, 0x0b, 0x16, 0xdd, 0x28, 0x0d, 0x6e, 0x52, 0xa8, 0xa3, 0x6a, 0x8f, 0xc1, 0x9a, 0xff, 0xae,
0x0c, 0x45, 0x9a, 0xc2, 0xc9, 0x50, 0xd5, 0xe6, 0x98, 0x4a, 0x7f, 0x30, 0xff, 0x52, 0x4f, 0xec,
0x78, 0xd2, 0x20, 0x85, 0x8c, 0x06, 0xf9, 0x36, 0x94, 0xa2, 0x20, 0x54, 0xf1, 0xf2, 0xcf, 0x29,
0x44, 0xed, 0x20, 0x54, 0xb6, 0x26, 0xe4, 0x3b, 0x50, 0x39, 0x71, 0x3d, 0x85, 0x8b, 0xa2, 0x27,
0xef, 0xb5, 0xf9, 0x78, 0xec, 0x10, 0x91, 0x1d, 0x13, 0xf3, 0xbd, 0xac, 0x30, 0x96, 0x89, 0xd3,
0xda, 0x7c, 0x9c, 0x66, 0xc9, 0xe8, 0x7d, 0x60, 0xdd, 0xe0, 0x4c, 0x86, 0x76, 0x26, 0xbe, 0xa8,
0x0f, 0xe9, 0x29, 0x38, 0x6f, 0x42, 0xb5, 0xef, 0x3a, 0x12, 0xed, 0x1c, 0xd2, 0x31, 0x55, 0x3b,
0x69, 0xf3, 0x4f, 0xa1, 0x4a, 0xfe, 0x01, 0x6a, 0xc5, 0xda, 0x4b, 0x4f, 0xbe, 0x76, 0x55, 0x62,
0x06, 0xf8, 0x21, 0xfa, 0xf8, 0x8e, 0xab, 0x28, 0xcc, 0x5c, 0xb5, 0x93, 0x36, 0x76, 0x98, 0xe4,
0x3d, 0xdb, 0xe1, 0xba, 0xee, 0xf0, 0x24, 0x9c, 0xbf, 0x05, 0x37, 0x08, 0x36, 0x71, 0x48, 0xe2,
0x56, 0x43, 0xa6, 0xb3, 0x5f, 0xa2, 0xc1, 0x32, 0x14, 0x3d, 0xb9, 0xe7, 0x0e, 0x5c, 0xd5, 0x58,
0xba, 0x9b, 0xbb, 0x57, 0xb2, 0x53, 0x00, 0x7f, 0x0d, 0x56, 0x1d, 0x79, 0x22, 0x46, 0x9e, 0xea,
0xc8, 0xc1, 0xd0, 0x13, 0x4a, 0xb6, 0x1c, 0x92, 0xd1, 0x9a, 0x3d, 0xfd, 0x82, 0xbf, 0x0e, 0xd7,
0x0c, 0xf0, 0x30, 0x49, 0x0e, 0xb4, 0x1c, 0x8a, 0xc2, 0xd5, 0xec, 0x59, 0xaf, 0xac, 0x7d, 0xa3,
0x86, 0xf1, 0x00, 0x45, 0x3f, 0x35, 0x56, 0xa0, 0x91, 0xd2, 0x27, 0xf2, 0x23, 0xe1, 0x79, 0x32,
0xbc, 0xd0, 0x4e, 0xee, 0xa7, 0xc2, 0x3f, 0x16, 0x3e, 0x2b, 0xd0, 0x19, 0x2b, 0x3c, 0xe9, 0x3b,
0x22, 0xd4, 0x27, 0xf2, 0x23, 0x3a, 0xd0, 0x4b, 0xd6, 0x3d, 0x28, 0xd2, 0x94, 0xd6, 0xa0, 0xa4,
0xbd, 0x24, 0xf2, 0x98, 0x8d, 0x87, 0x44, 0x1a, 0x79, 0x0f, 0xb7, 0x1f, 0xcb, 0x37, 0x7f, 0x56,
0x80, 0x6a, 0x3c, 0x79, 0x71, 0x2a, 0x20, 0x97, 0xa6, 0x02, 0xd0, 0x8c, 0x8b, 0x9e, 0xb8, 0x91,
0x7b, 0x6c, 0xcc, 0xd2, 0xaa, 0x9d, 0x02, 0xd0, 0x12, 0x7a, 0xee, 0x3a, 0xaa, 0x4f, 0x7b, 0xa6,
0x64, 0xeb, 0x06, 0xbf, 0x07, 0x2b, 0x0e, 0xce, 0x83, 0xdf, 0xf5, 0x46, 0x8e, 0xec, 0xe0, 0x29,
0xaa, 0xc3, 0x04, 0x93, 0x60, 0xfe, 0x3d, 0x00, 0xe5, 0x0e, 0xe4, 0x4e, 0x10, 0x0e, 0x84, 0x32,
0xbe, 0xc1, 0xfb, 0x2f, 0x27, 0xd5, 0x6b, 0x9d, 0x84, 0x81, 0x9d, 0x61, 0x86, 0xac, 0xf1, 0x6b,
0x86, 0x75, 0xe5, 0x0b, 0xb1, 0xde, 0x4a, 0x18, 0xd8, 0x19, 0x66, 0xd6, 0x6f, 0x00, 0xa4, 0x6f,
0xf8, 0x4d, 0xe0, 0xfb, 0x81, 0xaf, 0xfa, 0xeb, 0xc7, 0xc7, 0xe1, 0x86, 0x3c, 0x09, 0x42, 0xb9,
0x25, 0xf0, 0x58, 0xbb, 0x01, 0xab, 0x09, 0x7c, 0xfd, 0x44, 0xc9, 0x10, 0xc1, 0x34, 0xf5, 0xed,
0x7e, 0x10, 0x2a, 0x6d, 0x5b, 0xd1, 0xe3, 0xe3, 0x36, 0x2b, 0xe0, 0x51, 0xda, 0x6a, 0x1f, 0xb2,
0xa2, 0x75, 0x0f, 0x20, 0x1d, 0x12, 0xf9, 0x20, 0xf4, 0xf4, 0xc6, 0x43, 0xe3, 0x91, 0x50, 0xeb,
0xe1, 0x5b, 0x2c, 0xd7, 0xfc, 0xa3, 0x02, 0x14, 0x51, 0xd5, 0xa0, 0xfb, 0x95, 0xdd, 0x17, 0x7a,
0xf9, 0xb2, 0xa0, 0x2f, 0xa6, 0x20, 0x91, 0x77, 0x56, 0x41, 0xbe, 0x07, 0xf5, 0xee, 0x28, 0x52,
0xc1, 0x80, 0x4e, 0x07, 0x93, 0x47, 0xb9, 0x39, 0x15, 0xc8, 0x78, 0x22, 0xbc, 0x91, 0xb4, 0xb3,
0xa8, 0xfc, 0x6d, 0x28, 0x9f, 0xe8, 0x85, 0xd0, 0xa1, 0x8c, 0x5f, 0xb9, 0xe4, 0x00, 0x31, 0x93,
0x6d, 0x90, 0x71, 0x5c, 0xee, 0x94, 0x10, 0x65, 0x41, 0xe6, 0x20, 0x28, 0x27, 0x07, 0xc1, 0x6f,
0xc0, 0xb2, 0x44, 0xb3, 0xe2, 0xc8, 0x13, 0x5d, 0x39, 0x90, 0x7e, 0xbc, 0xf2, 0x6f, 0xbd, 0xc4,
0x88, 0xc9, 0x2e, 0xa1, 0x61, 0x4f, 0xf0, 0xb2, 0xbe, 0x6a, 0x36, 0x69, 0x05, 0x0a, 0xeb, 0x51,
0xd7, 0xb8, 0xdd, 0x32, 0xea, 0x6a, 0x9b, 0x7e, 0x93, 0x06, 0xcc, 0xf2, 0xd6, 0x1b, 0x50, 0x4b,
0x78, 0x70, 0x06, 0x8b, 0x07, 0x81, 0x6a, 0x0f, 0x65, 0xd7, 0x3d, 0x71, 0xa5, 0xa3, 0x03, 0x09,
0x6d, 0x25, 0x42, 0xa5, 0x23, 0x57, 0xdb, 0xbe, 0xc3, 0xf2, 0xcd, 0x7f, 0x5b, 0x81, 0xb2, 0xd6,
0xf8, 0x66, 0x48, 0xb5, 0x64, 0x48, 0xdf, 0x81, 0x6a, 0x30, 0x94, 0xa1, 0x50, 0x41, 0x68, 0xc2,
0x05, 0x6f, 0xbf, 0xcc, 0x09, 0xb2, 0x76, 0x68, 0x88, 0xed, 0x84, 0xcd, 0xa4, 0xbc, 0xe4, 0xa7,
0xe5, 0xe5, 0x3e, 0xb0, 0xf8, 0xb0, 0x38, 0x0a, 0x91, 0x4e, 0x5d, 0x18, 0xe7, 0x6f, 0x0a, 0xce,
0x3b, 0x50, 0xeb, 0x06, 0xbe, 0xe3, 0x26, 0xa1, 0x83, 0xe5, 0x87, 0xef, 0xbc, 0x54, 0x0f, 0x37,
0x63, 0x6a, 0x3b, 0x65, 0xc4, 0x5f, 0x83, 0xd2, 0x19, 0x0a, 0x12, 0x49, 0xcc, 0xe5, 0x62, 0xa6,
0x91, 0xf8, 0x67, 0x50, 0xff, 0xfe, 0xc8, 0xed, 0x9e, 0x1e, 0x66, 0x43, 0x53, 0xef, 0xbd, 0x54,
0x2f, 0xbe, 0x93, 0xd2, 0xdb, 0x59, 0x66, 0x19, 0xe1, 0xad, 0xfc, 0x29, 0x84, 0xb7, 0x3a, 0x25,
0xbc, 0xd6, 0x2b, 0x50, 0x8d, 0x17, 0x87, 0x44, 0xca, 0x47, 0xe9, 0x28, 0x43, 0xfe, 0x30, 0x64,
0x39, 0xeb, 0x8f, 0x73, 0x50, 0x4b, 0x26, 0x66, 0x3c, 0x0c, 0xb5, 0xfd, 0xfd, 0x91, 0xf0, 0x58,
0x8e, 0xfc, 0xa8, 0x40, 0xe9, 0x16, 0x69, 0x93, 0x47, 0x94, 0x8c, 0x0d, 0x59, 0x81, 0xce, 0x0e,
0x19, 0x45, 0xac, 0xc8, 0x39, 0x2c, 0x1b, 0xf0, 0x61, 0xa8, 0x51, 0x4b, 0xe8, 0x66, 0xe1, 0xdb,
0x18, 0x50, 0xd6, 0x47, 0xcd, 0xa9, 0xd4, 0x6e, 0xe4, 0x41, 0xa0, 0xa8, 0x51, 0xc5, 0xbe, 0xb4,
0x7c, 0x56, 0xc3, 0x6f, 0x1e, 0x04, 0xaa, 0xe5, 0x33, 0x48, 0xed, 0xf6, 0x7a, 0xfc, 0x79, 0x6a,
0x2d, 0x92, 0x57, 0xe0, 0x79, 0x2d, 0x9f, 0x2d, 0x99, 0x17, 0xba, 0xb5, 0x8c, 0x1c, 0xb7, 0xcf,
0x45, 0x17, 0xc9, 0x57, 0xf8, 0x32, 0x00, 0xd2, 0x98, 0x36, 0xc3, 0x6d, 0xb3, 0x7d, 0xee, 0x46,
0x2a, 0x62, 0xab, 0xd6, 0xbf, 0xc9, 0x41, 0x3d, 0xb3, 0x08, 0xe8, 0x17, 0x10, 0x22, 0xea, 0x5a,
0xed, 0x26, 0x7c, 0x4f, 0x46, 0x4a, 0x86, 0x4e, 0xac, 0x47, 0x3b, 0x01, 0x3e, 0xe6, 0xf1, 0x7b,
0x9d, 0x60, 0x10, 0x84, 0x61, 0xf0, 0x5c, 0x9f, 0x89, 0x7b, 0x22, 0x52, 0x4f, 0xa5, 0x3c, 0x65,
0x45, 0x1c, 0xea, 0xe6, 0x28, 0x0c, 0xa5, 0xaf, 0x01, 0x25, 0xea, 0x9c, 0x3c, 0xd7, 0xad, 0x32,
0x32, 0x45, 0x64, 0x52, 0xd4, 0xac, 0x82, 0x9b, 0xd5, 0x60, 0x6b, 0x48, 0x15, 0x11, 0x10, 0x5d,
0x37, 0x6b, 0xe8, 0x7a, 0x6b, 0xd7, 0xf5, 0xf0, 0x64, 0x4b, 0x5c, 0x44, 0xeb, 0xbd, 0x80, 0xc1,
0x24, 0xf0, 0x20, 0x78, 0xce, 0xea, 0xcd, 0x11, 0x40, 0x6a, 0xac, 0xa3, 0x93, 0x82, 0xb2, 0x96,
0x04, 0x97, 0x4d, 0x8b, 0x1f, 0x02, 0xe0, 0x13, 0x61, 0xc6, 0x9e, 0xca, 0x4b, 0x58, 0x50, 0x44,
0x67, 0x67, 0x58, 0x34, 0xff, 0x22, 0xd4, 0x92, 0x17, 0xe8, 0x9b, 0x92, 0xad, 0x93, 0x7c, 0x36,
0x6e, 0xe2, 0xc1, 0xed, 0xfa, 0x8e, 0x3c, 0xa7, 0xbd, 0x5f, 0xb2, 0x75, 0x03, 0x7b, 0xd9, 0x77,
0x1d, 0x47, 0xfa, 0x71, 0x0a, 0x40, 0xb7, 0x66, 0xe5, 0x5b, 0x8b, 0x33, 0xf3, 0xad, 0xcd, 0xdf,
0x84, 0x7a, 0xc6, 0x9b, 0xb8, 0x74, 0xd8, 0x99, 0x8e, 0xe5, 0xc7, 0x3b, 0x76, 0x1b, 0x6a, 0x71,
0x8e, 0x3f, 0xa2, 0x13, 0xa6, 0x66, 0xa7, 0x80, 0xe6, 0x3f, 0xcb, 0xa3, 0x89, 0x83, 0x43, 0x9b,
0xf4, 0x00, 0x76, 0xa0, 0x8c, 0xee, 0xf0, 0x28, 0x4e, 0x56, 0xcf, 0x69, 0x65, 0xb7, 0x89, 0x66,
0x77, 0xc1, 0x36, 0xd4, 0xfc, 0x43, 0x28, 0x28, 0xd1, 0x33, 0x11, 0xb4, 0xaf, 0xcf, 0xc7, 0xa4,
0x23, 0x7a, 0xbb, 0x0b, 0x36, 0xd2, 0xf1, 0x3d, 0xa8, 0x76, 0x4d, 0xd0, 0xc3, 0x28, 0xae, 0x39,
0x8d, 0xf4, 0x38, 0x54, 0xb2, 0xbb, 0x60, 0x27, 0x1c, 0xf8, 0xb7, 0xa1, 0x88, 0x66, 0x87, 0xc9,
0xe9, 0xcf, 0xe9, 0x7c, 0xe0, 0x76, 0xd9, 0x5d, 0xb0, 0x89, 0x72, 0xa3, 0x02, 0x25, 0xd2, 0x93,
0xcd, 0x06, 0x94, 0xf5, 0x58, 0x27, 0x67, 0xae, 0x79, 0x0b, 0x0a, 0x1d, 0xd1, 0x43, 0xd3, 0xcf,
0x75, 0x22, 0xe3, 0x43, 0xe3, 0x63, 0xf3, 0x2b, 0x69, 0x00, 0x27, 0x1b, 0x1b, 0xcc, 0x8d, 0xc5,
0x06, 0x9b, 0x65, 0x28, 0xe2, 0x17, 0x9b, 0xb7, 0xaf, 0x32, 0x23, 0x9b, 0xff, 0x23, 0x8f, 0x16,
0xa7, 0x92, 0xe7, 0x33, 0xe3, 0x9e, 0x9f, 0x40, 0x6d, 0x18, 0x06, 0x5d, 0x19, 0x45, 0x41, 0x68,
0x4c, 0x94, 0xd7, 0x5e, 0x9c, 0x5a, 0x5c, 0x3b, 0x8a, 0x69, 0xec, 0x94, 0xdc, 0xfa, 0x1b, 0x79,
0xa8, 0x25, 0x2f, 0xb4, 0xa1, 0xab, 0xe4, 0xb9, 0x8e, 0x71, 0xed, 0xcb, 0x70, 0x20, 0x5c, 0x47,
0x6b, 0x8f, 0xcd, 0xbe, 0x88, 0xad, 0xb0, 0xef, 0x05, 0x23, 0x35, 0x3a, 0x96, 0x3a, 0xb6, 0xf1,
0xc4, 0x1d, 0xc8, 0x80, 0x15, 0x29, 0xab, 0x80, 0x82, 0xdd, 0xf5, 0x82, 0x91, 0xc3, 0x4a, 0xd8,
0x7e, 0x44, 0x47, 0xd0, 0xbe, 0x18, 0x46, 0x5a, 0x67, 0xee, 0xbb, 0x61, 0xc0, 0x2a, 0x48, 0xb4,
0xe3, 0xf6, 0x06, 0x82, 0x55, 0x91, 0x59, 0xe7, 0xb9, 0xab, 0x50, 0x09, 0xd7, 0xf8, 0x2a, 0x2c,
0x1d, 0x0e, 0xa5, 0xdf, 0x56, 0xa1, 0x94, 0x6a, 0x5f, 0x0c, 0x75, 0xb0, 0xcb, 0x96, 0x8e, 0xe3,
0x2a, 0xad, 0x3f, 0x77, 0x44, 0x57, 0x1e, 0x07, 0xc1, 0x29, 0x5b, 0x44, 0x45, 0xd3, 0xf2, 0x23,
0x25, 0x7a, 0xa1, 0x18, 0x68, 0x1d, 0xda, 0x91, 0x9e, 0xa4, 0xd6, 0x32, 0x7d, 0xdb, 0x55, 0xfd,
0xd1, 0xf1, 0x23, 0x74, 0x08, 0x56, 0x74, 0x02, 0xc2, 0x91, 0x43, 0x89, 0x3a, 0x74, 0x11, 0xaa,
0x1b, 0xae, 0xe7, 0x1e, 0xbb, 0x9e, 0xcb, 0x56, 0x11, 0x75, 0xfb, 0xbc, 0x2b, 0x3c, 0xd7, 0x09,
0xc5, 0x73, 0xc6, 0x9b, 0xab, 0xb0, 0x32, 0x91, 0x42, 0x6d, 0x56, 0x8c, 0x8f, 0xd1, 0x5c, 0x82,
0x7a, 0x26, 0x29, 0xd6, 0x7c, 0x15, 0xaa, 0x71, 0xca, 0x0c, 0x7d, 0x31, 0x37, 0xd2, 0xc1, 0x3e,
0xb3, 0xe2, 0x49, 0xbb, 0xf9, 0x4f, 0x73, 0x50, 0xd6, 0x69, 0x47, 0xbe, 0x91, 0x94, 0x09, 0xe4,
0xe6, 0xc8, 0x51, 0x69, 0x22, 0x93, 0xe1, 0x4b, 0x6a, 0x05, 0xae, 0x43, 0xc9, 0x23, 0xa7, 0xcb,
0xe8, 0x22, 0x6a, 0x64, 0x54, 0x47, 0x21, 0xab, 0x3a, 0xac, 0x77, 0x93, 0xac, 0x62, 0x1c, 0x60,
0x22, 0x33, 0xac, 0x13, 0x4a, 0xa9, 0x83, 0x47, 0xe4, 0x33, 0xe5, 0x49, 0xf1, 0x07, 0x83, 0xa1,
0xe8, 0x2a, 0x02, 0x14, 0xac, 0x13, 0xa8, 0x1e, 0x05, 0xd1, 0xe4, 0x71, 0x5a, 0x81, 0x42, 0x27,
0x18, 0x6a, 0x03, 0x6e, 0x23, 0x50, 0x64, 0xc0, 0xe9, 0xd3, 0xf3, 0x44, 0x69, 0x79, 0xb0, 0xdd,
0x5e, 0x5f, 0x69, 0xef, 0xaa, 0xe5, 0xfb, 0x32, 0x64, 0x25, 0x9c, 0x7e, 0x5b, 0x0e, 0xd1, 0x2c,
0x64, 0x65, 0x9c, 0x70, 0x82, 0xef, 0xb8, 0x61, 0xa4, 0x58, 0xc5, 0x6a, 0xe1, 0x41, 0xe8, 0xf6,
0xe8, 0xfc, 0xa2, 0x07, 0x62, 0xb5, 0x80, 0x1d, 0xa2, 0xe6, 0xa6, 0xf4, 0x51, 0x3c, 0x28, 0x6d,
0xa5, 0xeb, 0x46, 0xe8, 0x03, 0x79, 0x3c, 0x7c, 0xa8, 0xfd, 0xc9, 0x28, 0x52, 0xee, 0xc9, 0x05,
0x2b, 0x58, 0x4f, 0x61, 0x69, 0xac, 0xc2, 0x84, 0x5f, 0x07, 0x36, 0x06, 0xc0, 0xae, 0x2f, 0xf0,
0x5b, 0x70, 0x6d, 0x0c, 0xba, 0xef, 0x3a, 0x0e, 0xc5, 0xef, 0x26, 0x5f, 0xc4, 0x03, 0xdc, 0xa8,
0x41, 0xa5, 0xab, 0xd7, 0xc4, 0x3a, 0x82, 0x25, 0x5a, 0xa4, 0x7d, 0xa9, 0xc4, 0xa1, 0xef, 0x5d,
0xfc, 0xa9, 0xcb, 0x80, 0xac, 0x6f, 0x40, 0x89, 0xe2, 0xed, 0xb8, 0xd5, 0x4f, 0xc2, 0x60, 0x40,
0xbc, 0x4a, 0x36, 0x3d, 0x23, 0x77, 0x15, 0x98, 0x95, 0xce, 0xab, 0xc0, 0xfa, 0x11, 0x40, 0x65,
0xbd, 0xdb, 0x0d, 0x46, 0xbe, 0x9a, 0xfa, 0xf2, 0xac, 0x90, 0xee, 0xdb, 0x50, 0x16, 0x67, 0x42,
0x89, 0xd0, 0xa8, 0xe8, 0x49, 0x63, 0xcc, 0xf0, 0x5a, 0x5b, 0x27, 0x24, 0xdb, 0x20, 0x23, 0x59,
0x37, 0xf0, 0x4f, 0xdc, 0x9e, 0xd1, 0xca, 0x97, 0x91, 0x6d, 0x12, 0x92, 0x6d, 0x90, 0x91, 0xcc,
0x9c, 0x2a, 0xa5, 0x2b, 0xc9, 0xb4, 0x6a, 0x4d, 0x0e, 0x91, 0x07, 0x50, 0x74, 0xfd, 0x93, 0xc0,
0x94, 0xff, 0xbd, 0x72, 0x09, 0x11, 0xd5, 0xc0, 0x11, 0x62, 0x53, 0x42, 0x59, 0x77, 0x98, 0xbf,
0x0f, 0x25, 0x4a, 0xab, 0x99, 0x44, 0xc6, 0x5c, 0x15, 0x3b, 0x9a, 0x82, 0xdf, 0x8c, 0xb3, 0x34,
0x34, 0x5f, 0x08, 0xa7, 0xe6, 0x46, 0x35, 0x9e, 0xb2, 0xe6, 0x7f, 0xca, 0x41, 0x59, 0x8f, 0x90,
0xbf, 0x0a, 0xcb, 0xd2, 0xc7, 0xcd, 0x1e, 0x9f, 0x1b, 0x66, 0x97, 0x4f, 0x40, 0xd1, 0x8a, 0x35,
0x10, 0x79, 0x3c, 0xea, 0x99, 0x08, 0x40, 0x16, 0xc4, 0xdf, 0x83, 0x5b, 0xba, 0x79, 0x14, 0xca,
0x50, 0x7a, 0x52, 0x44, 0x72, 0xb3, 0x2f, 0x7c, 0x5f, 0x7a, 0xc6, 0x8a, 0xb8, 0xec, 0x35, 0xb7,
0x60, 0x51, 0xbf, 0x6a, 0x0f, 0x45, 0x57, 0x46, 0x26, 0xeb, 0x34, 0x06, 0xe3, 0xdf, 0x84, 0x12,
0x15, 0x61, 0x36, 0x9c, 0xab, 0x85, 0x4f, 0x63, 0x35, 0x83, 0xe4, 0x98, 0x5b, 0x07, 0xd0, 0xab,
0x81, 0x5e, 0x98, 0xd1, 0x4e, 0x5f, 0xbe, 0x72, 0xf9, 0xc8, 0xe5, 0xcb, 0x10, 0x61, 0xff, 0x1c,
0xe9, 0x49, 0xaa, 0x96, 0xc3, 0x63, 0x38, 0x4f, 0xf1, 0xfd, 0x31, 0x58, 0xf3, 0xb7, 0x8b, 0x50,
0xc4, 0x85, 0x44, 0xe4, 0x7e, 0x30, 0x90, 0x49, 0x94, 0x53, 0x0b, 0xed, 0x18, 0x0c, 0xed, 0x28,
0xa1, 0x13, 0xcd, 0x09, 0x9a, 0x56, 0x6e, 0x93, 0x60, 0xc4, 0x1c, 0x86, 0xc1, 0x89, 0xeb, 0xa5,
0x98, 0xc6, 0xe2, 0x9a, 0x00, 0xf3, 0x77, 0xe0, 0xe6, 0x40, 0x84, 0xa7, 0x52, 0x91, 0x3e, 0x7a,
0x1a, 0x84, 0xa7, 0x11, 0xce, 0x5c, 0xcb, 0x31, 0xe1, 0xb1, 0x4b, 0xde, 0xa2, 0x82, 0x77, 0xe4,
0x99, 0x4b, 0x98, 0x55, 0x5d, 0x5c, 0x19, 0xb7, 0x51, 0x38, 0x84, 0x9e, 0x9a, 0xb6, 0xe1, 0x65,
0x32, 0x17, 0xe3, 0x50, 0x34, 0xd6, 0x74, 0xd1, 0x49, 0xd4, 0x72, 0x28, 0x62, 0x57, 0xb3, 0x53,
0x00, 0x8a, 0x0e, 0x7d, 0xec, 0x89, 0x56, 0xe3, 0x4b, 0xda, 0xcb, 0xcc, 0x80, 0x10, 0x43, 0xc9,
0x6e, 0x3f, 0xfe, 0x88, 0x0e, 0xa7, 0x65, 0x41, 0xfc, 0x0e, 0x40, 0x4f, 0x28, 0xf9, 0x5c, 0x5c,
0x3c, 0x0e, 0xbd, 0x86, 0xd4, 0xb1, 0xf4, 0x14, 0x82, 0x7e, 0xaa, 0x17, 0x74, 0x85, 0xd7, 0x56,
0x41, 0x28, 0x7a, 0xf2, 0x48, 0xa8, 0x7e, 0xa3, 0xa7, 0xfd, 0xd4, 0x49, 0x38, 0x8e, 0x58, 0xb9,
0x03, 0xf9, 0x59, 0xe0, 0xcb, 0x46, 0x5f, 0x8f, 0x38, 0x6e, 0x63, 0x4f, 0x84, 0x2f, 0xbc, 0x0b,
0xe5, 0x76, 0x71, 0x2c, 0xae, 0xee, 0x49, 0x06, 0x84, 0x63, 0xf5, 0xa5, 0x7a, 0x1e, 0x84, 0xa7,
0x2d, 0xa7, 0xf1, 0xb9, 0x1e, 0x6b, 0x02, 0xb0, 0x0e, 0x01, 0x52, 0x21, 0xc2, 0xb3, 0x64, 0x9d,
0xe2, 0xfd, 0x6c, 0x01, 0x9d, 0x83, 0x23, 0xe9, 0x3b, 0xae, 0xdf, 0xdb, 0x32, 0x72, 0xc3, 0x72,
0x08, 0xa4, 0x10, 0x80, 0x74, 0x12, 0x20, 0x19, 0x22, 0xd4, 0x92, 0x0e, 0x2b, 0x58, 0xff, 0x2b,
0x07, 0xf5, 0x4c, 0x56, 0xfc, 0xcf, 0x30, 0x93, 0x8f, 0x27, 0x3b, 0xea, 0x0b, 0x9c, 0x50, 0x2d,
0x53, 0x49, 0x1b, 0xa7, 0xdb, 0x24, 0xed, 0xf1, 0xad, 0x76, 0xf8, 0x33, 0x90, 0x2f, 0x94, 0xc5,
0xb7, 0x1e, 0x9a, 0xa8, 0x49, 0x1d, 0x2a, 0x8f, 0xfd, 0x53, 0x3f, 0x78, 0xee, 0xeb, 0x23, 0x9b,
0x4a, 0x33, 0xc6, 0x92, 0x4c, 0x71, 0xf5, 0x44, 0xc1, 0xfa, 0xc7, 0xc5, 0x89, 0x2a, 0xa6, 0x6d,
0x28, 0x6b, 0x37, 0x80, 0x2c, 0xd4, 0xe9, 0xb2, 0x93, 0x2c, 0xb2, 0x49, 0x68, 0x64, 0x40, 0xb6,
0x21, 0x46, 0xfb, 0x3c, 0x29, 0xd5, 0xcb, 0xcf, 0x4c, 0xbc, 0x8c, 0x31, 0x8a, 0xd5, 0xe0, 0x58,
0xb5, 0x6a, 0xc2, 0xa1, 0xf9, 0xd7, 0x72, 0x70, 0x7d, 0x16, 0x4a, 0xb6, 0xa6, 0x37, 0x37, 0x5e,
0xd3, 0xdb, 0x9e, 0xa8, 0x91, 0xcd, 0xd3, 0x68, 0x1e, 0xbc, 0x64, 0x27, 0xc6, 0x2b, 0x66, 0xad,
0x9f, 0xe4, 0x60, 0x75, 0x6a, 0xcc, 0x19, 0x23, 0x07, 0xa0, 0xac, 0x25, 0x4b, 0xd7, 0xbe, 0x24,
0xd5, 0x08, 0x3a, 0x9a, 0x4c, 0x67, 0x4a, 0xa4, 0xd3, 0xbb, 0xa6, 0x2a, 0x58, 0x9b, 0xbf, 0xb8,
0x6a, 0xa8, 0xab, 0x7b, 0x92, 0x95, 0xd0, 0x3a, 0xd1, 0x76, 0x97, 0x81, 0x94, 0xb5, 0x89, 0xaa,
0x43, 0xde, 0xac, 0x42, 0x35, 0x35, 0xa3, 0xa1, 0xe7, 0x76, 0xb1, 0x59, 0xe5, 0x4d, 0xb8, 0xa9,
0x4b, 0xc3, 0x8d, 0x3b, 0x78, 0xd2, 0xe9, 0xbb, 0xb4, 0x39, 0x58, 0xcd, 0xb2, 0xe1, 0xda, 0x8c,
0x31, 0x51, 0x2f, 0x9f, 0x98, 0x1e, 0x2f, 0x03, 0x6c, 0x3d, 0x89, 0xfb, 0xc9, 0x72, 0x9c, 0xc3,
0xf2, 0xd6, 0x93, 0x2c, 0x43, 0xb3, 0x5f, 0x9e, 0xa0, 0x26, 0x89, 0x58, 0xc1, 0xfa, 0x9d, 0x5c,
0x9c, 0xe7, 0x6e, 0xfe, 0x05, 0x58, 0xd2, 0x7d, 0x3c, 0x12, 0x17, 0x5e, 0x20, 0x1c, 0xbe, 0x0d,
0xcb, 0x51, 0x72, 0x5f, 0x21, 0x73, 0x1c, 0x4c, 0x9e, 0xe6, 0xed, 0x31, 0x24, 0x7b, 0x82, 0x28,
0xf6, 0x6a, 0xf2, 0x69, 0x70, 0x9c, 0x93, 0x7f, 0x26, 0x68, 0x97, 0x2d, 0x92, 0xc7, 0x25, 0xac,
0x6f, 0xc2, 0x2a, 0x29, 0x2f, 0xdd, 0x19, 0x6d, 0x31, 0xa3, 0x3c, 0x68, 0xbd, 0xbb, 0x15, 0xcb,
0x83, 0x69, 0x5a, 0x7f, 0x58, 0x06, 0x48, 0x13, 0x01, 0x33, 0xb6, 0xf9, 0x2c, 0x23, 0x68, 0x2a,
0x2d, 0x57, 0x78, 0xe9, 0xb4, 0xdc, 0x7b, 0x89, 0xe1, 0xae, 0x23, 0xb2, 0x93, 0x35, 0xba, 0x69,
0x9f, 0x26, 0xcd, 0xf5, 0xb1, 0xb2, 0x8f, 0xd2, 0x64, 0xd9, 0xc7, 0xdd, 0xe9, 0x1a, 0xb1, 0x09,
0xfd, 0x93, 0x06, 0x19, 0x2a, 0x63, 0x41, 0x86, 0x26, 0x54, 0x43, 0x29, 0x9c, 0xc0, 0xf7, 0x2e,
0xe2, 0xec, 0x4f, 0xdc, 0xe6, 0x6f, 0x42, 0x49, 0xd1, 0x95, 0x8b, 0x2a, 0x6d, 0x97, 0x17, 0x2c,
0x9c, 0xc6, 0x45, 0x65, 0xe6, 0x46, 0xa6, 0xb0, 0x4b, 0x9f, 0x60, 0x55, 0x3b, 0x03, 0xe1, 0x6b,
0xc0, 0x5d, 0xf4, 0xb8, 0x3c, 0x4f, 0x3a, 0x1b, 0x17, 0x5b, 0x3a, 0x29, 0x43, 0xa7, 0x66, 0xd5,
0x9e, 0xf1, 0x26, 0x5e, 0xff, 0xc5, 0x74, 0xfd, 0xa9, 0xcb, 0x67, 0x6e, 0x84, 0x23, 0x5d, 0x22,
0xe3, 0x20, 0x69, 0xe3, 0xb9, 0x1c, 0xef, 0x51, 0x3d, 0x97, 0x24, 0xbd, 0x69, 0x66, 0xf3, 0x92,
0xb7, 0xd6, 0xdf, 0xcb, 0x27, 0x0e, 0x4e, 0x0d, 0x4a, 0xc7, 0x22, 0x72, 0xbb, 0xda, 0x79, 0x35,
0x07, 0xbf, 0x76, 0x72, 0x54, 0xe0, 0x04, 0x2c, 0x8f, 0xde, 0x4b, 0x24, 0xd1, 0x4f, 0x59, 0x06,
0x48, 0xaf, 0xa1, 0xb0, 0x22, 0xee, 0xcd, 0x78, 0xbd, 0x75, 0x7d, 0x06, 0x91, 0x52, 0xbc, 0xcb,
0x49, 0x2a, 0xdf, 0xc8, 0x73, 0x25, 0xdd, 0xcf, 0xaa, 0x88, 0xe3, 0x07, 0x4a, 0xea, 0x68, 0x1f,
0x49, 0x27, 0x03, 0x64, 0x13, 0xd7, 0x55, 0xb3, 0x3a, 0xba, 0x13, 0x31, 0x53, 0x1d, 0xa2, 0x8b,
0xc8, 0xb5, 0x5a, 0xc4, 0xdd, 0x39, 0xfe, 0x82, 0x2d, 0x61, 0x8f, 0xd2, 0xdb, 0x2d, 0x6c, 0x19,
0xb9, 0x0a, 0xaa, 0x1a, 0x58, 0xc1, 0xc7, 0x33, 0xaa, 0x25, 0x60, 0xf8, 0x55, 0x07, 0x15, 0xc6,
0x2a, 0xf6, 0x2c, 0x31, 0x0d, 0x18, 0x47, 0x6f, 0x69, 0x28, 0xd0, 0x75, 0x71, 0x87, 0xc2, 0x57,
0xec, 0x1a, 0x0e, 0x75, 0xe8, 0x9c, 0xb0, 0xeb, 0xd6, 0x8f, 0xd2, 0xba, 0xd2, 0xd7, 0x13, 0x87,
0x61, 0x1e, 0x01, 0xbe, 0xcc, 0xa5, 0xd8, 0x86, 0xd5, 0x50, 0x7e, 0x7f, 0xe4, 0x8e, 0x15, 0x4d,
0x17, 0xae, 0x4e, 0xf7, 0x4f, 0x53, 0x58, 0x67, 0xb0, 0x1a, 0x37, 0x9e, 0xba, 0xaa, 0x4f, 0x61,
0x17, 0xfe, 0x66, 0xa6, 0xaa, 0x3b, 0x37, 0xf3, 0x36, 0x4c, 0xc2, 0x32, 0xad, 0xe2, 0x4e, 0x42,
0xdf, 0xf9, 0x39, 0x42, 0xdf, 0xd6, 0xff, 0x2c, 0x67, 0x22, 0x2f, 0xda, 0x85, 0x72, 0x12, 0x17,
0x6a, 0x3a, 0xa1, 0x97, 0x46, 0xb3, 0xf3, 0x2f, 0x13, 0xcd, 0x9e, 0x95, 0x1c, 0xff, 0x00, 0xed,
0x63, 0xda, 0x1b, 0x4f, 0xe6, 0x88, 0xd4, 0x8f, 0xe1, 0xf2, 0x0d, 0x4a, 0xcf, 0x89, 0xb6, 0xae,
0xdc, 0x28, 0xcd, 0xbc, 0x63, 0x91, 0xcd, 0xc3, 0x19, 0x4c, 0x3b, 0x43, 0x95, 0xd1, 0x24, 0xe5,
0x59, 0x9a, 0x04, 0xbd, 0x59, 0xa3, 0x63, 0x92, 0xb6, 0x4e, 0x6c, 0xe8, 0xe7, 0x98, 0x3d, 0xa5,
0x65, 0xab, 0xf6, 0x14, 0x1c, 0x2d, 0xac, 0xc1, 0xc8, 0x53, 0xae, 0x89, 0xdd, 0xeb, 0xc6, 0xe4,
0x25, 0xb0, 0xda, 0xf4, 0x25, 0xb0, 0x8f, 0x00, 0x22, 0x89, 0x92, 0xbf, 0xe5, 0x76, 0x95, 0xa9,
0xef, 0xb8, 0x73, 0xd9, 0xd8, 0x4c, 0xc6, 0x21, 0x43, 0x81, 0xfd, 0x1f, 0x88, 0xf3, 0x4d, 0xb4,
0xb4, 0x4d, 0x22, 0x3a, 0x69, 0x4f, 0xea, 0xd7, 0xe5, 0x69, 0xfd, 0xfa, 0x26, 0x94, 0xa2, 0x6e,
0x30, 0x94, 0x74, 0x8f, 0xe1, 0xf2, 0xf5, 0x5d, 0x6b, 0x23, 0x92, 0xad, 0x71, 0x29, 0xbe, 0x87,
0x1a, 0x28, 0x08, 0xe9, 0x06, 0x43, 0xcd, 0x8e, 0x9b, 0x63, 0x3a, 0xee, 0xe6, 0xb8, 0x8e, 0x6b,
0x3a, 0x50, 0x36, 0xb1, 0xfa, 0x19, 0xae, 0x3b, 0x45, 0xf9, 0xf2, 0x99, 0x28, 0x5f, 0x52, 0x45,
0x58, 0xc8, 0x56, 0x11, 0x4e, 0x5c, 0x72, 0x2a, 0x4d, 0x5d, 0x72, 0xb2, 0x3e, 0x83, 0x12, 0xf5,
0x15, 0x0d, 0x04, 0x3d, 0xcd, 0xda, 0x7e, 0xc4, 0x41, 0xb1, 0x1c, 0xbf, 0x0e, 0x2c, 0x92, 0x64,
0x60, 0xc8, 0xb6, 0x18, 0x48, 0x52, 0x80, 0x79, 0xde, 0x80, 0xeb, 0x1a, 0x37, 0x1a, 0x7f, 0x43,
0x56, 0x8e, 0xe7, 0x1e, 0x87, 0x22, 0xbc, 0x60, 0x45, 0xeb, 0x23, 0x4a, 0xe5, 0xc6, 0x02, 0x55,
0x4f, 0x2e, 0x95, 0x69, 0x95, 0xeb, 0xc8, 0x10, 0x4f, 0x0a, 0x9d, 0x81, 0x37, 0xbe, 0x8f, 0xae,
0x4b, 0x22, 0xe7, 0x82, 0x22, 0x34, 0x8b, 0xd9, 0x53, 0xf6, 0xcf, 0x6c, 0xbf, 0x59, 0x1b, 0x19,
0x33, 0x6d, 0xbc, 0x10, 0x29, 0x37, 0x6f, 0x21, 0x92, 0xf5, 0x29, 0xac, 0xd8, 0xe3, 0xfa, 0x9a,
0xbf, 0x07, 0x95, 0x60, 0x98, 0xe5, 0xf3, 0x22, 0xb9, 0x8c, 0xd1, 0xad, 0x9f, 0xe6, 0x60, 0xb1,
0xe5, 0x2b, 0x19, 0xfa, 0xc2, 0xdb, 0xf1, 0x44, 0x8f, 0xbf, 0x1b, 0x6b, 0xa9, 0xd9, 0xbe, 0x75,
0x16, 0x77, 0x5c, 0x61, 0x79, 0x26, 0x26, 0xcd, 0x6f, 0xc0, 0xaa, 0x74, 0x5c, 0x15, 0x84, 0xda,
0x38, 0x8d, 0xeb, 0xc1, 0xae, 0x03, 0xd3, 0xe0, 0x36, 0x6d, 0x89, 0x8e, 0x5e, 0xe6, 0x06, 0x5c,
0x1f, 0x83, 0xc6, 0x96, 0x67, 0x9e, 0xdf, 0x86, 0x46, 0x7a, 0xd2, 0x6c, 0x05, 0xbe, 0x6a, 0xf9,
0x8e, 0x3c, 0x27, 0x33, 0x87, 0x15, 0xac, 0xdf, 0xab, 0xc4, 0x06, 0xd6, 0x13, 0x53, 0x2d, 0x16,
0x06, 0x41, 0x7a, 0xa3, 0xd0, 0xb4, 0x32, 0x37, 0x57, 0xf3, 0x73, 0xdc, 0x5c, 0xfd, 0x28, 0xbd,
0x7d, 0xa8, 0x0f, 0x8a, 0xaf, 0xcc, 0x3c, 0x7d, 0xa8, 0xc8, 0xc5, 0x98, 0xd4, 0x6d, 0x99, 0xb9,
0x8a, 0xf8, 0x86, 0xf1, 0xa3, 0x8a, 0xf3, 0xd8, 0xa1, 0x3a, 0xf9, 0xfe, 0xf6, 0x64, 0xad, 0xfc,
0x7c, 0xc5, 0x68, 0x53, 0xa6, 0x22, 0xbc, 0xb4, 0xa9, 0xf8, 0xf1, 0x84, 0xcb, 0x52, 0x9d, 0x19,
0xd5, 0xba, 0xe2, 0x42, 0xdf, 0xc7, 0x50, 0xe9, 0xbb, 0x91, 0x0a, 0x42, 0x7d, 0xc9, 0x74, 0xfa,
0x52, 0x4c, 0x66, 0xb6, 0x76, 0x35, 0x22, 0x55, 0x06, 0xc5, 0x54, 0xfc, 0xbb, 0xb0, 0x4a, 0x13,
0x7f, 0x94, 0x5a, 0x04, 0x51, 0xa3, 0x3e, 0xb3, 0x22, 0x2b, 0xc3, 0x6a, 0x63, 0x82, 0xc4, 0x9e,
0x66, 0xd2, 0xec, 0x01, 0xa4, 0xeb, 0x33, 0xa5, 0xc5, 0xbe, 0xc0, 0x25, 0xd3, 0x9b, 0x50, 0x8e,
0x46, 0xc7, 0x69, 0xf2, 0xca, 0xb4, 0x9a, 0xe7, 0xd0, 0x9c, 0xb2, 0x0e, 0x8e, 0x64, 0xa8, 0xbb,
0x7b, 0xe5, 0x4d, 0xd7, 0x8f, 0xb2, 0x0b, 0xaf, 0x85, 0xf3, 0xee, 0x25, 0xab, 0x97, 0x70, 0xce,
0x48, 0x40, 0xf3, 0x6d, 0xa8, 0x67, 0x26, 0x15, 0x35, 0xf3, 0xc8, 0x77, 0x82, 0x38, 0x28, 0x8b,
0xcf, 0xfa, 0x8a, 0x90, 0x13, 0x87, 0x65, 0xe9, 0xb9, 0x69, 0x03, 0x9b, 0x9c, 0xc0, 0x2b, 0xdc,
0xda, 0xaf, 0xc0, 0x52, 0xc6, 0x5c, 0x4b, 0xc2, 0x5f, 0xe3, 0x40, 0xeb, 0x0c, 0x5e, 0xc9, 0xb0,
0x3b, 0x92, 0xe1, 0xc0, 0x8d, 0xf0, 0x20, 0xd1, 0xee, 0x1a, 0x45, 0x26, 0x1c, 0xe9, 0x2b, 0x57,
0xc5, 0x1a, 0x34, 0x69, 0xf3, 0x5f, 0x87, 0xd2, 0x50, 0x86, 0x83, 0xc8, 0x68, 0xd1, 0x49, 0x09,
0x9a, 0xc9, 0x36, 0xb2, 0x35, 0x8d, 0xf5, 0x0f, 0x73, 0x50, 0xdd, 0x97, 0x4a, 0xa0, 0xed, 0xc0,
0xf7, 0x27, 0xbe, 0x32, 0x9d, 0x70, 0x8d, 0x51, 0xd7, 0x8c, 0x03, 0xb9, 0xd6, 0x32, 0xf8, 0xa6,
0xbd, 0xbb, 0x90, 0x76, 0xac, 0xb9, 0x01, 0x15, 0x03, 0x6e, 0xbe, 0x0b, 0x2b, 0x13, 0x98, 0x34,
0x2f, 0xda, 0x6e, 0x6f, 0x5f, 0x0c, 0xe2, 0xda, 0x9c, 0x45, 0x7b, 0x1c, 0xb8, 0x51, 0x83, 0xca,
0x50, 0x13, 0x58, 0xff, 0xfa, 0x06, 0xd5, 0x8b, 0xb8, 0x27, 0xe8, 0x48, 0xcf, 0x3a, 0x59, 0xef,
0x00, 0xd0, 0xd1, 0xac, 0xab, 0x0a, 0x74, 0x48, 0x32, 0x03, 0xe1, 0x1f, 0x24, 0x21, 0xeb, 0xe2,
0x4c, 0xa3, 0x2a, 0xcb, 0x7c, 0x32, 0x6e, 0xdd, 0x80, 0x8a, 0x1b, 0xed, 0xe1, 0xd1, 0x66, 0x6a,
0x6d, 0xe2, 0x26, 0xff, 0x16, 0x94, 0xdd, 0xc1, 0x30, 0x08, 0x95, 0x89, 0x69, 0x5f, 0xc9, 0xb5,
0x45, 0x98, 0xbb, 0x0b, 0xb6, 0xa1, 0x41, 0x6a, 0x79, 0x4e, 0xd4, 0xd5, 0x17, 0x53, 0x6f, 0x9f,
0xc7, 0xd4, 0x9a, 0x86, 0x7f, 0x07, 0x96, 0x7a, 0xba, 0xfa, 0x4d, 0x33, 0x36, 0x4a, 0xe4, 0xeb,
0x57, 0x31, 0x79, 0x94, 0x25, 0xd8, 0x5d, 0xb0, 0xc7, 0x39, 0x20, 0x4b, 0x34, 0xe0, 0x65, 0xa4,
0x3a, 0xc1, 0x27, 0x81, 0xeb, 0x93, 0xc3, 0xf9, 0x02, 0x96, 0x76, 0x96, 0x00, 0x59, 0x8e, 0x71,
0xe0, 0xef, 0xa0, 0xc5, 0x13, 0x29, 0x73, 0xcf, 0xf7, 0xee, 0x55, 0x9c, 0x3a, 0x32, 0x32, 0x37,
0x74, 0x23, 0xc5, 0xcf, 0xa1, 0x99, 0xd9, 0x24, 0xe6, 0x23, 0xeb, 0xc3, 0x61, 0x18, 0xa0, 0xd7,
0xba, 0x44, 0xdc, 0xde, 0xb9, 0x8a, 0xdb, 0xd1, 0xa5, 0xd4, 0xbb, 0x0b, 0xf6, 0x15, 0xbc, 0x79,
0x07, 0xbd, 0x36, 0x33, 0x84, 0x3d, 0x29, 0xce, 0xe2, 0x5b, 0xc2, 0xf7, 0xe7, 0x9a, 0x05, 0xa2,
0xd8, 0x5d, 0xb0, 0x27, 0x78, 0xf0, 0xdf, 0x84, 0xd5, 0xb1, 0x6f, 0xd2, 0x8d, 0x42, 0x7d, 0x87,
0xf8, 0x9b, 0x73, 0x0f, 0x03, 0x89, 0x76, 0x17, 0xec, 0x69, 0x4e, 0x7c, 0x04, 0x5f, 0x9a, 0x1e,
0xd2, 0x96, 0xec, 0x7a, 0xae, 0x2f, 0xcd, 0x75, 0xe3, 0xb7, 0x5f, 0x6e, 0xb6, 0x0c, 0xf1, 0xee,
0x82, 0x7d, 0x39, 0x67, 0xfe, 0x97, 0xe0, 0xf6, 0x70, 0xa6, 0x8a, 0xd1, 0xaa, 0xcb, 0xdc, 0x56,
0x7e, 0x6f, 0xce, 0x2f, 0x4f, 0xd1, 0xef, 0x2e, 0xd8, 0x57, 0xf2, 0x47, 0xdb, 0x99, 0xbc, 0x63,
0x53, 0xa4, 0xab, 0x1b, 0xfc, 0x36, 0xd4, 0x44, 0xd7, 0xdb, 0x95, 0xc2, 0x49, 0xa2, 0xe7, 0x29,
0xa0, 0xf9, 0x5f, 0x73, 0x50, 0x36, 0xf2, 0x7e, 0x3b, 0x49, 0xb0, 0x27, 0xaa, 0x3b, 0x05, 0xf0,
0x0f, 0xa1, 0x26, 0xc3, 0x30, 0x08, 0x37, 0x03, 0x27, 0xae, 0x10, 0x9c, 0x0c, 0xed, 0x6a, 0x3e,
0x6b, 0xdb, 0x31, 0x9a, 0x9d, 0x52, 0xf0, 0x0f, 0x00, 0xf4, 0x3e, 0xef, 0xa4, 0x77, 0x2d, 0x9a,
0xb3, 0xe9, 0x75, 0x8a, 0x25, 0xc5, 0x4e, 0x03, 0x63, 0x71, 0x7e, 0x23, 0x6e, 0x26, 0x0e, 0x67,
0x29, 0xe3, 0x70, 0xde, 0x36, 0x31, 0x82, 0x03, 0x7c, 0x61, 0x6e, 0x1c, 0x25, 0x80, 0xe6, 0xbf,
0xca, 0x41, 0x59, 0x2b, 0x0f, 0xbe, 0x3d, 0x3d, 0xa2, 0xaf, 0xbd, 0x58, 0xe7, 0xac, 0x4d, 0x8e,
0xec, 0x5b, 0x00, 0x5a, 0x07, 0x65, 0x46, 0x76, 0x7b, 0x82, 0x8f, 0x21, 0x8d, 0xcb, 0x44, 0x53,
0x7c, 0xeb, 0xa1, 0xbe, 0x15, 0x43, 0x71, 0xd8, 0xc7, 0x7b, 0x7b, 0x6c, 0x81, 0xaf, 0xc2, 0xd2,
0xe3, 0x83, 0x4f, 0x0f, 0x0e, 0x9f, 0x1e, 0x3c, 0xdb, 0xb6, 0xed, 0x43, 0x5b, 0x87, 0x63, 0x37,
0xd6, 0xb7, 0x9e, 0xb5, 0x0e, 0x8e, 0x1e, 0x77, 0x58, 0xbe, 0xf9, 0xb3, 0x1c, 0x2c, 0x8d, 0xe9,
0xae, 0xff, 0xbb, 0x4b, 0x97, 0x99, 0xfe, 0xc2, 0xec, 0xe9, 0x2f, 0x5e, 0x36, 0xfd, 0xa5, 0xc9,
0xe9, 0xff, 0x47, 0x39, 0x58, 0x1a, 0xd3, 0x91, 0x59, 0xee, 0xb9, 0x71, 0xee, 0xd9, 0x93, 0x3e,
0x3f, 0x71, 0xd2, 0x5b, 0xb0, 0x18, 0x3f, 0x1f, 0xa4, 0x11, 0x87, 0x31, 0x58, 0x16, 0x87, 0xca,
0xd2, 0x8b, 0xe3, 0x38, 0x54, 0x9a, 0x7e, 0x75, 0x6f, 0xe9, 0x1a, 0x5e, 0x44, 0xb7, 0x94, 0x9b,
0x97, 0x6b, 0xd0, 0x2b, 0x86, 0xf0, 0x08, 0xea, 0xc3, 0x74, 0x9b, 0xbe, 0x9c, 0x59, 0x92, 0xa5,
0x7c, 0x41, 0x3f, 0x7f, 0x92, 0x83, 0xe5, 0x71, 0x9d, 0xfb, 0xff, 0xf5, 0xb4, 0xfe, 0x61, 0x0e,
0x56, 0xa7, 0x34, 0xf9, 0x95, 0x86, 0xdd, 0x64, 0xbf, 0xf2, 0x73, 0xf4, 0xab, 0x30, 0xa3, 0x5f,
0x97, 0x6b, 0x92, 0xab, 0x7b, 0xdc, 0x86, 0x2f, 0x5d, 0x7a, 0x26, 0x5c, 0x31, 0xd5, 0x63, 0x4c,
0x0b, 0x93, 0x4c, 0xff, 0x7e, 0x0e, 0x6e, 0x5f, 0xa5, 0xef, 0xff, 0x9f, 0xcb, 0xd5, 0x64, 0x0f,
0xad, 0x77, 0x93, 0x44, 0x79, 0x1d, 0x2a, 0xe6, 0x4f, 0x7c, 0x4c, 0x6d, 0x72, 0x3f, 0x78, 0xee,
0xeb, 0x28, 0xb3, 0x2d, 0x85, 0xb9, 0x1f, 0x6d, 0xcb, 0xa1, 0xe7, 0x52, 0x62, 0xf2, 0x16, 0xc0,
0x3a, 0xf9, 0x75, 0xf1, 0x75, 0x85, 0xcd, 0xbd, 0xc3, 0xf6, 0x36, 0x5b, 0xc8, 0x1a, 0xb1, 0x9f,
0xc5, 0x8a, 0xd8, 0x3a, 0x82, 0x72, 0x5a, 0xc8, 0xbe, 0x2f, 0xc2, 0x53, 0x47, 0xa7, 0xff, 0x16,
0xa1, 0x7a, 0x64, 0x5c, 0x28, 0xfd, 0xa9, 0x4f, 0xda, 0x87, 0x07, 0x3a, 0xa0, 0xbd, 0x75, 0xd8,
0xd1, 0xe5, 0xf0, 0xed, 0x27, 0x8f, 0x74, 0x1e, 0xea, 0x91, 0xbd, 0x7e, 0xb4, 0xfb, 0x8c, 0x30,
0x4a, 0xd6, 0xcf, 0xf2, 0xf1, 0xa9, 0x66, 0xd9, 0x26, 0xb1, 0x08, 0x50, 0x46, 0x6d, 0x1e, 0x18,
0xc6, 0xc9, 0x67, 0xa8, 0x42, 0x76, 0xfb, 0x5c, 0xc7, 0x21, 0x58, 0x9e, 0x97, 0x21, 0x7f, 0x74,
0xac, 0x8b, 0x63, 0x77, 0xd5, 0xc0, 0xd3, 0xf7, 0xd7, 0x3a, 0xe7, 0x8a, 0x95, 0xf0, 0x61, 0x33,
0x3a, 0x63, 0x65, 0xeb, 0x3f, 0xe6, 0xa0, 0x96, 0xa8, 0xca, 0x97, 0x51, 0xdd, 0x9c, 0xc3, 0x72,
0xeb, 0xa0, 0xb3, 0x6d, 0x1f, 0xac, 0xef, 0x19, 0x94, 0x02, 0x6f, 0xc0, 0xf5, 0x83, 0xc3, 0x67,
0x87, 0x1b, 0x9f, 0x6c, 0x6f, 0x76, 0xda, 0xcf, 0x3a, 0x87, 0xcf, 0x5a, 0xfb, 0x47, 0x87, 0x76,
0x87, 0x95, 0xf8, 0x4d, 0xe0, 0xfa, 0xf9, 0x59, 0xab, 0xfd, 0x6c, 0x73, 0xfd, 0x60, 0x73, 0x7b,
0x6f, 0x7b, 0x8b, 0x95, 0xf9, 0xd7, 0xe0, 0xd7, 0xf6, 0x5a, 0xfb, 0xad, 0xce, 0xb3, 0xc3, 0x9d,
0x67, 0xf6, 0xe1, 0xd3, 0xf6, 0xb3, 0x43, 0xfb, 0x99, 0xbd, 0xbd, 0xb7, 0xde, 0x69, 0x1d, 0x1e,
0xb4, 0x9f, 0x6d, 0x7f, 0x77, 0x73, 0x7b, 0x7b, 0x6b, 0x7b, 0x8b, 0x55, 0xf8, 0x35, 0x58, 0xd9,
0x69, 0xed, 0x6d, 0x3f, 0xdb, 0x3b, 0x5c, 0xdf, 0x32, 0xdf, 0xab, 0xf2, 0xdb, 0xd0, 0x68, 0x1d,
0xb4, 0x1f, 0xef, 0xec, 0xb4, 0x36, 0x5b, 0xdb, 0x07, 0x9d, 0x67, 0x47, 0xdb, 0xf6, 0x7e, 0xab,
0xdd, 0x46, 0x5a, 0x56, 0xb3, 0xbe, 0x0d, 0xe5, 0x96, 0x7f, 0xe6, 0x2a, 0x12, 0x3f, 0xb3, 0x56,
0xc6, 0x21, 0x89, 0x9b, 0x24, 0x35, 0x6e, 0xcf, 0xa7, 0x6b, 0xcb, 0x24, 0x7c, 0x8b, 0x76, 0x0a,
0xb0, 0xfe, 0x49, 0x1e, 0x96, 0x34, 0x8b, 0xd8, 0xc1, 0xb9, 0x07, 0x2b, 0x26, 0x52, 0xd8, 0x1a,
0xdf, 0xe1, 0x93, 0x60, 0xfa, 0x5b, 0x1f, 0x0d, 0xca, 0xec, 0xf3, 0x2c, 0x88, 0x32, 0x4b, 0xc4,
0x1c, 0x1d, 0x25, 0x9d, 0x53, 0x4b, 0x01, 0x5f, 0x74, 0x83, 0xa3, 0xf2, 0xd0, 0x88, 0xdd, 0xc0,
0xdf, 0x4c, 0x2e, 0x0b, 0x8c, 0xc1, 0xf8, 0x67, 0x70, 0x2b, 0x69, 0x6f, 0xfb, 0xdd, 0xf0, 0x62,
0x98, 0xfc, 0xfb, 0x56, 0x65, 0xa6, 0xc7, 0xbd, 0xe3, 0x7a, 0x72, 0x0c, 0xd1, 0xbe, 0x8c, 0x81,
0xf5, 0xc7, 0xb9, 0x8c, 0x5b, 0xa8, 0xdd, 0xbe, 0x2b, 0x15, 0xe2, 0xac, 0x14, 0x05, 0x3a, 0x66,
0xa6, 0xfb, 0xe6, 0x9c, 0x36, 0x4d, 0x7e, 0x04, 0xdc, 0x9d, 0xee, 0x74, 0x71, 0xce, 0x4e, 0xcf,
0xa0, 0x9d, 0x8c, 0x30, 0x97, 0xa6, 0x23, 0xcc, 0x77, 0x00, 0x7a, 0x5e, 0x70, 0x2c, 0xbc, 0x8c,
0x1d, 0x96, 0x81, 0x58, 0x1e, 0x54, 0xe3, 0xff, 0xf8, 0xe2, 0x37, 0xa1, 0x4c, 0xff, 0xf2, 0x95,
0xc4, 0xdb, 0x74, 0x8b, 0xef, 0xc2, 0xb2, 0x1c, 0xef, 0x73, 0x7e, 0xce, 0x3e, 0x4f, 0xd0, 0x59,
0xef, 0xc3, 0xea, 0x14, 0x12, 0x4e, 0xe2, 0x50, 0xa8, 0xe4, 0x86, 0x30, 0x3e, 0x4f, 0xe7, 0x6f,
0xad, 0x7f, 0x9f, 0x87, 0xc5, 0x7d, 0xe1, 0xbb, 0x27, 0x32, 0x52, 0x71, 0x6f, 0xa3, 0x6e, 0x5f,
0x0e, 0x44, 0xdc, 0x5b, 0xdd, 0x32, 0x4e, 0x78, 0x7e, 0xaa, 0x32, 0x2d, 0x9b, 0x0d, 0xb9, 0x09,
0x65, 0x31, 0x52, 0xfd, 0xa4, 0x36, 0xda, 0xb4, 0x70, 0xed, 0x3c, 0xb7, 0x2b, 0xfd, 0x28, 0x96,
0xcd, 0xb8, 0x99, 0x56, 0x70, 0x94, 0xaf, 0xa8, 0xe0, 0xa8, 0x4c, 0xcf, 0xff, 0x5d, 0xa8, 0x47,
0xdd, 0x50, 0x4a, 0x3f, 0xea, 0x07, 0x2a, 0xfe, 0x7f, 0xb8, 0x2c, 0x88, 0x2a, 0x97, 0x82, 0xe7,
0x3e, 0xee, 0xd0, 0x3d, 0xd7, 0x3f, 0x35, 0xe5, 0x3b, 0x63, 0x30, 0x94, 0x41, 0x0a, 0x41, 0xb8,
0x3f, 0x90, 0xe4, 0xfe, 0x96, 0xec, 0xa4, 0x4d, 0x41, 0x06, 0xa1, 0x64, 0x2f, 0x08, 0x5d, 0xa9,
0x23, 0x6d, 0x35, 0x3b, 0x03, 0x41, 0x5a, 0x4f, 0xf8, 0xbd, 0x91, 0xe8, 0x49, 0x93, 0x0f, 0x4d,
0xda, 0xd6, 0x7f, 0x2b, 0x01, 0xec, 0xcb, 0xc1, 0xb1, 0x0c, 0xa3, 0xbe, 0x3b, 0xa4, 0x4c, 0x80,
0x6b, 0x8a, 0x48, 0x97, 0x6c, 0x7a, 0xe6, 0xef, 0x8d, 0x15, 0x6b, 0x4f, 0xe7, 0xee, 0x52, 0xf2,
0xc9, 0x08, 0x05, 0x4e, 0x8e, 0x50, 0xd2, 0x14, 0xcf, 0xd0, 0xfc, 0x17, 0xed, 0x2c, 0x88, 0xea,
0x9a, 0x84, 0x92, 0xdb, 0xbe, 0xa3, 0x23, 0x20, 0x45, 0x3b, 0x69, 0xd3, 0x95, 0x8c, 0x68, 0x7d,
0xa4, 0x02, 0x5b, 0xfa, 0xf2, 0x79, 0x72, 0x9f, 0x28, 0x05, 0xf1, 0x7d, 0x58, 0x1a, 0x8a, 0x8b,
0x81, 0xf4, 0xd5, 0xbe, 0x54, 0xfd, 0xc0, 0x31, 0x95, 0x2e, 0x5f, 0xbb, 0xbc, 0x83, 0x47, 0x59,
0x74, 0x7b, 0x9c, 0x1a, 0x65, 0xc2, 0x8f, 0x68, 0x97, 0xe8, 0x65, 0x34, 0x2d, 0xbe, 0x01, 0xa0,
0x9f, 0xc8, 0xb1, 0xa8, 0xce, 0x0e, 0xd4, 0x88, 0x81, 0x8c, 0x64, 0x78, 0xe6, 0x6a, 0x3d, 0xa6,
0x5d, 0xa7, 0x94, 0x0a, 0xb5, 0xde, 0x28, 0x92, 0xe1, 0xf6, 0x40, 0xb8, 0x9e, 0x59, 0xe0, 0x14,
0xc0, 0xdf, 0x82, 0x1b, 0xd1, 0xe8, 0x18, 0x65, 0xe6, 0x58, 0x76, 0x82, 0x03, 0xf9, 0x3c, 0xf2,
0xa4, 0x52, 0x32, 0x34, 0xa9, 0xf5, 0xd9, 0x2f, 0xad, 0x5e, 0x62, 0x15, 0xd0, 0x9f, 0x18, 0xe0,
0x53, 0x5a, 0xb2, 0x93, 0x80, 0x4c, 0x3d, 0x13, 0xcb, 0x71, 0x06, 0x8b, 0x1a, 0x64, 0xca, 0x9d,
0xf2, 0xfc, 0xab, 0xf0, 0xe5, 0x31, 0x24, 0x5b, 0xe7, 0x49, 0xa3, 0x1d, 0xd7, 0x17, 0x9e, 0xfb,
0x03, 0x9d, 0x91, 0x2e, 0x58, 0x43, 0x58, 0x1a, 0x9b, 0x38, 0x3c, 0xe6, 0xf5, 0x93, 0x29, 0x00,
0x61, 0xb0, 0xa8, 0xdb, 0x6d, 0x15, 0xba, 0x94, 0x00, 0x48, 0x20, 0x9b, 0xb8, 0xcf, 0x03, 0x96,
0xe7, 0xd7, 0x81, 0x69, 0x48, 0xcb, 0x17, 0xc3, 0xe1, 0xfa, 0x70, 0xe8, 0x49, 0x56, 0xa0, 0x7b,
0x77, 0x29, 0x54, 0x97, 0x6c, 0xb3, 0xa2, 0xf5, 0x5d, 0xb8, 0x45, 0x33, 0xf3, 0x44, 0x86, 0x89,
0xdf, 0x67, 0xc6, 0x7a, 0x03, 0x56, 0xf5, 0xd3, 0x41, 0xa0, 0xf4, 0x6b, 0xb2, 0x85, 0x38, 0x2c,
0x6b, 0x30, 0x9a, 0x02, 0x6d, 0xe9, 0x2b, 0x5d, 0x87, 0xa2, 0x61, 0x09, 0x5e, 0xde, 0xfa, 0x69,
0x19, 0x78, 0x2a, 0x10, 0x1d, 0x57, 0x86, 0x5b, 0x42, 0x89, 0x4c, 0xe0, 0x6e, 0xe9, 0xd2, 0xd4,
0xf3, 0x8b, 0xab, 0xb5, 0x6e, 0x42, 0xd9, 0x8d, 0xd0, 0x53, 0x31, 0xd5, 0x91, 0xa6, 0xc5, 0xf7,
0x00, 0x86, 0x32, 0x74, 0x03, 0x87, 0x24, 0xa8, 0x34, 0xb3, 0x66, 0x7e, 0xba, 0x53, 0x6b, 0x47,
0x09, 0x8d, 0x9d, 0xa1, 0xc7, 0x7e, 0xe8, 0x96, 0x4e, 0xe4, 0x96, 0xa9, 0xd3, 0x59, 0x10, 0x7f,
0x1d, 0xae, 0x0d, 0x43, 0xb7, 0x2b, 0xf5, 0x72, 0x3c, 0x8e, 0x9c, 0x4d, 0xfa, 0x07, 0xaf, 0x0a,
0x61, 0xce, 0x7a, 0x85, 0x12, 0x28, 0x7c, 0xb2, 0xdf, 0x23, 0x4a, 0x5d, 0x9a, 0x7b, 0x9f, 0xba,
0xda, 0x70, 0xc9, 0x9e, 0xfd, 0x92, 0xdf, 0x07, 0x66, 0x5e, 0xec, 0xbb, 0xfe, 0x9e, 0xf4, 0x7b,
0xaa, 0x4f, 0xc2, 0xbd, 0x64, 0x4f, 0xc1, 0x49, 0x83, 0xe9, 0x3f, 0x58, 0xd1, 0x69, 0x8d, 0x9a,
0x9d, 0xb4, 0xf5, 0x5d, 0x62, 0x2f, 0x08, 0xdb, 0x2a, 0x34, 0x85, 0x90, 0x49, 0x1b, 0x6d, 0x96,
0x88, 0xfa, 0x7a, 0x14, 0x06, 0xce, 0x88, 0x82, 0xee, 0x5a, 0x89, 0x4d, 0x82, 0x53, 0xcc, 0x7d,
0xe1, 0x9b, 0x92, 0xb9, 0xa5, 0x2c, 0x66, 0x02, 0x26, 0x17, 0x25, 0x88, 0x52, 0x86, 0x2b, 0xc6,
0x45, 0xc9, 0xc0, 0x0c, 0x4e, 0xca, 0x8a, 0x25, 0x38, 0x29, 0x1f, 0x1a, 0xbf, 0x13, 0x06, 0xae,
0x93, 0xf2, 0x5a, 0xd5, 0x05, 0x8d, 0x93, 0xf0, 0x0c, 0x6e, 0xca, 0x93, 0x8f, 0xe1, 0x26, 0x70,
0xeb, 0x87, 0x39, 0x80, 0x74, 0xf1, 0x51, 0xe4, 0xd3, 0x56, 0xba, 0xc5, 0x6f, 0xc1, 0xb5, 0x2c,
0x98, 0x2a, 0xf1, 0x29, 0xff, 0xc9, 0x61, 0x39, 0x7d, 0xb1, 0x25, 0x2e, 0x22, 0x96, 0xd7, 0x95,
0x8d, 0x31, 0xec, 0xa9, 0x94, 0x54, 0x43, 0x76, 0x1d, 0x58, 0x0a, 0xa4, 0x5b, 0x53, 0x11, 0x2b,
0x8e, 0xa3, 0x7e, 0x4f, 0x8a, 0x30, 0x62, 0x25, 0x6b, 0x17, 0xca, 0x3a, 0xf7, 0x32, 0x23, 0x6b,
0xfa, 0x72, 0x25, 0x10, 0x7f, 0x3d, 0x07, 0xb0, 0xa5, 0x8b, 0x57, 0xf1, 0x14, 0x9f, 0xa7, 0x8e,
0x5c, 0xff, 0x6d, 0x07, 0x95, 0xf5, 0x16, 0x92, 0xbf, 0xed, 0xc0, 0x26, 0x4a, 0x8e, 0x88, 0x8b,
0x86, 0xf4, 0x9e, 0x4b, 0xda, 0xfa, 0x00, 0xd9, 0x0c, 0x7c, 0x5f, 0x76, 0xf1, 0xf8, 0x49, 0x0e,
0x90, 0x04, 0x74, 0xff, 0xc7, 0x05, 0x58, 0x1e, 0xcf, 0xdf, 0x51, 0x9d, 0xbf, 0xce, 0x1d, 0x1f,
0x7a, 0x4e, 0xa6, 0xf4, 0x91, 0xf1, 0x15, 0xa8, 0x1b, 0x8b, 0x90, 0x00, 0xab, 0xe4, 0x99, 0x04,
0x03, 0xc9, 0xee, 0x66, 0xff, 0x93, 0xea, 0x75, 0x74, 0x70, 0xf4, 0xd5, 0x09, 0x36, 0xe4, 0x35,
0xf3, 0xef, 0x1c, 0xbf, 0x95, 0xe7, 0x4b, 0x99, 0x02, 0xbc, 0x1f, 0xa3, 0x3a, 0x5c, 0xd9, 0x18,
0xf9, 0x8e, 0x27, 0x9d, 0x04, 0xfa, 0x0f, 0xb2, 0xd0, 0xa4, 0x9c, 0xee, 0xb7, 0xd0, 0xab, 0xaa,
0xb5, 0x47, 0xc7, 0xa6, 0x94, 0xee, 0x2f, 0x17, 0xf9, 0x4d, 0x58, 0x35, 0x58, 0x69, 0xdd, 0x0c,
0xfb, 0x2b, 0xb8, 0x70, 0xcb, 0xeb, 0x5a, 0xa7, 0x98, 0x8e, 0xb2, 0xbf, 0x5a, 0xc4, 0x2e, 0xd0,
0x9d, 0xbc, 0xdf, 0x26, 0x3e, 0x49, 0x51, 0x31, 0xfb, 0x9d, 0x22, 0x5f, 0x01, 0x68, 0x77, 0x92,
0x0f, 0xfd, 0x5e, 0x91, 0xd7, 0xa1, 0xdc, 0xee, 0x10, 0xb7, 0x1f, 0x16, 0xf9, 0x0d, 0x60, 0xe9,
0x5b, 0x53, 0x29, 0xf4, 0xfb, 0xba, 0x33, 0x49, 0xe9, 0xcf, 0x1f, 0x14, 0x71, 0x5c, 0xb1, 0xc1,
0xcc, 0xfe, 0x66, 0x91, 0x33, 0xa8, 0x67, 0xfc, 0x5d, 0xf6, 0xb7, 0x8a, 0x9c, 0xc3, 0xd2, 0x3e,
0xba, 0xb9, 0x7e, 0xcf, 0x8c, 0xe0, 0x77, 0xe9, 0xcb, 0x3b, 0x49, 0x5d, 0x34, 0xfb, 0x51, 0x91,
0xdf, 0x02, 0x9e, 0x8d, 0xf1, 0x99, 0x17, 0x7f, 0xbb, 0x78, 0xff, 0xa7, 0x14, 0x52, 0xc9, 0x66,
0xdd, 0xd1, 0x4f, 0xf4, 0x02, 0xbf, 0xa7, 0xf4, 0x5f, 0x77, 0x2d, 0x41, 0x2d, 0xea, 0x07, 0xa1,
0xa2, 0x26, 0xdd, 0x0c, 0xf1, 0xe9, 0x7a, 0x9f, 0x2e, 0x97, 0xd4, 0x96, 0x88, 0x76, 0x51, 0x95,
0xe8, 0xb1, 0x7a, 0x52, 0xc4, 0x54, 0x4c, 0x0a, 0xad, 0xe8, 0x9a, 0x61, 0x7c, 0x8d, 0x8b, 0x95,
0x11, 0x75, 0x14, 0x7a, 0xba, 0xe0, 0x4a, 0xe2, 0x29, 0xa4, 0xff, 0xa3, 0x67, 0xd8, 0xc7, 0xc3,
0xae, 0xa6, 0xa1, 0xc1, 0xe7, 0xae, 0xbe, 0x20, 0x64, 0x6a, 0x1c, 0x1c, 0xec, 0x47, 0x92, 0xc6,
0x63, 0xf2, 0xfe, 0x1f, 0xe4, 0x60, 0x31, 0xbe, 0x5c, 0xe7, 0xf6, 0x5c, 0x5f, 0x97, 0x6c, 0xc5,
0x7f, 0x88, 0xd6, 0xf5, 0xdc, 0x61, 0xfc, 0x07, 0x43, 0x2b, 0x50, 0x77, 0x42, 0xd1, 0x5b, 0xf7,
0x9d, 0xad, 0x30, 0x18, 0xea, 0x6e, 0xeb, 0xa0, 0xab, 0x2e, 0x15, 0x7b, 0x2e, 0x8f, 0x11, 0x7d,
0x28, 0x43, 0x56, 0xa4, 0xfa, 0x89, 0xbe, 0x08, 0x5d, 0xbf, 0x87, 0xae, 0xb2, 0x1f, 0xe9, 0x92,
0xb1, 0x3a, 0x54, 0x46, 0x91, 0xec, 0x8a, 0x48, 0xb2, 0x32, 0x36, 0x8e, 0x47, 0xae, 0xa7, 0x5c,
0x5f, 0xff, 0xaf, 0x4f, 0x52, 0x13, 0x56, 0xbd, 0xff, 0x2f, 0x73, 0x50, 0xa7, 0xc5, 0x4b, 0xa3,
0x09, 0xa9, 0x3a, 0xa9, 0x43, 0x65, 0x2f, 0xf9, 0x5f, 0x97, 0x32, 0xe4, 0x0f, 0x4f, 0x75, 0x34,
0xc1, 0x2c, 0x9e, 0xbe, 0x5f, 0xa3, 0xff, 0xe2, 0xa5, 0xc8, 0xbf, 0x04, 0x37, 0x6c, 0x39, 0x08,
0x94, 0x7c, 0x2a, 0x5c, 0x95, 0x2d, 0x97, 0x2e, 0xa1, 0xe5, 0xa1, 0x5f, 0xc5, 0xf5, 0xd1, 0x65,
0xb2, 0x3c, 0xf0, 0xb3, 0x31, 0xa4, 0x82, 0x83, 0x26, 0x88, 0x31, 0x45, 0xaa, 0x09, 0xca, 0x27,
0x81, 0xeb, 0xe3, 0xd7, 0xe8, 0x42, 0x16, 0x41, 0x28, 0x2c, 0x85, 0x20, 0xb8, 0x7f, 0x00, 0x37,
0x67, 0x07, 0x53, 0xf4, 0x55, 0x2d, 0xfa, 0x33, 0x41, 0x2a, 0xa0, 0x7d, 0x1a, 0xba, 0xfa, 0xda,
0x4e, 0x0d, 0x4a, 0x87, 0xcf, 0x7d, 0x92, 0x86, 0x55, 0x58, 0x3a, 0x08, 0x32, 0x34, 0xac, 0x70,
0xbf, 0x3b, 0x16, 0xff, 0x4a, 0x27, 0x25, 0xee, 0xc4, 0x42, 0xa6, 0x38, 0x3c, 0xa7, 0x23, 0x2b,
0xf4, 0xb7, 0xce, 0xfa, 0x1a, 0xab, 0x89, 0x3b, 0x39, 0xfa, 0x1a, 0x6b, 0xd2, 0xcd, 0xa2, 0xfe,
0xa3, 0x07, 0xbf, 0x2b, 0x3d, 0xe9, 0xb0, 0xd2, 0xfd, 0xf7, 0x60, 0xc5, 0x0c, 0xb5, 0x2b, 0xa3,
0x28, 0x2e, 0xae, 0x3e, 0x0a, 0xdd, 0x33, 0x7d, 0x55, 0x76, 0x11, 0xaa, 0x47, 0x32, 0x8c, 0x02,
0x9f, 0xae, 0x09, 0x03, 0x94, 0xdb, 0x7d, 0x11, 0xe2, 0x37, 0xee, 0x7f, 0x03, 0x6a, 0x54, 0x6c,
0xfd, 0xa9, 0xeb, 0x3b, 0x38, 0x92, 0x0d, 0x53, 0x5f, 0x58, 0x83, 0xd2, 0x66, 0x70, 0x46, 0xe3,
0xab, 0xea, 0xbf, 0x34, 0x63, 0xf9, 0xfb, 0x1f, 0x03, 0xd7, 0x7e, 0x9c, 0x23, 0xcf, 0x5d, 0xbf,
0x97, 0xdc, 0x1f, 0x04, 0xba, 0x0c, 0xec, 0xc8, 0x73, 0x32, 0x93, 0xea, 0x50, 0x89, 0x1b, 0xf1,
0x95, 0xe4, 0x9d, 0x60, 0xe4, 0xe3, 0xd7, 0x9e, 0xc0, 0x75, 0x2d, 0x1b, 0xf8, 0x79, 0xba, 0xd4,
0x71, 0xa9, 0x71, 0xa9, 0x2b, 0xe2, 0xd5, 0x28, 0x4a, 0x70, 0x59, 0x8e, 0xdf, 0x04, 0x9e, 0x18,
0x66, 0x29, 0x3c, 0x7f, 0xdf, 0x82, 0x6b, 0x33, 0xac, 0x63, 0xd2, 0x99, 0xda, 0x46, 0x60, 0x0b,
0x1b, 0xf7, 0xff, 0xe8, 0x17, 0x77, 0x72, 0x3f, 0xff, 0xc5, 0x9d, 0xdc, 0x7f, 0xfe, 0xc5, 0x9d,
0xdc, 0x0f, 0x7f, 0x79, 0x67, 0xe1, 0xe7, 0xbf, 0xbc, 0xb3, 0xf0, 0x1f, 0x7e, 0x79, 0x67, 0xe1,
0x33, 0x36, 0xf9, 0x97, 0xe8, 0xc7, 0x65, 0x3a, 0x56, 0xde, 0xfc, 0x3f, 0x01, 0x00, 0x00, 0xff,
0xff, 0x28, 0x05, 0xef, 0x24, 0x2d, 0x5d, 0x00, 0x00,
0x1c, 0x8d, 0xb9, 0x3f, 0x97, 0x4b, 0xb6, 0xf6, 0xc4, 0x95, 0xcf, 0x6d, 0x4d, 0xc8, 0xef, 0x00,
0x88, 0xae, 0x72, 0xcf, 0x24, 0x02, 0xcd, 0x66, 0xcf, 0x40, 0xf8, 0xdb, 0x59, 0xf3, 0xe5, 0xea,
0x70, 0x62, 0xc6, 0xae, 0xe1, 0x36, 0xd4, 0x71, 0xeb, 0x0e, 0x0f, 0x43, 0xdc, 0xed, 0x8d, 0x45,
0x22, 0x7c, 0x7d, 0xbe, 0xee, 0x3d, 0x4a, 0x08, 0xed, 0x2c, 0x13, 0xfe, 0x18, 0x16, 0x75, 0x68,
0xcc, 0x30, 0x5d, 0x22, 0xa6, 0x6f, 0xcc, 0xc7, 0xf4, 0x30, 0xa5, 0xb4, 0xc7, 0xd8, 0x4c, 0x47,
0x17, 0x4b, 0x2f, 0x1d, 0x5d, 0x7c, 0x15, 0x96, 0x3b, 0xe3, 0xbb, 0x40, 0x1f, 0x15, 0x13, 0x50,
0x6e, 0xc1, 0xa2, 0x1b, 0xa5, 0xc1, 0x4d, 0x0a, 0x75, 0x54, 0xed, 0x31, 0x58, 0xf3, 0xdf, 0x95,
0xa1, 0x48, 0x33, 0x3f, 0x19, 0xaa, 0xda, 0x1c, 0x53, 0xe9, 0x0f, 0xe6, 0x5f, 0xea, 0x89, 0x1d,
0x4f, 0x1a, 0xa4, 0x90, 0xd1, 0x20, 0xdf, 0x86, 0x52, 0x14, 0x84, 0x2a, 0x5e, 0xde, 0x39, 0x85,
0xa8, 0x1d, 0x84, 0xca, 0xd6, 0x84, 0x7c, 0x07, 0x2a, 0x27, 0xae, 0xa7, 0x70, 0x51, 0xf4, 0xe4,
0xbd, 0x36, 0x1f, 0x8f, 0x1d, 0x22, 0xb2, 0x63, 0x62, 0xbe, 0x97, 0x15, 0xb6, 0x32, 0x71, 0x5a,
0x9b, 0x8f, 0xd3, 0x2c, 0x19, 0xbc, 0x0f, 0xac, 0x1b, 0x9c, 0xc9, 0xd0, 0xce, 0xc4, 0x17, 0xf5,
0x21, 0x3d, 0x05, 0xe7, 0x4d, 0xa8, 0xf6, 0x5d, 0x47, 0xa2, 0x9d, 0x43, 0x3a, 0xa6, 0x6a, 0x27,
0x6d, 0xfe, 0x29, 0x54, 0xc9, 0x3f, 0x40, 0xad, 0x58, 0x7b, 0xe9, 0xc9, 0xd7, 0xae, 0x4a, 0xcc,
0x00, 0x3f, 0x44, 0x1f, 0xdf, 0x71, 0x15, 0x85, 0x99, 0xab, 0x76, 0xd2, 0xc6, 0x0e, 0x93, 0xbc,
0x67, 0x3b, 0x5c, 0xd7, 0x1d, 0x9e, 0x84, 0xf3, 0xb7, 0xe0, 0x06, 0xc1, 0x26, 0x0e, 0x49, 0xdc,
0x6a, 0xc8, 0x74, 0xf6, 0x4b, 0x34, 0x58, 0x86, 0xa2, 0x27, 0xf7, 0xdc, 0x81, 0xab, 0x1a, 0x4b,
0x77, 0x73, 0xf7, 0x4a, 0x76, 0x0a, 0xe0, 0xaf, 0xc1, 0xaa, 0x23, 0x4f, 0xc4, 0xc8, 0x53, 0x1d,
0x39, 0x18, 0x7a, 0x42, 0xc9, 0x96, 0x43, 0x32, 0x5a, 0xb3, 0xa7, 0x5f, 0xf0, 0xd7, 0xe1, 0x9a,
0x01, 0x1e, 0x26, 0xc9, 0x81, 0x96, 0x43, 0x51, 0xb8, 0x9a, 0x3d, 0xeb, 0x95, 0xb5, 0x6f, 0xd4,
0x30, 0x1e, 0xa0, 0xe8, 0xa7, 0xc6, 0x0a, 0x34, 0x52, 0xfa, 0x44, 0x7e, 0x24, 0x3c, 0x4f, 0x86,
0x17, 0xda, 0xc9, 0xfd, 0x54, 0xf8, 0xc7, 0xc2, 0x67, 0x05, 0x3a, 0x63, 0x85, 0x27, 0x7d, 0x47,
0x84, 0xfa, 0x44, 0x7e, 0x44, 0x07, 0x7a, 0xc9, 0xba, 0x07, 0x45, 0x9a, 0xd2, 0x1a, 0x94, 0xb4,
0x97, 0x44, 0x1e, 0xb3, 0xf1, 0x90, 0x48, 0x23, 0xef, 0xe1, 0xf6, 0x63, 0xf9, 0xe6, 0xcf, 0x0a,
0x50, 0x8d, 0x27, 0x2f, 0x4e, 0x05, 0xe4, 0xd2, 0x54, 0x00, 0x9a, 0x71, 0xd1, 0x13, 0x37, 0x72,
0x8f, 0x8d, 0x59, 0x5a, 0xb5, 0x53, 0x00, 0x5a, 0x42, 0xcf, 0x5d, 0x47, 0xf5, 0x69, 0xcf, 0x94,
0x6c, 0xdd, 0xe0, 0xf7, 0x60, 0xc5, 0xc1, 0x79, 0xf0, 0xbb, 0xde, 0xc8, 0x91, 0x1d, 0x3c, 0x45,
0x75, 0x98, 0x60, 0x12, 0xcc, 0xbf, 0x07, 0xa0, 0xdc, 0x81, 0xdc, 0x09, 0xc2, 0x81, 0x50, 0xc6,
0x37, 0x78, 0xff, 0xe5, 0xa4, 0x7a, 0xad, 0x93, 0x30, 0xb0, 0x33, 0xcc, 0x90, 0x35, 0x7e, 0xcd,
0xb0, 0xae, 0x7c, 0x21, 0xd6, 0x5b, 0x09, 0x03, 0x3b, 0xc3, 0xcc, 0xfa, 0x0d, 0x80, 0xf4, 0x0d,
0xbf, 0x09, 0x7c, 0x3f, 0xf0, 0x55, 0x7f, 0xfd, 0xf8, 0x38, 0xdc, 0x90, 0x27, 0x41, 0x28, 0xb7,
0x04, 0x1e, 0x6b, 0x37, 0x60, 0x35, 0x81, 0xaf, 0x9f, 0x28, 0x19, 0x22, 0x98, 0xa6, 0xbe, 0xdd,
0x0f, 0x42, 0xa5, 0x6d, 0x2b, 0x7a, 0x7c, 0xdc, 0x66, 0x05, 0x3c, 0x4a, 0x5b, 0xed, 0x43, 0x56,
0xb4, 0xee, 0x01, 0xa4, 0x43, 0x22, 0x1f, 0x84, 0x9e, 0xde, 0x78, 0x68, 0x3c, 0x12, 0x6a, 0x3d,
0x7c, 0x8b, 0xe5, 0x9a, 0x7f, 0x54, 0x80, 0x22, 0xaa, 0x1a, 0x74, 0xbf, 0xb2, 0xfb, 0x42, 0x2f,
0x5f, 0x16, 0xf4, 0xc5, 0x14, 0x24, 0xf2, 0xce, 0x2a, 0xc8, 0xf7, 0xa0, 0xde, 0x1d, 0x45, 0x2a,
0x18, 0xd0, 0xe9, 0x60, 0xf2, 0x28, 0x37, 0xa7, 0x02, 0x19, 0x4f, 0x84, 0x37, 0x92, 0x76, 0x16,
0x95, 0xbf, 0x0d, 0xe5, 0x13, 0xbd, 0x10, 0x3a, 0x94, 0xf1, 0x2b, 0x97, 0x1c, 0x20, 0x66, 0xb2,
0x0d, 0x32, 0x8e, 0xcb, 0x9d, 0x12, 0xa2, 0x2c, 0xc8, 0x1c, 0x04, 0xe5, 0xe4, 0x20, 0xf8, 0x0d,
0x58, 0x96, 0x68, 0x56, 0x1c, 0x79, 0xa2, 0x2b, 0x07, 0xd2, 0x8f, 0x57, 0xfe, 0xad, 0x97, 0x18,
0x31, 0xd9, 0x25, 0x34, 0xec, 0x09, 0x5e, 0xd6, 0x57, 0xcd, 0x26, 0xad, 0x40, 0x61, 0x3d, 0xea,
0x1a, 0xb7, 0x5b, 0x46, 0x5d, 0x6d, 0xd3, 0x6f, 0xd2, 0x80, 0x59, 0xde, 0x7a, 0x03, 0x6a, 0x09,
0x0f, 0xce, 0x60, 0xf1, 0x20, 0x50, 0xed, 0xa1, 0xec, 0xba, 0x27, 0xae, 0x74, 0x74, 0x20, 0xa1,
0xad, 0x44, 0xa8, 0x74, 0xe4, 0x6a, 0xdb, 0x77, 0x58, 0xbe, 0xf9, 0x6f, 0x2b, 0x50, 0xd6, 0x1a,
0xdf, 0x0c, 0xa9, 0x96, 0x0c, 0xe9, 0x3b, 0x50, 0x0d, 0x86, 0x32, 0x14, 0x2a, 0x08, 0x4d, 0xb8,
0xe0, 0xed, 0x97, 0x39, 0x41, 0xd6, 0x0e, 0x0d, 0xb1, 0x9d, 0xb0, 0x99, 0x94, 0x97, 0xfc, 0xb4,
0xbc, 0xdc, 0x07, 0x16, 0x1f, 0x16, 0x47, 0x21, 0xd2, 0xa9, 0x0b, 0xe3, 0xfc, 0x4d, 0xc1, 0x79,
0x07, 0x6a, 0xdd, 0xc0, 0x77, 0xdc, 0x24, 0x74, 0xb0, 0xfc, 0xf0, 0x9d, 0x97, 0xea, 0xe1, 0x66,
0x4c, 0x6d, 0xa7, 0x8c, 0xf8, 0x6b, 0x50, 0x3a, 0x43, 0x41, 0x22, 0x89, 0xb9, 0x5c, 0xcc, 0x34,
0x12, 0xff, 0x0c, 0xea, 0xdf, 0x1f, 0xb9, 0xdd, 0xd3, 0xc3, 0x6c, 0x68, 0xea, 0xbd, 0x97, 0xea,
0xc5, 0x77, 0x52, 0x7a, 0x3b, 0xcb, 0x2c, 0x23, 0xbc, 0x95, 0x3f, 0x85, 0xf0, 0x56, 0xa7, 0x84,
0xd7, 0x7a, 0x05, 0xaa, 0xf1, 0xe2, 0x90, 0x48, 0xf9, 0x28, 0x1d, 0x65, 0xc8, 0x1f, 0x86, 0x2c,
0x67, 0xfd, 0x71, 0x0e, 0x6a, 0xc9, 0xc4, 0x8c, 0x87, 0xa1, 0xb6, 0xbf, 0x3f, 0x12, 0x1e, 0xcb,
0x91, 0x1f, 0x15, 0x28, 0xdd, 0x22, 0x6d, 0xf2, 0x88, 0x92, 0xb1, 0x21, 0x2b, 0xd0, 0xd9, 0x21,
0xa3, 0x88, 0x15, 0x39, 0x87, 0x65, 0x03, 0x3e, 0x0c, 0x35, 0x6a, 0x09, 0xdd, 0x2c, 0x7c, 0x1b,
0x03, 0xca, 0xfa, 0xa8, 0x39, 0x95, 0xda, 0x8d, 0x3c, 0x08, 0x14, 0x35, 0xaa, 0xd8, 0x97, 0x96,
0xcf, 0x6a, 0xf8, 0xcd, 0x83, 0x40, 0xb5, 0x7c, 0x06, 0xa9, 0xdd, 0x5e, 0x8f, 0x3f, 0x4f, 0xad,
0x45, 0xf2, 0x0a, 0x3c, 0xaf, 0xe5, 0xb3, 0x25, 0xf3, 0x42, 0xb7, 0x96, 0x91, 0xe3, 0xf6, 0xb9,
0xe8, 0x22, 0xf9, 0x0a, 0x5f, 0x06, 0x40, 0x1a, 0xd3, 0x66, 0xb8, 0x6d, 0xb6, 0xcf, 0xdd, 0x48,
0x45, 0x6c, 0xd5, 0xfa, 0x37, 0x39, 0xa8, 0x67, 0x16, 0x01, 0xfd, 0x02, 0x42, 0x44, 0x5d, 0xab,
0xdd, 0x84, 0xef, 0xc9, 0x48, 0xc9, 0xd0, 0x89, 0xf5, 0x68, 0x27, 0xc0, 0xc7, 0x3c, 0x7e, 0xaf,
0x13, 0x0c, 0x82, 0x30, 0x0c, 0x9e, 0xeb, 0x33, 0x71, 0x4f, 0x44, 0xea, 0xa9, 0x94, 0xa7, 0xac,
0x88, 0x43, 0xdd, 0x1c, 0x85, 0xa1, 0xf4, 0x35, 0xa0, 0x44, 0x9d, 0x93, 0xe7, 0xba, 0x55, 0x46,
0xa6, 0x88, 0x4c, 0x8a, 0x9a, 0x55, 0x70, 0xb3, 0x1a, 0x6c, 0x0d, 0xa9, 0x22, 0x02, 0xa2, 0xeb,
0x66, 0x0d, 0x5d, 0x6f, 0xed, 0xba, 0x1e, 0x9e, 0x6c, 0x89, 0x8b, 0x68, 0xbd, 0x17, 0x30, 0x98,
0x04, 0x1e, 0x04, 0xcf, 0x59, 0xbd, 0x39, 0x02, 0x48, 0x8d, 0x75, 0x74, 0x52, 0x50, 0xd6, 0x92,
0xe0, 0xb2, 0x69, 0xf1, 0x43, 0x00, 0x7c, 0x22, 0xcc, 0xd8, 0x53, 0x79, 0x09, 0x0b, 0x8a, 0xe8,
0xec, 0x0c, 0x8b, 0xe6, 0x5f, 0x84, 0x5a, 0xf2, 0x02, 0x7d, 0x53, 0xb2, 0x75, 0x92, 0xcf, 0xc6,
0x4d, 0x3c, 0xb8, 0x5d, 0xdf, 0x91, 0xe7, 0xb4, 0xf7, 0x4b, 0xb6, 0x6e, 0x60, 0x2f, 0xfb, 0xae,
0xe3, 0x48, 0x3f, 0x4e, 0x01, 0xe8, 0xd6, 0xac, 0x7c, 0x6b, 0x71, 0x66, 0xbe, 0xb5, 0xf9, 0x9b,
0x50, 0xcf, 0x78, 0x13, 0x97, 0x0e, 0x3b, 0xd3, 0xb1, 0xfc, 0x78, 0xc7, 0x6e, 0x43, 0x2d, 0xce,
0xf1, 0x47, 0x74, 0xc2, 0xd4, 0xec, 0x14, 0xd0, 0xfc, 0x67, 0x79, 0x34, 0x71, 0x70, 0x68, 0x93,
0x1e, 0xc0, 0x0e, 0x94, 0xd1, 0x1d, 0x1e, 0xc5, 0xc9, 0xea, 0x39, 0xad, 0xec, 0x36, 0xd1, 0xec,
0x2e, 0xd8, 0x86, 0x9a, 0x7f, 0x08, 0x05, 0x25, 0x7a, 0x26, 0x82, 0xf6, 0xf5, 0xf9, 0x98, 0x74,
0x44, 0x6f, 0x77, 0xc1, 0x46, 0x3a, 0xbe, 0x07, 0xd5, 0xae, 0x09, 0x7a, 0x18, 0xc5, 0x35, 0xa7,
0x91, 0x1e, 0x87, 0x4a, 0x76, 0x17, 0xec, 0x84, 0x03, 0xff, 0x36, 0x14, 0xd1, 0xec, 0x30, 0x39,
0xfd, 0x39, 0x9d, 0x0f, 0xdc, 0x2e, 0xbb, 0x0b, 0x36, 0x51, 0x6e, 0x54, 0xa0, 0x44, 0x7a, 0xb2,
0xd9, 0x80, 0xb2, 0x1e, 0xeb, 0xe4, 0xcc, 0x35, 0x6f, 0x41, 0xa1, 0x23, 0x7a, 0x68, 0xfa, 0xb9,
0x4e, 0x64, 0x7c, 0x68, 0x7c, 0x6c, 0x7e, 0x25, 0x0d, 0xe0, 0x64, 0x63, 0x83, 0xb9, 0xb1, 0xd8,
0x60, 0xb3, 0x0c, 0x45, 0xfc, 0x62, 0xf3, 0xf6, 0x55, 0x66, 0x64, 0xf3, 0x7f, 0xe4, 0xd1, 0xe2,
0x54, 0xf2, 0x7c, 0x66, 0xdc, 0xf3, 0x13, 0xa8, 0x0d, 0xc3, 0xa0, 0x2b, 0xa3, 0x28, 0x08, 0x8d,
0x89, 0xf2, 0xda, 0x8b, 0x53, 0x8b, 0x6b, 0x47, 0x31, 0x8d, 0x9d, 0x92, 0x5b, 0x7f, 0x23, 0x0f,
0xb5, 0xe4, 0x85, 0x36, 0x74, 0x95, 0x3c, 0xd7, 0x31, 0xae, 0x7d, 0x19, 0x0e, 0x84, 0xeb, 0x68,
0xed, 0xb1, 0xd9, 0x17, 0xb1, 0x15, 0xf6, 0xbd, 0x60, 0xa4, 0x46, 0xc7, 0x52, 0xc7, 0x36, 0x9e,
0xb8, 0x03, 0x19, 0xb0, 0x22, 0x65, 0x15, 0x50, 0xb0, 0xbb, 0x5e, 0x30, 0x72, 0x58, 0x09, 0xdb,
0x8f, 0xe8, 0x08, 0xda, 0x17, 0xc3, 0x48, 0xeb, 0xcc, 0x7d, 0x37, 0x0c, 0x58, 0x05, 0x89, 0x76,
0xdc, 0xde, 0x40, 0xb0, 0x2a, 0x32, 0xeb, 0x3c, 0x77, 0x15, 0x2a, 0xe1, 0x1a, 0x5f, 0x85, 0xa5,
0xc3, 0xa1, 0xf4, 0xdb, 0x2a, 0x94, 0x52, 0xed, 0x8b, 0xa1, 0x0e, 0x76, 0xd9, 0xd2, 0x71, 0x5c,
0xa5, 0xf5, 0xe7, 0x8e, 0xe8, 0xca, 0xe3, 0x20, 0x38, 0x65, 0x8b, 0xa8, 0x68, 0x5a, 0x7e, 0xa4,
0x44, 0x2f, 0x14, 0x03, 0xad, 0x43, 0x3b, 0xd2, 0x93, 0xd4, 0x5a, 0xa6, 0x6f, 0xbb, 0xaa, 0x3f,
0x3a, 0x7e, 0x84, 0x0e, 0xc1, 0x8a, 0x4e, 0x40, 0x38, 0x72, 0x28, 0x51, 0x87, 0x2e, 0x42, 0x75,
0xc3, 0xf5, 0xdc, 0x63, 0xd7, 0x73, 0xd9, 0x2a, 0xa2, 0x6e, 0x9f, 0x77, 0x85, 0xe7, 0x3a, 0xa1,
0x78, 0xce, 0x78, 0x73, 0x15, 0x56, 0x26, 0x52, 0xa8, 0xcd, 0x8a, 0xf1, 0x31, 0x9a, 0x4b, 0x50,
0xcf, 0x24, 0xc5, 0x9a, 0xaf, 0x42, 0x35, 0x4e, 0x99, 0xa1, 0x2f, 0xe6, 0x46, 0x3a, 0xd8, 0x67,
0x56, 0x3c, 0x69, 0x37, 0xff, 0x69, 0x0e, 0xca, 0x3a, 0xed, 0xc8, 0x37, 0x92, 0x32, 0x81, 0xdc,
0x1c, 0x39, 0x2a, 0x4d, 0x64, 0x32, 0x7c, 0x49, 0xad, 0xc0, 0x75, 0x28, 0x79, 0xe4, 0x74, 0x19,
0x5d, 0x44, 0x8d, 0x8c, 0xea, 0x28, 0x64, 0x55, 0x87, 0xf5, 0x6e, 0x92, 0x55, 0x8c, 0x03, 0x4c,
0x64, 0x86, 0x75, 0x42, 0x29, 0x75, 0xf0, 0x88, 0x7c, 0xa6, 0x3c, 0x29, 0xfe, 0x60, 0x30, 0x14,
0x5d, 0x45, 0x80, 0x82, 0x75, 0x02, 0xd5, 0xa3, 0x20, 0x9a, 0x3c, 0x4e, 0x2b, 0x50, 0xe8, 0x04,
0x43, 0x6d, 0xc0, 0x6d, 0x04, 0x8a, 0x0c, 0x38, 0x7d, 0x7a, 0x9e, 0x28, 0x2d, 0x0f, 0xb6, 0xdb,
0xeb, 0x2b, 0xed, 0x5d, 0xb5, 0x7c, 0x5f, 0x86, 0xac, 0x84, 0xd3, 0x6f, 0xcb, 0x21, 0x9a, 0x85,
0xac, 0x8c, 0x13, 0x4e, 0xf0, 0x1d, 0x37, 0x8c, 0x14, 0xab, 0x58, 0x2d, 0x3c, 0x08, 0xdd, 0x1e,
0x9d, 0x5f, 0xf4, 0x40, 0xac, 0x16, 0xb0, 0x43, 0xd4, 0xdc, 0x94, 0x3e, 0x8a, 0x07, 0xa5, 0xad,
0x74, 0xdd, 0x08, 0x7d, 0x20, 0x8f, 0x87, 0x0f, 0xb5, 0x3f, 0x19, 0x45, 0xca, 0x3d, 0xb9, 0x60,
0x05, 0xeb, 0x29, 0x2c, 0x8d, 0x55, 0x98, 0xf0, 0xeb, 0xc0, 0xc6, 0x00, 0xd8, 0xf5, 0x05, 0x7e,
0x0b, 0xae, 0x8d, 0x41, 0xf7, 0x5d, 0xc7, 0xa1, 0xf8, 0xdd, 0xe4, 0x8b, 0x78, 0x80, 0x1b, 0x35,
0xa8, 0x74, 0xf5, 0x9a, 0x58, 0x47, 0xb0, 0x44, 0x8b, 0xb4, 0x2f, 0x95, 0x38, 0xf4, 0xbd, 0x8b,
0x3f, 0x75, 0x19, 0x90, 0xf5, 0x0d, 0x28, 0x51, 0xbc, 0x1d, 0xb7, 0xfa, 0x49, 0x18, 0x0c, 0x88,
0x57, 0xc9, 0xa6, 0x67, 0xe4, 0xae, 0x02, 0xb3, 0xd2, 0x79, 0x15, 0x58, 0x3f, 0x02, 0xa8, 0xac,
0x77, 0xbb, 0xc1, 0xc8, 0x57, 0x53, 0x5f, 0x9e, 0x15, 0xd2, 0x7d, 0x1b, 0xca, 0xe2, 0x4c, 0x28,
0x11, 0x1a, 0x15, 0x3d, 0x69, 0x8c, 0x19, 0x5e, 0x6b, 0xeb, 0x84, 0x64, 0x1b, 0x64, 0x24, 0xeb,
0x06, 0xfe, 0x89, 0xdb, 0x33, 0x5a, 0xf9, 0x32, 0xb2, 0x4d, 0x42, 0xb2, 0x0d, 0x32, 0x92, 0x99,
0x53, 0xa5, 0x74, 0x25, 0x99, 0x56, 0xad, 0xc9, 0x21, 0xf2, 0x00, 0x8a, 0xae, 0x7f, 0x12, 0x98,
0xf2, 0xbf, 0x57, 0x2e, 0x21, 0xa2, 0x1a, 0x38, 0x42, 0x6c, 0x4a, 0x28, 0xeb, 0x0e, 0xf3, 0xf7,
0xa1, 0x44, 0x69, 0x35, 0x93, 0xc8, 0x98, 0xab, 0x62, 0x47, 0x53, 0xf0, 0x9b, 0x71, 0x96, 0x86,
0xe6, 0x0b, 0xe1, 0xd4, 0xdc, 0xa8, 0xc6, 0x53, 0xd6, 0xfc, 0x4f, 0x39, 0x28, 0xeb, 0x11, 0xf2,
0x57, 0x61, 0x59, 0xfa, 0xb8, 0xd9, 0xe3, 0x73, 0xc3, 0xec, 0xf2, 0x09, 0x28, 0x5a, 0xb1, 0x06,
0x22, 0x8f, 0x47, 0x3d, 0x13, 0x01, 0xc8, 0x82, 0xf8, 0x7b, 0x70, 0x4b, 0x37, 0x8f, 0x42, 0x19,
0x4a, 0x4f, 0x8a, 0x48, 0x6e, 0xf6, 0x85, 0xef, 0x4b, 0xcf, 0x58, 0x11, 0x97, 0xbd, 0xe6, 0x16,
0x2c, 0xea, 0x57, 0xed, 0xa1, 0xe8, 0xca, 0xc8, 0x64, 0x9d, 0xc6, 0x60, 0xfc, 0x9b, 0x50, 0xa2,
0x22, 0xcc, 0x86, 0x73, 0xb5, 0xf0, 0x69, 0xac, 0x66, 0x90, 0x1c, 0x73, 0xeb, 0x00, 0x7a, 0x35,
0xd0, 0x0b, 0x33, 0xda, 0xe9, 0xcb, 0x57, 0x2e, 0x1f, 0xb9, 0x7c, 0x19, 0x22, 0xec, 0x9f, 0x23,
0x3d, 0x49, 0xd5, 0x72, 0x78, 0x0c, 0xe7, 0x29, 0xbe, 0x3f, 0x06, 0x6b, 0xfe, 0x76, 0x11, 0x8a,
0xb8, 0x90, 0x88, 0xdc, 0x0f, 0x06, 0x32, 0x89, 0x72, 0x6a, 0xa1, 0x1d, 0x83, 0xa1, 0x1d, 0x25,
0x74, 0xa2, 0x39, 0x41, 0xd3, 0xca, 0x6d, 0x12, 0x8c, 0x98, 0xc3, 0x30, 0x38, 0x71, 0xbd, 0x14,
0xd3, 0x58, 0x5c, 0x13, 0x60, 0xfe, 0x0e, 0xdc, 0x1c, 0x88, 0xf0, 0x54, 0x2a, 0xd2, 0x47, 0x4f,
0x83, 0xf0, 0x34, 0xc2, 0x99, 0x6b, 0x39, 0x26, 0x3c, 0x76, 0xc9, 0x5b, 0x54, 0xf0, 0x8e, 0x3c,
0x73, 0x09, 0xb3, 0xaa, 0x8b, 0x2b, 0xe3, 0x36, 0x0a, 0x87, 0xd0, 0x53, 0xd3, 0x36, 0xbc, 0x4c,
0xe6, 0x62, 0x1c, 0x8a, 0xc6, 0x9a, 0x2e, 0x3a, 0x89, 0x5a, 0x0e, 0x45, 0xec, 0x6a, 0x76, 0x0a,
0x40, 0xd1, 0xa1, 0x8f, 0x3d, 0xd1, 0x6a, 0x7c, 0x49, 0x7b, 0x99, 0x19, 0x10, 0x62, 0x28, 0xd9,
0xed, 0xc7, 0x1f, 0xd1, 0xe1, 0xb4, 0x2c, 0x88, 0xdf, 0x01, 0xe8, 0x09, 0x25, 0x9f, 0x8b, 0x8b,
0xc7, 0xa1, 0xd7, 0x90, 0x3a, 0x04, 0x9f, 0x42, 0xd0, 0x4f, 0xf5, 0x82, 0xae, 0xf0, 0xda, 0x2a,
0x08, 0x45, 0x4f, 0x1e, 0x09, 0xd5, 0x6f, 0xf4, 0xb4, 0x9f, 0x3a, 0x09, 0xc7, 0x11, 0x2b, 0x77,
0x20, 0x3f, 0x0b, 0x7c, 0xd9, 0xe8, 0xeb, 0x11, 0xc7, 0x6d, 0xec, 0x89, 0xf0, 0x85, 0x77, 0xa1,
0xdc, 0x2e, 0x8e, 0xc5, 0xd5, 0x3d, 0xc9, 0x80, 0x70, 0xac, 0xbe, 0x54, 0xcf, 0x83, 0xf0, 0xb4,
0xe5, 0x34, 0x3e, 0xd7, 0x63, 0x4d, 0x00, 0xd6, 0x21, 0x40, 0x2a, 0x44, 0x78, 0x96, 0xac, 0x53,
0x9a, 0x80, 0x2d, 0xa0, 0x73, 0x70, 0x24, 0x7d, 0xc7, 0xf5, 0x7b, 0x5b, 0x46, 0x6e, 0x58, 0x0e,
0x81, 0x14, 0x02, 0x90, 0x4e, 0x02, 0x24, 0x43, 0x84, 0x5a, 0xd2, 0x61, 0x05, 0xeb, 0x7f, 0xe5,
0xa0, 0x9e, 0xc9, 0x8a, 0xff, 0x19, 0x66, 0xf2, 0xf1, 0x64, 0x47, 0x7d, 0x81, 0x13, 0xaa, 0x65,
0x2a, 0x69, 0xe3, 0x74, 0x9b, 0xa4, 0x3d, 0xbe, 0xd5, 0x0e, 0x7f, 0x06, 0xf2, 0x85, 0xb2, 0xf8,
0xd6, 0x43, 0x13, 0x35, 0xa9, 0x43, 0xe5, 0xb1, 0x7f, 0xea, 0x07, 0xcf, 0x7d, 0x7d, 0x64, 0x53,
0x69, 0xc6, 0x58, 0x92, 0x29, 0xae, 0x9e, 0x28, 0x58, 0xff, 0xb8, 0x38, 0x51, 0xc5, 0xb4, 0x0d,
0x65, 0xed, 0x06, 0x90, 0x85, 0x3a, 0x5d, 0x76, 0x92, 0x45, 0x36, 0x09, 0x8d, 0x0c, 0xc8, 0x36,
0xc4, 0x68, 0x9f, 0x27, 0xa5, 0x7a, 0xf9, 0x99, 0x89, 0x97, 0x31, 0x46, 0xb1, 0x1a, 0x1c, 0xab,
0x56, 0x4d, 0x38, 0x34, 0xff, 0x5a, 0x0e, 0xae, 0xcf, 0x42, 0xc9, 0xd6, 0xf4, 0xe6, 0xc6, 0x6b,
0x7a, 0xdb, 0x13, 0x35, 0xb2, 0x79, 0x1a, 0xcd, 0x83, 0x97, 0xec, 0xc4, 0x78, 0xc5, 0xac, 0xf5,
0x93, 0x1c, 0xac, 0x4e, 0x8d, 0x39, 0x63, 0xe4, 0x00, 0x94, 0xb5, 0x64, 0xe9, 0xda, 0x97, 0xa4,
0x1a, 0x41, 0x47, 0x93, 0xe9, 0x4c, 0x89, 0x74, 0x7a, 0xd7, 0x54, 0x05, 0x6b, 0xf3, 0x17, 0x57,
0x0d, 0x75, 0x75, 0x4f, 0xb2, 0x12, 0x5a, 0x27, 0xda, 0xee, 0x32, 0x90, 0xb2, 0x36, 0x51, 0x75,
0xc8, 0x9b, 0x55, 0xa8, 0xa6, 0x66, 0x34, 0xf4, 0xdc, 0x2e, 0x36, 0xab, 0xbc, 0x09, 0x37, 0x75,
0x69, 0xb8, 0x71, 0x07, 0x4f, 0x3a, 0x7d, 0x97, 0x36, 0x07, 0xab, 0x59, 0x36, 0x5c, 0x9b, 0x31,
0x26, 0xea, 0xe5, 0x13, 0xd3, 0xe3, 0x65, 0x80, 0xad, 0x27, 0x71, 0x3f, 0x59, 0x8e, 0x73, 0x58,
0xde, 0x7a, 0x92, 0x65, 0x68, 0xf6, 0xcb, 0x13, 0xd4, 0x24, 0x11, 0x2b, 0x58, 0xbf, 0x93, 0x8b,
0xf3, 0xdc, 0xcd, 0xbf, 0x00, 0x4b, 0xba, 0x8f, 0x47, 0xe2, 0xc2, 0x0b, 0x84, 0xc3, 0xb7, 0x61,
0x39, 0x4a, 0xee, 0x2b, 0x64, 0x8e, 0x83, 0xc9, 0xd3, 0xbc, 0x3d, 0x86, 0x64, 0x4f, 0x10, 0xc5,
0x5e, 0x4d, 0x3e, 0x0d, 0x8e, 0x73, 0xf2, 0xcf, 0x04, 0xed, 0xb2, 0x45, 0xf2, 0xb8, 0x84, 0xf5,
0x4d, 0x58, 0x25, 0xe5, 0xa5, 0x3b, 0xa3, 0x2d, 0x66, 0x94, 0x07, 0xad, 0x77, 0xb7, 0x62, 0x79,
0x30, 0x4d, 0xeb, 0x0f, 0xcb, 0x00, 0x69, 0x22, 0x60, 0xc6, 0x36, 0x9f, 0x65, 0x04, 0x4d, 0xa5,
0xe5, 0x0a, 0x2f, 0x9d, 0x96, 0x7b, 0x2f, 0x31, 0xdc, 0x75, 0x44, 0x76, 0xb2, 0x46, 0x37, 0xed,
0xd3, 0xa4, 0xb9, 0x3e, 0x56, 0xf6, 0x51, 0x9a, 0x2c, 0xfb, 0xb8, 0x3b, 0x5d, 0x23, 0x36, 0xa1,
0x7f, 0xd2, 0x20, 0x43, 0x65, 0x2c, 0xc8, 0xd0, 0x84, 0x6a, 0x28, 0x85, 0x13, 0xf8, 0xde, 0x45,
0x9c, 0xfd, 0x89, 0xdb, 0xfc, 0x4d, 0x28, 0x29, 0xba, 0x72, 0x51, 0xa5, 0xed, 0xf2, 0x82, 0x85,
0xd3, 0xb8, 0xa8, 0xcc, 0xdc, 0xc8, 0x14, 0x76, 0xe9, 0x13, 0xac, 0x6a, 0x67, 0x20, 0x7c, 0x0d,
0xb8, 0x8b, 0x1e, 0x97, 0xe7, 0x49, 0x67, 0xe3, 0x62, 0x4b, 0x27, 0x65, 0xe8, 0xd4, 0xac, 0xda,
0x33, 0xde, 0xc4, 0xeb, 0xbf, 0x98, 0xae, 0x3f, 0x75, 0xf9, 0xcc, 0x8d, 0x70, 0xa4, 0x4b, 0x64,
0x1c, 0x24, 0x6d, 0x3c, 0x97, 0xe3, 0x3d, 0xaa, 0xe7, 0x92, 0xa4, 0x37, 0xcd, 0x6c, 0x5e, 0xf2,
0xd6, 0xfa, 0x7b, 0xf9, 0xc4, 0xc1, 0xa9, 0x41, 0xe9, 0x58, 0x44, 0x6e, 0x57, 0x3b, 0xaf, 0xe6,
0xe0, 0xd7, 0x4e, 0x8e, 0x0a, 0x9c, 0x80, 0xe5, 0xd1, 0x7b, 0x89, 0x24, 0xfa, 0x29, 0xcb, 0x00,
0xe9, 0x35, 0x14, 0x56, 0xc4, 0xbd, 0x19, 0xaf, 0xb7, 0xae, 0xcf, 0x20, 0x52, 0x8a, 0x77, 0x39,
0x49, 0xe5, 0x1b, 0x79, 0xae, 0xa4, 0xfb, 0x59, 0x15, 0x71, 0xfc, 0x40, 0x49, 0x1d, 0xed, 0x23,
0xe9, 0x64, 0x80, 0x6c, 0xe2, 0xba, 0x6a, 0x56, 0x47, 0x77, 0x22, 0x66, 0xaa, 0x43, 0x74, 0x11,
0xb9, 0x56, 0x8b, 0xb8, 0x3b, 0xc7, 0x5f, 0xb0, 0x25, 0xec, 0x51, 0x7a, 0xbb, 0x85, 0x2d, 0x23,
0x57, 0x41, 0x55, 0x03, 0x2b, 0xf8, 0x78, 0x46, 0xb5, 0x04, 0x0c, 0xbf, 0xea, 0xa0, 0xc2, 0x58,
0xc5, 0x9e, 0x25, 0xa6, 0x01, 0xe3, 0xe8, 0x2d, 0x0d, 0x05, 0xba, 0x2e, 0xee, 0x50, 0xf8, 0x8a,
0x5d, 0xc3, 0xa1, 0x0e, 0x9d, 0x13, 0x76, 0xdd, 0xfa, 0x51, 0x5a, 0x57, 0xfa, 0x7a, 0xe2, 0x30,
0xcc, 0x23, 0xc0, 0x97, 0xb9, 0x14, 0xdb, 0xb0, 0x1a, 0xca, 0xef, 0x8f, 0xdc, 0xb1, 0xa2, 0xe9,
0xc2, 0xd5, 0xe9, 0xfc, 0x69, 0x0a, 0xeb, 0x0c, 0x56, 0xe3, 0xc6, 0x53, 0x57, 0xf5, 0x29, 0xec,
0xc2, 0xdf, 0xcc, 0x54, 0x75, 0xe7, 0x66, 0xde, 0x86, 0x49, 0x58, 0xa6, 0x55, 0xdc, 0x49, 0xe8,
0x3b, 0x3f, 0x47, 0xe8, 0xdb, 0xfa, 0x9f, 0xe5, 0x4c, 0xe4, 0x45, 0xbb, 0x50, 0x4e, 0xe2, 0x42,
0x4d, 0x27, 0xf4, 0xd2, 0x68, 0x76, 0xfe, 0x65, 0xa2, 0xd9, 0xb3, 0x92, 0xe3, 0x1f, 0xa0, 0x7d,
0x4c, 0x7b, 0xe3, 0xc9, 0x1c, 0x91, 0xfa, 0x31, 0x5c, 0xbe, 0x41, 0xe9, 0x39, 0xd1, 0xd6, 0x95,
0x1b, 0xa5, 0x99, 0x77, 0x2c, 0xb2, 0x79, 0x38, 0x83, 0x69, 0x67, 0xa8, 0x32, 0x9a, 0xa4, 0x3c,
0x4b, 0x93, 0xa0, 0x37, 0x6b, 0x74, 0x4c, 0xd2, 0xd6, 0x89, 0x0d, 0xfd, 0x1c, 0xb3, 0xa7, 0xb4,
0x6c, 0xd5, 0x9e, 0x82, 0xa3, 0x85, 0x35, 0x18, 0x79, 0xca, 0x35, 0xb1, 0x7b, 0xdd, 0x98, 0xbc,
0x04, 0x56, 0x9b, 0xbe, 0x04, 0xf6, 0x11, 0x40, 0x24, 0x51, 0xf2, 0xb7, 0xdc, 0xae, 0x32, 0xf5,
0x1d, 0x77, 0x2e, 0x1b, 0x9b, 0xc9, 0x38, 0x64, 0x28, 0xb0, 0xff, 0x03, 0x71, 0xbe, 0x89, 0x96,
0xb6, 0x49, 0x44, 0x27, 0xed, 0x49, 0xfd, 0xba, 0x3c, 0xad, 0x5f, 0xdf, 0x84, 0x52, 0xd4, 0x0d,
0x86, 0x92, 0xee, 0x31, 0x5c, 0xbe, 0xbe, 0x6b, 0x6d, 0x44, 0xb2, 0x35, 0x2e, 0xc5, 0xf7, 0x50,
0x03, 0x05, 0x21, 0xdd, 0x60, 0xa8, 0xd9, 0x71, 0x73, 0x4c, 0xc7, 0xdd, 0x1c, 0xd7, 0x71, 0x4d,
0x07, 0xca, 0x26, 0x56, 0x3f, 0xc3, 0x75, 0xa7, 0x28, 0x5f, 0x3e, 0x13, 0xe5, 0x4b, 0xaa, 0x08,
0x0b, 0xd9, 0x2a, 0xc2, 0x89, 0x4b, 0x4e, 0xa5, 0xa9, 0x4b, 0x4e, 0xd6, 0x67, 0x50, 0xa2, 0xbe,
0xa2, 0x81, 0xa0, 0xa7, 0x59, 0xdb, 0x8f, 0x38, 0x28, 0x96, 0xe3, 0xd7, 0x81, 0x45, 0x92, 0x0c,
0x0c, 0xd9, 0x16, 0x03, 0x49, 0x0a, 0x30, 0xcf, 0x1b, 0x70, 0x5d, 0xe3, 0x46, 0xe3, 0x6f, 0xc8,
0xca, 0xf1, 0xdc, 0xe3, 0x50, 0x84, 0x17, 0xac, 0x68, 0x7d, 0x44, 0xa9, 0xdc, 0x58, 0xa0, 0xea,
0xc9, 0xa5, 0x32, 0xad, 0x72, 0x1d, 0x19, 0xe2, 0x49, 0xa1, 0x33, 0xf0, 0xc6, 0xf7, 0xd1, 0x75,
0x49, 0xe4, 0x5c, 0x50, 0x84, 0x66, 0x31, 0x7b, 0xca, 0xfe, 0x99, 0xed, 0x37, 0x6b, 0x23, 0x63,
0xa6, 0x8d, 0x17, 0x1a, 0xe5, 0xe6, 0x2d, 0x34, 0xb2, 0x3e, 0x85, 0x15, 0x7b, 0x5c, 0x5f, 0xf3,
0xf7, 0xa0, 0x12, 0x0c, 0xb3, 0x7c, 0x5e, 0x24, 0x97, 0x31, 0xba, 0xf5, 0xd3, 0x1c, 0x2c, 0xb6,
0x7c, 0x25, 0x43, 0x5f, 0x78, 0x3b, 0x9e, 0xe8, 0xf1, 0x77, 0x63, 0x2d, 0x35, 0xdb, 0xb7, 0xce,
0xe2, 0x8e, 0x2b, 0x2c, 0xcf, 0xc4, 0xa4, 0xf9, 0x0d, 0x58, 0x95, 0x8e, 0xab, 0x82, 0x50, 0x1b,
0xa7, 0x71, 0x3d, 0xd8, 0x75, 0x60, 0x1a, 0xdc, 0xa6, 0x2d, 0xd1, 0xd1, 0xcb, 0xdc, 0x80, 0xeb,
0x63, 0xd0, 0xd8, 0xf2, 0xcc, 0xf3, 0xdb, 0xd0, 0x48, 0x4f, 0x9a, 0xad, 0xc0, 0x57, 0x2d, 0xdf,
0x91, 0xe7, 0x64, 0xe6, 0xb0, 0x82, 0xf5, 0x7b, 0x95, 0xd8, 0xc0, 0x7a, 0x62, 0xaa, 0xc5, 0xc2,
0x20, 0x48, 0x6f, 0x14, 0x9a, 0x56, 0xe6, 0xe6, 0x6a, 0x7e, 0x8e, 0x9b, 0xab, 0x1f, 0xa5, 0xb7,
0x0f, 0xf5, 0x41, 0xf1, 0x95, 0x99, 0xa7, 0x0f, 0x15, 0xb9, 0x18, 0x93, 0xba, 0x2d, 0x33, 0x57,
0x11, 0xdf, 0x30, 0x7e, 0x54, 0x71, 0x1e, 0x3b, 0x54, 0x27, 0xdf, 0xdf, 0x9e, 0xac, 0x95, 0x9f,
0xaf, 0xd8, 0x6c, 0xca, 0x54, 0x84, 0x97, 0x36, 0x15, 0x3f, 0x9e, 0x70, 0x59, 0xaa, 0x33, 0xa3,
0x5a, 0x57, 0x5c, 0xe8, 0xfb, 0x18, 0x2a, 0x7d, 0x37, 0x52, 0x41, 0xa8, 0x2f, 0x99, 0x4e, 0x5f,
0x8a, 0xc9, 0xcc, 0xd6, 0xae, 0x46, 0xa4, 0xca, 0xa0, 0x98, 0x8a, 0x7f, 0x17, 0x56, 0x69, 0xe2,
0x8f, 0x52, 0x8b, 0x20, 0x6a, 0xd4, 0x67, 0x56, 0x64, 0x65, 0x58, 0x6d, 0x4c, 0x90, 0xd8, 0xd3,
0x4c, 0x9a, 0x3d, 0x80, 0x74, 0x7d, 0xa6, 0xb4, 0xd8, 0x17, 0xb8, 0x64, 0x7a, 0x13, 0xca, 0xd1,
0xe8, 0x38, 0x4d, 0x5e, 0x99, 0x56, 0xf3, 0x1c, 0x9a, 0x53, 0xd6, 0xc1, 0x91, 0x0c, 0x75, 0x77,
0xaf, 0xbc, 0xe9, 0xfa, 0x51, 0x76, 0xe1, 0xb5, 0x70, 0xde, 0xbd, 0x64, 0xf5, 0x12, 0xce, 0x19,
0x09, 0x68, 0xbe, 0x0d, 0xf5, 0xcc, 0xa4, 0xa2, 0x66, 0x1e, 0xf9, 0x4e, 0x10, 0x07, 0x65, 0xf1,
0x59, 0x5f, 0x11, 0x72, 0xe2, 0xb0, 0x2c, 0x3d, 0x37, 0x6d, 0x60, 0x93, 0x13, 0x78, 0x85, 0x5b,
0xfb, 0x15, 0x58, 0xca, 0x98, 0x6b, 0x49, 0xf8, 0x6b, 0x1c, 0x68, 0x9d, 0xc1, 0x2b, 0x19, 0x76,
0x47, 0x32, 0x1c, 0xb8, 0x11, 0x1e, 0x24, 0xda, 0x5d, 0xa3, 0xc8, 0x84, 0x23, 0x7d, 0xe5, 0xaa,
0x58, 0x83, 0x26, 0x6d, 0xfe, 0xeb, 0x50, 0x1a, 0xca, 0x70, 0x10, 0x19, 0x2d, 0x3a, 0x29, 0x41,
0x33, 0xd9, 0x46, 0xb6, 0xa6, 0xb1, 0xfe, 0x61, 0x0e, 0xaa, 0xfb, 0x52, 0x09, 0xb4, 0x1d, 0xf8,
0xfe, 0xc4, 0x57, 0xa6, 0x13, 0xae, 0x31, 0xea, 0x9a, 0x71, 0x20, 0xd7, 0x5a, 0x06, 0xdf, 0xb4,
0x77, 0x17, 0xd2, 0x8e, 0x35, 0x37, 0xa0, 0x62, 0xc0, 0xcd, 0x77, 0x61, 0x65, 0x02, 0x93, 0xe6,
0x45, 0xdb, 0xed, 0xed, 0x8b, 0x41, 0x5c, 0x9b, 0xb3, 0x68, 0x8f, 0x03, 0x37, 0x6a, 0x50, 0x19,
0x6a, 0x02, 0xeb, 0x5f, 0xdf, 0xa0, 0x7a, 0x11, 0xf7, 0x04, 0x1d, 0xe9, 0x59, 0x27, 0xeb, 0x1d,
0x00, 0x3a, 0x9a, 0x75, 0x55, 0x81, 0x0e, 0x49, 0x66, 0x20, 0xfc, 0x83, 0x24, 0x64, 0x5d, 0x9c,
0x69, 0x54, 0x65, 0x99, 0x4f, 0xc6, 0xad, 0x1b, 0x50, 0x71, 0xa3, 0x3d, 0x3c, 0xda, 0x4c, 0xad,
0x4d, 0xdc, 0xe4, 0xdf, 0x82, 0xb2, 0x3b, 0x18, 0x06, 0xa1, 0x32, 0x31, 0xed, 0x2b, 0xb9, 0xb6,
0x08, 0x73, 0x77, 0xc1, 0x36, 0x34, 0x48, 0x2d, 0xcf, 0x89, 0xba, 0xfa, 0x62, 0xea, 0xed, 0xf3,
0x98, 0x5a, 0xd3, 0xf0, 0xef, 0xc0, 0x52, 0x4f, 0x57, 0xbf, 0x69, 0xc6, 0x46, 0x89, 0x7c, 0xfd,
0x2a, 0x26, 0x8f, 0xb2, 0x04, 0xbb, 0x0b, 0xf6, 0x38, 0x07, 0x64, 0x89, 0x06, 0xbc, 0x8c, 0x54,
0x27, 0xf8, 0x24, 0x70, 0x7d, 0x72, 0x38, 0x5f, 0xc0, 0xd2, 0xce, 0x12, 0x20, 0xcb, 0x31, 0x0e,
0xfc, 0x1d, 0xb4, 0x78, 0x22, 0x65, 0xee, 0xf9, 0xde, 0xbd, 0x8a, 0x53, 0x47, 0x46, 0xe6, 0x86,
0x6e, 0xa4, 0xf8, 0x39, 0x34, 0x33, 0x9b, 0xc4, 0x7c, 0x64, 0x7d, 0x38, 0x0c, 0x03, 0xf4, 0x5a,
0x97, 0x88, 0xdb, 0x3b, 0x57, 0x71, 0x3b, 0xba, 0x94, 0x7a, 0x77, 0xc1, 0xbe, 0x82, 0x37, 0xef,
0xa0, 0xd7, 0x66, 0x86, 0xb0, 0x27, 0xc5, 0x59, 0x7c, 0x4b, 0xf8, 0xfe, 0x5c, 0xb3, 0x40, 0x14,
0xbb, 0x0b, 0xf6, 0x04, 0x0f, 0xfe, 0x9b, 0xb0, 0x3a, 0xf6, 0x4d, 0xba, 0x51, 0xa8, 0xef, 0x10,
0x7f, 0x73, 0xee, 0x61, 0x20, 0xd1, 0xee, 0x82, 0x3d, 0xcd, 0x89, 0x8f, 0xe0, 0x4b, 0xd3, 0x43,
0xda, 0x92, 0x5d, 0xcf, 0xf5, 0xa5, 0xb9, 0x6e, 0xfc, 0xf6, 0xcb, 0xcd, 0x96, 0x21, 0xde, 0x5d,
0xb0, 0x2f, 0xe7, 0xcc, 0xff, 0x12, 0xdc, 0x1e, 0xce, 0x54, 0x31, 0x5a, 0x75, 0x99, 0xdb, 0xca,
0xef, 0xcd, 0xf9, 0xe5, 0x29, 0xfa, 0xdd, 0x05, 0xfb, 0x4a, 0xfe, 0x68, 0x3b, 0x93, 0x77, 0x6c,
0x8a, 0x74, 0x75, 0x83, 0xdf, 0x86, 0x9a, 0xe8, 0x7a, 0xbb, 0x52, 0x38, 0x49, 0xf4, 0x3c, 0x05,
0x34, 0xff, 0x6b, 0x0e, 0xca, 0x46, 0xde, 0x6f, 0x27, 0x09, 0xf6, 0x44, 0x75, 0xa7, 0x00, 0xfe,
0x21, 0xd4, 0x64, 0x18, 0x06, 0xe1, 0x66, 0xe0, 0xc4, 0x15, 0x82, 0x93, 0xa1, 0x5d, 0xcd, 0x67,
0x6d, 0x3b, 0x46, 0xb3, 0x53, 0x0a, 0xfe, 0x01, 0x80, 0xde, 0xe7, 0x9d, 0xf4, 0xae, 0x45, 0x73,
0x36, 0xbd, 0x4e, 0xb1, 0xa4, 0xd8, 0x69, 0x60, 0x2c, 0xce, 0x6f, 0xc4, 0xcd, 0xc4, 0xe1, 0x2c,
0x65, 0x1c, 0xce, 0xdb, 0x26, 0x46, 0x70, 0x80, 0x2f, 0xcc, 0x8d, 0xa3, 0x04, 0xd0, 0xfc, 0x57,
0x39, 0x28, 0x6b, 0xe5, 0xc1, 0xb7, 0xa7, 0x47, 0xf4, 0xb5, 0x17, 0xeb, 0x9c, 0xb5, 0xc9, 0x91,
0x7d, 0x0b, 0x40, 0xeb, 0xa0, 0xcc, 0xc8, 0x6e, 0x4f, 0xf0, 0x31, 0xa4, 0x71, 0x99, 0x68, 0x8a,
0x6f, 0x3d, 0xd4, 0xb7, 0x62, 0x28, 0x0e, 0xfb, 0x78, 0x6f, 0x8f, 0x2d, 0xf0, 0x55, 0x58, 0x7a,
0x7c, 0xf0, 0xe9, 0xc1, 0xe1, 0xd3, 0x83, 0x67, 0xdb, 0xb6, 0x7d, 0x68, 0xeb, 0x70, 0xec, 0xc6,
0xfa, 0xd6, 0xb3, 0xd6, 0xc1, 0xd1, 0xe3, 0x0e, 0xcb, 0x37, 0x7f, 0x96, 0x83, 0xa5, 0x31, 0xdd,
0xf5, 0x7f, 0x77, 0xe9, 0x32, 0xd3, 0x5f, 0x98, 0x3d, 0xfd, 0xc5, 0xcb, 0xa6, 0xbf, 0x34, 0x39,
0xfd, 0xff, 0x28, 0x07, 0x4b, 0x63, 0x3a, 0x32, 0xcb, 0x3d, 0x37, 0xce, 0x3d, 0x7b, 0xd2, 0xe7,
0x27, 0x4e, 0x7a, 0x0b, 0x16, 0xe3, 0xe7, 0x83, 0x34, 0xe2, 0x30, 0x06, 0xcb, 0xe2, 0x50, 0x59,
0x7a, 0x71, 0x1c, 0x87, 0x4a, 0xd3, 0xaf, 0xee, 0x2d, 0x5d, 0xc3, 0x8b, 0xe8, 0x96, 0x72, 0xf3,
0x72, 0x0d, 0x7a, 0xc5, 0x10, 0x1e, 0x41, 0x7d, 0x98, 0x6e, 0xd3, 0x97, 0x33, 0x4b, 0xb2, 0x94,
0x2f, 0xe8, 0xe7, 0x4f, 0x72, 0xb0, 0x3c, 0xae, 0x73, 0xff, 0xbf, 0x9e, 0xd6, 0x3f, 0xcc, 0xc1,
0xea, 0x94, 0x26, 0xbf, 0xd2, 0xb0, 0x9b, 0xec, 0x57, 0x7e, 0x8e, 0x7e, 0x15, 0x66, 0xf4, 0xeb,
0x72, 0x4d, 0x72, 0x75, 0x8f, 0xdb, 0xf0, 0xa5, 0x4b, 0xcf, 0x84, 0x2b, 0xa6, 0x7a, 0x8c, 0x69,
0x61, 0x92, 0xe9, 0xdf, 0xcf, 0xc1, 0xed, 0xab, 0xf4, 0xfd, 0xff, 0x73, 0xb9, 0x9a, 0xec, 0xa1,
0xf5, 0x6e, 0x92, 0x28, 0xaf, 0x43, 0xc5, 0xfc, 0x89, 0x8f, 0xa9, 0x4d, 0xee, 0x07, 0xcf, 0x7d,
0x1d, 0x65, 0xb6, 0xa5, 0x30, 0xf7, 0xa3, 0x6d, 0x39, 0xf4, 0x5c, 0x4a, 0x4c, 0xde, 0x02, 0x58,
0x27, 0xbf, 0x2e, 0xbe, 0xae, 0xb0, 0xb9, 0x77, 0xd8, 0xde, 0x66, 0x0b, 0x59, 0x23, 0xf6, 0xb3,
0x58, 0x11, 0x5b, 0x47, 0x50, 0x4e, 0x0b, 0xd9, 0xf7, 0x45, 0x78, 0xea, 0xe8, 0xf4, 0xdf, 0x22,
0x54, 0x8f, 0x8c, 0x0b, 0xa5, 0x3f, 0xf5, 0x49, 0xfb, 0xf0, 0x40, 0x07, 0xb4, 0xb7, 0x0e, 0x3b,
0xba, 0x1c, 0xbe, 0xfd, 0xe4, 0x91, 0xce, 0x43, 0x3d, 0xb2, 0xd7, 0x8f, 0x76, 0x9f, 0x11, 0x46,
0xc9, 0xfa, 0x59, 0x3e, 0x3e, 0xd5, 0x2c, 0xdb, 0x24, 0x16, 0x01, 0xca, 0xa8, 0xcd, 0x03, 0xc3,
0x38, 0xf9, 0x0c, 0x55, 0xc8, 0x6e, 0x9f, 0xeb, 0x38, 0x04, 0xcb, 0xf3, 0x32, 0xe4, 0x8f, 0x8e,
0x75, 0x71, 0xec, 0xae, 0x1a, 0x78, 0xfa, 0xfe, 0x5a, 0xe7, 0x5c, 0xb1, 0x12, 0x3e, 0x6c, 0x46,
0x67, 0xac, 0x6c, 0xfd, 0xc7, 0x1c, 0xd4, 0x12, 0x55, 0xf9, 0x32, 0xaa, 0x9b, 0x73, 0x58, 0x6e,
0x1d, 0x74, 0xb6, 0xed, 0x83, 0xf5, 0x3d, 0x83, 0x52, 0xe0, 0x0d, 0xb8, 0x7e, 0x70, 0xf8, 0xec,
0x70, 0xe3, 0x93, 0xed, 0xcd, 0x4e, 0xfb, 0x59, 0xe7, 0xf0, 0x59, 0x6b, 0xff, 0xe8, 0xd0, 0xee,
0xb0, 0x12, 0xbf, 0x09, 0x5c, 0x3f, 0x3f, 0x6b, 0xb5, 0x9f, 0x6d, 0xae, 0x1f, 0x6c, 0x6e, 0xef,
0x6d, 0x6f, 0xb1, 0x32, 0xff, 0x1a, 0xfc, 0xda, 0x5e, 0x6b, 0xbf, 0xd5, 0x79, 0x76, 0xb8, 0xf3,
0xcc, 0x3e, 0x7c, 0xda, 0x7e, 0x76, 0x68, 0x3f, 0xb3, 0xb7, 0xf7, 0xd6, 0x3b, 0xad, 0xc3, 0x83,
0xf6, 0xb3, 0xed, 0xef, 0x6e, 0x6e, 0x6f, 0x6f, 0x6d, 0x6f, 0xb1, 0x0a, 0xbf, 0x06, 0x2b, 0x3b,
0xad, 0xbd, 0xed, 0x67, 0x7b, 0x87, 0xeb, 0x5b, 0xe6, 0x7b, 0x55, 0x7e, 0x1b, 0x1a, 0xad, 0x83,
0xf6, 0xe3, 0x9d, 0x9d, 0xd6, 0x66, 0x6b, 0xfb, 0xa0, 0xf3, 0xec, 0x68, 0xdb, 0xde, 0x6f, 0xb5,
0xdb, 0x48, 0xcb, 0x6a, 0xd6, 0xb7, 0xa1, 0xdc, 0xf2, 0xcf, 0x5c, 0x45, 0xe2, 0x67, 0xd6, 0xca,
0x38, 0x24, 0x71, 0x93, 0xa4, 0xc6, 0xed, 0xf9, 0x74, 0x6d, 0x99, 0x84, 0x6f, 0xd1, 0x4e, 0x01,
0xd6, 0x3f, 0xc9, 0xc3, 0x92, 0x66, 0x11, 0x3b, 0x38, 0xf7, 0x60, 0xc5, 0x44, 0x0a, 0x5b, 0xe3,
0x3b, 0x7c, 0x12, 0x4c, 0x7f, 0xeb, 0xa3, 0x41, 0x99, 0x7d, 0x9e, 0x05, 0x51, 0x66, 0x89, 0x98,
0xa3, 0xa3, 0xa4, 0x73, 0x6a, 0x29, 0xe0, 0x8b, 0x6e, 0x70, 0x54, 0x1e, 0x1a, 0xb1, 0x1b, 0xf8,
0x9b, 0xc9, 0x65, 0x81, 0x31, 0x18, 0xff, 0x0c, 0x6e, 0x25, 0xed, 0x6d, 0xbf, 0x1b, 0x5e, 0x0c,
0x93, 0x7f, 0xdf, 0xaa, 0xcc, 0xf4, 0xb8, 0x77, 0x5c, 0x4f, 0x8e, 0x21, 0xda, 0x97, 0x31, 0xb0,
0xfe, 0x38, 0x97, 0x71, 0x0b, 0xb5, 0xdb, 0x77, 0xa5, 0x42, 0x9c, 0x95, 0xa2, 0x40, 0xc7, 0xcc,
0x74, 0xdf, 0x9c, 0xd3, 0xa6, 0xc9, 0x8f, 0x80, 0xbb, 0xd3, 0x9d, 0x2e, 0xce, 0xd9, 0xe9, 0x19,
0xb4, 0x93, 0x11, 0xe6, 0xd2, 0x74, 0x84, 0xf9, 0x0e, 0x40, 0xcf, 0x0b, 0x8e, 0x85, 0x97, 0xb1,
0xc3, 0x32, 0x10, 0xcb, 0x83, 0x6a, 0xfc, 0x1f, 0x5f, 0xfc, 0x26, 0x94, 0xe9, 0x5f, 0xbe, 0x92,
0x78, 0x9b, 0x6e, 0xf1, 0x5d, 0x58, 0x96, 0xe3, 0x7d, 0xce, 0xcf, 0xd9, 0xe7, 0x09, 0x3a, 0xeb,
0x7d, 0x58, 0x9d, 0x42, 0xc2, 0x49, 0x1c, 0x0a, 0x95, 0xdc, 0x10, 0xc6, 0xe7, 0xe9, 0xfc, 0xad,
0xf5, 0xef, 0xf3, 0xb0, 0xb8, 0x2f, 0x7c, 0xf7, 0x44, 0x46, 0x2a, 0xee, 0x6d, 0xd4, 0xed, 0xcb,
0x81, 0x88, 0x7b, 0xab, 0x5b, 0xc6, 0x09, 0xcf, 0x4f, 0x55, 0xa6, 0x65, 0xb3, 0x21, 0x37, 0xa1,
0x2c, 0x46, 0xaa, 0x9f, 0xd4, 0x46, 0x9b, 0x16, 0xae, 0x9d, 0xe7, 0x76, 0xa5, 0x1f, 0xc5, 0xb2,
0x19, 0x37, 0xd3, 0x0a, 0x8e, 0xf2, 0x15, 0x15, 0x1c, 0x95, 0xe9, 0xf9, 0xbf, 0x0b, 0xf5, 0xa8,
0x1b, 0x4a, 0xe9, 0x47, 0xfd, 0x40, 0xc5, 0xff, 0x0f, 0x97, 0x05, 0x51, 0xe5, 0x52, 0xf0, 0xdc,
0xc7, 0x1d, 0xba, 0xe7, 0xfa, 0xa7, 0xa6, 0x7c, 0x67, 0x0c, 0x86, 0x32, 0x48, 0x21, 0x08, 0xf7,
0x07, 0x92, 0xdc, 0xdf, 0x92, 0x9d, 0xb4, 0x29, 0xc8, 0x20, 0x94, 0xec, 0x05, 0xa1, 0x2b, 0x75,
0xa4, 0xad, 0x66, 0x67, 0x20, 0x48, 0xeb, 0x09, 0xbf, 0x37, 0x12, 0x3d, 0x69, 0xf2, 0xa1, 0x49,
0xdb, 0xfa, 0x6f, 0x25, 0x80, 0x7d, 0x39, 0x38, 0x96, 0x61, 0xd4, 0x77, 0x87, 0x94, 0x09, 0x70,
0x4d, 0x11, 0xe9, 0x92, 0x4d, 0xcf, 0xfc, 0xbd, 0xb1, 0x62, 0xed, 0xe9, 0xdc, 0x5d, 0x4a, 0x3e,
0x19, 0xa1, 0xc0, 0xc9, 0x11, 0x4a, 0x9a, 0xe2, 0x19, 0x9a, 0xff, 0xa2, 0x9d, 0x05, 0x51, 0x5d,
0x93, 0x50, 0x72, 0xdb, 0x77, 0x74, 0x04, 0xa4, 0x68, 0x27, 0x6d, 0xba, 0x92, 0x11, 0xad, 0x8f,
0x54, 0x60, 0x4b, 0x5f, 0x3e, 0x4f, 0xee, 0x13, 0xa5, 0x20, 0xbe, 0x0f, 0x4b, 0x43, 0x71, 0x31,
0x90, 0xbe, 0xda, 0x97, 0xaa, 0x1f, 0x38, 0xa6, 0xd2, 0xe5, 0x6b, 0x97, 0x77, 0xf0, 0x28, 0x8b,
0x6e, 0x8f, 0x53, 0xa3, 0x4c, 0xf8, 0x11, 0xed, 0x12, 0xbd, 0x8c, 0xa6, 0xc5, 0x37, 0x00, 0xf4,
0x13, 0x39, 0x16, 0xd5, 0xd9, 0x81, 0x1a, 0x31, 0x90, 0x91, 0x0c, 0xcf, 0x5c, 0xad, 0xc7, 0xb4,
0xeb, 0x94, 0x52, 0xa1, 0xd6, 0x1b, 0x45, 0x32, 0xdc, 0x1e, 0x08, 0xd7, 0x33, 0x0b, 0x9c, 0x02,
0xf8, 0x5b, 0x70, 0x23, 0x1a, 0x1d, 0xa3, 0xcc, 0x1c, 0xcb, 0x4e, 0x70, 0x20, 0x9f, 0x47, 0x9e,
0x54, 0x4a, 0x86, 0x26, 0xb5, 0x3e, 0xfb, 0xa5, 0xd5, 0x4b, 0xac, 0x02, 0xfa, 0x13, 0x03, 0x7c,
0x4a, 0x4b, 0x76, 0x12, 0x90, 0xa9, 0x67, 0x62, 0x39, 0xce, 0x60, 0x51, 0x83, 0x4c, 0xb9, 0x53,
0x9e, 0x7f, 0x15, 0xbe, 0x3c, 0x86, 0x64, 0xeb, 0x3c, 0x69, 0xb4, 0xe3, 0xfa, 0xc2, 0x73, 0x7f,
0xa0, 0x33, 0xd2, 0x05, 0x6b, 0x08, 0x4b, 0x63, 0x13, 0x87, 0xc7, 0xbc, 0x7e, 0x32, 0x05, 0x20,
0x0c, 0x16, 0x75, 0xbb, 0xad, 0x42, 0x97, 0x12, 0x00, 0x09, 0x64, 0x13, 0xf7, 0x79, 0xc0, 0xf2,
0xfc, 0x3a, 0x30, 0x0d, 0x69, 0xf9, 0x62, 0x38, 0x5c, 0x1f, 0x0e, 0x3d, 0xc9, 0x0a, 0x74, 0xef,
0x2e, 0x85, 0xea, 0x92, 0x6d, 0x56, 0xb4, 0xbe, 0x0b, 0xb7, 0x68, 0x66, 0x9e, 0xc8, 0x30, 0xf1,
0xfb, 0xcc, 0x58, 0x6f, 0xc0, 0xaa, 0x7e, 0x3a, 0x08, 0x94, 0x7e, 0x4d, 0xb6, 0x10, 0x87, 0x65,
0x0d, 0x46, 0x53, 0xa0, 0x2d, 0x7d, 0xa5, 0xeb, 0x50, 0x34, 0x2c, 0xc1, 0xcb, 0x5b, 0x3f, 0x2d,
0x03, 0x4f, 0x05, 0xa2, 0xe3, 0xca, 0x70, 0x4b, 0x28, 0x91, 0x09, 0xdc, 0x2d, 0x5d, 0x9a, 0x7a,
0x7e, 0x71, 0xb5, 0xd6, 0x4d, 0x28, 0xbb, 0x11, 0x7a, 0x2a, 0xa6, 0x3a, 0xd2, 0xb4, 0xf8, 0x1e,
0xc0, 0x50, 0x86, 0x6e, 0xe0, 0x90, 0x04, 0x95, 0x66, 0xd6, 0xcc, 0x4f, 0x77, 0x6a, 0xed, 0x28,
0xa1, 0xb1, 0x33, 0xf4, 0xd8, 0x0f, 0xdd, 0xd2, 0x89, 0xdc, 0x32, 0x75, 0x3a, 0x0b, 0xe2, 0xaf,
0xc3, 0xb5, 0x61, 0xe8, 0x76, 0xa5, 0x5e, 0x8e, 0xc7, 0x91, 0xb3, 0x49, 0xff, 0xe0, 0x55, 0x21,
0xcc, 0x59, 0xaf, 0x50, 0x02, 0x85, 0x4f, 0xf6, 0x7b, 0x44, 0xa9, 0x4b, 0x73, 0xef, 0x53, 0x57,
0x1b, 0x2e, 0xd9, 0xb3, 0x5f, 0xf2, 0xfb, 0xc0, 0xcc, 0x8b, 0x7d, 0xd7, 0xdf, 0x93, 0x7e, 0x4f,
0xf5, 0x49, 0xb8, 0x97, 0xec, 0x29, 0x38, 0x69, 0x30, 0xfd, 0x07, 0x2b, 0x3a, 0xad, 0x51, 0xb3,
0x93, 0xb6, 0xbe, 0x4b, 0xec, 0x05, 0x61, 0x5b, 0x85, 0xa6, 0x10, 0x32, 0x69, 0xa3, 0xcd, 0x12,
0x51, 0x5f, 0x8f, 0xc2, 0xc0, 0x19, 0x51, 0xd0, 0x5d, 0x2b, 0xb1, 0x49, 0x70, 0x8a, 0xb9, 0x2f,
0x7c, 0x53, 0x32, 0xb7, 0x94, 0xc5, 0x4c, 0xc0, 0xe4, 0xa2, 0x04, 0x51, 0xca, 0x70, 0xc5, 0xb8,
0x28, 0x19, 0x98, 0xc1, 0x49, 0x59, 0xb1, 0x04, 0x27, 0xe5, 0x43, 0xe3, 0x77, 0xc2, 0xc0, 0x75,
0x52, 0x5e, 0xab, 0xba, 0xa0, 0x71, 0x12, 0x9e, 0xc1, 0x4d, 0x79, 0xf2, 0x31, 0xdc, 0x04, 0x6e,
0xfd, 0x30, 0x07, 0x90, 0x2e, 0x3e, 0x8a, 0x7c, 0xda, 0x4a, 0xb7, 0xf8, 0x2d, 0xb8, 0x96, 0x05,
0x53, 0x25, 0x3e, 0xe5, 0x3f, 0x39, 0x2c, 0xa7, 0x2f, 0xb6, 0xc4, 0x45, 0xc4, 0xf2, 0xba, 0xb2,
0x31, 0x86, 0x3d, 0x95, 0x92, 0x6a, 0xc8, 0xae, 0x03, 0x4b, 0x81, 0x74, 0x6b, 0x2a, 0x62, 0xc5,
0x71, 0xd4, 0xef, 0x49, 0x11, 0x46, 0xac, 0x64, 0xed, 0x42, 0x59, 0xe7, 0x5e, 0x66, 0x64, 0x4d,
0x5f, 0xae, 0x04, 0xe2, 0xaf, 0xe7, 0x00, 0xb6, 0x74, 0xf1, 0x2a, 0x9e, 0xe2, 0xf3, 0xd4, 0x91,
0xeb, 0xbf, 0xed, 0xa0, 0xb2, 0xde, 0x42, 0xf2, 0xb7, 0x1d, 0xd8, 0x44, 0xc9, 0x11, 0x71, 0xd1,
0x90, 0xde, 0x73, 0x49, 0x5b, 0x1f, 0x20, 0x9b, 0x81, 0xef, 0xcb, 0x2e, 0x1e, 0x3f, 0xc9, 0x01,
0x92, 0x80, 0xee, 0xff, 0xb8, 0x00, 0xcb, 0xe3, 0xf9, 0x3b, 0xaa, 0xf3, 0xd7, 0xb9, 0xe3, 0x43,
0xcf, 0xc9, 0x94, 0x3e, 0x32, 0xbe, 0x02, 0x75, 0x63, 0x11, 0x12, 0x60, 0x95, 0x3c, 0x93, 0x60,
0x20, 0xd9, 0xdd, 0xec, 0x7f, 0x52, 0xbd, 0x8e, 0x0e, 0x8e, 0xbe, 0x3a, 0xc1, 0x86, 0xbc, 0x66,
0xfe, 0x9d, 0xe3, 0xb7, 0xf2, 0x7c, 0x29, 0x53, 0x80, 0xf7, 0x63, 0x54, 0x87, 0x2b, 0x1b, 0x23,
0xdf, 0xf1, 0xa4, 0x93, 0x40, 0xff, 0x41, 0x16, 0x9a, 0x94, 0xd3, 0xfd, 0x16, 0x7a, 0x55, 0xb5,
0xf6, 0xe8, 0xd8, 0x94, 0xd2, 0xfd, 0xe5, 0x22, 0xbf, 0x09, 0xab, 0x06, 0x2b, 0xad, 0x9b, 0x61,
0x7f, 0x05, 0x17, 0x6e, 0x79, 0x5d, 0xeb, 0x14, 0xd3, 0x51, 0xf6, 0x57, 0x8b, 0xd8, 0x05, 0xba,
0x93, 0xf7, 0xdb, 0xc4, 0x27, 0x29, 0x2a, 0x66, 0xbf, 0x53, 0xe4, 0x2b, 0x00, 0xed, 0x4e, 0xf2,
0xa1, 0xdf, 0x2b, 0xf2, 0x3a, 0x94, 0xdb, 0x1d, 0xe2, 0xf6, 0xc3, 0x22, 0xbf, 0x01, 0x2c, 0x7d,
0x6b, 0x2a, 0x85, 0x7e, 0x5f, 0x77, 0x26, 0x29, 0xfd, 0xf9, 0x83, 0x22, 0x8e, 0x2b, 0x36, 0x98,
0xd9, 0xdf, 0x2c, 0x72, 0x06, 0xf5, 0x8c, 0xbf, 0xcb, 0xfe, 0x56, 0x91, 0x73, 0x58, 0xda, 0x47,
0x37, 0xd7, 0xef, 0x99, 0x11, 0xfc, 0x2e, 0x7d, 0x79, 0x27, 0xa9, 0x8b, 0x66, 0x3f, 0x2a, 0xf2,
0x5b, 0xc0, 0xb3, 0x31, 0x3e, 0xf3, 0xe2, 0x6f, 0x17, 0xef, 0xff, 0x94, 0x42, 0x2a, 0xd9, 0xac,
0x3b, 0xfa, 0x89, 0x5e, 0xe0, 0xf7, 0x94, 0xfe, 0xeb, 0xae, 0x25, 0xa8, 0x45, 0xfd, 0x20, 0x54,
0xd4, 0xa4, 0x9b, 0x21, 0x3e, 0x5d, 0xef, 0xd3, 0xe5, 0x92, 0xda, 0x12, 0xd1, 0x2e, 0xaa, 0x12,
0x3d, 0x56, 0x4f, 0x8a, 0x98, 0x8a, 0x49, 0xa1, 0x15, 0x5d, 0x33, 0x8c, 0xaf, 0x71, 0xb1, 0x32,
0xa2, 0x8e, 0x42, 0x4f, 0x17, 0x5c, 0x49, 0x3c, 0x85, 0xf4, 0x7f, 0xf4, 0x0c, 0xfb, 0x78, 0xd8,
0xd5, 0x34, 0x34, 0xf8, 0xdc, 0xd5, 0x17, 0x84, 0x4c, 0x8d, 0x83, 0x83, 0xfd, 0x48, 0xd2, 0x78,
0x4c, 0xde, 0xff, 0x83, 0x1c, 0x2c, 0xc6, 0x97, 0xeb, 0xdc, 0x9e, 0xeb, 0xeb, 0x92, 0xad, 0xf8,
0x0f, 0xd1, 0xba, 0x9e, 0x3b, 0x8c, 0xff, 0x60, 0x68, 0x05, 0xea, 0x4e, 0x28, 0x7a, 0xeb, 0xbe,
0xb3, 0x15, 0x06, 0x43, 0xdd, 0x6d, 0x1d, 0x74, 0xd5, 0xa5, 0x62, 0xcf, 0xe5, 0x31, 0xa2, 0x0f,
0x65, 0xc8, 0x8a, 0x54, 0x3f, 0xd1, 0x17, 0xa1, 0xeb, 0xf7, 0xd0, 0x55, 0xf6, 0x23, 0x5d, 0x32,
0x56, 0x87, 0xca, 0x28, 0x92, 0x5d, 0x11, 0x49, 0x56, 0xc6, 0xc6, 0xf1, 0xc8, 0xf5, 0x94, 0xeb,
0xeb, 0xff, 0xf5, 0x49, 0x6a, 0xc2, 0xaa, 0xf7, 0xff, 0x65, 0x0e, 0xea, 0xb4, 0x78, 0x69, 0x34,
0x21, 0x55, 0x27, 0x75, 0xa8, 0xec, 0x25, 0xff, 0xeb, 0x52, 0x86, 0xfc, 0xe1, 0xa9, 0x8e, 0x26,
0x98, 0xc5, 0xd3, 0xf7, 0x6b, 0xf4, 0x5f, 0xbc, 0x14, 0xf9, 0x97, 0xe0, 0x86, 0x2d, 0x07, 0x81,
0x92, 0x4f, 0x85, 0xab, 0xb2, 0xe5, 0xd2, 0x25, 0xb4, 0x3c, 0xf4, 0xab, 0xb8, 0x3e, 0xba, 0x4c,
0x96, 0x07, 0x7e, 0x36, 0x86, 0x54, 0x70, 0xd0, 0x04, 0x31, 0xa6, 0x48, 0x35, 0x41, 0xf9, 0x24,
0x70, 0x7d, 0xfc, 0x1a, 0x5d, 0xc8, 0x22, 0x08, 0x85, 0xa5, 0x10, 0x04, 0xf7, 0x0f, 0xe0, 0xe6,
0xec, 0x60, 0x8a, 0xbe, 0xaa, 0x45, 0x7f, 0x26, 0x48, 0x05, 0xb4, 0x4f, 0x43, 0x57, 0x5f, 0xdb,
0xa9, 0x41, 0xe9, 0xf0, 0xb9, 0x4f, 0xd2, 0xb0, 0x0a, 0x4b, 0x07, 0x41, 0x86, 0x86, 0x15, 0xee,
0x77, 0xc7, 0xe2, 0x5f, 0xe9, 0xa4, 0xc4, 0x9d, 0x58, 0xc8, 0x14, 0x87, 0xe7, 0x74, 0x64, 0x85,
0xfe, 0xd6, 0x59, 0x5f, 0x63, 0x35, 0x71, 0x27, 0x47, 0x5f, 0x63, 0x4d, 0xba, 0x59, 0xd4, 0x7f,
0xf4, 0xe0, 0x77, 0xa5, 0x27, 0x1d, 0x56, 0xba, 0xff, 0x1e, 0xac, 0x98, 0xa1, 0x76, 0x65, 0x14,
0xc5, 0xc5, 0xd5, 0x47, 0xa1, 0x7b, 0xa6, 0xaf, 0xca, 0x2e, 0x42, 0xf5, 0x48, 0x86, 0x51, 0xe0,
0xd3, 0x35, 0x61, 0x80, 0x72, 0xbb, 0x2f, 0x42, 0xfc, 0xc6, 0xfd, 0x6f, 0x40, 0x8d, 0x8a, 0xad,
0x3f, 0x75, 0x7d, 0x07, 0x47, 0xb2, 0x61, 0xea, 0x0b, 0x6b, 0x50, 0xda, 0x0c, 0xce, 0x68, 0x7c,
0x55, 0xfd, 0x97, 0x66, 0x2c, 0x7f, 0xff, 0x63, 0xe0, 0xda, 0x8f, 0x73, 0xe4, 0xb9, 0xeb, 0xf7,
0x92, 0xfb, 0x83, 0x40, 0x97, 0x81, 0x1d, 0x79, 0x4e, 0x66, 0x52, 0x1d, 0x2a, 0x71, 0x23, 0xbe,
0x92, 0xbc, 0x13, 0x8c, 0x7c, 0xfc, 0xda, 0x13, 0xb8, 0xae, 0x65, 0x03, 0x3f, 0x4f, 0x97, 0x3a,
0x2e, 0x35, 0x2e, 0x75, 0x45, 0xbc, 0x1a, 0x45, 0x09, 0x2e, 0xcb, 0xf1, 0x9b, 0xc0, 0x13, 0xc3,
0x2c, 0x85, 0xe7, 0xef, 0x5b, 0x70, 0x6d, 0x86, 0x75, 0x4c, 0x3a, 0x53, 0xdb, 0x08, 0x6c, 0x61,
0xe3, 0xfe, 0x1f, 0xfd, 0xe2, 0x4e, 0xee, 0xe7, 0xbf, 0xb8, 0x93, 0xfb, 0xcf, 0xbf, 0xb8, 0x93,
0xfb, 0xe1, 0x2f, 0xef, 0x2c, 0xfc, 0xfc, 0x97, 0x77, 0x16, 0xfe, 0xc3, 0x2f, 0xef, 0x2c, 0x7c,
0xc6, 0x26, 0xff, 0x12, 0xfd, 0xb8, 0x4c, 0xc7, 0xca, 0x9b, 0xff, 0x27, 0x00, 0x00, 0xff, 0xff,
0x31, 0x0c, 0x9a, 0x8e, 0x2d, 0x5d, 0x00, 0x00,
}
func (m *SmartBlockSnapshotBase) Marshal() (dAtA []byte, err error) {

View file

@ -333,9 +333,9 @@ message Block {
message Dataview {
repeated string source = 1;
repeated View views = 2;
string activeView = 3; // do not generate changes for this field
// deprecated
repeated model.Relation relations = 4;
string activeView = 3; // saved within a session
repeated GroupOrder groupOrders = 12;
repeated ObjectOrder objectOrders = 13;
repeated anytype.model.RelationLink relationLinks = 5;

View file

@ -920,6 +920,54 @@ func (_c *MockSpace_Do_Call) RunAndReturn(run func(string, func(smartblock.Smart
return _c
}
// DoCtx provides a mock function with given fields: ctx, objectId, apply
func (_m *MockSpace) DoCtx(ctx context.Context, objectId string, apply func(smartblock.SmartBlock) error) error {
ret := _m.Called(ctx, objectId, apply)
if len(ret) == 0 {
panic("no return value specified for DoCtx")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string, func(smartblock.SmartBlock) error) error); ok {
r0 = rf(ctx, objectId, apply)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockSpace_DoCtx_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DoCtx'
type MockSpace_DoCtx_Call struct {
*mock.Call
}
// DoCtx is a helper method to define mock.On call
// - ctx context.Context
// - objectId string
// - apply func(smartblock.SmartBlock) error
func (_e *MockSpace_Expecter) DoCtx(ctx interface{}, objectId interface{}, apply interface{}) *MockSpace_DoCtx_Call {
return &MockSpace_DoCtx_Call{Call: _e.mock.On("DoCtx", ctx, objectId, apply)}
}
func (_c *MockSpace_DoCtx_Call) Run(run func(ctx context.Context, objectId string, apply func(smartblock.SmartBlock) error)) *MockSpace_DoCtx_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string), args[2].(func(smartblock.SmartBlock) error))
})
return _c
}
func (_c *MockSpace_DoCtx_Call) Return(_a0 error) *MockSpace_DoCtx_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockSpace_DoCtx_Call) RunAndReturn(run func(context.Context, string, func(smartblock.SmartBlock) error) error) *MockSpace_DoCtx_Call {
_c.Call.Return(run)
return _c
}
// DoLockedIfNotExists provides a mock function with given fields: objectID, proc
func (_m *MockSpace) DoLockedIfNotExists(objectID string, proc func() error) error {
ret := _m.Called(objectID, proc)

View file

@ -47,6 +47,7 @@ type Space interface {
CommonSpace() commonspace.Space
Do(objectId string, apply func(sb smartblock.SmartBlock) error) error
DoCtx(ctx context.Context, objectId string, apply func(sb smartblock.SmartBlock) error) error
GetRelationIdByKey(ctx context.Context, key domain.RelationKey) (id string, err error)
GetTypeIdByKey(ctx context.Context, key domain.TypeKey) (id string, err error)
@ -208,7 +209,11 @@ func (s *space) WaitMandatoryObjects(ctx context.Context) (err error) {
}
func (s *space) Do(objectId string, apply func(sb smartblock.SmartBlock) error) error {
sb, err := s.GetObject(context.Background(), objectId)
return s.DoCtx(context.Background(), objectId, apply)
}
func (s *space) DoCtx(ctx context.Context, objectId string, apply func(sb smartblock.SmartBlock) error) error {
sb, err := s.GetObject(ctx, objectId)
if err != nil {
return err
}

View file

@ -27,7 +27,10 @@ func (s *service) initMarketplaceSpace(ctx context.Context) error {
}
func (s *service) initTechSpace(ctx context.Context) (err error) {
s.techSpace, err = s.factory.CreateAndSetTechSpace(ctx)
if s.techSpace, err = s.factory.CreateAndSetTechSpace(ctx); err != nil {
return err
}
close(s.techSpaceReady)
return
}

View file

@ -0,0 +1,7 @@
package dependencies
import "github.com/anyproto/anytype-heart/pkg/lib/database"
type QueryableStore interface {
Query(q database.Query) (records []database.Record, err error)
}

View file

@ -0,0 +1,12 @@
package dependencies
import (
"context"
"github.com/anyproto/anytype-heart/core/block/editor/smartblock"
)
type SpaceWithCtx interface {
DoCtx(ctx context.Context, objectId string, apply func(sb smartblock.SmartBlock) error) error
Id() string
}

View file

@ -0,0 +1,90 @@
package readonlyfixer
import (
"context"
"errors"
"fmt"
"github.com/anyproto/any-sync/app/logger"
"go.uber.org/zap"
"github.com/anyproto/anytype-heart/core/block/editor/basic"
"github.com/anyproto/anytype-heart/core/block/editor/smartblock"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
"github.com/anyproto/anytype-heart/pkg/lib/database"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
"github.com/anyproto/anytype-heart/space/internal/components/dependencies"
"github.com/anyproto/anytype-heart/util/pbtypes"
)
const MName = "ReadonlyRelationsFixer"
// Migration ReadonlyRelationsFixer performs setting readOnlyValue relation to true for all relations with Status and Tag format
// This migration was implemented to fix relations in accounts of users that are not able to modify its value (GO-2331)
type Migration struct{}
func (Migration) Name() string {
return MName
}
func (Migration) Run(ctx context.Context, log logger.CtxLogger, store dependencies.QueryableStore, space dependencies.SpaceWithCtx) (toMigrate, migrated int, err error) {
spaceId := space.Id()
relations, err := listReadonlyTagAndStatusRelations(store, spaceId)
toMigrate = len(relations)
if err != nil {
return toMigrate, 0, fmt.Errorf("failed to list all relations with tag and status format in space %s: %w", spaceId, err)
}
if toMigrate != 0 {
log.Debug(fmt.Sprintf("space %s contains %d relations of tag and status format with relationReadonlyValue=true", spaceId, toMigrate), zap.String("migration", MName))
}
for _, r := range relations {
var (
name = pbtypes.GetString(r.Details, bundle.RelationKeyName.String())
uk = pbtypes.GetString(r.Details, bundle.RelationKeyUniqueKey.String())
)
format := model.RelationFormat_name[int32(pbtypes.GetInt64(r.Details, bundle.RelationKeyRelationFormat.String()))]
log.Debug("setting relationReadonlyValue to FALSE for relation", zap.String("name", name), zap.String("uniqueKey", uk), zap.String("format", format), zap.String("migration", MName))
det := []*model.Detail{{
Key: bundle.RelationKeyRelationReadonlyValue.String(),
Value: pbtypes.Bool(false),
}}
e := space.DoCtx(ctx, pbtypes.GetString(r.Details, bundle.RelationKeyId.String()), func(sb smartblock.SmartBlock) error {
if ds, ok := sb.(basic.DetailsSettable); ok {
return ds.SetDetails(nil, det, false)
}
return nil
})
if e != nil {
err = errors.Join(err, fmt.Errorf("failed to set readOnlyValue=true to relation %s in space %s: %w", uk, spaceId, e))
} else {
migrated++
}
}
return
}
func listReadonlyTagAndStatusRelations(store dependencies.QueryableStore, spaceId string) ([]database.Record, error) {
return store.Query(database.Query{Filters: []*model.BlockContentDataviewFilter{
{
RelationKey: bundle.RelationKeyRelationFormat.String(),
Condition: model.BlockContentDataviewFilter_In,
Value: pbtypes.IntList(int(model.RelationFormat_status), int(model.RelationFormat_tag)),
},
{
RelationKey: bundle.RelationKeySpaceId.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.String(spaceId),
},
{
RelationKey: bundle.RelationKeyRelationReadonlyValue.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.Bool(true),
},
}})
}

View file

@ -0,0 +1,124 @@
package readonlyfixer
import (
"context"
"testing"
"github.com/anyproto/any-sync/app/logger"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"golang.org/x/exp/slices"
"github.com/anyproto/anytype-heart/core/block/editor/smartblock"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
"github.com/anyproto/anytype-heart/pkg/lib/localstore/objectstore"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
mock_space "github.com/anyproto/anytype-heart/space/clientspace/mock_clientspace"
"github.com/anyproto/anytype-heart/util/pbtypes"
)
func TestFixReadonlyInRelations(t *testing.T) {
store := objectstore.NewStoreFixture(t)
store.AddObjects(t, []objectstore.TestObject{
// space1
{
bundle.RelationKeySpaceId: pbtypes.String("space1"),
bundle.RelationKeyRelationFormat: pbtypes.Int64(int64(model.RelationFormat_status)),
bundle.RelationKeyId: pbtypes.String("rel-tag"),
bundle.RelationKeyRelationReadonlyValue: pbtypes.Bool(true),
},
{
bundle.RelationKeySpaceId: pbtypes.String("space1"),
bundle.RelationKeyRelationFormat: pbtypes.Int64(int64(model.RelationFormat_tag)),
bundle.RelationKeyId: pbtypes.String("rel-customTag"),
bundle.RelationKeyRelationReadonlyValue: pbtypes.Bool(true),
},
// space2
{
bundle.RelationKeySpaceId: pbtypes.String("space2"),
bundle.RelationKeyRelationFormat: pbtypes.Int64(0),
bundle.RelationKeyId: pbtypes.String("rel-id"),
bundle.RelationKeyRelationReadonlyValue: pbtypes.Bool(true),
},
{
bundle.RelationKeySpaceId: pbtypes.String("space2"),
bundle.RelationKeyRelationFormat: pbtypes.Int64(2),
bundle.RelationKeyId: pbtypes.String("rel-relationFormat"),
bundle.RelationKeyRelationReadonlyValue: pbtypes.Bool(true),
},
// space3
{
bundle.RelationKeySpaceId: pbtypes.String("space3"),
bundle.RelationKeyRelationFormat: pbtypes.Int64(int64(model.RelationFormat_tag)),
bundle.RelationKeyId: pbtypes.String("rel-category"),
bundle.RelationKeyRelationReadonlyValue: pbtypes.Bool(false),
},
{
bundle.RelationKeySpaceId: pbtypes.String("space3"),
bundle.RelationKeyRelationFormat: pbtypes.Int64(int64(model.RelationFormat_status)),
bundle.RelationKeyId: pbtypes.String("rel-genderCustom"),
bundle.RelationKeyRelationReadonlyValue: pbtypes.Bool(false),
},
})
fixer := &Migration{}
ctx := context.Background()
log := logger.NewNamed("test")
t.Run("fix tag and status relations with readonly=true", func(t *testing.T) {
// given
spc := mock_space.NewMockSpace(t)
spc.EXPECT().Id().Return("space1").Maybe()
// both relations will be processed
spc.EXPECT().DoCtx(mock.Anything, mock.Anything, mock.Anything).RunAndReturn(
func(ctx context.Context, objectId string, apply func(smartblock.SmartBlock) error) error {
assert.True(t, slices.Contains([]string{"rel-customTag", "rel-tag"}, objectId))
return nil
},
).Times(2)
// when
migrated, toMigrate, err := fixer.Run(ctx, log, store, spc)
// then
assert.NoError(t, err)
assert.Equal(t, 2, migrated)
assert.Equal(t, 2, toMigrate)
})
t.Run("do not process relations of other formats", func(t *testing.T) {
// given
spc := mock_space.NewMockSpace(t)
spc.EXPECT().Id().Return("space2").Maybe()
// none of relations will be processed
// sp.EXPECT().Do(mock.Anything, mock.Anything).Times(1).Return(nil)
// when
migrated, toMigrate, err := fixer.Run(ctx, log, store, spc)
// then
assert.NoError(t, err)
assert.Zero(t, migrated)
assert.Zero(t, toMigrate)
})
t.Run("do not process relations with readonly=false", func(t *testing.T) {
// given
spc := mock_space.NewMockSpace(t)
spc.EXPECT().Id().Return("space3").Maybe()
// none of relations will be processed
// sp.EXPECT().Do(mock.Anything, mock.Anything).Times(1).Return(nil)
// when
migrated, toMigrate, err := fixer.Run(ctx, log, store, spc)
// then
assert.NoError(t, err)
assert.Zero(t, migrated)
assert.Zero(t, toMigrate)
})
}

View file

@ -0,0 +1,120 @@
package migration
import (
"context"
"errors"
"fmt"
"github.com/anyproto/any-sync/app"
"github.com/anyproto/any-sync/app/logger"
"go.uber.org/zap"
"github.com/anyproto/anytype-heart/pkg/lib/localstore/objectstore"
"github.com/anyproto/anytype-heart/space/clientspace"
"github.com/anyproto/anytype-heart/space/internal/components/dependencies"
"github.com/anyproto/anytype-heart/space/internal/components/migration/readonlyfixer"
"github.com/anyproto/anytype-heart/space/internal/components/migration/systemobjectreviser"
"github.com/anyproto/anytype-heart/space/internal/components/spaceloader"
)
const (
CName = "common.components.migration"
errFormat = "failed to run migration '%s' in space '%s': %w. %d out of %d objects were migrated"
)
var log = logger.NewNamed(CName)
type Migration interface {
Run(context.Context, logger.CtxLogger, dependencies.QueryableStore, dependencies.SpaceWithCtx) (toMigrate, migrated int, err error)
Name() string
}
func New() *Runner {
return &Runner{}
}
type Runner struct {
store objectstore.ObjectStore
spaceLoader spaceloader.SpaceLoader
ctx context.Context
cancel context.CancelFunc
spc clientspace.Space
loadErr error
waitLoad chan struct{}
started bool
app.ComponentRunnable
}
func (r *Runner) Name() string {
return CName
}
func (r *Runner) Init(a *app.App) error {
r.store = app.MustComponent[objectstore.ObjectStore](a)
r.spaceLoader = app.MustComponent[spaceloader.SpaceLoader](a)
r.waitLoad = make(chan struct{})
return nil
}
func (r *Runner) Run(context.Context) error {
r.started = true
r.ctx, r.cancel = context.WithCancel(context.Background())
go r.waitSpace()
go r.runMigrations()
return nil
}
func (r *Runner) Close(context.Context) error {
if r.started {
r.cancel()
}
return nil
}
func (r *Runner) waitSpace() {
r.spc, r.loadErr = r.spaceLoader.WaitLoad(r.ctx)
close(r.waitLoad)
}
func (r *Runner) runMigrations() {
select {
case <-r.ctx.Done():
return
case <-r.waitLoad:
if r.loadErr != nil {
log.Error("failed to load space", zap.Error(r.loadErr))
return
}
break
}
if err := r.run(systemobjectreviser.Migration{}, readonlyfixer.Migration{}); err != nil {
log.Error("failed to run default migrations", zap.String("spaceId", r.spc.Id()), zap.Error(err))
}
}
func (r *Runner) run(migrations ...Migration) (err error) {
spaceId := r.spc.Id()
for _, m := range migrations {
if e := r.ctx.Err(); e != nil {
err = errors.Join(err, e)
return
}
toMigrate, migrated, e := m.Run(r.ctx, log, r.store, r.spc)
if e != nil {
err = errors.Join(err, wrapError(e, m.Name(), spaceId, migrated, toMigrate))
continue
}
log.Debug(fmt.Sprintf("migration '%s' in space '%s' is successful. %d out of %d objects were migrated",
m.Name(), spaceId, migrated, toMigrate))
}
return
}
func wrapError(err error, migrationName, spaceId string, migrated, toMigrate int) error {
return fmt.Errorf(errFormat, migrationName, spaceId, err, migrated, toMigrate)
}

View file

@ -0,0 +1,173 @@
package migration
import (
"context"
"errors"
"testing"
"time"
"github.com/anyproto/any-sync/app/logger"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/anyproto/anytype-heart/core/block/editor/smartblock"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
"github.com/anyproto/anytype-heart/pkg/lib/database"
"github.com/anyproto/anytype-heart/pkg/lib/localstore/objectstore"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
mock_space "github.com/anyproto/anytype-heart/space/clientspace/mock_clientspace"
"github.com/anyproto/anytype-heart/space/internal/components/dependencies"
"github.com/anyproto/anytype-heart/space/internal/components/migration/readonlyfixer"
"github.com/anyproto/anytype-heart/space/internal/components/migration/systemobjectreviser"
"github.com/anyproto/anytype-heart/util/pbtypes"
)
func TestRunner(t *testing.T) {
// TODO: we should revive this test when context query for ObjectStore will be implemented
// t.Run("context exceeds + store operation in progress -> context.Canceled", func(t *testing.T) {
// // given
// store := objectstore.NewStoreFixture(t)
// ctx, cancel := context.WithCancel(context.Background())
// space := mock_space.NewMockSpace(t)
// space.EXPECT().Id().Times(1).Return("")
// runner := Runner{ctx: ctx, store: store, spc: space}
//
// // when
// go func() {
// time.Sleep(10 * time.Millisecond)
// cancel()
// }()
// err := runner.run(longStoreMigration{})
//
// // then
// assert.Error(t, err)
// assert.True(t, errors.Is(err, context.Canceled))
// })
t.Run("context exceeds + space operation in progress -> context.Canceled", func(t *testing.T) {
// given
ctx, cancel := context.WithCancel(context.Background())
space := mock_space.NewMockSpace(t)
space.EXPECT().Id().Times(1).Return("")
space.EXPECT().DoCtx(mock.Anything, mock.Anything, mock.Anything).RunAndReturn(
func(ctx context.Context, _ string, _ func(smartblock.SmartBlock) error) error {
timer := time.NewTimer(1 * time.Millisecond)
select {
case <-ctx.Done():
return context.Canceled
case <-timer.C:
return nil
}
},
)
runner := Runner{ctx: ctx, spc: space}
// when
go func() {
time.Sleep(10 * time.Millisecond)
cancel()
}()
err := runner.run(longSpaceMigration{})
// then
assert.Error(t, err)
assert.True(t, errors.Is(err, context.Canceled))
})
t.Run("context exceeds + migration is finished -> no error", func(t *testing.T) {
// given
store := objectstore.NewStoreFixture(t)
ctx, cancel := context.WithCancel(context.Background())
space := mock_space.NewMockSpace(t)
space.EXPECT().Id().Times(1).Return("")
runner := Runner{ctx: ctx, store: store, spc: space}
// when
go func() {
time.Sleep(10 * time.Millisecond)
cancel()
}()
err := runner.run(instantMigration{})
// then
assert.NoError(t, err)
})
t.Run("no ctx exceed + migration is finished -> no error", func(t *testing.T) {
// given
store := objectstore.NewStoreFixture(t)
space := mock_space.NewMockSpace(t)
space.EXPECT().Id().Return("").Maybe()
runner := Runner{ctx: context.Background(), store: store, spc: space}
// when
err := runner.run(systemobjectreviser.Migration{})
// then
assert.NoError(t, err)
})
t.Run("no ctx exceed + migration failure -> error", func(t *testing.T) {
// given
store := objectstore.NewStoreFixture(t)
store.AddObjects(t, []objectstore.TestObject{{
bundle.RelationKeySpaceId: pbtypes.String("space1"),
bundle.RelationKeyRelationFormat: pbtypes.Int64(int64(model.RelationFormat_status)),
bundle.RelationKeyId: pbtypes.String("rel-tag"),
bundle.RelationKeyRelationReadonlyValue: pbtypes.Bool(true),
}})
spaceErr := errors.New("failed to get object")
space := mock_space.NewMockSpace(t)
space.EXPECT().Id().Return("space1").Maybe()
space.EXPECT().DoCtx(mock.Anything, mock.Anything, mock.Anything).Maybe().Return(spaceErr)
runner := Runner{ctx: context.Background(), store: store, spc: space}
// when
err := runner.run(readonlyfixer.Migration{})
// then
assert.Error(t, err)
assert.True(t, errors.Is(err, spaceErr))
})
}
type longStoreMigration struct{}
func (longStoreMigration) Name() string {
return "long migration"
}
func (longStoreMigration) Run(ctx context.Context, _ logger.CtxLogger, store dependencies.QueryableStore, _ dependencies.SpaceWithCtx) (toMigrate, migrated int, err error) {
for {
if _, err = store.Query(database.Query{}); err != nil {
return 0, 0, err
}
}
}
type longSpaceMigration struct{}
func (longSpaceMigration) Name() string {
return "long migration"
}
func (longSpaceMigration) Run(ctx context.Context, _ logger.CtxLogger, _ dependencies.QueryableStore, space dependencies.SpaceWithCtx) (toMigrate, migrated int, err error) {
for {
if err = space.DoCtx(ctx, "", func(smartblock.SmartBlock) error {
// do smth
return nil
}); err != nil {
return 0, 0, err
}
}
}
type instantMigration struct{}
func (instantMigration) Name() string {
return "instant migration"
}
func (instantMigration) Run(context.Context, logger.CtxLogger, dependencies.QueryableStore, dependencies.SpaceWithCtx) (toMigrate, migrated int, err error) {
return 0, 0, nil
}

View file

@ -1,8 +1,14 @@
package objectcreator
package systemobjectreviser
import (
"context"
"errors"
"fmt"
"github.com/anyproto/any-sync/app/logger"
"github.com/gogo/protobuf/types"
"github.com/samber/lo"
"go.uber.org/zap"
"github.com/anyproto/anytype-heart/core/block/editor/basic"
"github.com/anyproto/anytype-heart/core/block/editor/smartblock"
@ -12,26 +18,52 @@ import (
"github.com/anyproto/anytype-heart/pkg/lib/database"
"github.com/anyproto/anytype-heart/pkg/lib/localstore/addr"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
"github.com/anyproto/anytype-heart/space/clientspace"
"github.com/anyproto/anytype-heart/space/internal/components/dependencies"
"github.com/anyproto/anytype-heart/util/pbtypes"
)
const MName = "SystemObjectReviser"
var revisionKey = bundle.RelationKeyRevision.String()
func (s *service) reviseSystemObjects(space clientspace.Space, objects map[string]*types.Struct) {
marketObjects, err := s.listAllTypesAndRelations(addr.AnytypeMarketplaceWorkspace)
if err != nil {
log.Errorf("failed to get relations from marketplace space: %v", err)
return
}
// Migration SystemObjectReviser performs revision of all system object types and relations, so after Migration
// objects installed in space should correspond to bundled objects from library.
// To modify relations of system objects relation revision should be incremented in types.json or relations.json
// For more info see 'System Objects Update' section of docs/Flow.md
type Migration struct{}
for _, details := range objects {
reviseSystemObject(space, details, marketObjects)
}
func (Migration) Name() string {
return MName
}
func (s *service) listAllTypesAndRelations(spaceId string) (map[string]*types.Struct, error) {
records, err := s.objectStore.Query(database.Query{
func (Migration) Run(ctx context.Context, log logger.CtxLogger, store dependencies.QueryableStore, space dependencies.SpaceWithCtx) (toMigrate, migrated int, err error) {
spaceObjects, err := listAllTypesAndRelations(store, space.Id())
if err != nil {
return 0, 0, fmt.Errorf("failed to get relations and types from client space: %w", err)
}
marketObjects, err := listAllTypesAndRelations(store, addr.AnytypeMarketplaceWorkspace)
if err != nil {
return 0, 0, fmt.Errorf("failed to get relations from marketplace space: %w", err)
}
for _, details := range spaceObjects {
shouldBeRevised, e := reviseSystemObject(ctx, log, space, details, marketObjects)
if !shouldBeRevised {
continue
}
toMigrate++
if e != nil {
err = errors.Join(err, fmt.Errorf("failed to revise object: %w", e))
} else {
migrated++
}
}
return
}
func listAllTypesAndRelations(store dependencies.QueryableStore, spaceId string) (map[string]*types.Struct, error) {
records, err := store.Query(database.Query{
Filters: []*model.BlockContentDataviewFilter{
{
RelationKey: bundle.RelationKeyLayout.String(),
@ -50,30 +82,32 @@ func (s *service) listAllTypesAndRelations(spaceId string) (map[string]*types.St
}
details := make(map[string]*types.Struct, len(records))
for _, rec := range records {
id := pbtypes.GetString(rec.Details, bundle.RelationKeyId.String())
details[id] = rec.Details
for _, record := range records {
id := pbtypes.GetString(record.Details, bundle.RelationKeyId.String())
details[id] = record.Details
}
return details, nil
}
func reviseSystemObject(space clientspace.Space, localObject *types.Struct, marketObjects map[string]*types.Struct) {
func reviseSystemObject(ctx context.Context, log logger.CtxLogger, space dependencies.SpaceWithCtx, localObject *types.Struct, marketObjects map[string]*types.Struct) (toRevise bool, err error) {
source := pbtypes.GetString(localObject, bundle.RelationKeySourceObject.String())
marketObject, found := marketObjects[source]
if !found || !isSystemObject(localObject) || pbtypes.GetInt64(marketObject, revisionKey) <= pbtypes.GetInt64(localObject, revisionKey) {
return
return false, nil
}
details := buildDiffDetails(marketObject, localObject)
if len(details) != 0 {
if err := space.Do(pbtypes.GetString(localObject, bundle.RelationKeyId.String()), func(sb smartblock.SmartBlock) error {
log.Debug("updating system object", zap.String("source", source), zap.String("space", space.Id()))
if err := space.DoCtx(ctx, pbtypes.GetString(localObject, bundle.RelationKeyId.String()), func(sb smartblock.SmartBlock) error {
if ds, ok := sb.(basic.DetailsSettable); ok {
return ds.SetDetails(nil, details, false)
}
return nil
}); err != nil {
log.Errorf("failed to update system object %s in space %s: %v", source, space.Id(), err)
return true, fmt.Errorf("failed to update system object %s in space %s: %w", source, space.Id(), err)
}
}
return true, nil
}
func isSystemObject(details *types.Struct) bool {

View file

@ -1,9 +1,12 @@
package objectcreator
package systemobjectreviser
import (
"context"
"testing"
"github.com/anyproto/any-sync/app/logger"
"github.com/gogo/protobuf/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
@ -11,7 +14,9 @@ import (
"github.com/anyproto/anytype-heart/util/pbtypes"
)
func TestUpdateSystemObject(t *testing.T) {
func TestReviseSystemObject(t *testing.T) {
ctx := context.Background()
log := logger.NewNamed("tesr")
marketObjects := map[string]*types.Struct{
"_otnote": {Fields: map[string]*types.Value{revisionKey: pbtypes.Int64(3)}},
"_otpage": {Fields: map[string]*types.Value{revisionKey: pbtypes.Int64(2)}},
@ -23,48 +28,75 @@ func TestUpdateSystemObject(t *testing.T) {
}
t.Run("system object type is updated if revision is higher", func(t *testing.T) {
// given
objectType := &types.Struct{Fields: map[string]*types.Value{
bundle.RelationKeyRevision.String(): pbtypes.Int64(1),
bundle.RelationKeySourceObject.String(): pbtypes.String("_otnote"),
bundle.RelationKeyUniqueKey.String(): pbtypes.String("ot-note"),
}}
space := mock_space.NewMockSpace(t)
space.EXPECT().Do(mock.Anything, mock.Anything).Times(1).Return(nil)
space.EXPECT().DoCtx(mock.Anything, mock.Anything, mock.Anything).Times(1).Return(nil)
space.EXPECT().Id().Times(1).Return("")
reviseSystemObject(space, objectType, marketObjects)
// when
toRevise, err := reviseSystemObject(ctx, log, space, objectType, marketObjects)
// then
assert.NoError(t, err)
assert.True(t, toRevise)
})
t.Run("system object type is updated if no revision is set", func(t *testing.T) {
// given
objectType := &types.Struct{Fields: map[string]*types.Value{
bundle.RelationKeySourceObject.String(): pbtypes.String("_otpage"),
bundle.RelationKeyUniqueKey.String(): pbtypes.String("ot-page"),
}}
space := mock_space.NewMockSpace(t)
space.EXPECT().Do(mock.Anything, mock.Anything).Times(1).Return(nil)
space.EXPECT().DoCtx(mock.Anything, mock.Anything, mock.Anything).Times(1).Return(nil)
space.EXPECT().Id().Times(1).Return("")
reviseSystemObject(space, objectType, marketObjects)
// when
toRevise, err := reviseSystemObject(ctx, log, space, objectType, marketObjects)
// then
assert.NoError(t, err)
assert.True(t, toRevise)
})
t.Run("custom object type is not updated", func(t *testing.T) {
// given
objectType := &types.Struct{Fields: map[string]*types.Value{
bundle.RelationKeyUniqueKey.String(): pbtypes.String("ot-kitty"),
}}
space := mock_space.NewMockSpace(t) // if unexpected space.Do will be called, test will fail
reviseSystemObject(space, objectType, marketObjects)
// when
toRevise, err := reviseSystemObject(ctx, log, space, objectType, marketObjects)
// then
assert.NoError(t, err)
assert.False(t, toRevise)
})
t.Run("non system object type is not updated", func(t *testing.T) {
// given
objectType := &types.Struct{Fields: map[string]*types.Value{
bundle.RelationKeySourceObject.String(): pbtypes.String("_otcontact"),
bundle.RelationKeyUniqueKey.String(): pbtypes.String("ot-contact"),
}}
space := mock_space.NewMockSpace(t) // if unexpected space.Do will be called, test will fail
reviseSystemObject(space, objectType, marketObjects)
// when
toRevise, err := reviseSystemObject(ctx, log, space, objectType, marketObjects)
// then
assert.NoError(t, err)
assert.False(t, toRevise)
})
t.Run("system object type with same revision is not updated", func(t *testing.T) {
// given
objectType := &types.Struct{Fields: map[string]*types.Value{
bundle.RelationKeyRevision.String(): pbtypes.Int64(3),
bundle.RelationKeySourceObject.String(): pbtypes.String("_otnote"),
@ -72,42 +104,68 @@ func TestUpdateSystemObject(t *testing.T) {
}}
space := mock_space.NewMockSpace(t) // if unexpected space.Do will be called, test will fail
reviseSystemObject(space, objectType, marketObjects)
// when
toRevise, err := reviseSystemObject(ctx, log, space, objectType, marketObjects)
// then
assert.NoError(t, err)
assert.False(t, toRevise)
})
t.Run("system relation is updated if revision is higher", func(t *testing.T) {
// given
rel := &types.Struct{Fields: map[string]*types.Value{
bundle.RelationKeyRevision.String(): pbtypes.Int64(1),
bundle.RelationKeySourceObject.String(): pbtypes.String("_brdescription"),
bundle.RelationKeyUniqueKey.String(): pbtypes.String("rel-description"),
}}
space := mock_space.NewMockSpace(t)
space.EXPECT().Do(mock.Anything, mock.Anything).Times(1).Return(nil)
space.EXPECT().DoCtx(mock.Anything, mock.Anything, mock.Anything).Times(1).Return(nil)
space.EXPECT().Id().Times(1).Return("")
reviseSystemObject(space, rel, marketObjects)
// when
toRevise, err := reviseSystemObject(ctx, log, space, rel, marketObjects)
// then
assert.NoError(t, err)
assert.True(t, toRevise)
})
t.Run("system relation is updated if no revision is set", func(t *testing.T) {
// given
rel := &types.Struct{Fields: map[string]*types.Value{
bundle.RelationKeySourceObject.String(): pbtypes.String("_brid"),
bundle.RelationKeyUniqueKey.String(): pbtypes.String("rel-id"),
}}
space := mock_space.NewMockSpace(t)
space.EXPECT().Do(mock.Anything, mock.Anything).Times(1).Return(nil)
space.EXPECT().DoCtx(mock.Anything, mock.Anything, mock.Anything).Times(1).Return(nil)
space.EXPECT().Id().Times(1).Return("")
reviseSystemObject(space, rel, marketObjects)
// when
toRevise, err := reviseSystemObject(ctx, log, space, rel, marketObjects)
// then
assert.NoError(t, err)
assert.True(t, toRevise)
})
t.Run("custom relation is not updated", func(t *testing.T) {
// given
rel := &types.Struct{Fields: map[string]*types.Value{
bundle.RelationKeyUniqueKey.String(): pbtypes.String("rel-custom"),
}}
space := mock_space.NewMockSpace(t) // if unexpected space.Do will be called, test will fail
reviseSystemObject(space, rel, marketObjects)
// when
toRevise, err := reviseSystemObject(ctx, log, space, rel, marketObjects)
// then
assert.NoError(t, err)
assert.False(t, toRevise)
})
t.Run("non system relation is not updated", func(t *testing.T) {
// given
rel := &types.Struct{Fields: map[string]*types.Value{
bundle.RelationKeyRevision.String(): pbtypes.Int64(1),
bundle.RelationKeySourceObject.String(): pbtypes.String("_brlyrics"),
@ -115,10 +173,16 @@ func TestUpdateSystemObject(t *testing.T) {
}}
space := mock_space.NewMockSpace(t) // if unexpected space.Do will be called, test will fail
reviseSystemObject(space, rel, marketObjects)
// when
toRevise, err := reviseSystemObject(ctx, log, space, rel, marketObjects)
// then
assert.NoError(t, err)
assert.False(t, toRevise)
})
t.Run("system relation with same revision is not updated", func(t *testing.T) {
// given
rel := &types.Struct{Fields: map[string]*types.Value{
bundle.RelationKeyRevision.String(): pbtypes.Int64(3),
bundle.RelationKeySourceObject.String(): pbtypes.String("_brisReadonly"),
@ -126,10 +190,16 @@ func TestUpdateSystemObject(t *testing.T) {
}}
space := mock_space.NewMockSpace(t) // if unexpected space.Do will be called, test will fail
reviseSystemObject(space, rel, marketObjects)
// when
toRevise, err := reviseSystemObject(ctx, log, space, rel, marketObjects)
// then
assert.NoError(t, err)
assert.False(t, toRevise)
})
t.Run("relation with absent maxCount is updated", func(t *testing.T) {
// given
rel := &types.Struct{Fields: map[string]*types.Value{
bundle.RelationKeyRevision.String(): pbtypes.Int64(2),
bundle.RelationKeySourceObject.String(): pbtypes.String("_brisReadonly"),
@ -137,8 +207,14 @@ func TestUpdateSystemObject(t *testing.T) {
bundle.RelationKeyRelationMaxCount.String(): pbtypes.Int64(1),
}}
space := mock_space.NewMockSpace(t)
space.EXPECT().Do(mock.Anything, mock.Anything).Times(1).Return(nil)
space.EXPECT().DoCtx(mock.Anything, mock.Anything, mock.Anything).Times(1).Return(nil)
space.EXPECT().Id().Times(1).Return("")
reviseSystemObject(space, rel, marketObjects)
// when
toRevise, err := reviseSystemObject(ctx, log, space, rel, marketObjects)
// then
assert.NoError(t, err)
assert.True(t, toRevise)
})
}

View file

@ -3,6 +3,7 @@ package marketplacespace
import (
"context"
"fmt"
"sync"
"github.com/anyproto/any-sync/accountservice"
"github.com/anyproto/any-sync/app"
@ -21,6 +22,7 @@ func NewSpaceController(a *app.App, personalSpaceId string) spacecontroller.Spac
return &spaceController{
app: a,
personalSpaceId: personalSpaceId,
indexer: app.MustComponent[dependencies.SpaceIndexer](a),
}
}
@ -28,17 +30,18 @@ type spaceController struct {
app *app.App
personalSpaceId string
vs clientspace.Space
reindexOnce sync.Once
indexer dependencies.SpaceIndexer
}
func (s *spaceController) Start(ctx context.Context) (err error) {
indexer := app.MustComponent[dependencies.SpaceIndexer](s.app)
func (s *spaceController) Start(context.Context) (err error) {
s.vs = clientspace.NewVirtualSpace(
addr.AnytypeMarketplaceWorkspace,
clientspace.VirtualSpaceDeps{
ObjectFactory: app.MustComponent[objectcache.ObjectFactory](s.app),
AccountService: app.MustComponent[accountservice.Service](s.app),
PersonalSpaceId: s.personalSpaceId,
Indexer: app.MustComponent[dependencies.SpaceIndexer](s.app),
Indexer: s.indexer,
Installer: app.MustComponent[dependencies.BundledObjectsInstaller](s.app),
TypePrefix: addr.BundledObjectTypeURLPrefix,
RelationPrefix: addr.BundledRelationURLPrefix,
@ -53,10 +56,6 @@ func (s *spaceController) Start(ctx context.Context) (err error) {
if err != nil {
return fmt.Errorf("register builtin templates: %w", err)
}
err = indexer.ReindexMarketplaceSpace(s.vs)
if err != nil {
return fmt.Errorf("reindex marketplace space: %w", err)
}
return err
}
@ -64,7 +63,14 @@ func (s *spaceController) Mode() mode.Mode {
return mode.ModeLoading
}
func (s *spaceController) WaitLoad(ctx context.Context) (sp clientspace.Space, err error) {
func (s *spaceController) WaitLoad(context.Context) (sp clientspace.Space, err error) {
s.reindexOnce.Do(func() {
// TODO: GO-3557 Need to confirm moving ReindexMarketplaceSpace from Start to WaitLoad with mcrakhman
err = s.indexer.ReindexMarketplaceSpace(s.vs)
})
if err != nil {
return nil, err
}
return s.vs, nil
}

View file

@ -10,6 +10,7 @@ import (
"github.com/anyproto/anytype-heart/space/internal/components/aclobjectmanager"
"github.com/anyproto/anytype-heart/space/internal/components/builder"
"github.com/anyproto/anytype-heart/space/internal/components/invitemigrator"
"github.com/anyproto/anytype-heart/space/internal/components/migration"
"github.com/anyproto/anytype-heart/space/internal/components/participantwatcher"
"github.com/anyproto/anytype-heart/space/internal/components/spaceloader"
"github.com/anyproto/anytype-heart/space/internal/spaceprocess/components/aclindexcleaner"
@ -43,7 +44,8 @@ func New(app *app.App, params Params) Loader {
Register(aclnotifications.NewAclNotificationSender()).
Register(aclobjectmanager.New(params.OwnerMetadata)).
Register(invitemigrator.New()).
Register(participantwatcher.New())
Register(participantwatcher.New()).
Register(migration.New())
return &loader{
app: child,
}

View file

@ -7,6 +7,7 @@ import (
"github.com/anyproto/anytype-heart/space/clientspace"
"github.com/anyproto/anytype-heart/space/internal/components/builder"
"github.com/anyproto/anytype-heart/space/internal/components/migration"
"github.com/anyproto/anytype-heart/space/internal/components/spaceloader"
"github.com/anyproto/anytype-heart/space/internal/spaceprocess/components/aclindexcleaner"
"github.com/anyproto/anytype-heart/space/internal/spaceprocess/loader"
@ -32,7 +33,8 @@ func New(app *app.App, params Params) Remover {
child := app.ChildApp()
child.Register(aclindexcleaner.New()).
Register(builder.New()).
Register(spaceloader.New(params.StopIfMandatoryFail, true))
Register(spaceloader.New(params.StopIfMandatoryFail, true)).
Register(migration.New())
return &remover{
app: child,
}

View file

@ -83,6 +83,7 @@ type NotificationSender interface {
type service struct {
techSpace *clientspace.TechSpace
techSpaceReady chan struct{}
factory spacefactory.SpaceFactory
spaceCore spacecore.SpaceCoreService
accountService accountservice.Service
@ -137,6 +138,7 @@ func (s *service) Init(a *app.App) (err error) {
s.spaceControllers = make(map[string]spacecontroller.SpaceController)
s.updater = app.MustComponent[coordinatorStatusUpdater](a)
s.waiting = make(map[string]controllerWaiter)
s.techSpaceReady = make(chan struct{})
s.personalSpaceId, err = s.spaceCore.DeriveID(context.Background(), spacecore.SpaceType)
if err != nil {
return
@ -209,8 +211,8 @@ func (s *service) Wait(ctx context.Context, spaceId string) (sp clientspace.Spac
}
func (s *service) Get(ctx context.Context, spaceId string) (sp clientspace.Space, err error) {
if spaceId == s.techSpace.TechSpaceId() {
return s.techSpace, nil
if spaceId == s.techSpaceId {
return s.getTechSpace(ctx)
}
ctrl, err := s.getCtrl(ctx, spaceId)
if err != nil {
@ -355,5 +357,14 @@ func (s *service) AllSpaceIds() (ids []string) {
}
func (s *service) TechSpaceId() string {
return s.techSpace.TechSpaceId()
return s.techSpaceId
}
func (s *service) getTechSpace(ctx context.Context) (*clientspace.TechSpace, error) {
select {
case <-s.techSpaceReady:
return s.techSpace, nil
case <-ctx.Done():
return nil, ctx.Err()
}
}

View file

@ -5,6 +5,7 @@ import (
"fmt"
"strings"
"testing"
"time"
"github.com/anyproto/any-sync/accountservice/mock_accountservice"
"github.com/anyproto/any-sync/app"
@ -46,12 +47,37 @@ const (
// TODO Revive tests
func TestService_Init(t *testing.T) {
t.Skip("@roman should revive this test")
t.Run("tech space getter", func(t *testing.T) {
serv := New().(*service)
serv.techSpaceId = "tech.space"
factory := mock_spacefactory.NewMockSpaceFactory(t)
serv.factory = factory
serv.techSpaceReady = make(chan struct{})
// not initialized - expect context deadline
ctx, ctxCancel := context.WithTimeout(context.Background(), time.Millisecond)
defer ctxCancel()
_, err := serv.Get(ctx, serv.techSpaceId)
require.ErrorIs(t, err, context.DeadlineExceeded)
// initialized - expect space
ctx2, ctxCancel2 := context.WithTimeout(context.Background(), time.Millisecond)
defer ctxCancel2()
factory.EXPECT().CreateAndSetTechSpace(ctx2).Return(&clientspace.TechSpace{}, nil)
require.NoError(t, serv.initTechSpace(ctx2))
s, err := serv.Get(ctx2, serv.techSpaceId)
require.NoError(t, err)
assert.NotNil(t, s)
})
t.Run("existing account", func(t *testing.T) {
t.Skip("@roman should revive this test")
fx := newFixture(t, false)
defer fx.finish(t)
})
t.Run("new account", func(t *testing.T) {
t.Skip("@roman should revive this test")
fx := newFixture(t, true)
defer fx.finish(t)
})

58
util/bufferpool/buffer.go Normal file
View file

@ -0,0 +1,58 @@
package bufferpool
import (
"bytes"
"io"
"sync"
)
type Buffer interface {
io.Writer
io.Closer
GetReadSeekCloser() (io.ReadSeekCloser, error)
}
type buffer struct {
*bytes.Buffer
buf []byte
pool *sync.Pool
m sync.Mutex
closed bool
}
// GetReadSeekCloser returns a ReadSeekCloser that reads from the buffer.
// GetReadSeekCloser after Close will return EOF.
// It's a responsibility of the caller to Close the ReadSeekCloser to put the buffer back into the pool.
func (b *buffer) GetReadSeekCloser() (io.ReadSeekCloser, error) {
b.m.Lock()
defer b.m.Unlock()
if !b.closed {
b.closed = true
return newPoolReadSeekCloser(b.Buffer.Bytes(), b.pool), nil
}
return nil, io.EOF
}
// Close puts the buffer back into the pool.
// Close after GetReadSeekCloser does nothing.
func (b *buffer) Write(p []byte) (n int, err error) {
b.m.Lock()
defer b.m.Unlock()
if b.closed {
return 0, io.EOF
}
return b.Buffer.Write(p)
}
// Close puts the buffer back into the pool.
// Close after GetReadSeekCloser does nothing.
func (b *buffer) Close() error {
b.m.Lock()
defer b.m.Unlock()
if !b.closed {
b.pool.Put(b.buf)
b.closed = true
}
return nil
}

34
util/bufferpool/pool.go Normal file
View file

@ -0,0 +1,34 @@
package bufferpool
import (
"bytes"
"sync"
)
type Pool interface {
Get() Buffer
}
func NewPool() Pool {
return &bufferPoolWrapper{pool: &sync.Pool{
New: func() interface{} {
return []byte{}
},
}}
}
type bufferPoolWrapper struct {
pool *sync.Pool
}
func (bp *bufferPoolWrapper) Get() Buffer {
b := bp.pool.Get().([]byte)
buff := &buffer{
Buffer: bytes.NewBuffer(b[:0]),
buf: b,
pool: bp.pool,
}
return buff
}

View file

@ -0,0 +1,84 @@
package bufferpool
import (
"io"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewPool(t *testing.T) {
pool := NewPool()
require.NotNil(t, pool, "NewPool should not return nil")
}
func TestBuffer_Write(t *testing.T) {
pool := NewPool()
buf := pool.Get()
data := []byte("Hello, World!")
n, err := buf.Write(data)
require.NoError(t, err, "Write should not return an error")
assert.Equal(t, len(data), n, "Write should return the number of bytes written")
err = buf.Close()
require.NoError(t, err, "Close should not return an error")
}
func TestBuffer_Close(t *testing.T) {
pool := NewPool()
buf := pool.Get()
err := buf.Close()
require.NoError(t, err, "Close should not return an error")
n, err := buf.Write([]byte("Hello, World!"))
assert.ErrorIs(t, err, io.EOF, "Read after Close should return an error")
require.Zero(t, n, "Write after Close should not write any bytes")
}
func TestBuffer_GetReadSeekCloser(t *testing.T) {
pool := NewPool()
buf := pool.Get()
data := []byte("Hello, World!")
_, err := buf.Write(data)
require.NoError(t, err, "Write should not return an error")
rsc, err := buf.GetReadSeekCloser()
require.NoError(t, err, "GetReadSeekCloser should not return an error")
assert.NotNil(t, rsc, "GetReadSeekCloser should not return nil")
readData := make([]byte, len(data))
readData2 := make([]byte, len(data))
n, err := rsc.Read(readData)
require.NoError(t, err, "Read should not return an error")
assert.Equal(t, len(data), n, "Read should return the number of bytes read")
assert.Equal(t, data, readData, "Read data should match written data")
n2, err := rsc.Seek(0, io.SeekStart)
require.NoError(t, err, "Seek should not return an error")
assert.Equal(t, int64(0), n2, "Seek should return the new offset")
_, err = rsc.Read(readData2)
require.NoError(t, err, "Read after seek should not return an error")
assert.Equal(t, data, readData2, "Read data after seek should match written data")
err = rsc.Close()
require.NoError(t, err, "Close should not return an error")
_, err = rsc.Read(readData)
assert.Error(t, err, "Read after Close should return an error")
// take the existing buffer from the pool
buf = pool.Get()
// check underlying buffer is returned to the pool
assert.GreaterOrEqual(t, cap(buf.(*buffer).buf), 13, "we should get the same buffer from the pool")
assert.GreaterOrEqual(t, buf.(*buffer).Buffer.Cap(), 13, "we should get the same buffer from the pool")
assert.Equalf(t, 0, len(buf.(*buffer).Buffer.Bytes()), "we should get the reseted buffer from the pool")
assert.Equal(t, []byte("Hello, World!"), buf.(*buffer).buf[0:13])
assert.Equal(t, []byte("Hello, World!"), buf.(*buffer).Buffer.Bytes()[0:13])
err = rsc.Close()
require.NoError(t, err, "Close after Close should not return an error")
}

47
util/bufferpool/reader.go Normal file
View file

@ -0,0 +1,47 @@
package bufferpool
import (
"bytes"
"io"
"sync"
)
// poolReadSeekCloser is a custom type that wraps a byte slice and a sync.Pool.
type poolReadSeekCloser struct {
*bytes.Reader
buf []byte
pool *sync.Pool
m sync.RWMutex
closed bool
}
// NewPoolReadSeekCloser creates a new poolReadSeekCloser.
func newPoolReadSeekCloser(buf []byte, pool *sync.Pool) io.ReadSeekCloser {
return &poolReadSeekCloser{
Reader: bytes.NewReader(buf),
buf: buf,
pool: pool,
}
}
// Close puts the buffer back into the pool.
func (prsc *poolReadSeekCloser) Close() error {
prsc.m.Lock()
defer prsc.m.Unlock()
if prsc.closed {
return nil
}
prsc.closed = true
prsc.pool.Put(prsc.buf)
return nil
}
func (prsc *poolReadSeekCloser) Read(p []byte) (n int, err error) {
prsc.m.RLock()
defer prsc.m.RUnlock()
if prsc.closed {
return 0, io.EOF
}
return prsc.Reader.Read(p)
}

View file

@ -32,14 +32,14 @@ func (c *cache) Name() string {
return CName
}
func (c *cache) Fetch(ctx context.Context, url string) (lp model.LinkPreview, body []byte, err error) {
func (c *cache) Fetch(ctx context.Context, url string) (linkPreview model.LinkPreview, responseBody []byte, isFile bool, err error) {
if res, ok := c.cache.Get(url); ok {
return res.(model.LinkPreview), nil, nil
return res.(model.LinkPreview), nil, false, nil
}
lp, body, err = c.lp.Fetch(ctx, url)
linkPreview, responseBody, _, err = c.lp.Fetch(ctx, url)
if err != nil {
return
}
c.cache.Add(url, lp)
c.cache.Add(url, linkPreview)
return
}

View file

@ -14,7 +14,7 @@ func TestCache_Fetch(t *testing.T) {
ts := newTestServer("text/html", strings.NewReader(tetsHtml))
lp := NewWithCache()
lp.Init(nil)
info, _, err := lp.Fetch(ctx, ts.URL)
info, _, _, err := lp.Fetch(ctx, ts.URL)
require.NoError(t, err)
assert.Equal(t, model.LinkPreview{
Url: ts.URL,
@ -27,7 +27,7 @@ func TestCache_Fetch(t *testing.T) {
ts.Close()
info, _, err = lp.Fetch(ctx, ts.URL)
info, _, _, err = lp.Fetch(ctx, ts.URL)
require.NoError(t, err)
assert.Equal(t, model.LinkPreview{
Url: ts.URL,

View file

@ -5,6 +5,7 @@ import (
"context"
"fmt"
"io"
"mime"
"net/http"
"path/filepath"
"strings"
@ -40,7 +41,7 @@ const (
var log = logging.Logger("link-preview")
type LinkPreview interface {
Fetch(ctx context.Context, url string) (model.LinkPreview, []byte, error)
Fetch(ctx context.Context, url string) (linkPreview model.LinkPreview, responseBody []byte, isFile bool, err error)
app.Component
}
@ -57,27 +58,27 @@ func (l *linkPreview) Name() (name string) {
return CName
}
func (l *linkPreview) Fetch(ctx context.Context, fetchUrl string) (model.LinkPreview, []byte, error) {
func (l *linkPreview) Fetch(ctx context.Context, fetchUrl string) (linkPreview model.LinkPreview, responseBody []byte, isFile bool, err error) {
rt := &proxyRoundTripper{RoundTripper: http.DefaultTransport}
client := &http.Client{Transport: rt}
og := opengraph.New(fetchUrl)
og.URL = fetchUrl
og.Intent.Context = ctx
og.Intent.HTTPClient = client
err := og.Fetch()
err = og.Fetch()
if err != nil {
if resp := rt.lastResponse; resp != nil && resp.StatusCode == http.StatusOK {
preview, err := l.makeNonHtml(fetchUrl, resp)
preview, isFile, err := l.makeNonHtml(fetchUrl, resp)
if err != nil {
return preview, nil, err
return preview, nil, false, err
}
return preview, rt.lastBody, nil
return preview, rt.lastBody, isFile, nil
}
return model.LinkPreview{}, nil, err
return model.LinkPreview{}, nil, false, err
}
if resp := rt.lastResponse; resp != nil && resp.StatusCode != http.StatusOK {
return model.LinkPreview{}, nil, fmt.Errorf("invalid http code %d", resp.StatusCode)
return model.LinkPreview{}, nil, false, fmt.Errorf("invalid http code %d", resp.StatusCode)
}
res := l.convertOGToInfo(fetchUrl, og)
if len(res.Description) == 0 {
@ -93,7 +94,7 @@ func (l *linkPreview) Fetch(ctx context.Context, fetchUrl string) (model.LinkPre
if err != nil {
log.Errorf("failed to decode request %s", err)
}
return res, decodedResponse, err
return res, decodedResponse, false, nil
}
func decodeResponse(response *proxyRoundTripper) ([]byte, error) {
@ -149,7 +150,7 @@ func (l *linkPreview) findContent(data []byte) (content string) {
return
}
func (l *linkPreview) makeNonHtml(fetchUrl string, resp *http.Response) (i model.LinkPreview, err error) {
func (l *linkPreview) makeNonHtml(fetchUrl string, resp *http.Response) (i model.LinkPreview, isFile bool, err error) {
ct := resp.Header.Get("Content-Type")
i.Url = fetchUrl
i.Title = filepath.Base(fetchUrl)
@ -161,6 +162,7 @@ func (l *linkPreview) makeNonHtml(fetchUrl string, resp *http.Response) (i model
} else {
i.Type = model.LinkPreview_Unknown
}
isFile = checkFileType(fetchUrl, resp, ct)
pURL, e := uri.ParseURI(fetchUrl)
if e == nil {
pURL.Path = "favicon.ico"
@ -170,6 +172,17 @@ func (l *linkPreview) makeNonHtml(fetchUrl string, resp *http.Response) (i model
return
}
func checkFileType(url string, resp *http.Response, contentType string) bool {
ext := filepath.Ext(url)
mimeType := mime.TypeByExtension(ext)
return isContentFile(resp, contentType, mimeType)
}
func isContentFile(resp *http.Response, contentType, mimeType string) bool {
return contentType != "" || strings.Contains(resp.Header.Get("Content-Disposition"), "filename") ||
mimeType != ""
}
type proxyRoundTripper struct {
http.RoundTripper
lastResponse *http.Response

View file

@ -23,8 +23,9 @@ func TestLinkPreview_Fetch(t *testing.T) {
lp := New()
lp.Init(nil)
info, _, err := lp.Fetch(ctx, ts.URL)
info, _, isFile, err := lp.Fetch(ctx, ts.URL)
require.NoError(t, err)
assert.False(t, isFile)
assert.Equal(t, model.LinkPreview{
Url: ts.URL,
FaviconUrl: ts.URL + "/favicon.ico",
@ -41,7 +42,7 @@ func TestLinkPreview_Fetch(t *testing.T) {
lp := New()
lp.Init(nil)
info, _, err := lp.Fetch(ctx, ts.URL)
info, _, isFile, err := lp.Fetch(ctx, ts.URL)
require.NoError(t, err)
assert.Equal(t, model.LinkPreview{
Url: ts.URL,
@ -51,6 +52,7 @@ func TestLinkPreview_Fetch(t *testing.T) {
ImageUrl: "http://site.com/images/example.jpg",
Type: model.LinkPreview_Page,
}, info)
assert.False(t, isFile)
})
t.Run("binary image", func(t *testing.T) {
@ -60,7 +62,7 @@ func TestLinkPreview_Fetch(t *testing.T) {
url := ts.URL + "/filename.jpg"
lp := New()
lp.Init(nil)
info, _, err := lp.Fetch(ctx, url)
info, _, isFile, err := lp.Fetch(ctx, url)
require.NoError(t, err)
assert.Equal(t, model.LinkPreview{
Url: url,
@ -69,23 +71,48 @@ func TestLinkPreview_Fetch(t *testing.T) {
ImageUrl: url,
Type: model.LinkPreview_Image,
}, info)
assert.True(t, isFile)
})
t.Run("binary", func(t *testing.T) {
tr := testReader(0)
ts := newTestServer("binary/octed-stream", &tr)
defer ts.Close()
url := ts.URL + "/filename.jpg"
lp := New()
lp.Init(nil)
info, _, err := lp.Fetch(ctx, url)
require.NoError(t, err)
assert.Equal(t, model.LinkPreview{
Url: url,
Title: "filename.jpg",
FaviconUrl: ts.URL + "/favicon.ico",
Type: model.LinkPreview_Unknown,
}, info)
t.Run("check content is file by extension", func(t *testing.T) {
// given
resp := &http.Response{Header: map[string][]string{}}
// when
isFile := checkFileType("http://site.com/images/example.jpg", resp, "")
// then
assert.True(t, isFile)
})
t.Run("check content is file by content-type", func(t *testing.T) {
// given
resp := &http.Response{Header: map[string][]string{}}
// when
isFile := checkFileType("htt://example.com/filepath", resp, "application/pdf")
// then
assert.True(t, isFile)
})
t.Run("check content is file by content-disposition", func(t *testing.T) {
// given
resp := &http.Response{Header: map[string][]string{"Content-Disposition": {"attachment filename=\"user.csv\""}}}
// when
isFile := checkFileType("htt://example.com/filepath", resp, "")
// then
assert.True(t, isFile)
})
t.Run("check content is not file", func(t *testing.T) {
// given
resp := &http.Response{Header: map[string][]string{}}
// when
isFile := checkFileType("htt://example.com/notfile", resp, "")
// then
assert.False(t, isFile)
})
}

View file

@ -0,0 +1,204 @@
// Code generated by mockery. DO NOT EDIT.
package mock_linkpreview
import (
context "context"
app "github.com/anyproto/any-sync/app"
mock "github.com/stretchr/testify/mock"
model "github.com/anyproto/anytype-heart/pkg/lib/pb/model"
)
// MockLinkPreview is an autogenerated mock type for the LinkPreview type
type MockLinkPreview struct {
mock.Mock
}
type MockLinkPreview_Expecter struct {
mock *mock.Mock
}
func (_m *MockLinkPreview) EXPECT() *MockLinkPreview_Expecter {
return &MockLinkPreview_Expecter{mock: &_m.Mock}
}
// Fetch provides a mock function with given fields: ctx, url
func (_m *MockLinkPreview) Fetch(ctx context.Context, url string) (model.LinkPreview, []byte, bool, error) {
ret := _m.Called(ctx, url)
if len(ret) == 0 {
panic("no return value specified for Fetch")
}
var r0 model.LinkPreview
var r1 []byte
var r2 bool
var r3 error
if rf, ok := ret.Get(0).(func(context.Context, string) (model.LinkPreview, []byte, bool, error)); ok {
return rf(ctx, url)
}
if rf, ok := ret.Get(0).(func(context.Context, string) model.LinkPreview); ok {
r0 = rf(ctx, url)
} else {
r0 = ret.Get(0).(model.LinkPreview)
}
if rf, ok := ret.Get(1).(func(context.Context, string) []byte); ok {
r1 = rf(ctx, url)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).([]byte)
}
}
if rf, ok := ret.Get(2).(func(context.Context, string) bool); ok {
r2 = rf(ctx, url)
} else {
r2 = ret.Get(2).(bool)
}
if rf, ok := ret.Get(3).(func(context.Context, string) error); ok {
r3 = rf(ctx, url)
} else {
r3 = ret.Error(3)
}
return r0, r1, r2, r3
}
// MockLinkPreview_Fetch_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Fetch'
type MockLinkPreview_Fetch_Call struct {
*mock.Call
}
// Fetch is a helper method to define mock.On call
// - ctx context.Context
// - url string
func (_e *MockLinkPreview_Expecter) Fetch(ctx interface{}, url interface{}) *MockLinkPreview_Fetch_Call {
return &MockLinkPreview_Fetch_Call{Call: _e.mock.On("Fetch", ctx, url)}
}
func (_c *MockLinkPreview_Fetch_Call) Run(run func(ctx context.Context, url string)) *MockLinkPreview_Fetch_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string))
})
return _c
}
func (_c *MockLinkPreview_Fetch_Call) Return(_a0 model.LinkPreview, _a1 []byte, _a2 bool, _a3 error) *MockLinkPreview_Fetch_Call {
_c.Call.Return(_a0, _a1, _a2, _a3)
return _c
}
func (_c *MockLinkPreview_Fetch_Call) RunAndReturn(run func(context.Context, string) (model.LinkPreview, []byte, bool, error)) *MockLinkPreview_Fetch_Call {
_c.Call.Return(run)
return _c
}
// Init provides a mock function with given fields: a
func (_m *MockLinkPreview) Init(a *app.App) error {
ret := _m.Called(a)
if len(ret) == 0 {
panic("no return value specified for Init")
}
var r0 error
if rf, ok := ret.Get(0).(func(*app.App) error); ok {
r0 = rf(a)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockLinkPreview_Init_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Init'
type MockLinkPreview_Init_Call struct {
*mock.Call
}
// Init is a helper method to define mock.On call
// - a *app.App
func (_e *MockLinkPreview_Expecter) Init(a interface{}) *MockLinkPreview_Init_Call {
return &MockLinkPreview_Init_Call{Call: _e.mock.On("Init", a)}
}
func (_c *MockLinkPreview_Init_Call) Run(run func(a *app.App)) *MockLinkPreview_Init_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(*app.App))
})
return _c
}
func (_c *MockLinkPreview_Init_Call) Return(err error) *MockLinkPreview_Init_Call {
_c.Call.Return(err)
return _c
}
func (_c *MockLinkPreview_Init_Call) RunAndReturn(run func(*app.App) error) *MockLinkPreview_Init_Call {
_c.Call.Return(run)
return _c
}
// Name provides a mock function with given fields:
func (_m *MockLinkPreview) Name() string {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Name")
}
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// MockLinkPreview_Name_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Name'
type MockLinkPreview_Name_Call struct {
*mock.Call
}
// Name is a helper method to define mock.On call
func (_e *MockLinkPreview_Expecter) Name() *MockLinkPreview_Name_Call {
return &MockLinkPreview_Name_Call{Call: _e.mock.On("Name")}
}
func (_c *MockLinkPreview_Name_Call) Run(run func()) *MockLinkPreview_Name_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockLinkPreview_Name_Call) Return(name string) *MockLinkPreview_Name_Call {
_c.Call.Return(name)
return _c
}
func (_c *MockLinkPreview_Name_Call) RunAndReturn(run func() string) *MockLinkPreview_Name_Call {
_c.Call.Return(run)
return _c
}
// NewMockLinkPreview creates a new instance of MockLinkPreview. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockLinkPreview(t interface {
mock.TestingT
Cleanup(func())
}) *MockLinkPreview {
mock := &MockLinkPreview{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View file

@ -233,6 +233,21 @@ func (mr *MockObjectStoreMockRecorder) GetAccountStatus() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountStatus", reflect.TypeOf((*MockObjectStore)(nil).GetAccountStatus))
}
// GetActiveViews mocks base method.
func (m *MockObjectStore) GetActiveViews(arg0 string) (map[string]string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetActiveViews", arg0)
ret0, _ := ret[0].(map[string]string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetActiveViews indicates an expected call of GetActiveViews.
func (mr *MockObjectStoreMockRecorder) GetActiveViews(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetActiveViews", reflect.TypeOf((*MockObjectStore)(nil).GetActiveViews), arg0)
}
// GetByIDs mocks base method.
func (m *MockObjectStore) GetByIDs(arg0 string, arg1 []string) ([]*model.ObjectInfo, error) {
m.ctrl.T.Helper()
@ -767,6 +782,34 @@ func (mr *MockObjectStoreMockRecorder) SaveVirtualSpace(arg0 any) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveVirtualSpace", reflect.TypeOf((*MockObjectStore)(nil).SaveVirtualSpace), arg0)
}
// SetActiveView mocks base method.
func (m *MockObjectStore) SetActiveView(arg0, arg1, arg2 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetActiveView", arg0, arg1, arg2)
ret0, _ := ret[0].(error)
return ret0
}
// SetActiveView indicates an expected call of SetActiveView.
func (mr *MockObjectStoreMockRecorder) SetActiveView(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetActiveView", reflect.TypeOf((*MockObjectStore)(nil).SetActiveView), arg0, arg1, arg2)
}
// SetActiveViews mocks base method.
func (m *MockObjectStore) SetActiveViews(arg0 string, arg1 map[string]string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetActiveViews", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// SetActiveViews indicates an expected call of SetActiveViews.
func (mr *MockObjectStoreMockRecorder) SetActiveViews(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetActiveViews", reflect.TypeOf((*MockObjectStore)(nil).SetActiveViews), arg0, arg1)
}
// SubscribeForAll mocks base method.
func (m *MockObjectStore) SubscribeForAll(arg0 func(database.Record)) {
m.ctrl.T.Helper()