Add platform-dash: S3 artifact browser for Garage object store
Lightweight Next.js app for browsing, uploading, and downloading artifacts from the cluster-local Garage S3 bucket. Uses the harness design system. Features: - File/folder browser with breadcrumb navigation - Drag-and-drop upload - Download and delete - Ingress at platform.coreworlds.io (internal-only) Also adds platform-dash to CI/deploy workflows.
This commit is contained in:
90
apps/platform-dash/src/app/api/s3/route.ts
Normal file
90
apps/platform-dash/src/app/api/s3/route.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import {
|
||||
S3Client,
|
||||
ListObjectsV2Command,
|
||||
PutObjectCommand,
|
||||
DeleteObjectCommand,
|
||||
} from "@aws-sdk/client-s3";
|
||||
|
||||
const BUCKET = process.env.S3_BUCKET || "artifacts";
|
||||
|
||||
function getClient() {
|
||||
return new S3Client({
|
||||
endpoint: process.env.S3_ENDPOINT || "http://garage.platform.svc:3900",
|
||||
region: process.env.S3_REGION || "garage",
|
||||
credentials: {
|
||||
accessKeyId: process.env.S3_ACCESS_KEY_ID || "",
|
||||
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY || "",
|
||||
},
|
||||
forcePathStyle: true,
|
||||
});
|
||||
}
|
||||
|
||||
// GET — list objects
|
||||
export async function GET(request: NextRequest) {
|
||||
const prefix = request.nextUrl.searchParams.get("prefix") || "";
|
||||
const client = getClient();
|
||||
|
||||
try {
|
||||
const result = await client.send(
|
||||
new ListObjectsV2Command({ Bucket: BUCKET, Prefix: prefix || undefined })
|
||||
);
|
||||
|
||||
const objects = (result.Contents || []).map((obj) => ({
|
||||
key: obj.Key,
|
||||
size: obj.Size,
|
||||
lastModified: obj.LastModified?.toISOString(),
|
||||
}));
|
||||
|
||||
return NextResponse.json({ objects });
|
||||
} catch (e: unknown) {
|
||||
const msg = e instanceof Error ? e.message : "S3 error";
|
||||
return NextResponse.json({ error: msg }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// PUT — upload object
|
||||
export async function PUT(request: NextRequest) {
|
||||
const key = request.nextUrl.searchParams.get("key");
|
||||
if (!key) {
|
||||
return NextResponse.json({ error: "key required" }, { status: 400 });
|
||||
}
|
||||
|
||||
const body = await request.arrayBuffer();
|
||||
const client = getClient();
|
||||
|
||||
try {
|
||||
await client.send(
|
||||
new PutObjectCommand({
|
||||
Bucket: BUCKET,
|
||||
Key: key,
|
||||
Body: Buffer.from(body),
|
||||
ContentType: request.headers.get("content-type") || "application/octet-stream",
|
||||
})
|
||||
);
|
||||
return NextResponse.json({ ok: true });
|
||||
} catch (e: unknown) {
|
||||
const msg = e instanceof Error ? e.message : "Upload failed";
|
||||
return NextResponse.json({ error: msg }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE — remove object
|
||||
export async function DELETE(request: NextRequest) {
|
||||
const key = request.nextUrl.searchParams.get("key");
|
||||
if (!key) {
|
||||
return NextResponse.json({ error: "key required" }, { status: 400 });
|
||||
}
|
||||
|
||||
const client = getClient();
|
||||
|
||||
try {
|
||||
await client.send(
|
||||
new DeleteObjectCommand({ Bucket: BUCKET, Key: key })
|
||||
);
|
||||
return NextResponse.json({ ok: true });
|
||||
} catch (e: unknown) {
|
||||
const msg = e instanceof Error ? e.message : "Delete failed";
|
||||
return NextResponse.json({ error: msg }, { status: 500 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user