mirror of
https://github.com/Anil-matcha/Open-Generative-AI.git
synced 2026-05-07 01:17:18 +00:00
Until the team has MuAPI credits we cannot upload trainer/studio
images to MuAPI's CDN, which previously caused POST /api/trainers
to fail outright with a 502. Now the route always saves a local
copy first and only attempts MuAPI as an enhancement.
Behaviour change
----------------
- Trainer/studio rows are created even without a MuAPI key. The
imageUrl points at a new /api/uploads/<kind>/<filename> route
that streams the file out of /data/uploads/.
- If MuAPI is reachable (key present + upload succeeds) we still
prefer its CDN URL, identical to before for the happy-path team
who already has credits.
- The slice-4 worker will need to detect local-only imageUrls and
re-upload to MuAPI at job-submission time. Tracked, not in this PR.
Files
-----
- lib/localUploadStore.js: add readLocal() and publicUrlFor() so the
serving route and the upload route share the same naming scheme.
Path-traversal guard on the read side (no '/', '..', '\' in name).
- app/api/uploads/[kind]/[name]/route.js: GET serves the bytes with
the right Content-Type and a 1h private cache header. Whitelists
kind to {trainers, studios}.
- app/api/trainers/route.js, app/api/studios/route.js: rewritten so
the create flow is "DB row -> local backup -> optional MuAPI". On
total failure (both MuAPI and local) we delete the row to avoid
orphans. The response now also surfaces a `muapiNote` string so
the UI can hint at "no credits yet".
76 lines
2.3 KiB
JavaScript
76 lines
2.3 KiB
JavaScript
import { NextResponse } from 'next/server';
|
|
import prisma from '@/lib/prisma';
|
|
import { getApiKey } from '@/lib/batchAuth';
|
|
import { uploadFileToMuapi } from '@/lib/muapiUpload';
|
|
import { saveLocalBackup, publicUrlFor } from '@/lib/localUploadStore';
|
|
|
|
export async function GET() {
|
|
const studios = await prisma.studio.findMany({
|
|
orderBy: { createdAt: 'asc' },
|
|
});
|
|
return NextResponse.json({ studios });
|
|
}
|
|
|
|
export async function POST(request) {
|
|
let form;
|
|
try {
|
|
form = await request.formData();
|
|
} catch {
|
|
return NextResponse.json({ error: 'Invalid multipart body' }, { status: 400 });
|
|
}
|
|
|
|
const name = (form.get('name') || '').toString().trim();
|
|
const csvLabel = (form.get('csvLabel') || '').toString().trim() || null;
|
|
const file = form.get('image');
|
|
|
|
if (!name) return NextResponse.json({ error: 'name is required' }, { status: 400 });
|
|
if (!file || typeof file === 'string') {
|
|
return NextResponse.json({ error: 'image file is required' }, { status: 400 });
|
|
}
|
|
|
|
if (csvLabel) {
|
|
const existing = await prisma.studio.findUnique({ where: { csvLabel } });
|
|
if (existing) {
|
|
return NextResponse.json(
|
|
{ error: `csvLabel "${csvLabel}" is already used by studio "${existing.name}"` },
|
|
{ status: 409 },
|
|
);
|
|
}
|
|
}
|
|
|
|
const studio = await prisma.studio.create({
|
|
data: { name, csvLabel, imageUrl: '' },
|
|
});
|
|
|
|
const { localPath, fileName } = await saveLocalBackup('studios', studio.id, file);
|
|
|
|
let muapiUrl = null;
|
|
let muapiNote = null;
|
|
const apiKey = getApiKey(request);
|
|
if (apiKey) {
|
|
try {
|
|
muapiUrl = await uploadFileToMuapi(apiKey, file);
|
|
} catch (err) {
|
|
muapiNote = err.message;
|
|
}
|
|
} else {
|
|
muapiNote = 'No MuAPI key — local copy only. Set the key in /studio when credits are available.';
|
|
}
|
|
|
|
const imageUrl = muapiUrl || (fileName ? publicUrlFor('studios', fileName) : '');
|
|
|
|
if (!imageUrl) {
|
|
await prisma.studio.delete({ where: { id: studio.id } });
|
|
return NextResponse.json(
|
|
{ error: `Failed to persist image. MuAPI: ${muapiNote || 'n/a'}. Local: write failed.` },
|
|
{ status: 500 },
|
|
);
|
|
}
|
|
|
|
const updated = await prisma.studio.update({
|
|
where: { id: studio.id },
|
|
data: { imageUrl, localPath },
|
|
});
|
|
|
|
return NextResponse.json({ studio: updated, muapiNote }, { status: 201 });
|
|
}
|