Skip to content

Commit

Permalink
refactor: Transform s3 utility to an Unstorage driver
Browse files Browse the repository at this point in the history
  • Loading branch information
becem-gharbi committed Oct 3, 2023
1 parent ed470c1 commit bcb437e
Show file tree
Hide file tree
Showing 8 changed files with 151 additions and 85 deletions.
3 changes: 1 addition & 2 deletions playground/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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"
});
});
}
</script>
44 changes: 33 additions & 11 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
logger,
addImportsDir,
addServerHandler,
addTemplate,
} from "@nuxt/kit";
import { defu } from "defu";
import { fileURLToPath } from "url";
Expand Down Expand Up @@ -52,18 +53,39 @@ export default defineNuxtModule<ModuleOptions>({
},
});

//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
Expand Down
11 changes: 8 additions & 3 deletions src/runtime/server/api/mutation/create.ts
Original file line number Diff line number Diff line change
@@ -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";
}

Expand Down
8 changes: 5 additions & 3 deletions src/runtime/server/api/mutation/delete.ts
Original file line number Diff line number Diff line change
@@ -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";
});
10 changes: 6 additions & 4 deletions src/runtime/server/api/query/read.ts
Original file line number Diff line number Diff line change
@@ -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 });
});
2 changes: 2 additions & 0 deletions src/runtime/server/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./s3";
export * from "./key";
20 changes: 20 additions & 0 deletions src/runtime/server/utils/key.ts
Original file line number Diff line number Diff line change
@@ -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 };
138 changes: 76 additions & 62 deletions src/runtime/server/utils/s3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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 };

0 comments on commit bcb437e

Please sign in to comment.