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:
dailz
2026-04-15 09:23:09 +08:00
parent a114821615
commit f0847d3978
8 changed files with 2511 additions and 0 deletions

View File

@@ -0,0 +1,98 @@
package service
import (
"context"
"fmt"
"io"
"gcy_hpc_server/internal/model"
"gcy_hpc_server/internal/server"
"gcy_hpc_server/internal/storage"
"gcy_hpc_server/internal/store"
"go.uber.org/zap"
)
// DownloadService handles file downloads with streaming and Range support.
type DownloadService struct {
storage storage.ObjectStorage
blobStore *store.BlobStore
fileStore *store.FileStore
bucket string
logger *zap.Logger
}
// NewDownloadService creates a new DownloadService.
func NewDownloadService(storage storage.ObjectStorage, blobStore *store.BlobStore, fileStore *store.FileStore, bucket string, logger *zap.Logger) *DownloadService {
return &DownloadService{
storage: storage,
blobStore: blobStore,
fileStore: fileStore,
bucket: bucket,
logger: logger,
}
}
// Download returns a stream reader for the given file, optionally limited to a byte range.
// Returns (reader, file, blob, start, end, error).
func (s *DownloadService) Download(ctx context.Context, fileID int64, rangeHeader string) (io.ReadCloser, *model.File, *model.FileBlob, int64, int64, error) {
file, err := s.fileStore.GetByID(ctx, fileID)
if err != nil {
return nil, nil, nil, 0, 0, fmt.Errorf("get file: %w", err)
}
if file == nil {
return nil, nil, nil, 0, 0, fmt.Errorf("file not found")
}
blob, err := s.blobStore.GetBySHA256(ctx, file.BlobSHA256)
if err != nil {
return nil, nil, nil, 0, 0, fmt.Errorf("get blob: %w", err)
}
if blob == nil {
return nil, nil, nil, 0, 0, fmt.Errorf("blob not found")
}
var start, end int64
if rangeHeader != "" {
start, end, err = server.ParseRange(rangeHeader, blob.FileSize)
if err != nil {
return nil, nil, nil, 0, 0, fmt.Errorf("parse range: %w", err)
}
} else {
start = 0
end = blob.FileSize - 1
}
opts := storage.GetOptions{
Start: &start,
End: &end,
}
reader, _, err := s.storage.GetObject(ctx, s.bucket, blob.MinioKey, opts)
if err != nil {
return nil, nil, nil, 0, 0, fmt.Errorf("get object: %w", err)
}
return reader, file, blob, start, end, nil
}
// GetFileMetadata returns the file and its associated blob metadata.
func (s *DownloadService) GetFileMetadata(ctx context.Context, fileID int64) (*model.File, *model.FileBlob, error) {
file, err := s.fileStore.GetByID(ctx, fileID)
if err != nil {
return nil, nil, fmt.Errorf("get file: %w", err)
}
if file == nil {
return nil, nil, fmt.Errorf("file not found")
}
blob, err := s.blobStore.GetBySHA256(ctx, file.BlobSHA256)
if err != nil {
return nil, nil, fmt.Errorf("get blob: %w", err)
}
if blob == nil {
return nil, nil, fmt.Errorf("blob not found")
}
return file, blob, nil
}