- 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
155 lines
4.8 KiB
Go
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()
|
|
}
|