|
|
|
|
@@ -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)
|
|
|
|
|
}
|
|
|
|
|
|