file-system/internal/data/folder_repo.go
向宁 bcd637387a feat: add data layer with GORM models, S3 repo, PG repos
Replace old infrastructure layer with Kratos-style data layer:
- data.go: GORM connection, transaction support, Wire ProviderSet, PO models
- file_repo.go: All 12 S3 operations (upload, download, multipart, presign, buckets)
- folder_repo.go: GORM queries including recursive CTE for descendant files
- file_meta_repo.go: CRUD + move operations for file metadata
- share_repo.go: CRUD + increment download count for share links

Deleted old infrastructure/database, infrastructure/repository, infrastructure/s3.
Kept infrastructure/grpc for later integration.
2026-05-25 12:57:42 +08:00

149 lines
4.3 KiB
Go

package data
import (
"context"
"fmt"
"github.com/go-kratos/kratos/v2/log"
"gorm.io/gorm"
)
// FolderRepo implements folder persistence operations using GORM.
type FolderRepo struct {
data *Data
log *log.Helper
}
// NewFolderRepo creates a new FolderRepo.
func NewFolderRepo(data *Data, logger log.Logger) *FolderRepo {
return &FolderRepo{
data: data,
log: log.NewHelper(logger),
}
}
// FolderWithChildren holds a folder along with its sub-folders and files.
type FolderWithChildren struct {
Folder FolderPO
SubFolders []FolderPO
Files []FileMetaPO
}
// Create inserts a new folder record.
func (r *FolderRepo) Create(ctx context.Context, folder *FolderPO) error {
return r.data.DB(ctx).Create(folder).Error
}
// GetByID retrieves a folder by its ID. Returns nil if not found.
func (r *FolderRepo) GetByID(ctx context.Context, id string) (*FolderPO, error) {
var folder FolderPO
err := r.data.DB(ctx).Where("id = ?", id).First(&folder).Error
if err != nil {
if isRecordNotFound(err) {
return nil, nil
}
return nil, fmt.Errorf("failed to get folder: %w", err)
}
return &folder, nil
}
// GetWithChildren retrieves a folder with its sub-folders and files, filtered by ownerID.
func (r *FolderRepo) GetWithChildren(ctx context.Context, id string, ownerID string) (*FolderWithChildren, error) {
folder, err := r.GetByID(ctx, id)
if err != nil {
return nil, err
}
if folder == nil || folder.OwnerID != ownerID {
return nil, nil
}
var subFolders []FolderPO
if err := r.data.DB(ctx).
Where("parent_id = ? AND owner_id = ?", id, ownerID).
Order("name").
Find(&subFolders).Error; err != nil {
return nil, fmt.Errorf("failed to query sub-folders: %w", err)
}
var files []FileMetaPO
if err := r.data.DB(ctx).
Where("folder_id = ? AND owner_id = ?", id, ownerID).
Order("name").
Find(&files).Error; err != nil {
return nil, fmt.Errorf("failed to query files: %w", err)
}
return &FolderWithChildren{
Folder: *folder,
SubFolders: subFolders,
Files: files,
}, nil
}
// GetTree retrieves all folders owned by the given ownerID.
func (r *FolderRepo) GetTree(ctx context.Context, ownerID string) ([]FolderPO, error) {
var folders []FolderPO
err := r.data.DB(ctx).
Where("owner_id = ?", ownerID).
Order("name").
Find(&folders).Error
if err != nil {
return nil, fmt.Errorf("failed to get folder tree: %w", err)
}
return folders, nil
}
// Update modifies a folder's name and updated_at timestamp.
func (r *FolderRepo) Update(ctx context.Context, folder *FolderPO) error {
result := r.data.DB(ctx).Model(&FolderPO{}).
Where("id = ?", folder.ID).
Updates(map[string]interface{}{
"name": folder.Name,
"updated_at": folder.UpdatedAt,
})
if result.Error != nil {
return fmt.Errorf("failed to update folder: %w", result.Error)
}
return nil
}
// Delete removes a folder by ID and ownerID. Returns error if not found.
func (r *FolderRepo) Delete(ctx context.Context, id string, ownerID string) error {
result := r.data.DB(ctx).
Where("id = ? AND owner_id = ?", id, ownerID).
Delete(&FolderPO{})
if result.Error != nil {
return fmt.Errorf("failed to delete folder: %w", result.Error)
}
if result.RowsAffected == 0 {
return fmt.Errorf("folder not found or not owned by user")
}
return nil
}
// GetDescendantFileS3Keys returns all files in a folder and all its descendant folders.
// Uses a recursive CTE to walk the folder tree.
func (r *FolderRepo) GetDescendantFileS3Keys(ctx context.Context, id string, ownerID string) ([]FileMetaPO, error) {
var files []FileMetaPO
// Raw SQL with recursive CTE - GORM doesn't natively support recursive queries
err := r.data.DB(ctx).Raw(`
WITH RECURSIVE descendants AS (
SELECT id FROM folders WHERE id = ? AND owner_id = ?
UNION
SELECT f.id FROM folders f INNER JOIN descendants d ON f.parent_id = d.id
)
SELECT fi.id, fi.folder_id, fi.name, fi.s3_key, fi.s3_bucket, fi.size, fi.content_type, fi.owner_id, fi.created_at, fi.updated_at
FROM files fi INNER JOIN descendants d ON fi.folder_id = d.id
WHERE fi.owner_id = ?`,
id, ownerID, ownerID).Scan(&files).Error
if err != nil {
return nil, fmt.Errorf("failed to get descendant file S3 keys: %w", err)
}
return files, nil
}
// isRecordNotFound checks if the error is a GORM record-not-found error.
func isRecordNotFound(err error) bool {
return err == gorm.ErrRecordNotFound
}