- Object store is now a tab ("Object Browser") alongside "Buckets"
- Buckets tab: create and delete buckets
- New directory creation via NEW DIR button
- DOWNLOAD and DELETE buttons are now full words with borders and
spacing between them to prevent misclicks
- Bucket selector dropdown when multiple buckets exist
- All API routes accept optional bucket query param
83 lines
2.6 KiB
TypeScript
83 lines
2.6 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import {
|
|
S3Client,
|
|
ListObjectsV2Command,
|
|
PutObjectCommand,
|
|
DeleteObjectCommand,
|
|
} from "@aws-sdk/client-s3";
|
|
|
|
const DEFAULT_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,
|
|
});
|
|
}
|
|
|
|
export async function GET(request: NextRequest) {
|
|
const prefix = request.nextUrl.searchParams.get("prefix") || "";
|
|
const bucket = request.nextUrl.searchParams.get("bucket") || DEFAULT_BUCKET;
|
|
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 });
|
|
}
|
|
}
|
|
|
|
export async function PUT(request: NextRequest) {
|
|
const key = request.nextUrl.searchParams.get("key");
|
|
const bucket = request.nextUrl.searchParams.get("bucket") || DEFAULT_BUCKET;
|
|
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 });
|
|
}
|
|
}
|
|
|
|
export async function DELETE(request: NextRequest) {
|
|
const key = request.nextUrl.searchParams.get("key");
|
|
const bucket = request.nextUrl.searchParams.get("bucket") || DEFAULT_BUCKET;
|
|
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 });
|
|
}
|
|
}
|