Skip to content

Commit

Permalink
perf: fast move (#210)
Browse files Browse the repository at this point in the history
* perf: fast move

* chore(api): format code

* chore(api): generate docs

* fix(api): swagger comment

* chore(api): generate docs
  • Loading branch information
bouassaba authored Jul 23, 2024
1 parent a5bd376 commit 208fc94
Show file tree
Hide file tree
Showing 8 changed files with 244 additions and 147 deletions.
32 changes: 21 additions & 11 deletions api/docs/index.html

Large diffs are not rendered by default.

78 changes: 60 additions & 18 deletions api/docs/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -145,15 +145,6 @@ definitions:
- permission
- userId
type: object
router.FileMoveOptions:
properties:
ids:
items:
type: string
type: array
required:
- ids
type: object
router.FilePatchNameOptions:
properties:
name:
Expand Down Expand Up @@ -308,6 +299,29 @@ definitions:
totalPages:
type: integer
type: object
service.FileMoveManyOptions:
properties:
sourceIds:
items:
type: string
type: array
targetId:
type: string
required:
- sourceIds
- targetId
type: object
service.FileMoveManyResult:
properties:
failed:
items:
type: string
type: array
succeeded:
items:
type: string
type: array
type: object
service.FileQuery:
properties:
createTimeAfter:
Expand Down Expand Up @@ -1107,22 +1121,21 @@ paths:
summary: List
tags:
- Files
/files/{id}/move:
/files/{id}/move/{targetId}:
post:
description: Move
operationId: files_move
description: Move One
operationId: files_move_one
parameters:
- description: ID
in: path
name: id
required: true
type: string
- description: Body
in: body
name: body
- description: Target ID
in: path
name: targetId
required: true
schema:
$ref: '#/definitions/router.FileMoveOptions'
type: string
produces:
- application/json
responses:
Expand All @@ -1134,7 +1147,7 @@ paths:
description: Internal Server Error
schema:
$ref: '#/definitions/errorpkg.ErrorResponse'
summary: Move
summary: Move One
tags:
- Files
/files/{id}/name:
Expand Down Expand Up @@ -1636,6 +1649,35 @@ paths:
summary: List by Path
tags:
- Files
/files/move:
post:
description: Move Many
operationId: files_move_many
parameters:
- description: Body
in: body
name: body
required: true
schema:
$ref: '#/definitions/service.FileMoveManyOptions'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/service.FileMoveManyResult'
"404":
description: Not Found
schema:
$ref: '#/definitions/errorpkg.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/errorpkg.ErrorResponse'
summary: Move Many
tags:
- Files
/files/revoke_user_permission:
post:
description: Revoke User Permission
Expand Down
46 changes: 34 additions & 12 deletions api/router/file_router.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ func NewFileRouter() *FileRouter {
func (r *FileRouter) AppendRoutes(g fiber.Router) {
g.Post("/", r.Create)
g.Get("/list", r.ListByPath)
g.Post("/move", r.MoveMany)
g.Post("/copy", r.CopyMany)
g.Get("/", r.GetByPath)
g.Delete("/", r.Delete)
Expand All @@ -70,7 +71,7 @@ func (r *FileRouter) AppendRoutes(g fiber.Router) {
g.Get("/:id/list", r.List)
g.Get("/:id/count", r.GetCount)
g.Get("/:id/path", r.GetPath)
g.Post("/:id/move", r.Move)
g.Post("/:id/move/:targetId", r.MoveOne)
g.Patch("/:id/name", r.PatchName)
g.Post("/:id/copy/:targetId", r.CopyOne)
g.Get("/:id/size", r.GetSize)
Expand Down Expand Up @@ -481,31 +482,52 @@ type FileMoveOptions struct {
IDs []string `json:"ids" validate:"required"`
}

// Move godoc
// MoveOne godoc
//
// @Summary Move
// @Description Move
// @Summary Move One
// @Description Move One
// @Tags Files
// @Id files_move
// @Id files_move_one
// @Produce json
// @Param id path string true "ID"
// @Param body body FileMoveOptions true "Body"
// @Param id path string true "ID"
// @Param targetId path string true "Target ID"
// @Failure 404 {object} errorpkg.ErrorResponse
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /files/{id}/move/{targetId} [post]
func (r *FileRouter) MoveOne(c *fiber.Ctx) error {
userID := GetUserID(c)
if err := r.fileSvc.MoveOne(c.Params("id"), c.Params("targetId"), userID); err != nil {
return err
}
return c.SendStatus(http.StatusNoContent)
}

// MoveMany godoc
//
// @Summary Move Many
// @Description Move Many
// @Tags Files
// @Id files_move_many
// @Produce json
// @Param body body service.FileMoveManyOptions true "Body"
// @Success 200 {object} service.FileMoveManyResult
// @Failure 404 {object} errorpkg.ErrorResponse
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /files/{id}/move [post]
func (r *FileRouter) Move(c *fiber.Ctx) error {
// @Router /files/move [post]
func (r *FileRouter) MoveMany(c *fiber.Ctx) error {
userID := GetUserID(c)
opts := new(FileMoveOptions)
opts := new(service.FileMoveManyOptions)
if err := c.BodyParser(opts); err != nil {
return err
}
if err := validator.New().Struct(opts); err != nil {
return errorpkg.NewRequestBodyValidationError(err)
}
if _, err := r.fileSvc.Move(c.Params("id"), opts.IDs, userID); err != nil {
res, err := r.fileSvc.MoveMany(*opts, userID)
if err != nil {
return err
}
return c.SendStatus(http.StatusNoContent)
return c.JSON(res)
}

type FilePatchNameOptions struct {
Expand Down
165 changes: 91 additions & 74 deletions api/service/file_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -964,97 +964,114 @@ func (svc *FileService) CopyMany(opts FileCopyManyOptions, userID string) (*File
return res, nil
}

func (svc *FileService) Move(targetID string, sourceIDs []string, userID string) (parentIDs []string, err error) {
parentIDs = []string{}
func (svc *FileService) MoveOne(sourceID string, targetID string, userID string) error {
target, err := svc.fileCache.Get(targetID)
if err != nil {
return []string{}, err
return err
}
source, err := svc.fileCache.Get(sourceID)
if err != nil {
return err
}

task, err := svc.taskSvc.insertAndSync(repo.TaskInsertOptions{
ID: helper.NewID(),
Name: "Moving.",
UserID: userID,
IsIndeterminate: true,
Status: model.TaskStatusRunning,
Payload: map[string]string{"object": source.GetName()},
})
if err != nil {
return err
}
defer func(taskID string) {
if err := svc.taskSvc.deleteAndSync(taskID); err != nil {
log.GetLogger().Error(err)
}
}(task.GetID())

/* Do checks */
for _, id := range sourceIDs {
source, err := svc.fileCache.Get(id)
if source.GetParentID() != nil {
existing, err := svc.getChildWithName(target.GetID(), source.GetName())
if err != nil {
return []string{}, err
}
if source.GetParentID() != nil {
existing, err := svc.getChildWithName(targetID, source.GetName())
if err != nil {
return nil, err
}
if existing != nil {
return nil, errorpkg.NewFileWithSimilarNameExistsError()
}
}
if err := svc.fileGuard.Authorize(userID, target, model.PermissionEditor); err != nil {
return []string{}, err
}
if err := svc.fileGuard.Authorize(userID, source, model.PermissionEditor); err != nil {
return []string{}, err
}
if source.GetParentID() != nil && *source.GetParentID() == target.GetID() {
return []string{}, errorpkg.NewFileAlreadyChildOfDestinationError(source, target)
}
if target.GetID() == source.GetID() {
return []string{}, errorpkg.NewFileCannotBeMovedIntoItselfError(source)
}
if target.GetType() != model.FileTypeFolder {
return []string{}, errorpkg.NewFileIsNotAFolderError(target)
return err
}
targetIsGrandChildOfSource, _ := svc.fileRepo.IsGrandChildOf(target.GetID(), source.GetID())
if targetIsGrandChildOfSource {
return []string{}, errorpkg.NewTargetIsGrandChildOfSourceError(source)
if existing != nil {
return errorpkg.NewFileWithSimilarNameExistsError()
}
}
if err := svc.fileGuard.Authorize(userID, target, model.PermissionEditor); err != nil {
return err
}
if err := svc.fileGuard.Authorize(userID, source, model.PermissionEditor); err != nil {
return err
}
if source.GetParentID() != nil && *source.GetParentID() == target.GetID() {
return errorpkg.NewFileAlreadyChildOfDestinationError(source, target)
}
if target.GetID() == source.GetID() {
return errorpkg.NewFileCannotBeMovedIntoItselfError(source)
}
if target.GetType() != model.FileTypeFolder {
return errorpkg.NewFileIsNotAFolderError(target)
}
targetIsGrandChildOfSource, _ := svc.fileRepo.IsGrandChildOf(target.GetID(), source.GetID())
if targetIsGrandChildOfSource {
return errorpkg.NewTargetIsGrandChildOfSourceError(source)
}

/* Do moving */
for _, id := range sourceIDs {
source, _ := svc.fileCache.Get(id)
/* Move source into target */
if err := svc.fileRepo.MoveSourceIntoTarget(target.GetID(), source.GetID()); err != nil {
return err
}

/* Add old parent */
parentIDs = append(parentIDs, *source.GetParentID())
/* Get updated source */
source, err = svc.fileRepo.Find(source.GetID())
if err != nil {
return err
}

/* Move source into target */
if err := svc.fileRepo.MoveSourceIntoTarget(target.GetID(), source.GetID()); err != nil {
return []string{}, err
}
/* Refresh updateTime on source and target */
timeNow := time.Now().UTC().Format(time.RFC3339)
source.SetUpdateTime(&timeNow)
if err := svc.fileRepo.Save(source); err != nil {
return err
}
if err := svc.sync(source); err != nil {
return err
}
target.SetUpdateTime(&timeNow)
if err := svc.fileRepo.Save(target); err != nil {
return err
}
if err := svc.sync(target); err != nil {
return err
}

/* Get updated source */
source, err = svc.fileRepo.Find(source.GetID())
if err != nil {
return []string{}, err
}
return nil
}

// Add new parent
parentIDs = append(parentIDs, *source.GetParentID())
type FileMoveManyOptions struct {
SourceIDs []string `json:"sourceIds" validate:"required"`
TargetID string `json:"targetId" validate:"required"`
}

/* Refresh updateTime on source and target */
timeNow := helper.NewTimestamp()
source.SetUpdateTime(&timeNow)
if err := svc.fileRepo.Save(source); err != nil {
return []string{}, err
}
if err := svc.sync(source); err != nil {
return []string{}, err
}
target.SetUpdateTime(&timeNow)
if err := svc.fileRepo.Save(target); err != nil {
return []string{}, err
}
if err := svc.sync(target); err != nil {
return []string{}, err
}
sourceTree, err := svc.fileRepo.FindTree(source.GetID())
if err != nil {
return []string{}, err
}
for _, f := range sourceTree {
if err := svc.sync(f); err != nil {
return []string{}, err
}
type FileMoveManyResult struct {
Succeeded []string `json:"succeeded"`
Failed []string `json:"failed"`
}

func (svc *FileService) MoveMany(opts FileMoveManyOptions, userID string) (*FileMoveManyResult, error) {
res := &FileMoveManyResult{}
for _, id := range opts.SourceIDs {
if err := svc.MoveOne(opts.TargetID, id, userID); err != nil {
res.Failed = append(res.Failed, id)
} else {
res.Succeeded = append(res.Succeeded, id)
}
}
return parentIDs, nil
return res, nil
}

func (svc *FileService) PatchName(id string, name string, userID string) (*File, error) {
Expand Down
Loading

0 comments on commit 208fc94

Please sign in to comment.