向宁 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

92 lines
3.0 KiB
Go

package repository
import (
"context"
"database/sql"
"fmt"
"rag/file-system/internal/domain/model"
)
type FileMetaRepoImpl struct {
db *sql.DB
}
func NewFileMetaRepository(db *sql.DB) *FileMetaRepoImpl {
return &FileMetaRepoImpl{db: db}
}
func (r *FileMetaRepoImpl) Create(ctx context.Context, file *model.FileMeta) error {
_, err := r.db.ExecContext(ctx,
`INSERT INTO files (id, folder_id, name, s3_key, s3_bucket, size, content_type, owner_id, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
file.ID, file.FolderID, file.Name, file.S3Key, file.S3Bucket, file.Size, file.ContentType, file.OwnerID, file.CreatedAt, file.UpdatedAt)
if err != nil {
return fmt.Errorf("failed to create file meta: %w", err)
}
return nil
}
func (r *FileMetaRepoImpl) GetByID(ctx context.Context, id string) (*model.FileMeta, error) {
var f model.FileMeta
err := r.db.QueryRowContext(ctx,
`SELECT id, folder_id, name, s3_key, s3_bucket, size, content_type, owner_id, created_at, updated_at FROM files WHERE id = $1`, id).
Scan(&f.ID, &f.FolderID, &f.Name, &f.S3Key, &f.S3Bucket, &f.Size, &f.ContentType, &f.OwnerID, &f.CreatedAt, &f.UpdatedAt)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("failed to get file meta: %w", err)
}
return &f, nil
}
func (r *FileMetaRepoImpl) GetByFolder(ctx context.Context, folderID string) ([]model.FileMeta, error) {
rows, err := r.db.QueryContext(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 ORDER BY name`, folderID)
if err != nil {
return nil, fmt.Errorf("failed to get files by folder: %w", err)
}
defer rows.Close()
return scanFiles(rows)
}
func (r *FileMetaRepoImpl) Move(ctx context.Context, fileID string, targetFolderID string, ownerID string) error {
result, err := r.db.ExecContext(ctx,
`UPDATE files SET folder_id = $1, updated_at = now() WHERE id = $2 AND owner_id = $3`,
targetFolderID, fileID, ownerID)
if err != nil {
return fmt.Errorf("failed to move file: %w", err)
}
rows, _ := result.RowsAffected()
if rows == 0 {
return fmt.Errorf("file not found or not owned by user")
}
return nil
}
func (r *FileMetaRepoImpl) Delete(ctx context.Context, id string, ownerID string) error {
result, err := r.db.ExecContext(ctx, `DELETE FROM files WHERE id = $1 AND owner_id = $2`, id, ownerID)
if err != nil {
return fmt.Errorf("failed to delete file meta: %w", err)
}
rows, _ := result.RowsAffected()
if rows == 0 {
return fmt.Errorf("file not found or not owned by user")
}
return nil
}
func scanFiles(rows *sql.Rows) ([]model.FileMeta, error) {
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()
}