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:
@@ -130,14 +130,16 @@ type UploadSessionResponse struct {
|
||||
|
||||
// FileResponse is the DTO for a file in API responses.
|
||||
type FileResponse struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
FolderID *int64 `json:"folder_id,omitempty"`
|
||||
Size int64 `json:"size"`
|
||||
MimeType string `json:"mime_type"`
|
||||
SHA256 string `json:"sha256"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
FolderID *int64 `json:"folder_id"`
|
||||
FolderPath *string `json:"folder_path"`
|
||||
UserID *int64 `json:"user_id"`
|
||||
Size int64 `json:"size"`
|
||||
MimeType string `json:"mime_type"`
|
||||
SHA256 string `json:"sha256"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// FolderResponse is the DTO for a folder in API responses.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -78,7 +78,7 @@ func setupFileTestDB(t *testing.T) *gorm.DB {
|
||||
if err != nil {
|
||||
t.Fatalf("open sqlite: %v", err)
|
||||
}
|
||||
if err := db.AutoMigrate(&model.File{}, &model.FileBlob{}); err != nil {
|
||||
if err := db.AutoMigrate(&model.File{}, &model.FileBlob{}, &model.Folder{}); err != nil {
|
||||
t.Fatalf("migrate: %v", err)
|
||||
}
|
||||
return db
|
||||
@@ -88,7 +88,7 @@ func setupFileService(t *testing.T) (*FileService, *mockFileStorage, *gorm.DB) {
|
||||
t.Helper()
|
||||
db := setupFileTestDB(t)
|
||||
ms := &mockFileStorage{}
|
||||
svc := NewFileService(ms, store.NewBlobStore(db), store.NewFileStore(db), "test-bucket", db, zap.NewNop())
|
||||
svc := NewFileService(ms, store.NewBlobStore(db), store.NewFileStore(db), store.NewFolderStore(db), "test-bucket", db, zap.NewNop())
|
||||
return svc, ms, db
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ func createTestFile(t *testing.T, db *gorm.DB, name, blobSHA256 string, folderID
|
||||
func TestListFiles_Empty(t *testing.T) {
|
||||
svc, _, _ := setupFileService(t)
|
||||
|
||||
files, total, err := svc.ListFiles(context.Background(), nil, 1, 10, "")
|
||||
files, total, err := svc.ListFiles(context.Background(), nil, nil, 1, 10, "")
|
||||
if err != nil {
|
||||
t.Fatalf("ListFiles: %v", err)
|
||||
}
|
||||
@@ -142,7 +142,7 @@ func TestListFiles_WithFiles(t *testing.T) {
|
||||
createTestFile(t, db, "file1.txt", blob.SHA256, nil)
|
||||
createTestFile(t, db, "file2.txt", blob.SHA256, nil)
|
||||
|
||||
files, total, err := svc.ListFiles(context.Background(), nil, 1, 10, "")
|
||||
files, total, err := svc.ListFiles(context.Background(), nil, nil, 1, 10, "")
|
||||
if err != nil {
|
||||
t.Fatalf("ListFiles: %v", err)
|
||||
}
|
||||
@@ -173,7 +173,7 @@ func TestListFiles_Search(t *testing.T) {
|
||||
createTestFile(t, db, "document.pdf", "sha256other", nil)
|
||||
createTestBlob(t, db, "sha256other", "blobs/other", "application/pdf", 512, 1)
|
||||
|
||||
files, total, err := svc.ListFiles(context.Background(), nil, 1, 10, "photo")
|
||||
files, total, err := svc.ListFiles(context.Background(), nil, nil, 1, 10, "photo")
|
||||
if err != nil {
|
||||
t.Fatalf("ListFiles: %v", err)
|
||||
}
|
||||
@@ -446,7 +446,7 @@ func TestListFiles_WithFolderFilter(t *testing.T) {
|
||||
createTestFile(t, db, "in_folder.txt", blob.SHA256, &folderID)
|
||||
createTestFile(t, db, "root.txt", blob.SHA256, nil)
|
||||
|
||||
files, total, err := svc.ListFiles(context.Background(), &folderID, 1, 10, "")
|
||||
files, total, err := svc.ListFiles(context.Background(), &folderID, nil, 1, 10, "")
|
||||
if err != nil {
|
||||
t.Fatalf("ListFiles: %v", err)
|
||||
}
|
||||
@@ -460,7 +460,7 @@ func TestListFiles_WithFolderFilter(t *testing.T) {
|
||||
t.Errorf("expected in_folder.txt, got %s", files[0].Name)
|
||||
}
|
||||
|
||||
rootFiles, rootTotal, err := svc.ListFiles(context.Background(), nil, 1, 10, "")
|
||||
rootFiles, rootTotal, err := svc.ListFiles(context.Background(), nil, nil, 1, 10, "")
|
||||
if err != nil {
|
||||
t.Fatalf("ListFiles root: %v", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user