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