feat(service): add upload, download, file, and folder services
Add UploadService (dedup, chunk lifecycle, ComposeObject), DownloadService (Range support), FileService (ref counting), FolderService (path validation). Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
142
internal/service/folder_service.go
Normal file
142
internal/service/folder_service.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gcy_hpc_server/internal/model"
|
||||
"gcy_hpc_server/internal/store"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// FolderService provides CRUD operations for folders with path validation
|
||||
// and directory tree management.
|
||||
type FolderService struct {
|
||||
folderStore *store.FolderStore
|
||||
fileStore *store.FileStore
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewFolderService creates a new FolderService.
|
||||
func NewFolderService(folderStore *store.FolderStore, fileStore *store.FileStore, logger *zap.Logger) *FolderService {
|
||||
return &FolderService{
|
||||
folderStore: folderStore,
|
||||
fileStore: fileStore,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateFolder validates the name, computes a materialized path, checks for
|
||||
// duplicates, and persists the folder.
|
||||
func (s *FolderService) CreateFolder(ctx context.Context, name string, parentID *int64) (*model.FolderResponse, error) {
|
||||
if err := model.ValidateFolderName(name); err != nil {
|
||||
return nil, fmt.Errorf("invalid folder name: %w", err)
|
||||
}
|
||||
|
||||
var path string
|
||||
if parentID == nil {
|
||||
path = "/" + name + "/"
|
||||
} else {
|
||||
parent, err := s.folderStore.GetByID(ctx, *parentID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get parent folder: %w", err)
|
||||
}
|
||||
if parent == nil {
|
||||
return nil, fmt.Errorf("parent folder %d not found", *parentID)
|
||||
}
|
||||
path = parent.Path + name + "/"
|
||||
}
|
||||
|
||||
existing, err := s.folderStore.GetByPath(ctx, path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("check duplicate path: %w", err)
|
||||
}
|
||||
if existing != nil {
|
||||
return nil, fmt.Errorf("folder with path %q already exists", path)
|
||||
}
|
||||
|
||||
folder := &model.Folder{
|
||||
Name: name,
|
||||
ParentID: parentID,
|
||||
Path: path,
|
||||
}
|
||||
if err := s.folderStore.Create(ctx, folder); err != nil {
|
||||
return nil, fmt.Errorf("create folder: %w", err)
|
||||
}
|
||||
|
||||
return s.toFolderResponse(ctx, folder)
|
||||
}
|
||||
|
||||
// GetFolder retrieves a folder by ID with file and subfolder counts.
|
||||
func (s *FolderService) GetFolder(ctx context.Context, id int64) (*model.FolderResponse, error) {
|
||||
folder, err := s.folderStore.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get folder: %w", err)
|
||||
}
|
||||
if folder == nil {
|
||||
return nil, fmt.Errorf("folder %d not found", id)
|
||||
}
|
||||
|
||||
return s.toFolderResponse(ctx, folder)
|
||||
}
|
||||
|
||||
// ListFolders returns all direct children of the given parent folder (or root
|
||||
// if parentID is nil).
|
||||
func (s *FolderService) ListFolders(ctx context.Context, parentID *int64) ([]model.FolderResponse, error) {
|
||||
folders, err := s.folderStore.ListByParentID(ctx, parentID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list folders: %w", err)
|
||||
}
|
||||
|
||||
result := make([]model.FolderResponse, 0, len(folders))
|
||||
for i := range folders {
|
||||
resp, err := s.toFolderResponse(ctx, &folders[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, *resp)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// DeleteFolder soft-deletes a folder only if it has no children (sub-folders
|
||||
// or files).
|
||||
func (s *FolderService) DeleteFolder(ctx context.Context, id int64) error {
|
||||
hasChildren, err := s.folderStore.HasChildren(ctx, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("check children: %w", err)
|
||||
}
|
||||
if hasChildren {
|
||||
return fmt.Errorf("folder is not empty")
|
||||
}
|
||||
|
||||
if err := s.folderStore.Delete(ctx, id); err != nil {
|
||||
return fmt.Errorf("delete folder: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// toFolderResponse converts a Folder model into a FolderResponse DTO with
|
||||
// computed file and subfolder counts.
|
||||
func (s *FolderService) toFolderResponse(ctx context.Context, f *model.Folder) (*model.FolderResponse, error) {
|
||||
subFolders, err := s.folderStore.ListByParentID(ctx, &f.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("count subfolders: %w", err)
|
||||
}
|
||||
|
||||
_, fileCount, err := s.fileStore.List(ctx, &f.ID, 1, 1)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("count files: %w", err)
|
||||
}
|
||||
|
||||
return &model.FolderResponse{
|
||||
ID: f.ID,
|
||||
Name: f.Name,
|
||||
ParentID: f.ParentID,
|
||||
Path: f.Path,
|
||||
FileCount: fileCount,
|
||||
SubFolderCount: int64(len(subFolders)),
|
||||
CreatedAt: f.CreatedAt,
|
||||
}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user