Skip to content

Commit

Permalink
chore(storage): move bucket crud logic in useBlob
Browse files Browse the repository at this point in the history
  • Loading branch information
smarroufin committed Jan 30, 2024
1 parent a956284 commit c97f5a9
Show file tree
Hide file tree
Showing 6 changed files with 91 additions and 70 deletions.
108 changes: 82 additions & 26 deletions _nuxthub/server/utils/bucket.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import type { R2Bucket, R2ListOptions } from '@cloudflare/workers-types/experimental'
import type { MultiPartData } from 'h3'
import mime from 'mime'
import { imageMeta } from 'image-meta'
import { defu } from 'defu'
import { randomUUID } from 'uncrypto'

const _buckets: Record<string, R2Bucket> = {}

Expand All @@ -24,48 +26,102 @@ export function useBucket (name: string = '') {
return _buckets[bucketName]
}

export async function serveFiles (bucket: R2Bucket, options: R2ListOptions = {}) {
const resolvedOptions = defu(options, {
limit: 500,
include: ['httpMetadata' as const, 'customMetadata' as const],
})

// https://developers.cloudflare.com/r2/api/workers/workers-api-reference/#r2listoptions
const listed = await bucket.list(resolvedOptions)
let truncated = listed.truncated
let cursor = listed.truncated ? listed.cursor : undefined

while (truncated) {
const next = await bucket.list({
...options,
cursor: cursor,
})
listed.objects.push(...next.objects)

truncated = next.truncated
cursor = next.truncated ? next.cursor : undefined
}
export function useBlob (name: string = '') {
const isProxy = import.meta.dev && process.env.NUXT_HUB_URL

return {
async list (options: R2ListOptions = {}) {
if (isProxy) {
// TODO
} else {
const bucket = useBucket(name)

const resolvedOptions = defu(options, {
limit: 500,
include: ['httpMetadata' as const, 'customMetadata' as const],
})

// https://developers.cloudflare.com/r2/api/workers/workers-api-reference/#r2listoptions
const listed = await bucket.list(resolvedOptions)
let truncated = listed.truncated
let cursor = listed.truncated ? listed.cursor : undefined

while (truncated) {
const next = await bucket.list({
...options,
cursor: cursor,
})
listed.objects.push(...next.objects)

truncated = next.truncated
cursor = next.truncated ? next.cursor : undefined
}

return listed.objects
}
},
async get (key: string) {
if (isProxy) {
// TODO
} else {
const bucket = useBucket(name)
const object = await bucket.get(key)

return listed.objects
if (!object) {
throw createError({ message: 'File not found', statusCode: 404 })
}

setHeader(useEvent(), 'Content-Type', object.httpMetadata!.contentType!)
setHeader(useEvent(), 'Content-Length', object.size)

return object.body.getReader()
}
},
async put (file: MultiPartData) {
if (isProxy) {
// TODO
} else {
const bucket = useBucket(name)

const type = file.type || getContentType(file.filename)
const extension = getExtension(type)
// TODO: ensure filename unicity
const filename = [randomUUID(), extension].filter(Boolean).join('.')
const httpMetadata = { contentType: type }
const customMetadata = getMetadata(type, file.data)

return await bucket.put(filename, toArrayBuffer(file.data), { httpMetadata, customMetadata })
}
},
async delete (key: string) {
if (isProxy) {
// TODO
} else {
const bucket = useBucket(name)

return await bucket.delete(key)
}
}
}
}

export function getContentType (pathOrExtension?: string) {
function getContentType (pathOrExtension?: string) {
return (pathOrExtension && mime.getType(pathOrExtension)) || 'application/octet-stream'
}

export function getExtension (type?: string) {
function getExtension (type?: string) {
return (type && mime.getExtension(type)) || ''
}

export function getMetadata (type: string, buffer: Buffer) {
function getMetadata (type: string, buffer: Buffer) {
if (type.startsWith('image/')) {
return imageMeta(buffer) as Record<string, any>
} else {
return {}
}
}

export function toArrayBuffer (buffer: Buffer) {
function toArrayBuffer (buffer: Buffer) {
const arrayBuffer = new ArrayBuffer(buffer.length)
const view = new Uint8Array(arrayBuffer)
for (let i = 0; i < buffer.length; ++i) {
Expand Down
11 changes: 0 additions & 11 deletions server/api/bucket-test.ts

This file was deleted.

7 changes: 2 additions & 5 deletions server/api/storage/[key].delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,8 @@ import { useValidatedParams, z } from 'h3-zod'
export default eventHandler(async (event) => {
await requireUserSession(event)
const { key } = await useValidatedParams(event, {
key: z.string().min(1).max(100)
key: z.string().min(1)
})

// Delete entry for the current user
const bucket = useBucket()

return bucket.delete(key)
return useBlob().delete(key)
})
17 changes: 2 additions & 15 deletions server/api/storage/[key].get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,9 @@ import { useValidatedParams, z } from 'h3-zod'

export default eventHandler(async (event) => {
await requireUserSession(event)

const { key } = await useValidatedParams(event, {
key: z.string().min(1).max(100)
key: z.string().min(1)
})

// List files for the current user

const bucket = useBucket()
const object = await bucket.get(key)

if (!object) {
throw createError({ message: 'File not found', statusCode: 404 })
}

setHeader(event, 'Content-Type', object.httpMetadata!.contentType!)
setHeader(event, 'Content-Length', object.size)

return object.body.getReader()
return useBlob().get(key)
})
5 changes: 1 addition & 4 deletions server/api/storage/index.get.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
export default eventHandler(async (event) => {
await requireUserSession(event)

// List files for the current user

const bucket = useBucket()
return serveFiles(bucket)
return useBlob().list()
})
13 changes: 4 additions & 9 deletions server/api/storage/index.put.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { randomUUID } from 'uncrypto'

export default eventHandler(async (event) => {
await requireUserSession(event)

Expand All @@ -10,17 +8,14 @@ export default eventHandler(async (event) => {
if (!files) {
throw createError({ statusCode: 400, message: 'Missing files' })
}

const { put } = useBlob()

const objects = []

try {
for (const file of files) {
const type = file.type || getContentType(file.filename)
const extension = getExtension(type)
// TODO: ensure filename unicity
const filename = [randomUUID(), extension].filter(Boolean).join('.')
const httpMetadata = { contentType: type }
const customMetadata = getMetadata(type, file.data)
const object = await useBucket().put(filename, toArrayBuffer(file.data), { httpMetadata, customMetadata })
const object = await put(file)
objects.push(object)
}
} catch (e: any) {
Expand Down

0 comments on commit c97f5a9

Please sign in to comment.