Skip to content

Commit

Permalink
refactor: Create clean s3 driver
Browse files Browse the repository at this point in the history
  • Loading branch information
becem-gharbi committed Jan 2, 2024
1 parent d7805f6 commit 681cafc
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 143 deletions.
6 changes: 3 additions & 3 deletions src/runtime/server/api/mutation/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ export default defineEventHandler(async (event) => {
verifyType(file.type, config.public.s3.accept)
verifySize(file.data.length, config.public.s3.maxSizeMb)

const s3Meta = await getMeta(event)
const meta = await getMeta(event)

await event.context.s3.setItemRaw(normalizedKey, file.data, { s3Meta })
await event.context.s3.setItemRaw(normalizedKey, file.data, { meta })

await event.context.s3.setMeta(normalizedKey, s3Meta)
await event.context.s3.setMeta(normalizedKey, meta)

return { status: 'ok' }
}
Expand Down
5 changes: 2 additions & 3 deletions src/runtime/server/api/query/read.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,9 @@ export default defineEventHandler(async (event: H3Event) => {

const normalizedKey = normalizeKey(key)

const opts = { mimeType: undefined }
const data = await event.context.s3.getItemRaw(normalizedKey, opts)
const data = await event.context.s3.getItemRaw(normalizedKey)

const mimeType = opts.mimeType || mime.getType(key)
const mimeType = mime.getType(key)

if (mimeType) {
setResponseHeader(event, 'Content-Type', mimeType)
Expand Down
150 changes: 13 additions & 137 deletions src/runtime/server/plugins/storage.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,30 @@
import crypto from 'crypto'
import fsLiteDriver from 'unstorage/drivers/fs-lite'
import { createStorage } from 'unstorage'
import { AwsClient } from 'aws4fetch'
import { createError } from 'h3'
import { $fetch } from 'ofetch'
import mime from 'mime'
import type { Storage } from 'unstorage'
import type { S3ObjectMetadata, ModuleOptionsS3 } from '../../types'
import { denormalizeKey } from '#s3'
import s3Driver from '../utils/s3Driver'
import { defineNitroPlugin, useRuntimeConfig } from '#imports'

if (!globalThis.crypto) {
// @ts-ignore
globalThis.crypto = crypto
}

export default defineNitroPlugin((nitroApp) => {
const config = useRuntimeConfig()
let storage: Storage

if (config.s3.driver === 'fs') {
storage = createStorage({
driver: fsLiteDriver({ base: config.s3.fsBase })
driver: fsLiteDriver({
base: config.s3.fsBase
})
})
} else if (config.s3.driver === 's3') {
storage = createS3Storage(config.s3)
storage = createStorage({
driver: s3Driver({
accessKeyId: config.s3.accessKeyId,
secretAccessKey: config.s3.secretAccessKey,
endpoint: config.s3.endpoint,
region: config.s3.region,
bucket: config.s3.bucket
})
})
} else {
throw createError('[nuxt-s3] Invalid driver')
}
Expand All @@ -33,127 +33,3 @@ export default defineNitroPlugin((nitroApp) => {
event.context.s3 = storage
})
})

function createS3Storage (config: ModuleOptionsS3) {
const client = new AwsClient({
accessKeyId: config.accessKeyId,
secretAccessKey: config.secretAccessKey,
region: config.region,
service: 's3'
})

return createStorage({
// @ts-ignore
driver: {
name: 's3',

async getItemRaw (key, opts) {
key = denormalizeKey(key)

const request = await client.sign(
`${config.endpoint}/${config.bucket}/${key}`,
{
method: 'GET'
}
)

const res = await $fetch.raw(request).catch(() => {
throw createError({
message: 'get-failed',
statusCode: 404
})
})

const contentType = res.headers.get('Content-Type')

opts.mimeType = contentType

return res._data.stream()
},

async setItemRaw (key, value, opts) {
key = denormalizeKey(key)

const type = mime.getType(key)

const { s3Meta } = opts as { s3Meta?: S3ObjectMetadata }

const metaHeaders: S3ObjectMetadata = {}

s3Meta && Object.keys(s3Meta).forEach((key) => {
metaHeaders[`x-amz-meta-${key}`] = s3Meta[key]
})

const request = await client.sign(
`${config.endpoint}/${config.bucket}/${key}`,
{
method: 'PUT',
body: value,
headers: {
'Content-Type': type as string,
...metaHeaders
}
}
)

return $fetch(request).catch(() => {
throw createError({
message: 'put-failed',
statusCode: 500
})
})
},

async removeItem (key) {
key = denormalizeKey(key)

const request = await client.sign(
`${config.endpoint}/${config.bucket}/${key}`,
{
method: 'DELETE'
}
)

return $fetch(request).catch(() => {
throw createError({
message: 'delete-failed',
statusCode: 400
})
})
},

async getMeta (key, opts) {
key = denormalizeKey(key)

opts.nativeOnly = true

const request = await client.sign(
`${config.endpoint}/${config.bucket}/${key}`,
{
method: 'HEAD'
}
)

return $fetch(request, {
onResponse ({ response }) {
const metaHeaders: S3ObjectMetadata = {}

for (const item of response.headers.entries()) {
const match = /x-amz-meta-(.*)/.exec(item[0])
if (match) {
metaHeaders[match[1]] = item[1]
}
}

response._data = metaHeaders
}
}).catch(() => {
throw createError({
message: 'head-failed',
statusCode: 400
})
})
}
}
})
}
130 changes: 130 additions & 0 deletions src/runtime/server/utils/s3Driver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import crypto from 'crypto'
import { $fetch } from 'ofetch'
import { AwsClient } from 'aws4fetch'
import { defineDriver } from 'unstorage'

if (!globalThis.crypto) {
// @ts-ignore
globalThis.crypto = crypto
}

interface DriverOptions {
accessKeyId: string;
secretAccessKey: string;
endpoint: string;
region: string;
bucket: string;
}

interface GetItemOptions {
headers?: HeadersInit;
}

interface SetItemOptions {
headers?: HeadersInit;
meta?: Record<string, string>;
}

const DRIVER_NAME = 's3'

// @ts-ignore
export default defineDriver((options: DriverOptions) => {
const awsClient = new AwsClient({
accessKeyId: options.accessKeyId,
secretAccessKey: options.secretAccessKey,
region: options.region,
service: DRIVER_NAME
})

const normalizedKey = (key: string) => key.replace(/:/g, '/')

const awsUrlWithoutKey = () => `${options.endpoint}/${options.bucket}`

const awsUrlWithKey = (key: string) => `${awsUrlWithoutKey()}/${normalizedKey(key)}`

// https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadObject.html
async function _getMeta (key: string) {
const request = await awsClient.sign(awsUrlWithKey(key), {
method: 'HEAD'
})

return $fetch.raw(request)
.then((res) => {
const metaHeaders: HeadersInit = {}
for (const [key, value] of res.headers.entries()) {
const match = /x-amz-meta-(.*)/.exec(key)
if (match) {
metaHeaders[match[1]] = value
}
}
return metaHeaders
})
}

return {
name: DRIVER_NAME,
options,

getItem () {},
setItem () {},
getItems () {},
setItems () {},
clear () {},
getKeys () {},
dispose () {},
watch () {},

// https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObject.html
async getItemRaw (key, opts: GetItemOptions) {
const request = await awsClient.sign(awsUrlWithKey(key), {
method: 'GET'
})

return $fetch.raw(request)
.then((res) => {
opts.headers = res.headers
return res._data
})
.catch(() => null)
},

// https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObject.html
async setItemRaw (key, value, opts: SetItemOptions) {
const metaHeaders: HeadersInit = {}

if (typeof opts.meta === 'object') {
for (const [key, value] of Object.entries(opts.meta)) {
metaHeaders[`x-amz-meta-${key}`] = value
}
}

const request = await awsClient.sign(awsUrlWithKey(key), {
method: 'PUT',
body: value,
headers: {
...opts.headers,
...metaHeaders
}
})

return $fetch(request)
},

// https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObject.html
async removeItem (key) {
const request = await awsClient.sign(awsUrlWithKey(key), {
method: 'DELETE'
})

return $fetch(request)
},

async hasItem (key) {
return await _getMeta(key)
.then(() => true)
.catch(() => false)
},

getMeta: key => _getMeta(key).catch(() => ({}))
}
})

0 comments on commit 681cafc

Please sign in to comment.