- 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
92 lines
3.0 KiB
Go
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()
|
|
}
|