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

91 lines
3.4 KiB
Go

package repository
import (
"context"
"database/sql"
"fmt"
"rag/file-system/internal/domain/model"
)
type ShareRepoImpl struct {
db *sql.DB
}
func NewShareRepository(db *sql.DB) *ShareRepoImpl {
return &ShareRepoImpl{db: db}
}
func (r *ShareRepoImpl) Create(ctx context.Context, share *model.ShareLink) error {
_, err := r.db.ExecContext(ctx,
`INSERT INTO share_links (id, resource_type, resource_id, token, password, expires_at, download_count, max_downloads, created_by, created_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
share.ID, share.ResourceType, share.ResourceID, share.Token, share.Password, share.ExpiresAt,
share.DownloadCount, share.MaxDownloads, share.CreatedBy, share.CreatedAt)
if err != nil {
return fmt.Errorf("failed to create share link: %w", err)
}
return nil
}
func (r *ShareRepoImpl) GetByToken(ctx context.Context, token string) (*model.ShareLink, error) {
return r.queryOne(ctx, `SELECT id, resource_type, resource_id, token, password, expires_at, download_count, max_downloads, created_by, created_at FROM share_links WHERE token = $1`, token)
}
func (r *ShareRepoImpl) GetByID(ctx context.Context, id string) (*model.ShareLink, error) {
return r.queryOne(ctx, `SELECT id, resource_type, resource_id, token, password, expires_at, download_count, max_downloads, created_by, created_at FROM share_links WHERE id = $1`, id)
}
func (r *ShareRepoImpl) Delete(ctx context.Context, id string, createdBy string) error {
result, err := r.db.ExecContext(ctx, `DELETE FROM share_links WHERE id = $1 AND created_by = $2`, id, createdBy)
if err != nil {
return fmt.Errorf("failed to delete share link: %w", err)
}
rows, _ := result.RowsAffected()
if rows == 0 {
return fmt.Errorf("share link not found or not owned by user")
}
return nil
}
func (r *ShareRepoImpl) IncrementDownloadCount(ctx context.Context, token string) error {
_, err := r.db.ExecContext(ctx, `UPDATE share_links SET download_count = download_count + 1 WHERE token = $1`, token)
if err != nil {
return fmt.Errorf("failed to increment download count: %w", err)
}
return nil
}
func (r *ShareRepoImpl) ListByResource(ctx context.Context, resourceType string, resourceID string) ([]model.ShareLink, error) {
rows, err := r.db.QueryContext(ctx,
`SELECT id, resource_type, resource_id, token, password, expires_at, download_count, max_downloads, created_by, created_at FROM share_links WHERE resource_type = $1 AND resource_id = $2 ORDER BY created_at DESC`,
resourceType, resourceID)
if err != nil {
return nil, fmt.Errorf("failed to list share links: %w", err)
}
defer rows.Close()
var links []model.ShareLink
for rows.Next() {
var s model.ShareLink
if err := rows.Scan(&s.ID, &s.ResourceType, &s.ResourceID, &s.Token, &s.Password, &s.ExpiresAt, &s.DownloadCount, &s.MaxDownloads, &s.CreatedBy, &s.CreatedAt); err != nil {
return nil, fmt.Errorf("failed to scan share link: %w", err)
}
links = append(links, s)
}
return links, rows.Err()
}
func (r *ShareRepoImpl) queryOne(ctx context.Context, query string, args ...interface{}) (*model.ShareLink, error) {
var s model.ShareLink
err := r.db.QueryRowContext(ctx, query, args...).
Scan(&s.ID, &s.ResourceType, &s.ResourceID, &s.Token, &s.Password, &s.ExpiresAt, &s.DownloadCount, &s.MaxDownloads, &s.CreatedBy, &s.CreatedAt)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("failed to query share link: %w", err)
}
return &s, nil
}