diff --git a/cmd/daemon.go b/cmd/daemon.go index 425a0e4c..bcd1b2c4 100644 --- a/cmd/daemon.go +++ b/cmd/daemon.go @@ -51,12 +51,11 @@ var daemonCmd = &cobra.Command{ fx_opt.Override(new(block.Adapter), factory.BuildBlockAdapter), //database fx_opt.Override(new(*bun.DB), models.SetupDatabase), - fx_opt.Override(fx_opt.NextInvoke(), migrations.MigrateDatabase), - fx_opt.Override(new(models.IUserRepo), models.NewUserRepo), - fx_opt.Override(new(models.IRepositoryRepo), models.NewRepositoryRepo), - fx_opt.Override(new(models.IRefRepo), models.NewRefRepo), - fx_opt.Override(new(models.IObjectRepo), models.NewObjectRepo), + fx_opt.Override(new(models.IRepo), func(db *bun.DB) models.IRepo { + return models.NewRepo(db) + }), + fx_opt.Override(fx_opt.NextInvoke(), migrations.MigrateDatabase), //api fx_opt.Override(fx_opt.NextInvoke(), apiImpl.SetupAPI), ) diff --git a/config/default.go b/config/default.go index b1709816..bd678e01 100644 --- a/config/default.go +++ b/config/default.go @@ -1,5 +1,7 @@ package config +import "github.com/jiaozifs/jiaozifs/utils" + var DefaultLocalBSPath = "~/.jiaozifs/blockstore" var defaultCfg = Config{ @@ -12,7 +14,7 @@ var defaultCfg = Config{ }, Blockstore: BlockStoreConfig{ Type: "local", - DefaultNamespacePrefix: nil, + DefaultNamespacePrefix: utils.String("data"), Local: (*struct { Path string `mapstructure:"path"` ImportEnabled bool `mapstructure:"import_enabled"` diff --git a/controller/object_ctl.go b/controller/object_ctl.go index 9d6f2ddd..c1b4d54a 100644 --- a/controller/object_ctl.go +++ b/controller/object_ctl.go @@ -9,13 +9,10 @@ import ( "net/http" "time" - "github.com/jiaozifs/jiaozifs/config" - - "github.com/jiaozifs/jiaozifs/versionmgr" - + "github.com/go-openapi/swag" "github.com/jiaozifs/jiaozifs/models/filemode" - "github.com/go-openapi/swag" + "github.com/jiaozifs/jiaozifs/versionmgr" "github.com/jiaozifs/jiaozifs/block" @@ -33,13 +30,7 @@ type ObjectController struct { BlockAdapter block.Adapter - StashRepo models.StashRepo - UserRepo models.IUserRepo - Repository models.RepositoryRepo - Object models.ObjectRepo - Ref models.RefRepo - - Cfg config.BlockStoreConfig + Repo models.IRepo } func (oct ObjectController) DeleteObject(ctx context.Context, w *api.JiaozifsResponse, r *http.Request, user string, repository string, params api.DeleteObjectParams) { //nolint @@ -53,13 +44,13 @@ func (oct ObjectController) GetObject(ctx context.Context, w *api.JiaozifsRespon } func (oct ObjectController) HeadObject(ctx context.Context, w *api.JiaozifsResponse, r *http.Request, userName string, repository string, params api.HeadObjectParams) { //nolint - user, err := oct.UserRepo.Get(ctx, &models.GetUserParam{Name: utils.String(userName)}) + user, err := oct.Repo.UserRepo().Get(ctx, &models.GetUserParam{Name: utils.String(userName)}) if err != nil { w.Error(err) return } - repo, err := oct.Repository.Get(ctx, &models.GetRepoParams{ + repo, err := oct.Repo.RepositoryRepo().Get(ctx, &models.GetRepoParams{ CreateID: user.ID, Name: utils.String(repository), }) @@ -68,7 +59,7 @@ func (oct ObjectController) HeadObject(ctx context.Context, w *api.JiaozifsRespo return } - ref, err := oct.Ref.Get(ctx, &models.GetRefParams{ + ref, err := oct.Repo.RefRepo().Get(ctx, &models.GetRefParams{ RepositoryID: repo.ID, Name: utils.String(params.Branch), }) @@ -77,46 +68,44 @@ func (oct ObjectController) HeadObject(ctx context.Context, w *api.JiaozifsRespo return } - commit, err := oct.Object.Commit(ctx, ref.CommitHash) + commit, err := oct.Repo.ObjectRepo().Commit(ctx, ref.CommitHash) if err != nil { w.Error(err) return } - treeOp := versionmgr.NewTreeOp(oct.Object) - - treeNode, err := oct.Object.TreeNode(ctx, commit.TreeHash) + objRepo := oct.Repo.ObjectRepo() + treeOp := versionmgr.NewTreeOp(oct.Repo.ObjectRepo()) + treeNode, err := objRepo.TreeNode(ctx, commit.TreeHash) if err != nil { w.Error(err) return } - existNodes, missingPath, err := treeOp.MatchPath(ctx, treeNode, params.Path) if err != nil { w.Error(err) return } - if len(missingPath) == 0 { w.Error(versionmgr.ErrPathNotFound) return } - blobWithName := existNodes[len(existNodes)-1] + objectWithName := existNodes[len(existNodes)-1] - blob, err := oct.Object.Blob(ctx, blobWithName.Node.Hash) + blob, err := objRepo.Blob(ctx, objectWithName.Node.Hash) if err != nil { w.Error(err) return } //lookup files - etag := httputil.ETag(blobWithName.Node.Hash.Hex()) + etag := httputil.ETag(objectWithName.Node.Hash.Hex()) w.Header().Set("ETag", etag) - lastModified := httputil.HeaderTimestamp(blobWithName.Node.CreatedAt) + lastModified := httputil.HeaderTimestamp(objectWithName.Node.CreatedAt) w.Header().Set("Last-Modified", lastModified) w.Header().Set("Accept-Ranges", "bytes") - w.Header().Set("Content-Type", httputil.ExtensionsByType(blobWithName.Name)) + w.Header().Set("Content-Type", httputil.ExtensionsByType(objectWithName.Name)) // for security, make sure the browser and any proxies en route don't cache the response w.Header().Set("Cache-Control", "no-store, must-revalidate") w.Header().Set("Expires", "0") @@ -144,16 +133,9 @@ func (oct ObjectController) UploadObject(ctx context.Context, w *api.JiaozifsRes w.Error(err) return } - treeOp := versionmgr.TreeOp{Object: oct.Object} - var blob *models.Blob - if mediaType != "multipart/form-data" { - // handle non-multipart, direct content upload - blob, err = treeOp.WriteBlob(ctx, oct.BlockAdapter, r.Body, r.ContentLength, block.PutOpts{}) - if err != nil { - w.Error(err) - return - } - } else { + + reader := r.Body + if mediaType == "multipart/form-data" { // handle multipart upload boundary, ok := p["boundary"] if !ok { @@ -162,9 +144,9 @@ func (oct ObjectController) UploadObject(ctx context.Context, w *api.JiaozifsRes } contentUploaded := false - reader := multipart.NewReader(r.Body, boundary) + partReader := multipart.NewReader(r.Body, boundary) for !contentUploaded { - part, err := reader.NextPart() + part, err := partReader.NextPart() if err == io.EOF { break } @@ -175,73 +157,74 @@ func (oct ObjectController) UploadObject(ctx context.Context, w *api.JiaozifsRes contentType = part.Header.Get("Content-Type") partName := part.FormName() if partName == "content" { - // upload the first "content" and exit the loop - blob, err = treeOp.WriteBlob(ctx, oct.BlockAdapter, part, -1, block.PutOpts{}) - if err != nil { - _ = part.Close() - w.Error(err) - return - } + reader = part contentUploaded = true + } else { //close not target part + _ = part.Close() } - _ = part.Close() + } if !contentUploaded { w.Error(fmt.Errorf("multipart upload missing key 'content': %w", http.ErrMissingFile)) return } } + defer reader.Close() //nolint - user, err := oct.UserRepo.Get(ctx, &models.GetUserParam{Name: utils.String(userName)}) - if err != nil { - w.Error(err) - return - } + var response api.ObjectStats + err = oct.Repo.Transaction(ctx, func(dRepo models.IRepo) error { + treeOp := versionmgr.TreeOp{Object: dRepo.ObjectRepo()} + blob, err := treeOp.WriteBlob(ctx, oct.BlockAdapter, reader, r.ContentLength, block.PutOpts{}) + if err != nil { + return err + } - repo, err := oct.Repository.Get(ctx, &models.GetRepoParams{ - CreateID: user.ID, - Name: utils.String(repository), - }) - if err != nil { - w.Error(err) - return - } + user, err := dRepo.UserRepo().Get(ctx, &models.GetUserParam{Name: utils.String(userName)}) + if err != nil { + return err + } - stash, err := oct.StashRepo.Get(ctx, &models.GetStashParam{ - RepositoryID: repo.ID, - CreateID: user.ID, - }) - if err != nil { - w.Error(err) - return - } + repo, err := dRepo.RepositoryRepo().Get(ctx, &models.GetRepoParams{ + CreateID: user.ID, + Name: utils.String(repository), + }) + if err != nil { + return err + } - workingTreeID, err := oct.Object.TreeNode(ctx, stash.CurrentTree) - if err != nil { - w.Error(err) - return - } + stash, err := dRepo.WipRepo().Get(ctx, &models.GetWipParam{ + RepositoryID: repo.ID, + CreateID: user.ID, + }) + if err != nil { + return err + } - newRoot, err := treeOp.AddLeaf(ctx, workingTreeID, params.Path, blob) - if err != nil { - w.Error(err) - return - } + workingTreeID, err := dRepo.ObjectRepo().TreeNode(ctx, stash.CurrentTree) + if err != nil { + return err + } + + newRoot, err := treeOp.AddLeaf(ctx, workingTreeID, params.Path, blob) + if err != nil { + return err + } + response = api.ObjectStats{ + Checksum: blob.Hash.Hex(), + Mtime: time.Now().Unix(), + Path: params.Path, + PathMode: utils.Uint32(uint32(filemode.Regular)), + SizeBytes: swag.Int64(blob.Size), + ContentType: &contentType, + Metadata: &api.ObjectUserMetadata{}, + } + return dRepo.WipRepo().UpdateCurrentHash(ctx, stash.ID, newRoot.Hash) + }) - err = oct.StashRepo.UpdateCurrentHash(ctx, stash.ID, newRoot.Hash) if err != nil { w.Error(err) return } - response := api.ObjectStats{ - Checksum: blob.Hash.Hex(), - Mtime: time.Now().Unix(), - Path: params.Path, - PathMode: utils.Uint32(uint32(filemode.Regular)), - SizeBytes: swag.Int64(blob.Size), - ContentType: &contentType, - Metadata: &api.ObjectUserMetadata{}, - } w.JSON(response) } diff --git a/controller/user_ctl.go b/controller/user_ctl.go index 9b0e7f7b..baa16b38 100644 --- a/controller/user_ctl.go +++ b/controller/user_ctl.go @@ -20,11 +20,11 @@ const ( type UserController struct { fx.In - Repo models.IUserRepo + Repo models.IRepo Config *config.Config } -func (A UserController) Login(ctx context.Context, w *api.JiaozifsResponse, r *http.Request) { +func (userCtl UserController) Login(ctx context.Context, w *api.JiaozifsResponse, r *http.Request) { // Decode requestBody var login auth.Login decoder := json.NewDecoder(r.Body) @@ -34,7 +34,7 @@ func (A UserController) Login(ctx context.Context, w *api.JiaozifsResponse, r *h } // perform login - authToken, err := login.Login(ctx, A.Repo, A.Config) + authToken, err := login.Login(ctx, userCtl.Repo.UserRepo(), userCtl.Config) if err != nil { w.Error(err) return @@ -42,7 +42,7 @@ func (A UserController) Login(ctx context.Context, w *api.JiaozifsResponse, r *h w.JSON(authToken) } -func (A UserController) Register(ctx context.Context, w *api.JiaozifsResponse, r *http.Request) { +func (userCtl UserController) Register(ctx context.Context, w *api.JiaozifsResponse, r *http.Request) { // Decode requestBody var register auth.Register decoder := json.NewDecoder(r.Body) @@ -51,7 +51,7 @@ func (A UserController) Register(ctx context.Context, w *api.JiaozifsResponse, r return } // perform register - err := register.Register(ctx, A.Repo) + err := register.Register(ctx, userCtl.Repo.UserRepo()) if err != nil { w.Error(err) return @@ -59,13 +59,13 @@ func (A UserController) Register(ctx context.Context, w *api.JiaozifsResponse, r w.OK() } -func (A UserController) GetUserInfo(ctx context.Context, w *api.JiaozifsResponse, r *http.Request) { +func (userCtl UserController) GetUserInfo(ctx context.Context, w *api.JiaozifsResponse, r *http.Request) { // Get token from Header tokenString := r.Header.Get(AuthHeader) userInfo := &auth.UserInfo{Token: tokenString} // perform GetUserInfo - usrInfo, err := userInfo.UserProfile(ctx, A.Repo, A.Config) + usrInfo, err := userInfo.UserProfile(ctx, userCtl.Repo.UserRepo(), userCtl.Config) if err != nil { w.Error(err) return diff --git a/models/object.go b/models/object.go index fbaef3c6..4a11bc77 100644 --- a/models/object.go +++ b/models/object.go @@ -295,11 +295,11 @@ type IObjectRepo interface { var _ IObjectRepo = (*ObjectRepo)(nil) type ObjectRepo struct { - db *bun.DB + db bun.IDB } -func NewObjectRepo(db *bun.DB) IObjectRepo { - return &ObjectRepo{db} +func NewObjectRepo(db bun.IDB) IObjectRepo { + return &ObjectRepo{db: db} } func (o ObjectRepo) Insert(ctx context.Context, obj *Object) (*Object, error) { diff --git a/models/ref.go b/models/ref.go index f7caffb2..8cc168a3 100644 --- a/models/ref.go +++ b/models/ref.go @@ -41,11 +41,11 @@ type IRefRepo interface { var _ IRefRepo = (*RefRepo)(nil) type RefRepo struct { - db *bun.DB + db bun.IDB } -func NewRefRepo(db *bun.DB) IRefRepo { - return &RefRepo{db} +func NewRefRepo(db bun.IDB) IRefRepo { + return &RefRepo{db: db} } func (r RefRepo) Insert(ctx context.Context, ref *Ref) (*Ref, error) { diff --git a/models/repo.go b/models/repo.go new file mode 100644 index 00000000..894bff59 --- /dev/null +++ b/models/repo.go @@ -0,0 +1,65 @@ +package models + +import ( + "context" + "database/sql" + + "github.com/uptrace/bun" +) + +type TxOption func(*sql.TxOptions) + +func IsolationLevelOption(level sql.IsolationLevel) TxOption { + return func(opts *sql.TxOptions) { + opts.Isolation = level + } +} + +type IRepo interface { + Transaction(ctx context.Context, fn func(repo IRepo) error, opts ...TxOption) error + UserRepo() IUserRepo + ObjectRepo() IObjectRepo + RefRepo() IRefRepo + RepositoryRepo() IRepositoryRepo + WipRepo() IWipRepo +} + +type PgRepo struct { + db bun.IDB +} + +func NewRepo(db bun.IDB) IRepo { + return &PgRepo{ + db: db, + } +} + +func (repo *PgRepo) Transaction(ctx context.Context, fn func(repo IRepo) error, opts ...TxOption) error { + sqlOpt := &sql.TxOptions{} + for _, opt := range opts { + opt(sqlOpt) + } + return repo.db.RunInTx(ctx, sqlOpt, func(ctx context.Context, tx bun.Tx) error { + return fn(NewRepo(tx)) + }) +} + +func (repo *PgRepo) UserRepo() IUserRepo { + return NewUserRepo(repo.db) +} + +func (repo *PgRepo) ObjectRepo() IObjectRepo { + return NewObjectRepo(repo.db) +} + +func (repo *PgRepo) RefRepo() IRefRepo { + return NewRefRepo(repo.db) +} + +func (repo *PgRepo) RepositoryRepo() IRepositoryRepo { + return NewRepositoryRepo(repo.db) +} + +func (repo *PgRepo) WipRepo() IWipRepo { + return NewWipRepo(repo.db) +} diff --git a/models/repo_test.go b/models/repo_test.go new file mode 100644 index 00000000..66ebb409 --- /dev/null +++ b/models/repo_test.go @@ -0,0 +1,63 @@ +package models_test + +import ( + "context" + "database/sql" + "errors" + "fmt" + "testing" + + "github.com/google/uuid" + + "github.com/brianvoe/gofakeit/v6" + "github.com/jiaozifs/jiaozifs/models" + "github.com/jiaozifs/jiaozifs/testhelper" + "github.com/stretchr/testify/require" +) + +func TestRepoTransaction(t *testing.T) { + ctx := context.Background() + postgres, _, db := testhelper.SetupDatabase(ctx, t) + defer postgres.Stop() //nolint + + t.Run("simple", func(t *testing.T) { + pgRepo := models.NewRepo(db) + err := pgRepo.Transaction(ctx, func(repo models.IRepo) error { + userModel := &models.User{} + require.NoError(t, gofakeit.Struct(userModel)) + _, err := repo.UserRepo().Insert(ctx, userModel) + require.NoError(t, err) + return nil + }) + require.NoError(t, err) + }) + + t.Run("transaction", func(t *testing.T) { + pgRepo := models.NewRepo(db) + err := pgRepo.Transaction(ctx, func(repo models.IRepo) error { + object := &models.Object{} + require.NoError(t, gofakeit.Struct(object)) + _, err := repo.ObjectRepo().Insert(ctx, object) + require.NoError(t, err) + return err + }) + require.NoError(t, err) + }) + + t.Run("transaction rollback", func(t *testing.T) { + pgRepo := models.NewRepo(db) + var id uuid.UUID + err := pgRepo.Transaction(ctx, func(repo models.IRepo) error { + repositoryModel := &models.Repository{} + require.NoError(t, gofakeit.Struct(repositoryModel)) + insertedModel, err := repo.RepositoryRepo().Insert(ctx, repositoryModel) + require.NoError(t, err) + id = insertedModel.ID + return fmt.Errorf("rollback") + }) + require.Error(t, err) + + _, err = pgRepo.RepositoryRepo().Get(ctx, &models.GetRepoParams{ID: id}) + require.True(t, errors.Is(err, sql.ErrNoRows)) + }) +} diff --git a/models/repository.go b/models/repository.go index d781d814..7f15f565 100644 --- a/models/repository.go +++ b/models/repository.go @@ -34,11 +34,11 @@ type IRepositoryRepo interface { var _ IRepositoryRepo = (*RepositoryRepo)(nil) type RepositoryRepo struct { - db *bun.DB + db bun.IDB } -func NewRepositoryRepo(db *bun.DB) IRepositoryRepo { - return &RepositoryRepo{db} +func NewRepositoryRepo(db bun.IDB) IRepositoryRepo { + return &RepositoryRepo{db: db} } func (r *RepositoryRepo) Insert(ctx context.Context, repo *Repository) (*Repository, error) { diff --git a/models/user.go b/models/user.go index 7b6ba929..df760a3f 100644 --- a/models/user.go +++ b/models/user.go @@ -2,7 +2,6 @@ package models import ( "context" - "time" "github.com/google/uuid" @@ -41,11 +40,11 @@ type IUserRepo interface { var _ IUserRepo = (*UserRepo)(nil) type UserRepo struct { - db *bun.DB + db bun.IDB } -func NewUserRepo(db *bun.DB) IUserRepo { - return &UserRepo{db} +func NewUserRepo(db bun.IDB) IUserRepo { + return &UserRepo{db: db} } func (userRepo *UserRepo) Get(ctx context.Context, params *GetUserParam) (*User, error) { diff --git a/models/wip.go b/models/wip.go index f408c973..f3d283c7 100644 --- a/models/wip.go +++ b/models/wip.go @@ -21,8 +21,8 @@ const ( Modify ) -type Stash struct { - bun.BaseModel `bun:"table:stash"` +type WorkingInProcess struct { + bun.BaseModel `bun:"table:wip"` ID uuid.UUID `bun:"id,pk,type:uuid,default:uuid_generate_v4()"` CurrentTree hash.Hash `bun:"current_tree,type:bytea,notnull"` ParentTree hash.Hash `bun:"parent_tree,type:bytea,notnull"` @@ -32,29 +32,29 @@ type Stash struct { UpdatedAt time.Time `bun:"updated_at"` } -type GetStashParam struct { +type GetWipParam struct { ID uuid.UUID CreateID uuid.UUID RepositoryID uuid.UUID } -type IStashRepo interface { - Insert(ctx context.Context, repo *Stash) (*Stash, error) - Get(ctx context.Context, params *GetStashParam) (*Stash, error) +type IWipRepo interface { + Insert(ctx context.Context, repo *WorkingInProcess) (*WorkingInProcess, error) + Get(ctx context.Context, params *GetWipParam) (*WorkingInProcess, error) UpdateCurrentHash(ctx context.Context, id uuid.UUID, newTreeHash hash.Hash) error } -var _ IStashRepo = (*StashRepo)(nil) +var _ IWipRepo = (*WipRepo)(nil) -type StashRepo struct { - db *bun.DB +type WipRepo struct { + db bun.IDB } -func NewStashRepo(db *bun.DB) IStashRepo { - return &StashRepo{db} +func NewWipRepo(db bun.IDB) IWipRepo { + return &WipRepo{db: db} } -func (s *StashRepo) Insert(ctx context.Context, repo *Stash) (*Stash, error) { +func (s *WipRepo) Insert(ctx context.Context, repo *WorkingInProcess) (*WorkingInProcess, error) { _, err := s.db.NewInsert().Model(repo).Exec(ctx) if err != nil { return nil, err @@ -62,8 +62,8 @@ func (s *StashRepo) Insert(ctx context.Context, repo *Stash) (*Stash, error) { return repo, nil } -func (s *StashRepo) Get(ctx context.Context, params *GetStashParam) (*Stash, error) { - repo := &Stash{} +func (s *WipRepo) Get(ctx context.Context, params *GetWipParam) (*WorkingInProcess, error) { + repo := &WorkingInProcess{} query := s.db.NewSelect().Model(repo) if uuid.Nil != params.ID { @@ -80,8 +80,8 @@ func (s *StashRepo) Get(ctx context.Context, params *GetStashParam) (*Stash, err return repo, query.Scan(ctx, repo) } -func (s *StashRepo) UpdateCurrentHash(ctx context.Context, id uuid.UUID, newTreeHash hash.Hash) error { - repo := &Stash{ +func (s *WipRepo) UpdateCurrentHash(ctx context.Context, id uuid.UUID, newTreeHash hash.Hash) error { + repo := &WorkingInProcess{ CurrentTree: newTreeHash, } _, err := s.db.NewUpdate().Model(repo).OmitZero().Column("current_tree"). diff --git a/versionmgr/tree.go b/versionmgr/tree.go index 7b8585d5..7b6b7232 100644 --- a/versionmgr/tree.go +++ b/versionmgr/tree.go @@ -32,7 +32,7 @@ var ( ErrNotDiretory = fmt.Errorf("path must be a directory") ) -type TreeNodeWithNode struct { +type ObjectWithName struct { Node *models.Object Name string } @@ -211,9 +211,9 @@ func (treeOp *TreeOp) ReplaceTreeEntry(ctx context.Context, tn *models.TreeNode, return obj.TreeNode(), nil } -func (treeOp *TreeOp) MatchPath(ctx context.Context, tn *models.TreeNode, path string) ([]TreeNodeWithNode, []string, error) { +func (treeOp *TreeOp) MatchPath(ctx context.Context, tn *models.TreeNode, path string) ([]ObjectWithName, []string, error) { pathSegs := strings.Split(filepath.Clean(path), fmt.Sprintf("%c", os.PathSeparator)) - var existNodes []TreeNodeWithNode + var existNodes []ObjectWithName var missingPath []string //a/b/c/d/e //a/b/c @@ -230,7 +230,7 @@ func (treeOp *TreeOp) MatchPath(ctx context.Context, tn *models.TreeNode, path s if err != nil { return nil, nil, err } - existNodes = append(existNodes, TreeNodeWithNode{ + existNodes = append(existNodes, ObjectWithName{ Node: tn.Object(), Name: entry.Name, }) @@ -240,7 +240,7 @@ func (treeOp *TreeOp) MatchPath(ctx context.Context, tn *models.TreeNode, path s if err != nil { return nil, nil, err } - existNodes = append(existNodes, TreeNodeWithNode{ + existNodes = append(existNodes, ObjectWithName{ Node: blob.Object(), Name: entry.Name, }) @@ -303,7 +303,7 @@ func (treeOp *TreeOp) AddLeaf(ctx context.Context, root *models.TreeNode, fullPa } slices.Reverse(existNode) - existNode = append(existNode, TreeNodeWithNode{ + existNode = append(existNode, ObjectWithName{ Node: root.Object(), Name: "", //root node have no name }) @@ -344,7 +344,7 @@ func (treeOp *TreeOp) ReplaceLeaf(ctx context.Context, root *models.TreeNode, fu } slices.Reverse(existNode) - existNode = append(existNode, TreeNodeWithNode{ + existNode = append(existNode, ObjectWithName{ Node: root.Object(), Name: "", //root node have no name }) @@ -396,7 +396,7 @@ func (treeOp *TreeOp) RemoveEntry(ctx context.Context, root *models.TreeNode, fu } slices.Reverse(existNode) - existNode = append(existNode, TreeNodeWithNode{ + existNode = append(existNode, ObjectWithName{ Node: root.Object(), Name: "", //root node have no name })