Add S3 storage configuration and client

This commit is contained in:
lucast committed 2026-09-29 17:02:57 +02:00
1 parent 39cb48a656
commit c2dcec2c31
7 files changed
+575 -7

No files matched your search

+5 -1
View File
@@ -10,7 +10,11 @@ export function getBackendRuntimeConfig(event?: H3Event) {
return {
mongodbUri: config.mongodbUri || process.env.MONGODB_URI || "",
uploadDir: config.uploadDir || process.env.UPLOAD_DIR || "",
tokenExpire: config.tokenExpire || process.env.TOKEN_EXPIRE || "1h",
s3Endpoint: config.s3Endpoint || process.env.S3_ENDPOINT || "",
s3Bucket: config.s3Bucket || process.env.S3_BUCKET || "",
s3Region: config.s3Region || process.env.S3_REGION || "us-east-1",
s3AccessKeyId: config.s3AccessKeyId || process.env.S3_ACCESS_KEY_ID || "",
s3SecretAccessKey: config.s3SecretAccessKey || process.env.S3_SECRET_ACCESS_KEY || "",
}
}
+101
View File
@@ -0,0 +1,101 @@
import { DeleteObjectCommand, DeleteObjectsCommand, ListObjectsV2Command, PutObjectCommand, S3Client } from "@aws-sdk/client-s3"
import type { H3Event } from "h3"
import { getBackendRuntimeConfig } from "./backend-config"
function getStorage(event: H3Event) {
const config = getBackendRuntimeConfig(event)
if (!config.s3Endpoint || !config.s3Bucket || !config.s3AccessKeyId || !config.s3SecretAccessKey) {
throw new Error("Configure NUXT_S3_ENDPOINT, NUXT_S3_BUCKET, NUXT_S3_ACCESS_KEY_ID, and NUXT_S3_SECRET_ACCESS_KEY")
}
const client = new S3Client({
endpoint: config.s3Endpoint,
region: config.s3Region,
forcePathStyle: true,
credentials: { accessKeyId: config.s3AccessKeyId, secretAccessKey: config.s3SecretAccessKey },
})
return { client, bucket: config.s3Bucket }
}
export type StoredObject = { key: string; body: Buffer; contentType: string }
export async function putObjects(event: H3Event, objects: StoredObject[]) {
const { client, bucket } = getStorage(event)
const uploaded: string[] = []
try {
for (const object of objects) {
await client.send(new PutObjectCommand({
Bucket: bucket,
Key: object.key,
Body: object.body,
ContentType: object.contentType || "application/octet-stream",
}))
uploaded.push(object.key)
}
return uploaded
} catch (error) {
await deleteObjects(event, uploaded).catch((cleanupError) => console.error("Unable to clean up partial bucket upload", cleanupError))
throw error
} finally {
client.destroy()
}
}
export async function deleteObjects(event: H3Event, keys: string[]) {
if (!keys.length) return
const { client, bucket } = getStorage(event)
try {
for (let index = 0; index < keys.length; index += 1000) {
const batch = keys.slice(index, index + 1000)
const deleted = await client.send(new DeleteObjectsCommand({
Bucket: bucket,
Delete: { Objects: batch.map((Key) => ({ Key })) },
}))
if (deleted.Errors?.length) throw new Error(`Unable to delete ${deleted.Errors.length} bucket object(s)`)
}
} finally {
client.destroy()
}
}
export async function deleteObject(event: H3Event, key: string) {
const { client, bucket } = getStorage(event)
try {
await client.send(new DeleteObjectCommand({ Bucket: bucket, Key: key }))
} finally {
client.destroy()
}
}
export async function deleteObjectPrefix(event: H3Event, prefix: string) {
const { client, bucket } = getStorage(event)
try {
let continuationToken: string | undefined
do {
const listed = await client.send(new ListObjectsV2Command({ Bucket: bucket, Prefix: prefix, ContinuationToken: continuationToken }))
const keys = listed.Contents?.flatMap((object) => object.Key ? [object.Key] : []) ?? []
if (keys.length) {
const deleted = await client.send(new DeleteObjectsCommand({ Bucket: bucket, Delete: { Objects: keys.map((Key) => ({ Key })) } }))
if (deleted.Errors?.length) throw new Error(`Unable to delete ${deleted.Errors.length} bucket object(s)`)
}
continuationToken = listed.IsTruncated ? listed.NextContinuationToken : undefined
} while (continuationToken)
} finally {
client.destroy()
}
}
export async function tryDeleteObject(event: H3Event, key: string) {
try {
await deleteObject(event, key)
} catch (error) {
console.error("Unable to delete bucket object", key, error)
}
}
export async function tryDeleteObjectPrefix(event: H3Event, prefix: string) {
try {
await deleteObjectPrefix(event, prefix)
} catch (error) {
console.error("Unable to delete bucket object prefix", prefix, error)
}
}