Mulimage 2.0 #1
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -183,16 +184,18 @@ type folderCountsRow struct {
|
||||
// `/photos?count=1000` per folder from the browser (≈1 MB JSON per
|
||||
// folder × N folders) to populate the left-sidebar tree. Moving the
|
||||
// fan-out into the sidecar keeps the same correctness profile — same
|
||||
// q-DSL, same `merged=false` UID dedupe, same 1000-row server cap —
|
||||
// but the wire payload back to the browser collapses to a single small
|
||||
// JSON object (`{path: count}`).
|
||||
// q-DSL, same `merged=false` UID dedupe — but the wire payload back
|
||||
// to the browser collapses to a single small JSON object
|
||||
// (`{path: count}`).
|
||||
//
|
||||
// We bounce off PhotoPrism with `count=1000` and dedupe UIDs server-
|
||||
// side rather than trusting a count header: PhotoPrism's `/photos`
|
||||
// X-Count is the *per-page* row count (per existing front-end
|
||||
// comment), not the total-match count, so we'd silently undercount if
|
||||
// we used it. Lifting the 1000 cap would mean either iterating offsets
|
||||
// or growing PhotoPrism's response cap — both out of scope here.
|
||||
// We bounce off PhotoPrism with paginated `count=1000` calls and dedupe
|
||||
// UIDs server-side rather than trusting a count header: PhotoPrism's
|
||||
// `/photos` X-Count is the *per-page* row count (per existing front-end
|
||||
// comment), not the total-match count, so we'd silently undercount any
|
||||
// folder with more than 1000 files. The loop walks offsets until PP
|
||||
// returns a short page, so the result is correct regardless of folder
|
||||
// size (until the wider fan-out becomes the bottleneck, which is many
|
||||
// orders of magnitude away on this hardware).
|
||||
//
|
||||
// Bounded concurrency caps the fan-out so a library with hundreds of
|
||||
// folders doesn't open hundreds of connections to PhotoPrism at once.
|
||||
@@ -245,31 +248,39 @@ func handleFolderCounts(pp *ppClient) gin.HandlerFunc {
|
||||
// UID — which is what the old client-side code did, and
|
||||
// what we keep doing here.
|
||||
q := url.QueryEscape(`path:"` + path + `*"`)
|
||||
resp, err := pp.call(c.Request.Context(), http.MethodGet,
|
||||
"/api/v1/photos?count=1000&offset=0&merged=false&q="+q, token, nil)
|
||||
if err != nil || !resp.OK {
|
||||
slog.Warn("folder.counts: pp call failed",
|
||||
"path", path,
|
||||
"err", err,
|
||||
"status", func() int {
|
||||
if resp != nil {
|
||||
return resp.Status
|
||||
}
|
||||
return 0
|
||||
}())
|
||||
return
|
||||
}
|
||||
var rows []folderCountsRow
|
||||
if err := json.Unmarshal(resp.Body, &rows); err != nil {
|
||||
slog.Warn("folder.counts: parse failed", "path", path, "err", err)
|
||||
return
|
||||
}
|
||||
seen := make(map[string]struct{}, len(rows))
|
||||
for _, r := range rows {
|
||||
if r.UID == "" {
|
||||
continue
|
||||
const pageSize = 1000
|
||||
seen := make(map[string]struct{})
|
||||
for offset := 0; ; offset += pageSize {
|
||||
resp, err := pp.call(c.Request.Context(), http.MethodGet,
|
||||
fmt.Sprintf("/api/v1/photos?count=%d&offset=%d&merged=false&q=%s", pageSize, offset, q),
|
||||
token, nil)
|
||||
if err != nil || !resp.OK {
|
||||
slog.Warn("folder.counts: pp call failed",
|
||||
"path", path,
|
||||
"offset", offset,
|
||||
"err", err,
|
||||
"status", func() int {
|
||||
if resp != nil {
|
||||
return resp.Status
|
||||
}
|
||||
return 0
|
||||
}())
|
||||
return
|
||||
}
|
||||
var rows []folderCountsRow
|
||||
if err := json.Unmarshal(resp.Body, &rows); err != nil {
|
||||
slog.Warn("folder.counts: parse failed", "path", path, "offset", offset, "err", err)
|
||||
return
|
||||
}
|
||||
for _, r := range rows {
|
||||
if r.UID == "" {
|
||||
continue
|
||||
}
|
||||
seen[r.UID] = struct{}{}
|
||||
}
|
||||
if len(rows) < pageSize {
|
||||
break
|
||||
}
|
||||
seen[r.UID] = struct{}{}
|
||||
}
|
||||
mu.Lock()
|
||||
counts[path] = len(seen)
|
||||
|
||||
@@ -143,7 +143,7 @@
|
||||
// BasePath shape and never fall back to the category-count.
|
||||
const labelsCountQuery = createQuery<number>(() => ({
|
||||
queryKey: ['photos', 'scoped-count', 'labels', userBasePath(), isAdminUser],
|
||||
queryFn: () => countPhotos(scoped('all:true label:*')),
|
||||
queryFn: () => countPhotos(scoped('label:*')),
|
||||
enabled: isAuthenticated(),
|
||||
staleTime: 60_000
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user