Session 2026-05-29: profile layout 2-col, file browser copy/paste, fbq-proxy/backfill fns, migrations, UI updates
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
|
||||
|
||||
const FBQ_URL = Deno.env.get('FBQ_URL') ?? '';
|
||||
const FBQ_TOKEN = Deno.env.get('FBQ_TOKEN') ?? '';
|
||||
const SUPABASE_URL = Deno.env.get('SUPABASE_URL') ?? '';
|
||||
const SUPABASE_SERVICE_KEY = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? '';
|
||||
const SOURCE = 'srv';
|
||||
const fbqH = { Authorization: `Bearer ${FBQ_TOKEN}` };
|
||||
|
||||
function safe(v: string) {
|
||||
return String(v||'').trim().replace(/[\\/:*?"<>|#%{}^~[\]`]+/g,'-').replace(/\s+/g,' ').replace(/^-+|-+$/g,'');
|
||||
}
|
||||
|
||||
// 200 = created new, 409 = already existed
|
||||
async function mkdir(path: string): Promise<boolean> {
|
||||
const p = path.endsWith('/') ? path : path + '/';
|
||||
const r = await fetch(`${FBQ_URL}/api/resources?source=${SOURCE}&path=${encodeURIComponent(p)}&isDir=true`, {
|
||||
method: 'POST', headers: fbqH,
|
||||
});
|
||||
return r.status === 200;
|
||||
}
|
||||
|
||||
async function upload(path: string, data: ArrayBuffer, ct: string) {
|
||||
await fetch(`${FBQ_URL}/api/resources?source=${SOURCE}&path=${encodeURIComponent(path)}`, {
|
||||
method: 'POST', headers: { ...fbqH, 'Content-Type': ct }, body: data,
|
||||
});
|
||||
}
|
||||
|
||||
function mime(name: string) {
|
||||
const e = name.split('.').pop()?.toLowerCase() ?? '';
|
||||
const m: Record<string,string> = { pdf:'application/pdf', jpg:'image/jpeg', jpeg:'image/jpeg', png:'image/png', gif:'image/gif', webp:'image/webp', svg:'image/svg+xml', ai:'application/postscript', eps:'application/postscript', zip:'application/zip', mp4:'video/mp4', mov:'video/quicktime' };
|
||||
return m[e] ?? 'application/octet-stream';
|
||||
}
|
||||
|
||||
async function chunkAll<T>(items: T[], fn: (item: T) => Promise<void>, size = 8) {
|
||||
for (let i = 0; i < items.length; i += size) {
|
||||
await Promise.all(items.slice(i, i + size).map(fn));
|
||||
}
|
||||
}
|
||||
|
||||
Deno.serve(async () => {
|
||||
const sb = createClient(SUPABASE_URL, SUPABASE_SERVICE_KEY);
|
||||
const log: string[] = [];
|
||||
let created = 0, skipped = 0, fileCount = 0, errors = 0;
|
||||
|
||||
// fetch all data upfront in parallel
|
||||
const [usersRes, companiesRes, tasksRes] = await Promise.all([
|
||||
sb.from('profiles').select('name,role').in('role',['team','external']),
|
||||
sb.from('companies').select('name'),
|
||||
sb.from('tasks').select('id,title,projects!project_id(name,companies!company_id(name))'),
|
||||
]);
|
||||
|
||||
log.push(`db: ${usersRes.data?.length} users, ${companiesRes.data?.length} companies, ${tasksRes.data?.length ?? 0} tasks`);
|
||||
if (tasksRes.error) log.push(`task err: ${tasksRes.error.message}`);
|
||||
|
||||
// 1. user folders in parallel
|
||||
await mkdir('/Team');
|
||||
await chunkAll(usersRes.data ?? [], async (u) => {
|
||||
if (!u.name) return;
|
||||
const isNew = await mkdir(`/Team/${safe(u.name)}`);
|
||||
if (isNew) { log.push(`+ /Team/${safe(u.name)}`); created++; } else skipped++;
|
||||
});
|
||||
|
||||
// 2. company folders in parallel
|
||||
await mkdir('/Clients');
|
||||
await chunkAll(companiesRes.data ?? [], async (c) => {
|
||||
if (!c.name) return;
|
||||
const isNew = await mkdir(`/Clients/${safe(c.name)}`);
|
||||
if (isNew) { log.push(`+ /Clients/${safe(c.name)}`); created++; } else skipped++;
|
||||
});
|
||||
|
||||
// 3. tasks — project structure + task folder in parallel, then files sequentially for new ones
|
||||
const newTasks: { task: any; base: string; co: string; pr: string }[] = [];
|
||||
|
||||
await chunkAll(tasksRes.data ?? [], async (task: any) => {
|
||||
const coName = task?.projects?.companies?.name;
|
||||
const prjName = task?.projects?.name;
|
||||
if (!coName || !prjName || !task.title) return;
|
||||
|
||||
const co = safe(coName);
|
||||
const pr = safe(prjName);
|
||||
const tk = safe(task.title);
|
||||
const base = `/Clients/${co}/Projects/${pr}/${tk}`;
|
||||
|
||||
// ensure project structure (idempotent)
|
||||
await Promise.all([
|
||||
mkdir(`/Clients/${co}/Projects`),
|
||||
mkdir(`/Clients/${co}/Projects/${pr}`),
|
||||
mkdir(`/Clients/${co}/Projects/${pr}/00 Project Files`),
|
||||
]);
|
||||
|
||||
const taskIsNew = await mkdir(base);
|
||||
await Promise.all([mkdir(`${base}/Request Info`), mkdir(`${base}/Old Book`)]);
|
||||
|
||||
if (taskIsNew) { newTasks.push({ task, base, co, pr }); created++; }
|
||||
else skipped++;
|
||||
}, 6);
|
||||
|
||||
log.push(`new task folders: ${newTasks.length}`);
|
||||
newTasks.forEach(({ base }) => log.push(`+ ${base}`));
|
||||
|
||||
// 4. upload files for newly created task folders (sequential to avoid memory spikes)
|
||||
for (const { task, base } of newTasks) {
|
||||
const { data: subs } = await sb.from('submissions').select('id,version_number').eq('task_id', task.id);
|
||||
for (const sub of subs ?? []) {
|
||||
const { data: subFiles } = await sb.from('submission_files').select('name,storage_path').eq('submission_id', sub.id);
|
||||
if (!subFiles?.length) continue;
|
||||
const rev = `R${String(sub.version_number).padStart(2,'0')}`;
|
||||
await mkdir(`${base}/Request Info/${rev}`);
|
||||
for (const f of subFiles) {
|
||||
try {
|
||||
const { data: blob, error } = await sb.storage.from('submissions').download(f.storage_path);
|
||||
if (error || !blob) { errors++; continue; }
|
||||
await upload(`${base}/Request Info/${rev}/${f.name}`, await blob.arrayBuffer(), mime(f.name));
|
||||
fileCount++;
|
||||
} catch { errors++; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({ created, skipped, files: fileCount, errors, log }, null, 2), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user