Skip to content

Commit 2e3e512

Browse files
committed
admin: paginate episodes/ list — R2 caps at 1000 per call
Cloudflare R2 list() returns at most 1000 objects per call. The episodes/ prefix has 2609 objects (172 MP3s + companion files), so my previous single-call listing only saw 1000 of them. Most of the published basenames I was supposed to be filtering against weren't in publishedBasenames, so the stale-orphan filter only caught a fraction of the orphans. Result: the visible draft count stayed inflated even after v2026.05.05.4. Fix by paginating with the cursor returned when result.truncated is true. Capped at 20 iterations as a safety net against an infinite loop. Single Promise.all keeps the parallelism win. Bump release to admin-v2026.05.05.5. Generated-by: Claude AI Signed-off-by: Luis Chamberlain <mcgrof@kernel.org>
1 parent 47828f9 commit 2e3e512

2 files changed

Lines changed: 29 additions & 12 deletions

File tree

admin/src/release.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export const ADMIN_RELEASE_TAG = 'admin-v2026.05.05.4';
1+
export const ADMIN_RELEASE_TAG = 'admin-v2026.05.05.5';

admin/src/worker.js

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3915,20 +3915,37 @@ async function getDrafts(env) {
39153915
// Note: published episodes live at episodes/{filename}.mp3 in
39163916
// the public R2 bucket, NOT at public/. The public/ prefix is
39173917
// empty for MP3s — episode audio always uses episodes/.
3918-
const [list, publishJobs, episodesList] = await Promise.all([
3918+
// R2 list() returns a maximum of 1000 objects per call. Our
3919+
// episodes/ prefix has 2600+ objects, so a single list() call
3920+
// misses most of them. We paginate via the `cursor` returned
3921+
// when the result is truncated. This is essential for the
3922+
// stale-draft filter to work correctly — without it, drafts
3923+
// whose published episode was uploaded later in the bucket
3924+
// listing get missed and render as fake pending drafts.
3925+
async function listAllMp3Basenames(prefix) {
3926+
const out = new Set();
3927+
let cursor = undefined;
3928+
// Cap iterations so a misbehaving listing can't loop forever.
3929+
for (let i = 0; i < 20; i++) {
3930+
const opts = cursor ? { prefix, cursor } : { prefix };
3931+
const page = await env.PODCAST_BUCKET.list(opts);
3932+
for (const obj of page.objects || []) {
3933+
if (!obj.key.endsWith('.mp3')) continue;
3934+
const base = obj.key.split('/').pop().replace(/\.mp3$/, '');
3935+
if (base) out.add(base);
3936+
}
3937+
if (!page.truncated) break;
3938+
cursor = page.cursor;
3939+
if (!cursor) break;
3940+
}
3941+
return out;
3942+
}
3943+
3944+
const [list, publishJobs, publishedBasenames] = await Promise.all([
39193945
env.PODCAST_BUCKET.list({ prefix: 'drafts/' }),
39203946
listPublishJobs(env),
3921-
env.PODCAST_BUCKET.list({ prefix: 'episodes/' }),
3947+
listAllMp3Basenames('episodes/'),
39223948
]);
3923-
// Build a set of basenames that already exist under episodes/.
3924-
// If a drafts/ MP3 has the same basename as an episodes/ file,
3925-
// the episode is already live and the draft is stale.
3926-
const publishedBasenames = new Set();
3927-
for (const obj of (episodesList.objects || [])) {
3928-
if (!obj.key.endsWith('.mp3')) continue;
3929-
const base = obj.key.split('/').pop().replace(/\.mp3$/, '');
3930-
if (base) publishedBasenames.add(base);
3931-
}
39323949
const latestPublishJobByDraft = new Map();
39333950
for (const job of publishJobs) {
39343951
if (!job?.draft_key) continue;

0 commit comments

Comments
 (0)