mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 11:16:37 +08:00
96 lines
3.2 KiB
Go
96 lines
3.2 KiB
Go
// Package service — image proxy.
|
||
//
|
||
// Some deployments cannot reach image.tmdb.org directly (GFW, internal-only
|
||
// networks). ImageProxy fronts a remote image URL so the browser only ever
|
||
// talks to the MMTL origin. The proxy:
|
||
//
|
||
// - validates the URL scheme is http/https,
|
||
// - streams bytes through with a small disk cache under cache/images,
|
||
// - falls back to a transparent 1×1 PNG on upstream failure so the UI
|
||
// never breaks layout,
|
||
// - honors HTTP(S)_PROXY environment variables so users behind GFW can
|
||
// route image fetches through their proxy.
|
||
package service
|
||
|
||
import (
|
||
"net/http"
|
||
"path/filepath"
|
||
"sync"
|
||
"time"
|
||
|
||
"go.uber.org/zap"
|
||
|
||
"github.com/ShukeBta/MMTL/internal/config"
|
||
)
|
||
|
||
// ImageProxy fetches and caches remote images on behalf of the browser.
|
||
type ImageProxy struct {
|
||
cfg *config.Config
|
||
log *zap.Logger
|
||
client *http.Client
|
||
cacheDir string
|
||
mu sync.Mutex
|
||
|
||
// libraryRootsFn returns the configured media library roots so that
|
||
// sidecar poster/artwork files stored alongside media (under arbitrary
|
||
// per-library paths) are allowed by isAllowedLocalPath. It is provided
|
||
// by the service container after construction and may be nil in tests.
|
||
libraryRootsFn func() []string
|
||
libRootsMu sync.Mutex
|
||
libRootsCache []string
|
||
libRootsAt time.Time
|
||
}
|
||
|
||
const (
|
||
imageBrowserCacheControl = "public, max-age=2592000, immutable"
|
||
imagePlaceholderCacheControl = "no-store"
|
||
)
|
||
|
||
// NewImageProxy is the constructor.
|
||
func NewImageProxy(cfg *config.Config, log *zap.Logger) *ImageProxy {
|
||
// Honor HTTP(S)_PROXY env vars so deployments behind GFW can pull
|
||
// from image.tmdb.org via their HTTP proxy without extra config. On
|
||
// Windows we also honor the current user's system proxy settings.
|
||
transport := NewExternalTransport()
|
||
return &ImageProxy{
|
||
cfg: cfg,
|
||
log: log,
|
||
cacheDir: filepath.Join(cfg.Cache.CacheDir, "images"),
|
||
client: &http.Client{Timeout: 30 * time.Second, Transport: transport},
|
||
}
|
||
}
|
||
|
||
// SetLibraryRootsProvider injects a callback that returns the current set of
|
||
// media library root directories. Sidecar posters live under these roots
|
||
// (which are arbitrary, user-defined, and not necessarily under the
|
||
// configured movies/tv/anime dirs), so they must be treated as allowed
|
||
// local-image locations.
|
||
func (p *ImageProxy) SetLibraryRootsProvider(fn func() []string) {
|
||
p.libraryRootsFn = fn
|
||
}
|
||
|
||
// libraryRoots returns the cached library roots, refreshing at most every
|
||
// 30 seconds to avoid a DB hit per image request (posters load in bulk).
|
||
func (p *ImageProxy) libraryRoots() []string {
|
||
if p.libraryRootsFn == nil {
|
||
return nil
|
||
}
|
||
p.libRootsMu.Lock()
|
||
defer p.libRootsMu.Unlock()
|
||
if p.libRootsCache != nil && time.Since(p.libRootsAt) < 30*time.Second {
|
||
return p.libRootsCache
|
||
}
|
||
p.libRootsCache = p.libraryRootsFn()
|
||
p.libRootsAt = time.Now()
|
||
return p.libRootsCache
|
||
}
|
||
|
||
// Prune removes oldest cached images until disk usage is within the configured limit.
|
||
func (p *ImageProxy) Prune() (PruneImageCacheResult, error) {
|
||
if p.cfg == nil || p.cfg.Cache.ImagesMaxSizeMB <= 0 {
|
||
return PruneImageCacheResult{}, nil
|
||
}
|
||
maxBytes := int64(p.cfg.Cache.ImagesMaxSizeMB) * 1024 * 1024
|
||
return PruneImageCache(p.cacheDir, maxBytes)
|
||
}
|