Port collection templates to bundled poster storage
- Store bundled template posters in public S3 when available - Presign imported user collection posters in API responses - Thread frontend assets into router and collection handlers
This commit is contained in:
+3
-2
@@ -1357,15 +1357,16 @@ func main() {
|
||||
compatServer.SessionStore().DeleteByUserID(userID)
|
||||
}
|
||||
}
|
||||
router := api.NewRouter(deps)
|
||||
|
||||
// Step 8: Expose Prometheus metrics endpoint (not behind auth).
|
||||
distFS, fsErr := fs.Sub(siloweb.DistFS, "dist")
|
||||
if fsErr != nil {
|
||||
log.Fatalf("failed to create frontend FS: %v", fsErr)
|
||||
}
|
||||
deps.FrontendFS = distFS
|
||||
server.WebDistFS = distFS
|
||||
router := api.NewRouter(deps)
|
||||
|
||||
// Step 8: Expose Prometheus metrics endpoint (not behind auth).
|
||||
metricsMux := http.NewServeMux()
|
||||
metricsMux.Handle("/metrics", promhttp.Handler())
|
||||
metricsMux.Handle("/api/", router)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
@@ -18,10 +19,41 @@ import (
|
||||
const (
|
||||
adminCollectionImagePrefix = "collection-images"
|
||||
userCollectionImagePrefix = "user-collection-images"
|
||||
collectionTemplateImageDir = "/images/collection-templates/"
|
||||
|
||||
collectionImageMaxBytes = 10 << 20 // 10 MB
|
||||
)
|
||||
|
||||
// storeBundledCollectionPosterIfS3Configured stores a built-in collection
|
||||
// template poster in S3 when public asset storage is configured. Non-S3
|
||||
// installs and non-template paths keep the original persisted path.
|
||||
func storeBundledCollectionPosterIfS3Configured(
|
||||
ctx context.Context,
|
||||
s3GP *s3client.Client,
|
||||
frontendFS fs.FS,
|
||||
collectionID, prefix, posterPath string,
|
||||
) (storedPath, thumbhashStr string, stored bool, err error) {
|
||||
posterPath = strings.TrimSpace(posterPath)
|
||||
if s3GP == nil || !strings.HasPrefix(posterPath, collectionTemplateImageDir) {
|
||||
return posterPath, "", false, nil
|
||||
}
|
||||
if frontendFS == nil {
|
||||
return "", "", false, fmt.Errorf("frontend assets are not available")
|
||||
}
|
||||
|
||||
assetPath := strings.TrimPrefix(posterPath, "/")
|
||||
data, err := fs.ReadFile(frontendFS, assetPath)
|
||||
if err != nil {
|
||||
return "", "", false, fmt.Errorf("reading bundled poster %q: %w", posterPath, err)
|
||||
}
|
||||
|
||||
storedPath, thumbhashStr, err = uploadCollectionImageVariants(ctx, s3GP, prefix, collectionID, "poster", data)
|
||||
if err != nil {
|
||||
return "", "", false, err
|
||||
}
|
||||
return storedPath, thumbhashStr, true, nil
|
||||
}
|
||||
|
||||
// readCollectionImageMultipart reads a single image file from a multipart
|
||||
// request, validating MIME type and size.
|
||||
func readCollectionImageMultipart(r *http.Request, fieldName string) ([]byte, error) {
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/jpeg"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
"github.com/Silo-Server/silo-server/internal/s3client"
|
||||
)
|
||||
|
||||
type collectionArtworkS3Recorder struct {
|
||||
server *httptest.Server
|
||||
mu sync.Mutex
|
||||
puts []string
|
||||
}
|
||||
|
||||
func newCollectionArtworkS3Recorder(t *testing.T) *collectionArtworkS3Recorder {
|
||||
t.Helper()
|
||||
|
||||
recorder := &collectionArtworkS3Recorder{}
|
||||
recorder.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = io.Copy(io.Discard, r.Body)
|
||||
_ = r.Body.Close()
|
||||
if r.Method == http.MethodPut {
|
||||
recorder.mu.Lock()
|
||||
recorder.puts = append(recorder.puts, r.URL.Path)
|
||||
recorder.mu.Unlock()
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
t.Cleanup(recorder.server.Close)
|
||||
return recorder
|
||||
}
|
||||
|
||||
func (r *collectionArtworkS3Recorder) client() *s3client.Client {
|
||||
return s3client.NewClient(s3client.BucketConfig{
|
||||
Endpoint: r.server.URL,
|
||||
Region: "us-east-1",
|
||||
Bucket: "public-assets",
|
||||
AccessKey: "test",
|
||||
SecretKey: "test",
|
||||
PathStyle: true,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *collectionArtworkS3Recorder) putPaths() []string {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
out := make([]string, len(r.puts))
|
||||
copy(out, r.puts)
|
||||
return out
|
||||
}
|
||||
|
||||
func TestStoreBundledCollectionPosterIfS3Configured_NoS3KeepsPath(t *testing.T) {
|
||||
path := "/images/collection-templates/template.jpg"
|
||||
gotPath, gotThumbhash, stored, err := storeBundledCollectionPosterIfS3Configured(
|
||||
context.Background(),
|
||||
nil,
|
||||
fstest.MapFS{},
|
||||
"collection-1",
|
||||
adminCollectionImagePrefix,
|
||||
path,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("storeBundledCollectionPosterIfS3Configured: %v", err)
|
||||
}
|
||||
if stored {
|
||||
t.Fatal("stored = true, want false")
|
||||
}
|
||||
if gotPath != path {
|
||||
t.Fatalf("path = %q, want %q", gotPath, path)
|
||||
}
|
||||
if gotThumbhash != "" {
|
||||
t.Fatalf("thumbhash = %q, want empty", gotThumbhash)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreBundledCollectionPosterIfS3Configured_IgnoresNonTemplatePath(t *testing.T) {
|
||||
recorder := newCollectionArtworkS3Recorder(t)
|
||||
path := "collection-images/existing/poster/original.webp"
|
||||
|
||||
gotPath, gotThumbhash, stored, err := storeBundledCollectionPosterIfS3Configured(
|
||||
context.Background(),
|
||||
recorder.client(),
|
||||
fstest.MapFS{},
|
||||
"collection-1",
|
||||
adminCollectionImagePrefix,
|
||||
path,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("storeBundledCollectionPosterIfS3Configured: %v", err)
|
||||
}
|
||||
if stored {
|
||||
t.Fatal("stored = true, want false")
|
||||
}
|
||||
if gotPath != path {
|
||||
t.Fatalf("path = %q, want %q", gotPath, path)
|
||||
}
|
||||
if gotThumbhash != "" {
|
||||
t.Fatalf("thumbhash = %q, want empty", gotThumbhash)
|
||||
}
|
||||
if puts := recorder.putPaths(); len(puts) != 0 {
|
||||
t.Fatalf("PUT paths = %#v, want none", puts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreBundledCollectionPosterIfS3Configured_UploadsTemplatePoster(t *testing.T) {
|
||||
recorder := newCollectionArtworkS3Recorder(t)
|
||||
frontendFS := fstest.MapFS{
|
||||
"images/collection-templates/template.jpg": {
|
||||
Data: testCollectionPosterJPEG(t),
|
||||
},
|
||||
}
|
||||
|
||||
gotPath, gotThumbhash, stored, err := storeBundledCollectionPosterIfS3Configured(
|
||||
context.Background(),
|
||||
recorder.client(),
|
||||
frontendFS,
|
||||
"collection-1",
|
||||
adminCollectionImagePrefix,
|
||||
"/images/collection-templates/template.jpg",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("storeBundledCollectionPosterIfS3Configured: %v", err)
|
||||
}
|
||||
if !stored {
|
||||
t.Fatal("stored = false, want true")
|
||||
}
|
||||
if gotPath != "collection-images/collection-1/poster/original.webp" {
|
||||
t.Fatalf("path = %q", gotPath)
|
||||
}
|
||||
if gotThumbhash == "" {
|
||||
t.Fatal("thumbhash is empty")
|
||||
}
|
||||
|
||||
want := map[string]bool{
|
||||
"/public-assets/collection-images/collection-1/poster/original.webp": true,
|
||||
"/public-assets/collection-images/collection-1/poster/w500.webp": true,
|
||||
"/public-assets/collection-images/collection-1/poster/w300.webp": true,
|
||||
}
|
||||
puts := recorder.putPaths()
|
||||
if len(puts) != len(want) {
|
||||
t.Fatalf("PUT paths = %#v", puts)
|
||||
}
|
||||
for _, path := range puts {
|
||||
if !want[path] {
|
||||
t.Fatalf("unexpected PUT path %q in %#v", path, puts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testCollectionPosterJPEG(t *testing.T) []byte {
|
||||
t.Helper()
|
||||
|
||||
img := image.NewRGBA(image.Rect(0, 0, 32, 48))
|
||||
for y := 0; y < 48; y++ {
|
||||
for x := 0; x < 32; x++ {
|
||||
img.Set(x, y, color.RGBA{R: uint8(x * 6), G: uint8(y * 4), B: 120, A: 255})
|
||||
}
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := jpeg.Encode(&buf, img, &jpeg.Options{Quality: 90}); err != nil {
|
||||
t.Fatalf("encode jpeg: %v", err)
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
"slices"
|
||||
@@ -41,6 +42,7 @@ type LibraryCollectionHandler struct {
|
||||
presignTTL time.Duration
|
||||
httpClient *http.Client
|
||||
s3GP *s3client.Client
|
||||
FrontendFS fs.FS
|
||||
SectionRepo *sections.Repository
|
||||
UserCollectionPool *pgxpool.Pool
|
||||
GroupRepo *catalog.LibraryCollectionGroupRepository
|
||||
@@ -110,6 +112,39 @@ func (h *LibraryCollectionHandler) SetupCollage() {
|
||||
slog.Info("collage: auto-generation enabled")
|
||||
}
|
||||
|
||||
func (h *LibraryCollectionHandler) storeBundledTemplatePoster(
|
||||
ctx context.Context,
|
||||
collectionID, posterPath string,
|
||||
fromTemplate bool,
|
||||
) (bool, string, string, error) {
|
||||
storedPath, thumbhash, stored, err := storeBundledCollectionPosterIfS3Configured(
|
||||
ctx,
|
||||
h.s3GP,
|
||||
h.FrontendFS,
|
||||
collectionID,
|
||||
adminCollectionImagePrefix,
|
||||
posterPath,
|
||||
)
|
||||
if err != nil || !stored {
|
||||
return false, storedPath, thumbhash, err
|
||||
}
|
||||
|
||||
notAutoGenerated := false
|
||||
input := catalog.UpdateLibraryCollectionInput{
|
||||
ID: collectionID,
|
||||
PosterURL: &storedPath,
|
||||
PosterThumbhash: &thumbhash,
|
||||
PosterAutoGenerated: ¬AutoGenerated,
|
||||
}
|
||||
if fromTemplate {
|
||||
input.PosterFromTemplate = &fromTemplate
|
||||
}
|
||||
if err := h.repo.Update(ctx, input); err != nil {
|
||||
return false, "", "", fmt.Errorf("updating bundled poster path: %w", err)
|
||||
}
|
||||
return true, storedPath, thumbhash, nil
|
||||
}
|
||||
|
||||
// GenerateCollectionPoster implements catalog.CollageGenerator.
|
||||
// It fetches item poster images, composes a collage, and stores the result.
|
||||
func (h *LibraryCollectionHandler) GenerateCollectionPoster(ctx context.Context, collectionID string) error {
|
||||
@@ -1107,11 +1142,16 @@ func (h *LibraryCollectionHandler) HandleCreateAdminCollection(w http.ResponseWr
|
||||
return
|
||||
}
|
||||
|
||||
posterStored, _, _, err := h.storeBundledTemplatePoster(r.Context(), collection.ID, req.PosterURL, false)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to process template poster")
|
||||
return
|
||||
}
|
||||
if err := h.processArtworkInputs(r, collection.ID, req.PosterSourceURL, req.BackdropSourceURL); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to process uploaded images")
|
||||
return
|
||||
}
|
||||
if r.MultipartForm != nil || strings.TrimSpace(req.PosterSourceURL) != "" || strings.TrimSpace(req.BackdropSourceURL) != "" {
|
||||
if posterStored || r.MultipartForm != nil || strings.TrimSpace(req.PosterSourceURL) != "" || strings.TrimSpace(req.BackdropSourceURL) != "" {
|
||||
collection, err = h.repo.GetByID(r.Context(), collection.ID)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to load collection")
|
||||
@@ -1818,6 +1858,18 @@ func (h *LibraryCollectionHandler) ensureTemplatePoster(
|
||||
if collection.PosterURL == posterPath && collection.PosterFromTemplate {
|
||||
return
|
||||
}
|
||||
if stored, _, _, err := h.storeBundledTemplatePoster(ctx, collection.ID, posterPath, true); err != nil {
|
||||
slog.Warn("failed to store template poster",
|
||||
"collection_id", collection.ID,
|
||||
"template_id", tmpl.ID,
|
||||
"poster_path", posterPath,
|
||||
"error", err,
|
||||
)
|
||||
return
|
||||
} else if stored {
|
||||
return
|
||||
}
|
||||
|
||||
emptyThumbhash := ""
|
||||
notAutoGenerated := false
|
||||
fromTemplate := true
|
||||
@@ -2405,6 +2457,16 @@ func (h *LibraryCollectionHandler) createMDBListCollection(
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating collection: %w", err)
|
||||
}
|
||||
if stored, path, thumbhash, err := h.storeBundledTemplatePoster(ctx, collection.ID, req.PosterURL, req.PosterFromTemplate); err != nil {
|
||||
return nil, err
|
||||
} else if stored {
|
||||
collection.PosterURL = path
|
||||
collection.PosterThumbhash = thumbhash
|
||||
collection.PosterAutoGenerated = false
|
||||
if req.PosterFromTemplate {
|
||||
collection.PosterFromTemplate = true
|
||||
}
|
||||
}
|
||||
return collection, nil
|
||||
}
|
||||
|
||||
@@ -2455,6 +2517,16 @@ func (h *LibraryCollectionHandler) createTMDBCollection(
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating collection: %w", err)
|
||||
}
|
||||
if stored, path, thumbhash, err := h.storeBundledTemplatePoster(ctx, collection.ID, req.PosterURL, req.PosterFromTemplate); err != nil {
|
||||
return nil, err
|
||||
} else if stored {
|
||||
collection.PosterURL = path
|
||||
collection.PosterThumbhash = thumbhash
|
||||
collection.PosterAutoGenerated = false
|
||||
if req.PosterFromTemplate {
|
||||
collection.PosterFromTemplate = true
|
||||
}
|
||||
}
|
||||
return collection, nil
|
||||
}
|
||||
|
||||
@@ -2514,6 +2586,16 @@ func (h *LibraryCollectionHandler) createTMDBFranchiseCollection(
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating collection: %w", err)
|
||||
}
|
||||
if stored, path, thumbhash, err := h.storeBundledTemplatePoster(ctx, collection.ID, req.PosterURL, req.PosterFromTemplate); err != nil {
|
||||
return nil, err
|
||||
} else if stored {
|
||||
collection.PosterURL = path
|
||||
collection.PosterThumbhash = thumbhash
|
||||
collection.PosterAutoGenerated = false
|
||||
if req.PosterFromTemplate {
|
||||
collection.PosterFromTemplate = true
|
||||
}
|
||||
}
|
||||
return collection, nil
|
||||
}
|
||||
|
||||
@@ -2567,6 +2649,16 @@ func (h *LibraryCollectionHandler) createTMDBDiscoverCollection(
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating collection: %w", err)
|
||||
}
|
||||
if stored, path, thumbhash, err := h.storeBundledTemplatePoster(ctx, collection.ID, req.PosterURL, req.PosterFromTemplate); err != nil {
|
||||
return nil, err
|
||||
} else if stored {
|
||||
collection.PosterURL = path
|
||||
collection.PosterThumbhash = thumbhash
|
||||
collection.PosterAutoGenerated = false
|
||||
if req.PosterFromTemplate {
|
||||
collection.PosterFromTemplate = true
|
||||
}
|
||||
}
|
||||
return collection, nil
|
||||
}
|
||||
|
||||
@@ -2758,6 +2850,10 @@ func (h *LibraryCollectionHandler) HandleImportTraktCollection(w http.ResponseWr
|
||||
return
|
||||
}
|
||||
|
||||
if _, _, _, err := h.storeBundledTemplatePoster(r.Context(), collection.ID, req.PosterURL, false); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to process template poster")
|
||||
return
|
||||
}
|
||||
if err := h.processArtworkInputs(r, collection.ID, req.PosterSourceURL, req.BackdropSourceURL); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to process uploaded images")
|
||||
return
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -13,6 +15,7 @@ import (
|
||||
apimw "github.com/Silo-Server/silo-server/internal/api/middleware"
|
||||
"github.com/Silo-Server/silo-server/internal/collections/templates"
|
||||
"github.com/Silo-Server/silo-server/internal/mdblist"
|
||||
"github.com/Silo-Server/silo-server/internal/s3client"
|
||||
"github.com/Silo-Server/silo-server/internal/usercollections"
|
||||
"github.com/Silo-Server/silo-server/internal/userstore"
|
||||
)
|
||||
@@ -26,6 +29,9 @@ type UserCollectionImportHandler struct {
|
||||
scheduler *usercollections.Scheduler
|
||||
registry *templates.Registry
|
||||
mdblist *mdblist.Client
|
||||
s3GP *s3client.Client
|
||||
frontendFS fs.FS
|
||||
presignTTL time.Duration
|
||||
}
|
||||
|
||||
func NewUserCollectionImportHandler(
|
||||
@@ -34,6 +40,9 @@ func NewUserCollectionImportHandler(
|
||||
scheduler *usercollections.Scheduler,
|
||||
registry *templates.Registry,
|
||||
mdblistClient *mdblist.Client,
|
||||
s3GP *s3client.Client,
|
||||
frontendFS fs.FS,
|
||||
presignTTL time.Duration,
|
||||
) *UserCollectionImportHandler {
|
||||
if registry == nil {
|
||||
registry = templates.Default
|
||||
@@ -44,6 +53,9 @@ func NewUserCollectionImportHandler(
|
||||
scheduler: scheduler,
|
||||
registry: registry,
|
||||
mdblist: mdblistClient,
|
||||
s3GP: s3GP,
|
||||
frontendFS: frontendFS,
|
||||
presignTTL: presignTTL,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,6 +225,11 @@ func (h *UserCollectionImportHandler) createImportedCollection(
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.storeBundledTemplatePoster(r, store, collection, strings.TrimSpace(shared.PosterURL)); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to process template poster")
|
||||
return
|
||||
}
|
||||
|
||||
syncResult, updated, syncErr := h.sync.RunSync(r.Context(), store, collection)
|
||||
if syncErr != nil {
|
||||
// Persist failure state inline so the UI shows the error and the user
|
||||
@@ -230,11 +247,75 @@ func (h *UserCollectionImportHandler) createImportedCollection(
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusCreated, userImportResponse{
|
||||
Collection: toCollectionResponse(*updated),
|
||||
Collection: h.toCollectionResponse(r, *updated),
|
||||
Sync: syncResult,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *UserCollectionImportHandler) storeBundledTemplatePoster(
|
||||
r *http.Request,
|
||||
store userstore.UserStore,
|
||||
collection *userstore.Collection,
|
||||
posterPath string,
|
||||
) error {
|
||||
if collection == nil {
|
||||
return nil
|
||||
}
|
||||
storedPath, thumbhash, stored, err := storeBundledCollectionPosterIfS3Configured(
|
||||
r.Context(),
|
||||
h.s3GP,
|
||||
h.frontendFS,
|
||||
collection.ID,
|
||||
userCollectionImagePrefix,
|
||||
posterPath,
|
||||
)
|
||||
if err != nil || !stored {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := store.UpdateCollection(r.Context(), userstore.UpdateCollectionInput{
|
||||
ID: collection.ID,
|
||||
RequestProfileID: collection.CreatorProfileID,
|
||||
PosterURL: &storedPath,
|
||||
PosterThumbhash: &thumbhash,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("persisting template poster: %w", err)
|
||||
}
|
||||
collection.PosterURL = storedPath
|
||||
collection.PosterThumbhash = thumbhash
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *UserCollectionImportHandler) toCollectionResponse(r *http.Request, c userstore.Collection) collectionResponse {
|
||||
resp := toCollectionResponse(c)
|
||||
resp.PosterURL = h.presignCollectionPoster(r.Context(), c.PosterURL)
|
||||
return resp
|
||||
}
|
||||
|
||||
func (h *UserCollectionImportHandler) presignCollectionPoster(ctx context.Context, path string) string {
|
||||
if path == "" {
|
||||
return ""
|
||||
}
|
||||
if strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://") {
|
||||
return path
|
||||
}
|
||||
if strings.HasPrefix(path, "/") {
|
||||
return path
|
||||
}
|
||||
if h.s3GP == nil {
|
||||
return ""
|
||||
}
|
||||
ttl := h.presignTTL
|
||||
if ttl <= 0 {
|
||||
ttl = 4 * time.Hour
|
||||
}
|
||||
url, err := h.s3GP.PresignGetURL(ctx, h.s3GP.Bucket(), cardThumbnailPath(path), ttl)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
func (h *UserCollectionImportHandler) HandleSync(w http.ResponseWriter, r *http.Request) {
|
||||
collectionID := chi.URLParam(r, "id")
|
||||
if collectionID == "" {
|
||||
|
||||
@@ -4,6 +4,7 @@ package api
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@@ -69,6 +70,7 @@ type Dependencies struct {
|
||||
BootstrapSensitiveValues map[string]string
|
||||
AppContext context.Context
|
||||
DB *pgxpool.Pool
|
||||
FrontendFS fs.FS
|
||||
S3Public *s3client.Client // public assets bucket client (may be nil)
|
||||
S3Private *s3client.Client // private internal bucket client (may be nil)
|
||||
S3UserDB *s3client.Client // user-db bucket client (may be nil)
|
||||
@@ -843,6 +845,7 @@ func NewRouter(deps Dependencies) chi.Router {
|
||||
nil,
|
||||
deps.S3Public,
|
||||
)
|
||||
libraryCollectionHandler.FrontendFS = deps.FrontendFS
|
||||
libraryCollectionHandler.Executor = &catalog.QueryExecutor{Pool: deps.DB}
|
||||
libraryCollectionHandler.SectionRepo = sectionRepo
|
||||
libraryCollectionHandler.UserCollectionPool = deps.DB
|
||||
@@ -1298,6 +1301,9 @@ func NewRouter(deps Dependencies) chi.Router {
|
||||
deps.UserCollectionScheduler,
|
||||
nil,
|
||||
deps.MDBListClient,
|
||||
deps.S3Public,
|
||||
deps.FrontendFS,
|
||||
4*time.Hour,
|
||||
)
|
||||
}
|
||||
r.Route("/collections", func(r chi.Router) {
|
||||
|
||||
Reference in New Issue
Block a user