Go-Backend (chi, pgx, golang-migrate) + React/TS-Frontend als PWA. Enthalten: Auth inkl. OIDC, Dateien mit Versionen und Papierkorb, Freigaben (Links + intern), WebDAV, Gruppenordner, Admin/Theming, Volltextsuche mit OCR, eigener PDF-Betrachter und WOPI-Office (Collabora/OnlyOffice). Nicht im Repo: deploy/.env (Sitzungsschlüssel, DB- und Admin-Passwort) sowie alle Daten- und Build-Verzeichnisse – siehe .gitignore. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
124 lines
3.4 KiB
Go
124 lines
3.4 KiB
Go
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
|
|
}
|