f2c534f636
- mongo-driver v2 connection with bounded startup retry + /readyz gate - idempotent creation of all §4.9 indexes - UpdateVersioned: version-filtered updates, conflict vs not-found errors - AES-256-GCM Seal/Open for ticketing credentials, base64(nonce|ct) - GridFS file store: MIME sniffing, MAX_UPLOAD_MB cap, sha256 metadata - HMAC-signed short-lived file URL tokens (§5.1, 1h TTL) - integration tests against compose Mongo via docker-compose.test.yml overlay Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
74 lines
2.0 KiB
Go
74 lines
2.0 KiB
Go
// Package store owns all MongoDB access: connection lifecycle, index
|
|
// bootstrap, optimistic-concurrency updates, and (via internal/files) GridFS.
|
|
// No other package talks to Mongo directly.
|
|
package store
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"time"
|
|
|
|
"go.mongodb.org/mongo-driver/v2/mongo"
|
|
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
|
"go.mongodb.org/mongo-driver/v2/mongo/readpref"
|
|
|
|
"bountyboard/internal/config"
|
|
)
|
|
|
|
type Store struct {
|
|
Client *mongo.Client
|
|
DB *mongo.Database
|
|
}
|
|
|
|
// Connect builds the client (the driver dials lazily) and verifies
|
|
// connectivity with a bounded retry so a compose race at boot does not kill
|
|
// the app, then ensures all indexes.
|
|
func Connect(ctx context.Context, cfg *config.Config, log *slog.Logger) (*Store, error) {
|
|
opts := options.Client().
|
|
ApplyURI(cfg.MongoURI).
|
|
SetAppName("bountyboard").
|
|
SetServerSelectionTimeout(5 * time.Second)
|
|
if cfg.MongoUsername != "" {
|
|
opts.SetAuth(options.Credential{Username: cfg.MongoUsername, Password: cfg.MongoPassword})
|
|
}
|
|
client, err := mongo.Connect(opts)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("mongo connect: %w", err)
|
|
}
|
|
s := &Store{Client: client, DB: client.Database(cfg.MongoDB)}
|
|
|
|
const attempts = 6
|
|
for i := 1; ; i++ {
|
|
err = s.Ping(ctx)
|
|
if err == nil {
|
|
break
|
|
}
|
|
if i == attempts || ctx.Err() != nil {
|
|
return nil, fmt.Errorf("mongo ping (%d attempts): %w", i, err)
|
|
}
|
|
log.Warn("mongo not ready, retrying", "attempt", i, "err", err)
|
|
select {
|
|
case <-time.After(time.Duration(i) * time.Second):
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
}
|
|
}
|
|
|
|
if err := EnsureIndexes(ctx, s.DB); err != nil {
|
|
return nil, fmt.Errorf("ensure indexes: %w", err)
|
|
}
|
|
log.Info("mongo connected", "db", cfg.MongoDB)
|
|
return s, nil
|
|
}
|
|
|
|
func (s *Store) Ping(ctx context.Context) error {
|
|
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
|
defer cancel()
|
|
return s.Client.Ping(ctx, readpref.Primary())
|
|
}
|
|
|
|
func (s *Store) Close(ctx context.Context) error {
|
|
return s.Client.Disconnect(ctx)
|
|
}
|