package sharing import ( "context" "crypto/rand" "encoding/base64" "errors" "fmt" "time" "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" ) var ErrNotFound = errors.New("share link not found") var ErrExpired = errors.New("share link expired") type Store struct { db *pgxpool.Pool } func NewStore(db *pgxpool.Pool) *Store { return &Store{db: db} } func (s *Store) Create(ctx context.Context, fileID, ownerID uuid.UUID, passwordHash string, expiresAt *time.Time) (*ShareLink, error) { token, err := generateToken() if err != nil { return nil, fmt.Errorf("generate token: %w", err) } var ph *string if passwordHash != "" { ph = &passwordHash } var link ShareLink err = s.db.QueryRow(ctx, `INSERT INTO share_links (file_id, owner_id, token, password_hash, expires_at) VALUES ($1, $2, $3, $4, $5) RETURNING id, file_id, owner_id, token, (password_hash IS NOT NULL), expires_at, download_count, created_at`, fileID, ownerID, token, ph, expiresAt, ).Scan(&link.ID, &link.FileID, &link.OwnerID, &link.Token, &link.HasPassword, &link.ExpiresAt, &link.DownloadCount, &link.CreatedAt) if err != nil { return nil, fmt.Errorf("create share link: %w", err) } return &link, nil } func (s *Store) GetByToken(ctx context.Context, token string) (*ShareLink, string, error) { var link ShareLink var passwordHash *string err := s.db.QueryRow(ctx, `SELECT id, file_id, owner_id, token, (password_hash IS NOT NULL), password_hash, expires_at, download_count, created_at FROM share_links WHERE token = $1`, token, ).Scan(&link.ID, &link.FileID, &link.OwnerID, &link.Token, &link.HasPassword, &passwordHash, &link.ExpiresAt, &link.DownloadCount, &link.CreatedAt) if errors.Is(err, pgx.ErrNoRows) { return nil, "", ErrNotFound } if err != nil { return nil, "", fmt.Errorf("get share link: %w", err) } if link.ExpiresAt != nil && time.Now().After(*link.ExpiresAt) { return nil, "", ErrExpired } ph := "" if passwordHash != nil { ph = *passwordHash } return &link, ph, nil } func (s *Store) ListByOwner(ctx context.Context, ownerID uuid.UUID) ([]*ShareLink, error) { rows, err := s.db.Query(ctx, `SELECT id, file_id, owner_id, token, (password_hash IS NOT NULL), expires_at, download_count, created_at FROM share_links WHERE owner_id = $1 ORDER BY created_at DESC`, ownerID, ) if err != nil { return nil, fmt.Errorf("list share links: %w", err) } defer rows.Close() var links []*ShareLink for rows.Next() { var l ShareLink if err := rows.Scan(&l.ID, &l.FileID, &l.OwnerID, &l.Token, &l.HasPassword, &l.ExpiresAt, &l.DownloadCount, &l.CreatedAt); err != nil { return nil, err } links = append(links, &l) } return links, rows.Err() } func (s *Store) Delete(ctx context.Context, id, ownerID uuid.UUID) error { tag, err := s.db.Exec(ctx, `DELETE FROM share_links WHERE id = $1 AND owner_id = $2`, id, ownerID) if err != nil { return fmt.Errorf("delete share link: %w", err) } if tag.RowsAffected() == 0 { return ErrNotFound } return nil } func (s *Store) IncrementDownloads(ctx context.Context, token string) { _, _ = s.db.Exec(ctx, `UPDATE share_links SET download_count = download_count + 1 WHERE token = $1`, token) } func generateToken() (string, error) { b := make([]byte, 24) if _, err := rand.Read(b); err != nil { return "", err } return base64.RawURLEncoding.EncodeToString(b), nil }