From bcb437e067c99b7b1280dec894788c5c84d30ef9 Mon Sep 17 00:00:00 2001 From: becem-gharbi Date: Tue, 3 Oct 2023 16:31:05 +0100 Subject: [PATCH] refactor: Transform s3 utility to an Unstorage driver --- playground/app.vue | 3 +- src/module.ts | 44 +++++-- src/runtime/server/api/mutation/create.ts | 11 +- src/runtime/server/api/mutation/delete.ts | 8 +- src/runtime/server/api/query/read.ts | 10 +- src/runtime/server/utils/index.ts | 2 + src/runtime/server/utils/key.ts | 20 ++++ src/runtime/server/utils/s3.ts | 138 ++++++++++++---------- 8 files changed, 151 insertions(+), 85 deletions(-) create mode 100644 src/runtime/server/utils/index.ts create mode 100644 src/runtime/server/utils/key.ts diff --git a/playground/app.vue b/playground/app.vue index f5a55ff..b12ba6f 100644 --- a/playground/app.vue +++ b/playground/app.vue @@ -26,7 +26,6 @@ async function handleChange(files: File[]) { url.value = await upload(files[0], { url: url.value, prefix: "images/", - key: "my_folder/my_file" - }); + }); } \ No newline at end of file diff --git a/src/module.ts b/src/module.ts index e01a574..ea0e75c 100644 --- a/src/module.ts +++ b/src/module.ts @@ -4,6 +4,7 @@ import { logger, addImportsDir, addServerHandler, + addTemplate, } from "@nuxt/kit"; import { defu } from "defu"; import { fileURLToPath } from "url"; @@ -52,18 +53,39 @@ export default defineNuxtModule({ }, }); - //Create virtual imports for server-side - nuxt.hook("nitro:config", (nitroConfig) => { - nitroConfig.alias = nitroConfig.alias || {}; + // Add server utils + nuxt.options.nitro = defu( + { + alias: { + "#s3": resolve(runtimeDir, "server/utils"), + }, + }, + nuxt.options.nitro + ); - // Inline module runtime in Nitro bundle - nitroConfig.externals = defu( - typeof nitroConfig.externals === "object" ? nitroConfig.externals : {}, - { - inline: [resolve(runtimeDir)], - } - ); - nitroConfig.alias["#s3"] = resolve(runtimeDir, "server/utils/s3"); + addTemplate({ + filename: "types/s3.d.ts", + getContents: () => + [ + "declare module '#s3' {", + `const s3Storage: typeof import('${resolve( + runtimeDir, + "server/utils" + )}').s3Storage`, + `const normalizeKey: typeof import('${resolve( + runtimeDir, + "server/utils" + )}').normalizeKey`, + `const denormalizeKey: typeof import('${resolve( + runtimeDir, + "server/utils" + )}').denormalizeKey`, + `const getKey: typeof import('${resolve( + runtimeDir, + "server/utils" + )}').getKey`, + "}", + ].join("\n"), }); // Get object diff --git a/src/runtime/server/api/mutation/create.ts b/src/runtime/server/api/mutation/create.ts index 51632a4..6d10338 100644 --- a/src/runtime/server/api/mutation/create.ts +++ b/src/runtime/server/api/mutation/create.ts @@ -1,16 +1,21 @@ import { defineEventHandler } from "#imports"; -import { putObject } from "#s3"; +import { normalizeKey, s3Storage, getKey } from "#s3"; import { readMultipartFormData, createError } from "h3"; export default defineEventHandler(async (event) => { - const key = event.path.split("/s3/mutation/")[1]; + const key = getKey(event); const multipartFormData = await readMultipartFormData(event); const file = multipartFormData?.find((el) => el.name === "file"); if (file && file.type) { - await putObject(key, file.data, file.type); + const normalizedKey = normalizeKey(key); + + await s3Storage.setItemRaw(normalizedKey, file.data, { + type: file.type, + }); + return "ok"; } diff --git a/src/runtime/server/api/mutation/delete.ts b/src/runtime/server/api/mutation/delete.ts index f5917e6..0f1f518 100644 --- a/src/runtime/server/api/mutation/delete.ts +++ b/src/runtime/server/api/mutation/delete.ts @@ -1,10 +1,12 @@ import { defineEventHandler } from "#imports"; -import { deleteObject } from "#s3"; +import { s3Storage, getKey, normalizeKey } from "#s3"; export default defineEventHandler(async (event) => { - const key = event.path.split("/s3/mutation/")[1]; + const key = getKey(event); - await deleteObject(key); + const normalizedKey = normalizeKey(key); + + await s3Storage.removeItem(normalizedKey); return "ok"; }); diff --git a/src/runtime/server/api/query/read.ts b/src/runtime/server/api/query/read.ts index f54a419..09b15cc 100644 --- a/src/runtime/server/api/query/read.ts +++ b/src/runtime/server/api/query/read.ts @@ -1,9 +1,11 @@ import { defineEventHandler } from "#imports"; -import { getObject } from "#s3"; +import { s3Storage, getKey, normalizeKey } from "#s3"; import type { H3Event } from "h3"; -export default defineEventHandler(async (event: H3Event) => { - const key = event.path.split("/s3/query/")[1]; +export default defineEventHandler((event: H3Event) => { + const key = getKey(event); - return getObject(event, key); + const normalizedKey = normalizeKey(key); + + return s3Storage.getItemRaw(normalizedKey, { event }); }); diff --git a/src/runtime/server/utils/index.ts b/src/runtime/server/utils/index.ts new file mode 100644 index 0000000..96abd49 --- /dev/null +++ b/src/runtime/server/utils/index.ts @@ -0,0 +1,2 @@ +export * from "./s3"; +export * from "./key"; diff --git a/src/runtime/server/utils/key.ts b/src/runtime/server/utils/key.ts new file mode 100644 index 0000000..41985bb --- /dev/null +++ b/src/runtime/server/utils/key.ts @@ -0,0 +1,20 @@ +import { parseURL } from "ufo"; +import type { H3Event } from "h3"; + +function normalizeKey(key: string) { + return key.replace(/\//g, ":"); +} + +function denormalizeKey(key: string) { + return key.replace(/:/g, "/"); +} + +function getKey(event: H3Event) { + const regex = new RegExp("^/api/s3/(mutation|query)/"); + + const pathname = parseURL(event.path).pathname; + + return pathname.replace(regex, ""); +} + +export { normalizeKey, denormalizeKey, getKey }; diff --git a/src/runtime/server/utils/s3.ts b/src/runtime/server/utils/s3.ts index bc82d83..feffb54 100644 --- a/src/runtime/server/utils/s3.ts +++ b/src/runtime/server/utils/s3.ts @@ -2,11 +2,12 @@ import { useRuntimeConfig } from "#imports"; import { AwsClient } from "aws4fetch"; import { createError, setResponseHeader } from "h3"; import crypto from "crypto"; +import { createStorage } from "unstorage"; +import { denormalizeKey } from "./key"; import { $fetch } from "ofetch"; -import type { H3Event } from "h3"; - if (!globalThis.crypto) { + //@ts-ignore globalThis.crypto = crypto; } @@ -19,77 +20,90 @@ const client = new AwsClient({ service: "s3", }); -async function deleteObject(key: string, bucket = config.s3.bucket) { - const request = await client.sign(`${config.s3.endpoint}/${bucket}/${key}`, { - method: "DELETE", - }); +function checkType(type: string) { + const regex = new RegExp(config.public.s3.accept); - return $fetch(request).catch(() => { + if (!regex.test(type)) { throw createError({ - message: "delete-failed", - statusCode: 400, + message: "invalid-type", + status: 400, }); - }); + } } -async function getObject( - event: H3Event, - key: string, - bucket = config.s3.bucket -) { - const request = await client.sign(`${config.s3.endpoint}/${bucket}/${key}`, { - method: "GET", - }); +const s3Storage = createStorage({ + //@ts-ignore + driver: { + name: "s3", - const res = await $fetch.raw(request).catch(() => { - throw createError({ - message: "get-failed", - statusCode: 404, - }); - }); + async getItemRaw(key, opts) { + key = denormalizeKey(key); - const contentType = res.headers.get("Content-Type"); + const request = await client.sign( + `${config.s3.endpoint}/${config.s3.bucket}/${key}`, + { + method: "GET", + } + ); - if (contentType) { - setResponseHeader(event, "Content-Type", contentType); - } + const res = await $fetch.raw(request).catch(() => { + throw createError({ + message: "get-failed", + statusCode: 404, + }); + }); - return res._data.stream(); -} + const contentType = res.headers.get("Content-Type"); -async function putObject( - key: string, - data: Buffer, - type: string, - bucket = config.s3.bucket -) { - checkType(type); - - const request = await client.sign(`${config.s3.endpoint}/${bucket}/${key}`, { - method: "PUT", - body: data, - headers: { - "Content-Type": type, - }, - }); + if (contentType) { + setResponseHeader(opts.event, "Content-Type", contentType); + } - return $fetch(request).catch(() => { - throw createError({ - message: "put-failed", - statusCode: 500, - }); - }); -} + return res._data as Blob; // TODO return a stream + }, -function checkType(type: string) { - const regex = new RegExp(config.public.s3.accept); + async setItemRaw(key, value, opts) { + key = denormalizeKey(key); + + checkType(opts.type); + + const request = await client.sign( + `${config.s3.endpoint}/${config.s3.bucket}/${key}`, + { + method: "PUT", + body: value, + headers: { + "Content-Type": opts.type, + }, + } + ); + + return $fetch(request).catch(() => { + throw createError({ + message: "put-failed", + statusCode: 500, + }); + }); + }, - if (!regex.test(type)) { - throw createError({ - message: "invalid-type", - status: 400, - }); - } -} + async removeItem(key) { + key = denormalizeKey(key); + + const request = await client.sign( + `${config.s3.endpoint}/${config.s3.bucket}/${key}`, + { + method: "DELETE", + } + ); + + return $fetch(request).catch(() => { + throw createError({ + message: "delete-failed", + statusCode: 400, + }); + }); + }, + }, +}); -export { putObject, getObject, deleteObject }; +export { s3Storage };