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" "gorm.io/gorm" ) // FileService handles file listing, metadata, download, and deletion operations. type FileService struct { storage storage.ObjectStorage blobStore *store.BlobStore fileStore *store.FileStore bucket string db *gorm.DB logger *zap.Logger } // NewFileService creates a new FileService. func NewFileService(storage storage.ObjectStorage, blobStore *store.BlobStore, fileStore *store.FileStore, bucket string, db *gorm.DB, logger *zap.Logger) *FileService { return &FileService{ storage: storage, blobStore: blobStore, fileStore: fileStore, bucket: bucket, db: db, logger: logger, } } // ListFiles returns a paginated list of files, optionally filtered by folder or search query. func (s *FileService) ListFiles(ctx context.Context, folderID *int64, page, pageSize int, search string) ([]model.FileResponse, int64, error) { var files []model.File var total int64 var err error if search != "" { files, total, err = s.fileStore.Search(ctx, search, page, pageSize) } else { files, total, err = s.fileStore.List(ctx, folderID, page, pageSize) } if err != nil { return nil, 0, fmt.Errorf("list files: %w", err) } responses := make([]model.FileResponse, 0, len(files)) for _, f := range files { blob, err := s.blobStore.GetBySHA256(ctx, f.BlobSHA256) if err != nil { return nil, 0, fmt.Errorf("get blob for file %d: %w", f.ID, err) } if blob == nil { return nil, 0, fmt.Errorf("blob not found for file %d", f.ID) } responses = append(responses, model.FileResponse{ ID: f.ID, Name: f.Name, FolderID: f.FolderID, Size: blob.FileSize, MimeType: blob.MimeType, SHA256: f.BlobSHA256, CreatedAt: f.CreatedAt, UpdatedAt: f.UpdatedAt, }) } return responses, total, nil } // GetFileMetadata returns the file and its associated blob metadata. func (s *FileService) 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: %d", fileID) } 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 for file %d", fileID) } return file, blob, nil } // DownloadFile returns a reader for the file content, along with file and blob metadata. // If rangeHeader is non-empty, it parses the range and returns partial content. func (s *FileService) DownloadFile(ctx context.Context, fileID int64, rangeHeader string) (io.ReadCloser, *model.File, *model.FileBlob, int64, int64, error) { file, blob, err := s.GetFileMetadata(ctx, fileID) if err != nil { return nil, nil, nil, 0, 0, err } 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 } reader, _, err := s.storage.GetObject(ctx, s.bucket, blob.MinioKey, storage.GetOptions{ Start: &start, End: &end, }) if err != nil { return nil, nil, nil, 0, 0, fmt.Errorf("get object: %w", err) } return reader, file, blob, start, end, nil } // DeleteFile soft-deletes a file. If no other active files reference the same blob, // it decrements the blob ref count and removes the object from storage when ref count reaches 0. func (s *FileService) DeleteFile(ctx context.Context, fileID int64) error { return s.db.Transaction(func(tx *gorm.DB) error { txFileStore := store.NewFileStore(tx) txBlobStore := store.NewBlobStore(tx) blobSHA256, err := txFileStore.GetBlobSHA256ByID(ctx, fileID) if err != nil { return fmt.Errorf("get blob sha256: %w", err) } if blobSHA256 == "" { return fmt.Errorf("file not found: %d", fileID) } if err := tx.Delete(&model.File{}, fileID).Error; err != nil { return fmt.Errorf("soft delete file: %w", err) } activeCount, err := txFileStore.CountByBlobSHA256(ctx, blobSHA256) if err != nil { return fmt.Errorf("count active refs: %w", err) } if activeCount == 0 { newRefCount, err := txBlobStore.DecrementRef(ctx, blobSHA256) if err != nil { return fmt.Errorf("decrement ref: %w", err) } if newRefCount == 0 { blob, err := txBlobStore.GetBySHA256(ctx, blobSHA256) if err != nil { return fmt.Errorf("get blob for cleanup: %w", err) } if blob != nil { if err := s.storage.RemoveObject(ctx, s.bucket, blob.MinioKey, storage.RemoveObjectOptions{}); err != nil { return fmt.Errorf("remove object: %w", err) } if err := txBlobStore.Delete(ctx, blobSHA256); err != nil { return fmt.Errorf("delete blob: %w", err) } } } } return nil }) }