diff --git a/src/api.js b/src/api.js index 1ddeac9..56b1950 100644 --- a/src/api.js +++ b/src/api.js @@ -21,7 +21,7 @@ const s3 = new S3Client({ }, }); -/** Helpers **/ +/* ---------------- helpers ---------------- */ const toListed = (folderKey) => (o) => { const d = o.LastModified ?? new Date(0); @@ -49,34 +49,40 @@ const byNewest = (a, b) => b.sort - a.sort; const sidecarsFor = (key) => [key + ".sha256", key + ".sig"]; const parseName = (fname) => { - // Expected pattern: parts[3] = platform (windows|darwin|linux), parts[4] = arch + // Expected convention: parts[3] = platform (windows|darwin|linux), parts[4] = arch const parts = fname.split("_"); - const platform = parts[3] ?? ""; + const platform = (parts[3] ?? "").toLowerCase(); const arch = parts[4] ?? ""; const groupId = `${parts[0] ?? ""}_${platform}_${arch}`; return { parts, platform, arch, groupId }; }; -const windowsCompanionFromTar = (tarKey) => { - // .../name.tar.gz -> .../name_setup.exe - const base = tarKey.slice(0, -TAR_EXT.length); - return `${base}_setup.exe`; +// Remove sidecar suffixes first, then packaging suffix to compute a common base across variants +const stripSidecar = (name) => { + if (name.endsWith(".sha256")) return name.slice(0, -".sha256".length); + if (name.endsWith(".sig")) return name.slice(0, -".sig".length); + return name; }; -const darwinCompanionFromTar = (tarKey) => { - // .../name.tar.gz -> .../name.dmg - const base = tarKey.slice(0, -TAR_EXT.length); - return `${base}.dmg`; +const stripPackaging = (name) => { + if (name.endsWith(TAR_EXT)) return name.slice(0, -TAR_EXT.length); // correct length + if (name.endsWith("_setup.exe")) return name.slice(0, -"_setup.exe".length); + if (name.endsWith(".dmg")) return name.slice(0, -".dmg".length); + return name; }; -/** Public API **/ +const baseStem = (name) => stripPackaging(stripSidecar(name)); + +const isTar = (name) => name.endsWith(TAR_EXT); +const isDmg = (name) => name.endsWith(".dmg"); +const isSetup = (name) => name.endsWith("_setup.exe"); + +/* ---------------- public API ---------------- */ const cleanOldItems = async () => { const keys = Array.from(oldKeys); console.log(`cleaning|count|${keys.length}`); - if (keys.length === 0) { - return; - } + if (keys.length === 0) return; const deletes = keys.map((Key) => s3 @@ -84,7 +90,6 @@ const cleanOldItems = async () => { .then(() => console.log(`cleaning|key|${Key}`)) .catch((err) => console.error(`cleaning|error|${Key}`, err)), ); - await Promise.allSettled(deletes); oldKeys.clear(); }; @@ -115,86 +120,117 @@ const getBucketFiles = async (folderName) => { ); const contents = (data.Contents ?? []).filter((it) => it.Key !== folderKey); - - // Normalize listing const listed = contents.map(toListed(folderKey)).sort(byNewest); - // Consider only .tar.gz for retention policy - const tarItems = listed.filter((i) => i.name.endsWith(TAR_EXT)); - - // Nightly policy: keep top 3 per (name_platform_arch); mark others + companions for deletion - const keepSet = new Set(); - if (folder === "nightly") { - const groupCounts = {}; - for (const t of tarItems) { - const { groupId, platform } = parseName(t.name); - groupCounts[groupId] = (groupCounts[groupId] ?? 0) + 1; - const count = groupCounts[groupId]; - - if (count <= 3) { - keepSet.add(t.key); - } else { - [t.key, ...sidecarsFor(t.key)].forEach((k) => oldKeys.add(k)); - - if (platform === "windows") { - const setupKey = windowsCompanionFromTar(t.key); - [setupKey, ...sidecarsFor(setupKey)].forEach((k) => oldKeys.add(k)); - } else if (platform === "darwin") { - const dmgKey = darwinCompanionFromTar(t.key); - [dmgKey, ...sidecarsFor(dmgKey)].forEach((k) => oldKeys.add(k)); - } - } + // Build indexes + const byKey = Object.fromEntries(listed.map((i) => [i.key, i])); + const byBase = new Map(); // base -> { tar, dmg, setup, sidecars:Set } + for (const it of listed) { + const base = baseStem(it.name); + let g = byBase.get(base); + if (!g) { + g = { tar: null, dmg: null, setup: null, sidecars: new Set() }; + byBase.set(base, g); + } + if (isTar(it.name)) { + g.tar = it; + } else if (isDmg(it.name)) { + g.dmg = it; + } else if (isSetup(it.name)) { + g.setup = it; + } else if (it.name.endsWith(".sha256") || it.name.endsWith(".sig")) { + g.sidecars.add(it.key); } - } else { - tarItems.forEach((t) => keepSet.add(t.key)); } - // Build final output list (tar + sidecars + optional platform companion + its sidecars) - const byKey = Object.fromEntries(listed.map((i) => [i.key, i])); + const groups = Array.from(byBase.values()); + + // Nightly retention: keep newest 3 per (name_platform_arch). + const keepBaseSet = new Set(); + if (folder === "nightly") { + const buckets = new Map(); // groupId -> array of group objects + for (const g of groups) { + const rep = g.tar ?? g.dmg ?? g.setup; + if (!rep) continue; + const { groupId } = parseName(rep.name); + if (!buckets.has(groupId)) buckets.set(groupId, []); + buckets.get(groupId).push(g); + } + + for (const [, arr] of buckets) { + arr.sort((a, b) => { + const ta = (a.tar ?? a.dmg ?? a.setup)?.sort ?? 0; + const tb = (b.tar ?? b.dmg ?? b.setup)?.sort ?? 0; + return tb - ta; + }); + + arr.forEach((g, idx) => { + const rep = g.tar ?? g.dmg ?? g.setup; + if (!rep) return; + const base = baseStem(rep.name); + if (idx < 3) { + keepBaseSet.add(base); + } else { + // mark all artifacts in the group as old + if (g.tar) + [g.tar.key, ...sidecarsFor(g.tar.key)].forEach((k) => + oldKeys.add(k), + ); + if (g.dmg) + [g.dmg.key, ...sidecarsFor(g.dmg.key)].forEach((k) => + oldKeys.add(k), + ); + if (g.setup) + [g.setup.key, ...sidecarsFor(g.setup.key)].forEach((k) => + oldKeys.add(k), + ); + } + }); + } + } else { + // keep all groups in non-nightly + for (const g of groups) { + const rep = g.tar ?? g.dmg ?? g.setup; + if (rep) keepBaseSet.add(baseStem(rep.name)); + } + } + + // Emit kept groups in newest-first order, using representative timestamp + const kept = groups + .filter((g) => { + const rep = g.tar ?? g.dmg ?? g.setup; + return rep && keepBaseSet.has(baseStem(rep.name)); + }) + .sort((a, b) => { + const ta = (a.tar ?? a.dmg ?? a.setup)?.sort ?? 0; + const tb = (b.tar ?? b.dmg ?? b.setup)?.sort ?? 0; + return tb - ta; + }); + const out = []; - - for (const t of tarItems) { - if (!keepSet.has(t.key)) { - continue; - } - - // tar.gz - out.push(t); - - // tar sidecars - for (const sk of sidecarsFor(t.key)) { - const it = byKey[sk]; - if (it) { - out.push(it); + for (const g of kept) { + // tar + sidecars + if (g.tar) { + out.push(g.tar); + for (const sk of sidecarsFor(g.tar.key)) { + const it = byKey[sk]; + if (it) out.push(it); } } - - // platform companions - const { platform } = parseName(t.name); - - if (platform === "windows") { - const setupKey = windowsCompanionFromTar(t.key); - const setupItem = byKey[setupKey]; - if (setupItem) { - out.push(setupItem); - for (const sk of sidecarsFor(setupKey)) { - const it = byKey[sk]; - if (it) { - out.push(it); - } - } + // dmg + sidecars + if (g.dmg) { + out.push(g.dmg); + for (const sk of sidecarsFor(g.dmg.key)) { + const it = byKey[sk]; + if (it) out.push(it); } - } else if (platform === "darwin") { - const dmgKey = darwinCompanionFromTar(t.key); - const dmgItem = byKey[dmgKey]; - if (dmgItem) { - out.push(dmgItem); - for (const sk of sidecarsFor(dmgKey)) { - const it = byKey[sk]; - if (it) { - out.push(it); - } - } + } + // setup + sidecars + if (g.setup) { + out.push(g.setup); + for (const sk of sidecarsFor(g.setup.key)) { + const it = byKey[sk]; + if (it) out.push(it); } } }