fix: migrate Old Book → Old Books via Vercel API endpoint

- api/migrate-old-books.js: one-time migration endpoint with proper FBQ
  auth (token + admin login fallback); merges contents if both exist
- fbq-backfill: diagnostic logging on listDir (401 confirms FBQ_TOKEN
  has no read permission in edge fn context)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Krao Hasanee
2026-05-31 11:17:47 -04:00
parent 94d9c3904b
commit ab7a6b5a57
2 changed files with 165 additions and 22 deletions
+26 -22
View File
@@ -32,42 +32,46 @@ function mime(name: string) {
return m[e] ?? 'application/octet-stream';
}
async function listDir(path: string): Promise<{ name: string; type: string }[] | null> {
async function listDir(path: string, log?: string[]): Promise<{ name: string }[] | null> {
const r = await fetch(`${FBQ_URL}/api/resources?source=${SOURCE}&path=${encodeURIComponent(path)}`, { headers: fbqH });
if (log) log.push(`listDir ${path}${r.status}`);
if (!r.ok) return null;
const data = await r.json().catch(() => null);
return [...(data?.folders ?? []), ...(data?.files ?? [])];
const entries = Array.isArray(data?.entries) ? data.entries
: [...(data?.folders ?? []), ...(data?.files ?? [])];
return entries;
}
async function renameDir(fromPath: string, toPath: string) {
await fetch(`${FBQ_URL}/api/resources?source=${SOURCE}`, {
async function fbDelete(path: string): Promise<boolean> {
const r = await fetch(`${FBQ_URL}/api/resources?source=${SOURCE}&path=${encodeURIComponent(path)}`, { method: 'DELETE', headers: fbqH });
return r.ok;
}
async function fbRename(fromPath: string, toPath: string): Promise<boolean> {
const r = await fetch(`${FBQ_URL}/api/resources?source=${SOURCE}`, {
method: 'PATCH',
headers: { ...fbqH, 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'rename', items: [{ fromSource: SOURCE, fromPath, toSource: SOURCE, toPath }], overwrite: false }),
body: JSON.stringify({ action: 'rename', items: [{ fromSource: SOURCE, fromPath, toSource: SOURCE, toPath }], overwrite: true }),
});
return r.ok;
}
async function deleteDir(path: string) {
await fetch(`${FBQ_URL}/api/resources?source=${SOURCE}&path=${encodeURIComponent(path.endsWith('/') ? path : path + '/')}`, {
method: 'DELETE', headers: fbqH,
});
}
async function migrateOldBook(base: string) {
async function migrateOldBook(base: string, log: string[]): Promise<void> {
const src = `${base}/Old Book`;
const dst = `${base}/Old Books`;
const srcItems = await listDir(src);
if (srcItems === null) return; // Old Book doesn't exist
const dstItems = await listDir(dst);
if (dstItems === null) {
// Old Books doesn't exist — simple rename
await renameDir(src, dst);
const srcItems = await listDir(src, log);
if (srcItems === null) return;
if (srcItems.length === 0) {
// Empty — just delete it
const ok = await fbDelete(src);
log.push(`${ok ? 'migrated' : 'migrate-fail'} (delete empty): ${src}`);
} else {
// Both exist — move each item from Old Book into Old Books, then delete Old Book
// Has contents — move each item then delete
for (const item of srcItems) {
await renameDir(`${src}/${item.name}`, `${dst}/${item.name}`);
await fbRename(`${src}/${item.name}`, `${dst}/${item.name}`);
}
await deleteDir(src);
const ok = await fbDelete(src);
log.push(`${ok ? 'migrated' : 'migrate-fail'} (merge+delete): ${src}${dst}`);
}
}
@@ -130,7 +134,7 @@ Deno.serve(async () => {
const taskIsNew = await mkdir(base);
await Promise.all([mkdir(`${base}/Request Info`), mkdir(`${base}/Old Books`)]);
await migrateOldBook(base);
if (log.filter(l => l.startsWith('listDir')).length < 3) await migrateOldBook(base, log);
if (taskIsNew) { newTasks.push({ task, base, co, pr }); created++; }
else skipped++;