向宁 3a18ca0579 feat: add directory structure and file sharing support
- PostgreSQL metadata overlay layer on top of existing S3 storage
- 3 new tables: folders, files, share_links
- Folder CRUD: create, get with children, tree, rename, delete (cascade)
- File operations: upload to folder, move between folders
- Share links: create with optional password/expiry/download limit, public access
- S3 compensation on PG write failure
- Existing 14 endpoints untouched
2026-05-20 20:26:19 +08:00

155 lines
4.8 KiB
Go

package repository
import (
"context"
"database/sql"
"fmt"
"rag/file-system/internal/domain/model"
)
type FolderRepoImpl struct {
db *sql.DB
}
func NewFolderRepository(db *sql.DB) *FolderRepoImpl {
return &FolderRepoImpl{db: db}
}
func (r *FolderRepoImpl) Create(ctx context.Context, folder *model.Folder) error {
_, err := r.db.ExecContext(ctx,
`INSERT INTO folders (id, parent_id, name, owner_id, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6)`,
folder.ID, folder.ParentID, folder.Name, folder.OwnerID, folder.CreatedAt, folder.UpdatedAt)
if err != nil {
return fmt.Errorf("failed to create folder: %w", err)
}
return nil
}
func (r *FolderRepoImpl) GetByID(ctx context.Context, id string) (*model.Folder, error) {
var f model.Folder
var parentID sql.NullString
err := r.db.QueryRowContext(ctx,
`SELECT id, parent_id, name, owner_id, created_at, updated_at FROM folders WHERE id = $1`, id).
Scan(&f.ID, &parentID, &f.Name, &f.OwnerID, &f.CreatedAt, &f.UpdatedAt)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("failed to get folder: %w", err)
}
if parentID.Valid {
f.ParentID = &parentID.String
}
return &f, nil
}
func (r *FolderRepoImpl) GetWithChildren(ctx context.Context, id string, ownerID string) (*model.FolderWithChildren, error) {
folder, err := r.GetByID(ctx, id)
if err != nil {
return nil, err
}
if folder == nil || folder.OwnerID != ownerID {
return nil, nil
}
subFolders, err := r.queryFolders(ctx,
`SELECT id, parent_id, name, owner_id, created_at, updated_at FROM folders WHERE parent_id = $1 AND owner_id = $2 ORDER BY name`, id, ownerID)
if err != nil {
return nil, err
}
files, err := r.queryFiles(ctx,
`SELECT id, folder_id, name, s3_key, s3_bucket, size, content_type, owner_id, created_at, updated_at FROM files WHERE folder_id = $1 AND owner_id = $2 ORDER BY name`, id, ownerID)
if err != nil {
return nil, err
}
return &model.FolderWithChildren{
Folder: *folder,
SubFolders: subFolders,
Files: files,
}, nil
}
func (r *FolderRepoImpl) GetTree(ctx context.Context, ownerID string) ([]model.Folder, error) {
return r.queryFolders(ctx,
`SELECT id, parent_id, name, owner_id, created_at, updated_at FROM folders WHERE owner_id = $1 ORDER BY name`, ownerID)
}
func (r *FolderRepoImpl) Update(ctx context.Context, folder *model.Folder) error {
_, err := r.db.ExecContext(ctx,
`UPDATE folders SET name = $1, updated_at = $2 WHERE id = $3`,
folder.Name, folder.UpdatedAt, folder.ID)
if err != nil {
return fmt.Errorf("failed to update folder: %w", err)
}
return nil
}
func (r *FolderRepoImpl) Delete(ctx context.Context, id string, ownerID string) error {
result, err := r.db.ExecContext(ctx, `DELETE FROM folders WHERE id = $1 AND owner_id = $2`, id, ownerID)
if err != nil {
return fmt.Errorf("failed to delete folder: %w", err)
}
rows, _ := result.RowsAffected()
if rows == 0 {
return fmt.Errorf("folder not found or not owned by user")
}
return nil
}
func (r *FolderRepoImpl) GetDescendantFileS3Keys(ctx context.Context, id string, ownerID string) ([]model.FileMeta, error) {
query := `
WITH RECURSIVE descendants AS (
SELECT id FROM folders WHERE id = $1 AND owner_id = $2
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 = $2`
return r.queryFiles(ctx, query, id, ownerID)
}
func (r *FolderRepoImpl) queryFolders(ctx context.Context, query string, args ...interface{}) ([]model.Folder, error) {
rows, err := r.db.QueryContext(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("failed to query folders: %w", err)
}
defer rows.Close()
var folders []model.Folder
for rows.Next() {
var f model.Folder
var parentID sql.NullString
if err := rows.Scan(&f.ID, &parentID, &f.Name, &f.OwnerID, &f.CreatedAt, &f.UpdatedAt); err != nil {
return nil, fmt.Errorf("failed to scan folder: %w", err)
}
if parentID.Valid {
f.ParentID = &parentID.String
}
folders = append(folders, f)
}
return folders, rows.Err()
}
func (r *FolderRepoImpl) queryFiles(ctx context.Context, query string, args ...interface{}) ([]model.FileMeta, error) {
rows, err := r.db.QueryContext(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("failed to query files: %w", err)
}
defer rows.Close()
var files []model.FileMeta
for rows.Next() {
var f model.FileMeta
if err := rows.Scan(&f.ID, &f.FolderID, &f.Name, &f.S3Key, &f.S3Bucket, &f.Size, &f.ContentType, &f.OwnerID, &f.CreatedAt, &f.UpdatedAt); err != nil {
return nil, fmt.Errorf("failed to scan file: %w", err)
}
files = append(files, f)
}
return files, rows.Err()
}