package service import ( "bytes" "fmt" "os" "path/filepath" "sort" "strings" "time" "voltaserve/cache" "voltaserve/client" "voltaserve/config" "voltaserve/errorpkg" "voltaserve/guard" "voltaserve/helper" "voltaserve/infra" "voltaserve/model" "voltaserve/repo" "voltaserve/search" "github.com/reactivex/rxgo/v2" ) type File struct { ID string `json:"id"` WorkspaceID string `json:"workspaceId"` Name string `json:"name"` Type string `json:"type"` ParentID *string `json:"parentId,omitempty"` Version *int64 `json:"version,omitempty"` Original *Download `json:"original,omitempty"` Preview *Download `json:"preview,omitempty"` Thumbnail *Thumbnail `json:"thumbnail,omitempty"` Status string `json:"status,omitempty"` Snapshots []*Snapshot `json:"snapshots,omitempty"` Permission string `json:"permission"` IsShared *bool `json:"isShared,omitempty"` CreateTime string `json:"createTime"` UpdateTime *string `json:"updateTime,omitempty"` } type FileList struct { Data []*File `json:"data"` TotalPages uint `json:"totalPages"` TotalElements uint `json:"totalElements"` Page uint `json:"page"` Size uint `json:"size"` Query *FileQuery `json:"query,omitempty"` } type FileListOptions struct { Page uint Size uint SortBy string SortOrder string Query *FileQuery } type FileQuery struct { Text string `json:"text" validate:"required"` Type *string `json:"type,omitempty" validate:"omitempty,oneof=file folder"` CreateTimeAfter *int64 `json:"createTimeAfter,omitempty"` CreateTimeBefore *int64 `json:"createTimeBefore,omitempty"` UpdateTimeAfter *int64 `json:"updateTimeAfter,omitempty"` UpdateTimeBefore *int64 `json:"updateTimeBefore,omitempty"` } type FileCreateOptions struct { WorkspaceID string `json:"workspaceId" validate:"required"` Name string `json:"name" validate:"required,max=255"` Type string `json:"type" validate:"required,oneof=file folder"` ParentID *string `json:"parentId" validate:"required"` } type FileCreateFolderOptions struct { WorkspaceID string `json:"workspaceId" validate:"required"` Name string `json:"name" validate:"required,max=255"` ParentID *string `json:"parentId"` } type FileCopyOptions struct { IDs []string `json:"ids" validate:"required"` } type FileBatchDeleteOptions struct { IDs []string `json:"ids" validate:"required"` } type FileBatchGetOptions struct { IDs []string `json:"ids" validate:"required"` } type FileGrantUserPermissionOptions struct { UserID string `json:"userId" validate:"required"` IDs []string `json:"ids" validate:"required"` Permission string `json:"permission" validate:"required,oneof=viewer editor owner"` } type FileRevokeUserPermissionOptions struct { IDs []string `json:"ids" validate:"required"` UserID string `json:"userId" validate:"required"` } type FileGrantGroupPermissionOptions struct { GroupID string `json:"groupId" validate:"required"` IDs []string `json:"ids" validate:"required"` Permission string `json:"permission" validate:"required,oneof=viewer editor owner"` } type FileRevokeGroupPermissionOptions struct { IDs []string `json:"ids" validate:"required"` GroupID string `json:"groupId" validate:"required"` } type FileMoveOptions struct { IDs []string `json:"ids" validate:"required"` } type FileRenameOptions struct { Name string `json:"name" validate:"required,max=255"` } type FileUpdateOCRLanguageOptions struct { ID string `json:"id" validate:"required"` } type Snapshot struct { ID string `json:"id"` Version int64 `json:"version"` Original *Download `json:"original,omitempty"` Preview *Download `json:"preview,omitempty"` OCR *Download `json:"ocr,omitempty"` Thumbnail *Thumbnail `json:"thumbnail,omitempty"` Language *string `json:"language,omitempty"` Status string `json:"status,omitempty"` } type ImageProps struct { Width int `json:"width"` Height int `json:"height"` } type Thumbnail struct { Base64 string `json:"base64"` Width int `json:"width"` Height int `json:"height"` } type Download struct { Extension string `json:"extension"` Size int64 `json:"size"` Image *ImageProps `json:"image,omitempty"` Language *string `json:"language,omitempty"` } type UserPermission struct { ID string `json:"id"` User *User `json:"user"` Permission string `json:"permission"` } type GroupPermission struct { ID string `json:"id"` Group *Group `json:"group"` Permission string `json:"permission"` } type SnapshotUpdateOptions struct { Options client.PipelineRunOptions `json:"options"` Original *model.S3Object `json:"original,omitempty"` Preview *model.S3Object `json:"preview,omitempty"` Text *model.S3Object `json:"text,omitempty"` OCR *model.S3Object `json:"ocr,omitempty"` Thumbnail *model.Thumbnail `json:"thumbnail,omitempty"` Status string `json:"status,omitempty"` } type FileService struct { fileRepo repo.FileRepo fileSearch *search.FileSearch fileGuard *guard.FileGuard fileMapper *FileMapper fileCache *cache.FileCache workspaceCache *cache.WorkspaceCache workspaceRepo repo.WorkspaceRepo workspaceGuard *guard.WorkspaceGuard workspaceSvc *WorkspaceService snapshotRepo repo.SnapshotRepo userRepo repo.UserRepo userMapper *userMapper groupCache *cache.GroupCache groupGuard *guard.GroupGuard groupMapper *groupMapper permissionRepo repo.PermissionRepo fileIdent *infra.FileIdentifier s3 *infra.S3Manager conversionClient *client.ConversionClient config config.Config } func NewFileService() *FileService { return &FileService{ fileRepo: repo.NewFileRepo(), fileCache: cache.NewFileCache(), fileSearch: search.NewFileSearch(), fileGuard: guard.NewFileGuard(), fileMapper: NewFileMapper(), workspaceGuard: guard.NewWorkspaceGuard(), workspaceCache: cache.NewWorkspaceCache(), workspaceRepo: repo.NewWorkspaceRepo(), workspaceSvc: NewWorkspaceService(), snapshotRepo: repo.NewSnapshotRepo(), userRepo: repo.NewUserRepo(), userMapper: newUserMapper(), groupCache: cache.NewGroupCache(), groupGuard: guard.NewGroupGuard(), groupMapper: newGroupMapper(), permissionRepo: repo.NewPermissionRepo(), fileIdent: infra.NewFileIdentifier(), s3: infra.NewS3Manager(), conversionClient: client.NewConversionClient(), config: config.GetConfig(), } } func (svc *FileService) Create(opts FileCreateOptions, userID string) (*File, error) { var components []string for _, component := range strings.Split(opts.Name, "/") { if component != "" { components = append(components, component) } } parentID := opts.ParentID if len(components) > 1 { for _, component := range components[:len(components)-1] { existing, err := svc.getChildWithName(*parentID, component) if err != nil { return nil, err } if existing != nil { parentID = new(string) *parentID = existing.GetID() } else { res, err := svc.create(FileCreateOptions{ Name: component, Type: model.FileTypeFolder, ParentID: parentID, WorkspaceID: opts.WorkspaceID, }, userID) if err != nil { return nil, err } parentID = &res.ID } } } name := components[len(components)-1] return svc.create(FileCreateOptions{ WorkspaceID: opts.WorkspaceID, Name: name, Type: opts.Type, ParentID: parentID, }, userID) } func (svc *FileService) create(opts FileCreateOptions, userID string) (*File, error) { if len(*opts.ParentID) > 0 { if err := svc.validateParent(*opts.ParentID, userID); err != nil { return nil, err } existing, err := svc.getChildWithName(*opts.ParentID, opts.Name) if err != nil { return nil, err } if existing != nil { return nil, errorpkg.NewFileWithSimilarNameExistsError() } } file, err := svc.fileRepo.Insert(repo.FileInsertOptions{ Name: opts.Name, WorkspaceID: opts.WorkspaceID, ParentID: opts.ParentID, Type: opts.Type, }) if err != nil { return nil, err } if err := svc.fileRepo.GrantUserPermission(file.GetID(), userID, model.PermissionOwner); err != nil { return nil, err } file, err = svc.fileRepo.Find(file.GetID()) if err != nil { return nil, err } if err = svc.fileSearch.Index([]model.File{file}); err != nil { return nil, err } if err = svc.fileCache.Set(file); err != nil { return nil, err } res, err := svc.fileMapper.mapOne(file, userID) if err != nil { return nil, err } return res, nil } func (svc *FileService) validateParent(id string, userID string) error { user, err := svc.userRepo.Find(userID) if err != nil { return err } file, err := svc.fileCache.Get(id) if err != nil { return err } if err = svc.fileGuard.Authorize(user, file, model.PermissionEditor); err != nil { return err } if file.GetType() != model.FileTypeFolder { return errorpkg.NewFileIsNotAFolderError(file) } return nil } func (svc *FileService) Store(fileID string, filePath string, userID string) (*File, error) { file, err := svc.fileRepo.Find(fileID) if err != nil { return nil, err } if err = svc.fileCache.Set(file); err != nil { return nil, err } workspace, err := svc.workspaceCache.Get(file.GetWorkspaceID()) if err != nil { return nil, err } latestVersion, err := svc.snapshotRepo.GetLatestVersionForFile(fileID) if err != nil { return nil, err } snapshotID := helper.NewID() snapshot := repo.NewSnapshot() snapshot.SetID(snapshotID) snapshot.SetVersion(latestVersion) snapshot.SetStatus(model.SnapshotStatusNew) if err = svc.snapshotRepo.Save(snapshot); err != nil { return nil, err } if err = svc.snapshotRepo.MapWithFile(snapshotID, fileID); err != nil { return nil, err } stat, err := os.Stat(filePath) if err != nil { return nil, err } original := model.S3Object{ Bucket: workspace.GetBucket(), Key: fileID + "/" + snapshotID + "/original" + strings.ToLower(filepath.Ext(filePath)), Size: stat.Size(), } if err = svc.s3.PutFile(original.Key, filePath, infra.DetectMimeFromFile(filePath), workspace.GetBucket()); err != nil { return nil, err } snapshot.SetOriginal(&original) if err := svc.snapshotRepo.Save(snapshot); err != nil { return nil, err } file, err = svc.fileCache.Refresh(file.GetID()) if err != nil { return nil, err } res, err := svc.fileMapper.mapOne(file, userID) if err != nil { return nil, err } if err := svc.conversionClient.RunPipeline(&client.PipelineRunOptions{ FileID: file.GetID(), SnapshotID: snapshot.GetID(), Bucket: original.Bucket, Key: original.Key, }); err != nil { return nil, err } return res, nil } func (svc *FileService) UpdateSnapshot(opts SnapshotUpdateOptions, apiKey string) error { if apiKey != svc.config.Security.APIKey { return errorpkg.NewInvalidAPIKeyError() } if err := svc.snapshotRepo.Update(opts.Options.SnapshotID, repo.SnapshotUpdateOptions{ Thumbnail: opts.Thumbnail, Original: opts.Original, Preview: opts.Preview, Text: opts.Text, Status: opts.Status, }); err != nil { return err } file, err := svc.fileCache.Refresh(opts.Options.FileID) if err != nil { return err } if err = svc.fileSearch.Update([]model.File{file}); err != nil { return err } return nil } func (svc *FileService) DownloadOriginalBuffer(id string, userID string) (*bytes.Buffer, model.File, model.Snapshot, error) { user, err := svc.userRepo.Find(userID) if err != nil { return nil, nil, nil, err } file, err := svc.fileCache.Get(id) if err != nil { return nil, nil, nil, err } if err = svc.fileGuard.Authorize(user, file, model.PermissionViewer); err != nil { return nil, nil, nil, err } snapshots := file.GetSnapshots() if len(snapshots) == 0 { return nil, nil, nil, errorpkg.NewSnapshotNotFoundError(nil) } latestSnapshot := snapshots[len(snapshots)-1] if latestSnapshot.HasOriginal() { original := latestSnapshot.GetOriginal() buf, err := svc.s3.GetObject(original.Key, original.Bucket) if err != nil { return nil, nil, nil, err } return buf, file, latestSnapshot, nil } else { return nil, nil, nil, errorpkg.NewS3ObjectNotFoundError(nil) } } func (svc *FileService) DownloadPreviewBuffer(id string, userID string) (*bytes.Buffer, model.File, model.Snapshot, error) { user, err := svc.userRepo.Find(userID) if err != nil { return nil, nil, nil, err } file, err := svc.fileCache.Get(id) if err != nil { return nil, nil, nil, err } if err = svc.fileGuard.Authorize(user, file, model.PermissionViewer); err != nil { return nil, nil, nil, err } snapshots := file.GetSnapshots() if len(snapshots) == 0 { return nil, nil, nil, errorpkg.NewSnapshotNotFoundError(nil) } latestSnapshot := snapshots[len(snapshots)-1] if latestSnapshot.HasPreview() { preview := latestSnapshot.GetPreview() buf, err := svc.s3.GetObject(preview.Key, preview.Bucket) if err != nil { return nil, nil, nil, err } return buf, file, latestSnapshot, nil } else { return nil, nil, nil, errorpkg.NewS3ObjectNotFoundError(nil) } } func (svc *FileService) FindByID(ids []string, userID string) ([]*File, error) { user, err := svc.userRepo.Find(userID) if err != nil { return nil, err } var res []*File for _, id := range ids { file, err := svc.fileCache.Get(id) if err != nil { continue } if err = svc.fileGuard.Authorize(user, file, model.PermissionViewer); err != nil { return nil, err } f, err := svc.fileMapper.mapOne(file, userID) if err != nil { return nil, err } res = append(res, f) } return res, nil } func (svc *FileService) FindByPath(path string, userID string) (*File, error) { user, err := svc.userRepo.Find(userID) if err != nil { return nil, err } if path == "/" { return &File{ ID: user.GetID(), WorkspaceID: "", Name: "/", Type: model.FileTypeFolder, Permission: "owner", CreateTime: user.GetCreateTime(), UpdateTime: nil, }, nil } components := []string{} for _, v := range strings.Split(path, "/") { if v != "" { components = append(components, v) } } if len(components) == 0 || components[0] == "" { return nil, errorpkg.NewInvalidPathError(fmt.Errorf("invalid path '%s'", path)) } workspace, err := svc.workspaceSvc.Find(helper.WorkspaceIDFromSlug(components[0]), userID) if err != nil { return nil, err } if len(components) == 1 { return &File{ ID: workspace.RootID, WorkspaceID: workspace.ID, Name: helper.SlugFromWorkspace(workspace.ID, workspace.Name), Type: model.FileTypeFolder, Permission: workspace.Permission, CreateTime: workspace.CreateTime, UpdateTime: workspace.UpdateTime, }, nil } currentID := workspace.RootID components = components[1:] for _, component := range components { ids, err := svc.fileRepo.GetChildrenIDs(currentID) if err != nil { return nil, err } authorized, err := svc.doAuthorizationByIDs(ids, user) if err != nil { return nil, err } var filtered []model.File for _, f := range authorized { if f.GetName() == component { filtered = append(filtered, f) } } if len(filtered) > 0 { item := filtered[0] currentID = item.GetID() if item.GetType() == model.FileTypeFolder { continue } else if item.GetType() == model.FileTypeFile { break } } else { return nil, errorpkg.NewFileNotFoundError(fmt.Errorf("component not found '%s'", component)) } } result, err := svc.FindByID([]string{currentID}, userID) if err != nil { return nil, err } if len(result) == 0 { return nil, errorpkg.NewFileNotFoundError(fmt.Errorf("item not found '%s'", currentID)) } return result[0], nil } func (svc *FileService) ListByPath(path string, userID string) ([]*File, error) { user, err := svc.userRepo.Find(userID) if err != nil { return nil, err } if path == "/" { workspaces, err := svc.workspaceSvc.findAll(userID) if err != nil { return nil, err } result := []*File{} for _, w := range workspaces { result = append(result, &File{ ID: w.RootID, WorkspaceID: w.ID, Name: helper.SlugFromWorkspace(w.ID, w.Name), Type: model.FileTypeFolder, Permission: w.Permission, CreateTime: w.CreateTime, UpdateTime: w.UpdateTime, }) } return result, nil } components := []string{} for _, v := range strings.Split(path, "/") { if v != "" { components = append(components, v) } } if len(components) == 0 || components[0] == "" { return nil, errorpkg.NewInvalidPathError(fmt.Errorf("invalid path '%s'", path)) } workspace, err := svc.workspaceRepo.Find(helper.WorkspaceIDFromSlug(components[0])) if err != nil { return nil, err } currentID := workspace.GetRootID() currentType := model.FileTypeFolder components = components[1:] for _, component := range components { ids, err := svc.fileRepo.GetChildrenIDs(currentID) if err != nil { return nil, err } authorized, err := svc.doAuthorizationByIDs(ids, user) if err != nil { return nil, err } var filtered []model.File for _, f := range authorized { if f.GetName() == component { filtered = append(filtered, f) } } if len(filtered) > 0 { item := filtered[0] currentID = item.GetID() currentType = item.GetType() if item.GetType() == model.FileTypeFolder { continue } else if item.GetType() == model.FileTypeFile { break } } else { return nil, errorpkg.NewFileNotFoundError(fmt.Errorf("component not found '%s'", component)) } } if currentType == model.FileTypeFolder { ids, err := svc.fileRepo.GetChildrenIDs(currentID) if err != nil { return nil, err } authorized, err := svc.doAuthorizationByIDs(ids, user) if err != nil { return nil, err } result, err := svc.fileMapper.mapMany(authorized, userID) if err != nil { return nil, err } return result, nil } else if currentType == model.FileTypeFile { result, err := svc.FindByID([]string{currentID}, userID) if err != nil { return nil, err } return result, nil } else { return nil, errorpkg.NewInternalServerError(fmt.Errorf("invalid file type %s", currentType)) } } func (svc *FileService) List(id string, opts FileListOptions, userID string) (*FileList, error) { user, err := svc.userRepo.Find(userID) if err != nil { return nil, err } file, err := svc.fileCache.Get(id) if err != nil { return nil, err } if err = svc.fileGuard.Authorize(user, file, model.PermissionViewer); err != nil { return nil, err } if opts.Page < 1 { return nil, errorpkg.NewInvalidPageParameterError() } if opts.Size < 1 { return nil, errorpkg.NewInvalidSizeParameterError() } ids, err := svc.fileRepo.GetChildrenIDs(id) if err != nil { return nil, err } var data []model.File for _, id := range ids { var f model.File f, err := svc.fileCache.Get(id) if err != nil { return nil, err } data = append(data, f) } var filtered []model.File for _, f := range data { if opts.Query == nil || *opts.Query.Type == "" || f.GetType() == *opts.Query.Type { filtered = append(filtered, f) } } authorized, err := svc.doAuthorization(filtered, user) if err != nil { return nil, err } sorted := svc.doSorting(authorized, opts.SortBy, opts.SortOrder, userID) paged, totalElements, totalPages := svc.doPagination(sorted, opts.Page, opts.Size) mapped, err := svc.fileMapper.mapMany(paged, userID) if err != nil { return nil, err } return &FileList{ Data: mapped, TotalElements: totalElements, TotalPages: totalPages, Page: opts.Page, Size: opts.Size, }, nil } func (svc *FileService) Search(id string, opts FileListOptions, userID string) (*FileList, error) { user, err := svc.userRepo.Find(userID) if err != nil { return nil, err } parent, err := svc.fileCache.Get(id) if err != nil { return nil, err } workspace, err := svc.workspaceRepo.Find(parent.GetWorkspaceID()) if err != nil { return nil, err } if err := svc.workspaceGuard.Authorize(user, workspace, model.PermissionViewer); err != nil { return nil, err } data, err := svc.fileSearch.Query(opts.Query.Text) if err != nil { return nil, err } filtered, err := svc.doQueryFiltering(data, *opts.Query, parent) if err != nil { return nil, err } authorized, err := svc.doAuthorization(filtered, user) if err != nil { return nil, err } sorted := svc.doSorting(authorized, opts.SortBy, opts.SortOrder, userID) paged, totalElements, totalPages := svc.doPagination(sorted, opts.Page, opts.Size) v, err := svc.fileMapper.mapMany(paged, userID) if err != nil { return nil, err } res := &FileList{ Data: v, TotalElements: totalElements, TotalPages: totalPages, Page: opts.Page, Size: opts.Size, Query: opts.Query, } return res, nil } func (svc *FileService) GetPath(id string, userID string) ([]*File, error) { user, err := svc.userRepo.Find(userID) if err != nil { return nil, err } file, err := svc.fileCache.Get(id) if err != nil { return nil, err } if err = svc.fileGuard.Authorize(user, file, model.PermissionViewer); err != nil { return nil, err } path, err := svc.fileRepo.FindPath(id) if err != nil { return nil, err } res := []*File{} for _, f := range path { v, err := svc.fileMapper.mapOne(f, userID) if err != nil { return nil, err } res = append([]*File{v}, res...) } return res, nil } func (svc *FileService) GetIDs(id string, userID string) ([]string, error) { user, err := svc.userRepo.Find(userID) if err != nil { return nil, err } file, err := svc.fileCache.Get(id) if err != nil { return nil, err } if err = svc.fileGuard.Authorize(user, file, model.PermissionViewer); err != nil { return nil, err } ids, err := svc.fileRepo.GetChildrenIDs(id) if err != nil { return nil, err } return ids, nil } func (svc *FileService) Copy(targetID string, sourceIDs []string, userID string) (copiedFiles []*File, err error) { user, err := svc.userRepo.Find(userID) if err != nil { return nil, err } target, err := svc.fileCache.Get(targetID) if err != nil { return nil, err } /* Do checks */ for _, sourceID := range sourceIDs { var source model.File if source, err = svc.fileCache.Get(sourceID); err != nil { return nil, err } if err = svc.fileGuard.Authorize(user, target, model.PermissionEditor); err != nil { return nil, err } if err = svc.fileGuard.Authorize(user, source, model.PermissionEditor); err != nil { return nil, err } if source.GetID() == target.GetID() { return nil, errorpkg.NewFileCannotBeCopiedIntoIselfError(source) } if target.GetType() != model.FileTypeFolder { return nil, errorpkg.NewFileIsNotAFolderError(target) } if yes, _ := svc.fileRepo.IsGrandChildOf(target.GetID(), source.GetID()); yes { return nil, errorpkg.NewFileCannotBeCopiedIntoOwnSubtreeError(source) } } /* Do copying */ allClones := []model.File{} for _, sourceID := range sourceIDs { /* Get original tree */ var sourceTree []model.File if sourceTree, err = svc.fileRepo.FindTree(sourceID); err != nil { return nil, err } /* Clone source tree */ var rootCloneIndex int var cloneIDs = make(map[string]string) var originalIDs = make(map[string]string) var clones []model.File var permissions []*repo.UserPermission for i, o := range sourceTree { c := repo.NewFile() c.SetID(helper.NewID()) c.SetParentID(o.GetParentID()) c.SetWorkspaceID(o.GetWorkspaceID()) c.SetType(o.GetType()) c.SetName(o.GetName()) c.SetCreateTime(time.Now().UTC().Format(time.RFC3339)) if o.GetID() == sourceID { rootCloneIndex = i } cloneIDs[o.GetID()] = c.GetID() originalIDs[c.GetID()] = o.GetID() clones = append(clones, c) permissions = append(permissions, &repo.UserPermission{ ID: helper.NewID(), UserID: userID, ResourceID: c.GetID(), Permission: model.PermissionOwner, CreateTime: time.Now().UTC().Format(time.RFC3339), }) } /* Set parent IDs of clones */ for i, c := range clones { id := cloneIDs[*c.GetParentID()] clones[i].SetParentID(&id) } rootClone := clones[rootCloneIndex] /* Parent ID of root clone is target ID */ if clones != nil { rootClone.SetParentID(&targetID) } /* If there is a file with similar name, append a prefix */ existing, err := svc.getChildWithName(targetID, rootClone.GetName()) if err != nil { return nil, err } if existing != nil { rootClone.SetName(fmt.Sprintf("Copy of %s", rootClone.GetName())) } /* Persist clones */ if err = svc.fileRepo.BulkInsert(clones, 100); err != nil { return nil, err } /* Persist permissions */ if err = svc.fileRepo.BulkInsertPermissions(permissions, 100); err != nil { return nil, err } /* Assign snapshots to clones */ for _, c := range clones { if err := svc.fileRepo.AssignSnapshots(c.GetID(), originalIDs[c.GetID()]); err != nil { return nil, err } } /* Index clones for search */ if err := svc.fileSearch.Index(clones); err != nil { return nil, err } /* Create cache for clones */ for _, c := range clones { if _, err := svc.fileCache.Refresh(c.GetID()); err != nil { return nil, err } } allClones = append(allClones, clones...) } /* Refresh updateTime on target */ timeNow := time.Now().UTC().Format(time.RFC3339) target.SetUpdateTime(&timeNow) if err := svc.fileRepo.Save(target); err != nil { return nil, err } copiedFiles, err = svc.fileMapper.mapMany(allClones, userID) if err != nil { return nil, err } return copiedFiles, nil } func (svc *FileService) Move(targetID string, sourceIDs []string, userID string) (parentIDs []string, err error) { parentIDs = []string{} user, err := svc.userRepo.Find(userID) if err != nil { return []string{}, err } target, err := svc.fileCache.Get(targetID) if err != nil { return []string{}, err } /* Do checks */ for _, id := range sourceIDs { source, err := svc.fileCache.Get(id) 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(user, target, model.PermissionEditor); err != nil { return []string{}, err } if err := svc.fileGuard.Authorize(user, 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) } targetIsGrandChildOfSource, _ := svc.fileRepo.IsGrandChildOf(target.GetID(), source.GetID()) if targetIsGrandChildOfSource { return []string{}, errorpkg.NewTargetIsGrandChildOfSourceError(source) } } /* Do moving */ for _, id := range sourceIDs { source, _ := svc.fileCache.Get(id) /* Add old parent */ parentIDs = append(parentIDs, *source.GetParentID()) /* Move source into target */ if err := svc.fileRepo.MoveSourceIntoTarget(target.GetID(), source.GetID()); err != nil { return []string{}, err } /* Get updated source */ source, err = svc.fileRepo.Find(source.GetID()) if err != nil { return []string{}, err } // Add new parent parentIDs = append(parentIDs, *source.GetParentID()) /* 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 []string{}, err } target.SetUpdateTime(&timeNow) if err := svc.fileRepo.Save(target); err != nil { return []string{}, err } if err := svc.fileSearch.Update([]model.File{source}); 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.fileCache.Set(f); err != nil { return []string{}, err } } } return parentIDs, nil } func (svc *FileService) Rename(id string, name string, userID string) (*File, error) { user, err := svc.userRepo.Find(userID) if err != nil { return nil, err } file, err := svc.fileCache.Get(id) if err != nil { return nil, err } if file.GetParentID() != nil { existing, err := svc.getChildWithName(*file.GetParentID(), name) if err != nil { return nil, err } if existing != nil { return nil, errorpkg.NewFileWithSimilarNameExistsError() } } if err = svc.fileGuard.Authorize(user, file, model.PermissionEditor); err != nil { return nil, err } file.SetName(name) if err = svc.fileRepo.Save(file); err != nil { return nil, err } if err = svc.fileSearch.Update([]model.File{file}); err != nil { return nil, err } err = svc.fileCache.Set(file) if err != nil { return nil, err } res, err := svc.fileMapper.mapOne(file, userID) if err != nil { return nil, err } return res, nil } func (svc *FileService) Delete(ids []string, userID string) ([]string, error) { var res []string for _, id := range ids { var user model.User user, err := svc.userRepo.Find(userID) if err != nil { return nil, err } file, err := svc.fileCache.Get(id) if err != nil { return nil, err } if file.GetParentID() == nil { workspace, err := svc.workspaceCache.Get(file.GetWorkspaceID()) if err != nil { return []string{}, err } return nil, errorpkg.NewCannotDeleteWorkspaceRootError(file, workspace) } if err = svc.fileGuard.Authorize(user, file, model.PermissionOwner); err != nil { return nil, err } // Add parent res = append(res, *file.GetParentID()) var tree []model.File tree, err = svc.fileRepo.FindTree(file.GetID()) if err != nil { return nil, err } var treeIDs []string for _, f := range tree { treeIDs = append(treeIDs, f.GetID()) } if err := svc.fileSearch.Delete(treeIDs); err != nil { // Here we don't return an error or panic but we just print the error fmt.Println(err) } for _, f := range tree { if err = svc.fileCache.Delete(f.GetID()); err != nil { return nil, err } } for _, f := range tree { if err = svc.fileRepo.Delete(f.GetID()); err != nil { return nil, err } if err = svc.snapshotRepo.DeleteMappingsForFile(f.GetID()); err != nil { return nil, err } } var danglingSnapshots []model.Snapshot danglingSnapshots, err = svc.snapshotRepo.FindAllDangling() if err != nil { return nil, err } for _, s := range danglingSnapshots { if s.HasOriginal() { if err = svc.s3.RemoveObject(s.GetOriginal().Key, s.GetOriginal().Bucket); err != nil { return nil, err } } if s.HasPreview() { if err = svc.s3.RemoveObject(s.GetPreview().Key, s.GetPreview().Bucket); err != nil { return nil, err } } if s.HasText() { if err = svc.s3.RemoveObject(s.GetText().Key, s.GetText().Bucket); err != nil { return nil, err } } } if err = svc.snapshotRepo.DeleteAllDangling(); err != nil { return nil, err } } return res, nil } func (svc *FileService) GetSize(id string, userID string) (int64, error) { user, err := svc.userRepo.Find(userID) if err != nil { return -1, err } file, err := svc.fileCache.Get(id) if err != nil { return -1, err } if err := svc.fileGuard.Authorize(user, file, model.PermissionViewer); err != nil { return -1, err } res, err := svc.fileRepo.GetSize(id) if err != nil { return -1, err } return res, nil } func (svc *FileService) GetItemCount(id string, userID string) (int64, error) { user, err := svc.userRepo.Find(userID) if err != nil { return 0, err } file, err := svc.fileCache.Get(id) if err != nil { return 0, err } if err := svc.fileGuard.Authorize(user, file, model.PermissionViewer); err != nil { return 0, err } res, err := svc.fileRepo.GetItemCount(id) if err != nil { return 0, err } return res, nil } func (svc *FileService) GrantUserPermission(ids []string, assigneeID string, permission string, userID string) error { user, err := svc.userRepo.Find(userID) if err != nil { return err } for _, id := range ids { file, err := svc.fileCache.Get(id) if err != nil { return err } if err = svc.fileGuard.Authorize(user, file, model.PermissionOwner); err != nil { return err } if _, err := svc.userRepo.Find(assigneeID); err != nil { return err } if err = svc.fileRepo.GrantUserPermission(id, assigneeID, permission); err != nil { return err } if _, err := svc.fileCache.Refresh(file.GetID()); err != nil { return err } workspace, err := svc.workspaceRepo.Find(file.GetWorkspaceID()) if err != nil { return err } if err = svc.workspaceCache.Set(workspace); err != nil { return err } path, err := svc.fileRepo.FindPath(id) if err != nil { return err } for _, f := range path { if err := svc.fileCache.Set(f); err != nil { return err } } tree, err := svc.fileRepo.FindTree(id) if err != nil { return err } for _, f := range tree { if err := svc.fileCache.Set(f); err != nil { return err } } } return nil } func (svc *FileService) RevokeUserPermission(ids []string, assigneeID string, userID string) error { user, err := svc.userRepo.Find(userID) if err != nil { return err } for _, id := range ids { file, err := svc.fileCache.Get(id) if err != nil { return err } if err := svc.fileGuard.Authorize(user, file, model.PermissionOwner); err != nil { return err } if _, err := svc.userRepo.Find(assigneeID); err != nil { return err } if err := svc.fileRepo.RevokeUserPermission(id, assigneeID); err != nil { return err } if _, err := svc.fileCache.Refresh(file.GetID()); err != nil { return err } } return nil } func (svc *FileService) GrantGroupPermission(ids []string, groupID string, permission string, userID string) error { user, err := svc.userRepo.Find(userID) if err != nil { return err } for _, id := range ids { file, err := svc.fileCache.Get(id) if err != nil { return err } if err = svc.fileGuard.Authorize(user, file, model.PermissionOwner); err != nil { return err } group, err := svc.groupCache.Get(groupID) if err != nil { return err } if err := svc.groupGuard.Authorize(user, group, model.PermissionViewer); err != nil { return err } if err = svc.fileRepo.GrantGroupPermission(id, groupID, permission); err != nil { return err } if _, err := svc.fileCache.Refresh(file.GetID()); err != nil { return err } workspace, err := svc.workspaceRepo.Find(file.GetWorkspaceID()) if err != nil { return err } if err = svc.workspaceCache.Set(workspace); err != nil { return err } path, err := svc.fileRepo.FindPath(id) if err != nil { return err } for _, f := range path { if err := svc.fileCache.Set(f); err != nil { return err } } tree, err := svc.fileRepo.FindTree(id) if err != nil { return err } for _, f := range tree { if err := svc.fileCache.Set(f); err != nil { return err } } } return nil } func (svc *FileService) RevokeGroupPermission(ids []string, groupID string, userID string) error { user, err := svc.userRepo.Find(userID) if err != nil { return err } for _, id := range ids { file, err := svc.fileCache.Get(id) if err != nil { return err } if err := svc.fileGuard.Authorize(user, file, model.PermissionOwner); err != nil { return err } group, err := svc.groupCache.Get(groupID) if err != nil { return err } if err := svc.groupGuard.Authorize(user, group, model.PermissionViewer); err != nil { return err } if err := svc.fileRepo.RevokeGroupPermission(id, groupID); err != nil { return err } if _, err := svc.fileCache.Refresh(file.GetID()); err != nil { return err } } return nil } func (svc *FileService) GetUserPermissions(id string, userID string) ([]*UserPermission, error) { user, err := svc.userRepo.Find(userID) if err != nil { return nil, err } file, err := svc.fileCache.Get(id) if err != nil { return nil, err } if err := svc.fileGuard.Authorize(user, file, model.PermissionOwner); err != nil { return nil, err } permissions, err := svc.permissionRepo.GetUserPermissions(id) if err != nil { return nil, err } res := make([]*UserPermission, 0) for _, p := range permissions { if p.UserID == userID { continue } u, err := svc.userRepo.Find(p.UserID) if err != nil { return nil, err } res = append(res, &UserPermission{ ID: p.ID, User: svc.userMapper.mapOne(u), Permission: p.Permission, }) } return res, nil } func (svc *FileService) GetGroupPermissions(id string, userID string) ([]*GroupPermission, error) { user, err := svc.userRepo.Find(userID) if err != nil { return nil, err } file, err := svc.fileCache.Get(id) if err != nil { return nil, err } if err := svc.fileGuard.Authorize(user, file, model.PermissionOwner); err != nil { return nil, err } permissions, err := svc.permissionRepo.GetGroupPermissions(id) if err != nil { return nil, err } res := make([]*GroupPermission, 0) for _, p := range permissions { m, err := svc.groupCache.Get(p.GroupID) if err != nil { return nil, err } g, err := svc.groupMapper.mapOne(m, userID) if err != nil { return nil, err } res = append(res, &GroupPermission{ ID: p.ID, Group: g, Permission: p.Permission, }) } return res, nil } func (svc *FileService) doAuthorization(data []model.File, user model.User) ([]model.File, error) { var res []model.File for _, f := range data { if svc.fileGuard.IsAuthorized(user, f, model.PermissionViewer) { res = append(res, f) } } return res, nil } func (svc *FileService) doAuthorizationByIDs(ids []string, user model.User) ([]model.File, error) { var res []model.File for _, id := range ids { var f model.File f, err := svc.fileCache.Get(id) if err != nil { return nil, err } if svc.fileGuard.IsAuthorized(user, f, model.PermissionViewer) { res = append(res, f) } } return res, nil } func (svc *FileService) doSorting(data []model.File, sortBy string, sortOrder string, userID string) []model.File { if sortBy == SortByName { sort.Slice(data, func(i, j int) bool { if sortOrder == SortOrderDesc { return data[i].GetName() > data[j].GetName() } else { return data[i].GetName() < data[j].GetName() } }) return data } else if sortBy == SortBySize { sort.Slice(data, func(i, j int) bool { fileA, err := svc.fileMapper.mapOne(data[i], userID) if err != nil { return false } fileB, err := svc.fileMapper.mapOne(data[j], userID) if err != nil { return false } var sizeA int64 = 0 if fileA.Original != nil { sizeA = int64(fileA.Original.Size) } var sizeB int64 = 0 if fileB.Original != nil { sizeB = int64(fileB.Original.Size) } if sortOrder == SortOrderDesc { return sizeA > sizeB } else { return sizeA < sizeB } }) return data } else if sortBy == SortByDateCreated { sort.Slice(data, func(i, j int) bool { a, _ := time.Parse(time.RFC3339, data[i].GetCreateTime()) b, _ := time.Parse(time.RFC3339, data[j].GetCreateTime()) if sortOrder == SortOrderDesc { return a.UnixMilli() > b.UnixMilli() } else { return a.UnixMilli() < b.UnixMilli() } }) return data } else if sortBy == SortByDateModified { sort.Slice(data, func(i, j int) bool { if data[i].GetUpdateTime() != nil && data[j].GetUpdateTime() != nil { a, _ := time.Parse(time.RFC3339, *data[i].GetUpdateTime()) b, _ := time.Parse(time.RFC3339, *data[j].GetUpdateTime()) if sortOrder == SortOrderDesc { return a.UnixMilli() > b.UnixMilli() } else { return a.UnixMilli() < b.UnixMilli() } } else { return false } }) return data } else if sortBy == SortByKind { folders, _ := rxgo.Just(data)(). Filter(func(v interface{}) bool { return v.(model.File).GetType() == model.FileTypeFolder }). ToSlice(0) files, _ := rxgo.Just(data)(). Filter(func(v interface{}) bool { return v.(model.File).GetType() == model.FileTypeFile }). ToSlice(0) images, _ := rxgo.Just(files)(). Filter(func(v interface{}) bool { f, err := svc.fileMapper.mapOne(v.(model.File), userID) if err != nil { return false } if f.Original == nil { return false } if svc.fileIdent.IsImage(f.Original.Extension) { return true } return false }). ToSlice(0) pdfs, _ := rxgo.Just(files)(). Filter(func(v interface{}) bool { f, err := svc.fileMapper.mapOne(v.(model.File), userID) if err != nil { return false } if f.Original == nil { return false } if svc.fileIdent.IsPDF(f.Original.Extension) { return true } return false }). ToSlice(0) documents, _ := rxgo.Just(files)(). Filter(func(v interface{}) bool { f, err := svc.fileMapper.mapOne(v.(model.File), userID) if err != nil { return false } if f.Original == nil { return false } if svc.fileIdent.IsOffice(f.Original.Extension) { return true } return false }). ToSlice(0) videos, _ := rxgo.Just(files)(). Filter(func(v interface{}) bool { f, err := svc.fileMapper.mapOne(v.(model.File), userID) if err != nil { return false } if f.Original == nil { return false } if svc.fileIdent.IsVideo(f.Original.Extension) { return true } return false }). ToSlice(0) texts, _ := rxgo.Just(files)(). Filter(func(v interface{}) bool { f, err := svc.fileMapper.mapOne(v.(model.File), userID) if err != nil { return false } if f.Original == nil { return false } if svc.fileIdent.IsPlainText(f.Original.Extension) { return true } return false }). ToSlice(0) others, _ := rxgo.Just(files)(). Filter(func(v interface{}) bool { f, err := svc.fileMapper.mapOne(v.(model.File), userID) if err != nil { return false } if f.Original == nil { return false } if !svc.fileIdent.IsImage(f.Original.Extension) && !svc.fileIdent.IsPDF(f.Original.Extension) && !svc.fileIdent.IsOffice(f.Original.Extension) && !svc.fileIdent.IsVideo(f.Original.Extension) && !svc.fileIdent.IsPlainText(f.Original.Extension) { return true } return false }). ToSlice(0) var res []model.File for _, v := range folders { var file model.File file, err := svc.fileCache.Get(v.(model.File).GetID()) if err != nil { return data } res = append(res, file) } for _, v := range images { var file model.File file, err := svc.fileCache.Get(v.(model.File).GetID()) if err != nil { return data } res = append(res, file) } for _, v := range pdfs { var file model.File file, err := svc.fileCache.Get(v.(model.File).GetID()) if err != nil { return data } res = append(res, file) } for _, v := range documents { var file model.File file, err := svc.fileCache.Get(v.(model.File).GetID()) if err != nil { return data } res = append(res, file) } for _, v := range videos { var file model.File file, err := svc.fileCache.Get(v.(model.File).GetID()) if err != nil { return data } res = append(res, file) } for _, v := range texts { var file model.File file, err := svc.fileCache.Get(v.(model.File).GetID()) if err != nil { return data } res = append(res, file) } for _, v := range others { var file model.File file, err := svc.fileCache.Get(v.(model.File).GetID()) if err != nil { return data } res = append(res, file) } return res } return data } func (svc *FileService) doPagination(data []model.File, page, size uint) ([]model.File, uint, uint) { totalElements := uint(len(data)) totalPages := (totalElements + size - 1) / size if page > totalPages { return nil, totalElements, totalPages } startIndex := (page - 1) * size endIndex := startIndex + size if endIndex > totalElements { endIndex = totalElements } pageData := data[startIndex:endIndex] return pageData, totalElements, totalPages } func (svc *FileService) doQueryFiltering(data []model.File, opts FileQuery, parent model.File) ([]model.File, error) { filtered, _ := rxgo.Just(data)(). Filter(func(v interface{}) bool { return v.(model.File).GetWorkspaceID() == parent.GetWorkspaceID() }). Filter(func(v interface{}) bool { if opts.Type != nil { return v.(model.File).GetType() == *opts.Type } else { return true } }). Filter(func(v interface{}) bool { file := v.(model.File) res, err := svc.fileRepo.IsGrandChildOf(file.GetID(), parent.GetID()) if err != nil { return false } return res }). Filter(func(v interface{}) bool { if opts.CreateTimeBefore != nil { t, _ := time.Parse(time.RFC3339, v.(model.File).GetCreateTime()) return t.UnixMilli() >= *opts.CreateTimeAfter } else { return true } }). Filter(func(v interface{}) bool { if opts.CreateTimeBefore != nil { t, _ := time.Parse(time.RFC3339, v.(model.File).GetCreateTime()) return t.UnixMilli() <= *opts.CreateTimeBefore } else { return true } }). Filter(func(v interface{}) bool { if opts.UpdateTimeAfter != nil { file := v.(model.File) t, _ := time.Parse(time.RFC3339, v.(model.File).GetCreateTime()) return file.GetUpdateTime() != nil && t.UnixMilli() >= *opts.UpdateTimeAfter } else { return true } }). Filter(func(v interface{}) bool { if opts.UpdateTimeBefore != nil { file := v.(model.File) t, _ := time.Parse(time.RFC3339, v.(model.File).GetCreateTime()) return file.GetUpdateTime() != nil && t.UnixMilli() <= *opts.UpdateTimeBefore } else { return true } }). ToSlice(0) var res []model.File for _, v := range filtered { var file model.File file, err := svc.fileCache.Get(v.(model.File).GetID()) if err != nil { return nil, err } res = append(res, file) } return res, nil } func (svc *FileService) getChildWithName(id string, name string) (model.File, error) { children, err := svc.fileRepo.FindChildren(id) if err != nil { return nil, err } for _, child := range children { if child.GetName() == name { return child, nil } } return nil, nil } type FileMapper struct { groupCache *cache.GroupCache config config.Config } func NewFileMapper() *FileMapper { return &FileMapper{ groupCache: cache.NewGroupCache(), config: config.GetConfig(), } } func (mp *FileMapper) mapOne(m model.File, userID string) (*File, error) { snapshots := m.GetSnapshots() res := &File{ ID: m.GetID(), WorkspaceID: m.GetWorkspaceID(), Name: m.GetName(), Type: m.GetType(), ParentID: m.GetParentID(), Snapshots: mp.mapSnapshots(snapshots), CreateTime: m.GetCreateTime(), UpdateTime: m.GetUpdateTime(), } if len(snapshots) > 0 { latest := mp.mapSnapshot(snapshots[len(snapshots)-1]) res.Version = &latest.Version res.Original = latest.Original res.Preview = latest.Preview res.Thumbnail = latest.Thumbnail res.Status = latest.Status } res.Permission = "" for _, p := range m.GetUserPermissions() { if p.GetUserID() == userID && model.GetPermissionWeight(p.GetValue()) > model.GetPermissionWeight(res.Permission) { res.Permission = p.GetValue() } } for _, p := range m.GetGroupPermissions() { g, err := mp.groupCache.Get(p.GetGroupID()) if err != nil { return nil, err } for _, u := range g.GetUsers() { if u == userID && model.GetPermissionWeight(p.GetValue()) > model.GetPermissionWeight(res.Permission) { res.Permission = p.GetValue() } } } shareCount := 0 for _, p := range m.GetUserPermissions() { if p.GetUserID() != userID { shareCount++ } } if res.Permission == model.PermissionOwner { shareCount += len(m.GetGroupPermissions()) res.IsShared = new(bool) if shareCount > 0 { *res.IsShared = true } else { *res.IsShared = false } } return res, nil } func (mp *FileMapper) mapMany(data []model.File, userID string) ([]*File, error) { res := make([]*File, 0) for _, f := range data { v, err := mp.mapOne(f, userID) if err != nil { return nil, err } res = append(res, v) } return res, nil } func (mp *FileMapper) mapSnapshot(m model.Snapshot) *Snapshot { s := &Snapshot{ ID: m.GetID(), Version: m.GetVersion(), Status: m.GetStatus(), } if m.HasOriginal() { s.Original = mp.mapOriginal(m.GetOriginal()) } if m.HasPreview() { s.Preview = mp.mapPreview(m.GetPreview()) } if m.HasThumbnail() { s.Thumbnail = mp.mapThumbnail(m.GetThumbnail()) } return s } func (mp *FileMapper) mapOriginal(m *model.S3Object) *Download { download := &Download{ Extension: filepath.Ext(m.Key), Size: m.Size, } if m.Image != nil { download.Image = &ImageProps{ Width: m.Image.Width, Height: m.Image.Height, } } return download } func (mp *FileMapper) mapPreview(m *model.S3Object) *Download { download := &Download{ Extension: filepath.Ext(m.Key), Size: m.Size, } if m.Image != nil { download.Image = &ImageProps{ Width: m.Image.Width, Height: m.Image.Height, } } return download } func (mp *FileMapper) mapThumbnail(m *model.Thumbnail) *Thumbnail { return &Thumbnail{ Base64: m.Base64, Width: m.Width, Height: m.Height, } } func (mp *FileMapper) mapSnapshots(snapshots []model.Snapshot) []*Snapshot { res := make([]*Snapshot, 0) for _, s := range snapshots { res = append(res, mp.mapSnapshot(s)) } return res }