v2 #1

Open
kamil wants to merge 231 commits from v2 into main
4 changed files with 34 additions and 9 deletions
Showing only changes of commit 1b05b6ebc6 - Show all commits

View File

@@ -136,6 +136,10 @@ services:
MINIO_USE_SSL: "false"
MINIO_BUCKET_CHAPTERS: "${MINIO_BUCKET_CHAPTERS:-libnovel-chapters}"
MINIO_BUCKET_AUDIO: "${MINIO_BUCKET_AUDIO:-libnovel-audio}"
# Public endpoint used to sign presigned audio URLs so browsers can reach them.
# Leave empty to use MINIO_ENDPOINT (fine for local dev).
MINIO_PUBLIC_ENDPOINT: "${MINIO_PUBLIC_ENDPOINT:-}"
MINIO_PUBLIC_USE_SSL: "${MINIO_PUBLIC_USE_SSL:-true}"
# PocketBase
POCKETBASE_URL: "http://pocketbase:8090"
POCKETBASE_ADMIN_EMAIL: "${POCKETBASE_ADMIN_EMAIL:-admin@libnovel.local}"

View File

@@ -99,9 +99,11 @@ func run(log *slog.Logger) error {
// ── Storage backends ────────────────────────────────────────────────────
minioCfg := storage.MinioConfig{
Endpoint: envOr("MINIO_ENDPOINT", "localhost:9000"),
PublicEndpoint: envOr("MINIO_PUBLIC_ENDPOINT", ""),
AccessKey: envOr("MINIO_ACCESS_KEY", "admin"),
SecretKey: envOr("MINIO_SECRET_KEY", "changeme123"),
UseSSL: strings.ToLower(os.Getenv("MINIO_USE_SSL")) == "true",
PublicUseSSL: strings.ToLower(os.Getenv("MINIO_PUBLIC_USE_SSL")) != "false",
BucketChapters: envOr("MINIO_BUCKET_CHAPTERS", "libnovel-chapters"),
BucketAudio: envOr("MINIO_BUCKET_AUDIO", "libnovel-audio"),
}

View File

@@ -14,10 +14,12 @@ import (
// MinioConfig holds connection parameters for MinIO.
type MinioConfig struct {
Endpoint string // e.g. "minio:9000"
Endpoint string // e.g. "minio:9000" — internal address used for all operations
PublicEndpoint string // e.g. "minio.kalekber.cc" — used to sign presigned URLs so browsers can reach them; leave empty to use Endpoint
AccessKey string
SecretKey string
UseSSL bool
PublicUseSSL bool // TLS for the public endpoint (usually true in prod)
BucketChapters string // e.g. "libnovel-chapters"
BucketAudio string // e.g. "libnovel-audio"
}
@@ -25,7 +27,8 @@ type MinioConfig struct {
// MinioClient wraps a minio.Client and exposes object operations for
// chapters and audio files.
type MinioClient struct {
c *minio.Client
c *minio.Client // internal client — used for all read/write operations
pub *minio.Client // public client — used only for generating presigned URLs
cfg MinioConfig
}
@@ -40,7 +43,21 @@ func NewMinioClient(ctx context.Context, cfg MinioConfig) (*MinioClient, error)
return nil, fmt.Errorf("minio: new client: %w", err)
}
mc := &MinioClient{c: c, cfg: cfg}
// Public client: signs presigned URLs with the public hostname so browsers
// can fetch them directly. Falls back to the internal client if no public
// endpoint is configured.
pub := c
if cfg.PublicEndpoint != "" && cfg.PublicEndpoint != cfg.Endpoint {
pub, err = minio.New(cfg.PublicEndpoint, &minio.Options{
Creds: credentials.NewStaticV4(cfg.AccessKey, cfg.SecretKey, ""),
Secure: cfg.PublicUseSSL,
})
if err != nil {
return nil, fmt.Errorf("minio: new public client: %w", err)
}
}
mc := &MinioClient{c: c, pub: pub, cfg: cfg}
for _, bucket := range []string{cfg.BucketChapters, cfg.BucketAudio} {
if err := mc.ensureBucket(ctx, bucket); err != nil {
return nil, err
@@ -168,9 +185,8 @@ func (m *MinioClient) AudioExists(ctx context.Context, key string) bool {
// ─── Presigned URLs ───────────────────────────────────────────────────────────
// PresignChapter returns a presigned GET URL for a chapter object, valid for
// the given duration. The URL is signed with the MinIO credentials and can be
// fetched directly by the browser without authentication.
// PresignChapter returns a presigned GET URL for a chapter object signed with
// the internal endpoint — intended for server-side fetches only.
func (m *MinioClient) PresignChapter(ctx context.Context, slug string, vol, n int, expires time.Duration) (string, error) {
key := chapterKey(slug, vol, n)
u, err := m.c.PresignedGetObject(ctx, m.cfg.BucketChapters, key, expires, nil)
@@ -180,9 +196,10 @@ func (m *MinioClient) PresignChapter(ctx context.Context, slug string, vol, n in
return u.String(), nil
}
// PresignAudio returns a presigned GET URL for an audio object.
// PresignAudio returns a presigned GET URL for an audio object signed with
// the public endpoint so the browser can fetch it directly.
func (m *MinioClient) PresignAudio(ctx context.Context, key string, expires time.Duration) (string, error) {
u, err := m.c.PresignedGetObject(ctx, m.cfg.BucketAudio, key, expires, nil)
u, err := m.pub.PresignedGetObject(ctx, m.cfg.BucketAudio, key, expires, nil)
if err != nil {
return "", fmt.Errorf("minio: presign audio %s: %w", key, err)
}

View File

@@ -90,5 +90,7 @@ export async function presignAudio(
}
const data = (await res.json()) as { url: string };
log.debug('minio', 'presign audio ok', { slug, n });
return rewriteHost(data.url);
// The scraper now signs audio URLs with the public endpoint directly,
// so no host rewrite is needed here.
return data.url;
}