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:
98
internal/service/download_service.go
Normal file
98
internal/service/download_service.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user