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 }