feat(model,service): add folder_path and user_id to FileResponse, add user_id filter to ListFiles

- FileResponse gains folder_path ("/" for root) and user_id fields

- folder_id no longer uses omitempty, root files return null

- ListFiles accepts optional userID parameter for filtering by owner

- New buildFileResponse helper populates folder_path from FolderStore

- New GetFileResponse method wraps GetFileMetadata + buildFileResponse

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-16 16:04:22 +08:00
parent 9092278d26
commit 1d591efeba
3 changed files with 85 additions and 40 deletions

View File

@@ -16,28 +16,59 @@ import (
// 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
storage storage.ObjectStorage
blobStore *store.BlobStore
fileStore *store.FileStore
folderStore *store.FolderStore
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 {
func NewFileService(storage storage.ObjectStorage, blobStore *store.BlobStore, fileStore *store.FileStore, folderStore *store.FolderStore, bucket string, db *gorm.DB, logger *zap.Logger) *FileService {
return &FileService{
storage: storage,
blobStore: blobStore,
fileStore: fileStore,
bucket: bucket,
db: db,
logger: logger,
storage: storage,
blobStore: blobStore,
fileStore: fileStore,
folderStore: folderStore,
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) {
// buildFileResponse creates a FileResponse from a File and FileBlob, including folder_path and user_id.
func (s *FileService) buildFileResponse(ctx context.Context, f model.File, blob model.FileBlob) model.FileResponse {
resp := model.FileResponse{
ID: f.ID,
Name: f.Name,
FolderID: f.FolderID,
UserID: f.UserID,
Size: blob.FileSize,
MimeType: blob.MimeType,
SHA256: f.BlobSHA256,
CreatedAt: f.CreatedAt,
UpdatedAt: f.UpdatedAt,
}
// Determine folder_path
if f.FolderID != nil && s.folderStore != nil {
folder, err := s.folderStore.GetByID(ctx, *f.FolderID)
if err == nil && folder != nil {
resp.FolderPath = &folder.Path
}
}
if resp.FolderPath == nil {
rootPath := "/"
resp.FolderPath = &rootPath
}
return resp
}
// ListFiles returns a paginated list of files, optionally filtered by folder, user, or search query.
func (s *FileService) ListFiles(ctx context.Context, folderID *int64, userID *int64, page, pageSize int, search string) ([]model.FileResponse, int64, error) {
var files []model.File
var total int64
var err error
@@ -51,6 +82,17 @@ func (s *FileService) ListFiles(ctx context.Context, folderID *int64, page, page
return nil, 0, fmt.Errorf("list files: %w", err)
}
// Apply user_id filtering in service layer
if userID != nil {
filtered := make([]model.File, 0, len(files))
for _, f := range files {
if f.UserID != nil && *f.UserID == *userID {
filtered = append(filtered, f)
}
}
files = filtered
}
responses := make([]model.FileResponse, 0, len(files))
for _, f := range files {
blob, err := s.blobStore.GetBySHA256(ctx, f.BlobSHA256)
@@ -61,16 +103,7 @@ func (s *FileService) ListFiles(ctx context.Context, folderID *int64, page, page
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,
})
responses = append(responses, s.buildFileResponse(ctx, f, *blob))
}
return responses, total, nil
@@ -97,6 +130,16 @@ func (s *FileService) GetFileMetadata(ctx context.Context, fileID int64) (*model
return file, blob, nil
}
// GetFileResponse returns a fully populated FileResponse for a given file ID.
func (s *FileService) GetFileResponse(ctx context.Context, fileID int64) (*model.FileResponse, error) {
file, blob, err := s.GetFileMetadata(ctx, fileID)
if err != nil {
return nil, err
}
resp := s.buildFileResponse(ctx, *file, *blob)
return &resp, 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) {