Skip to content

Commit 7ac3252

Browse files
committed
chore(storage): serve blob + deps
1 parent 329e932 commit 7ac3252

File tree

8 files changed

+206
-150
lines changed

8 files changed

+206
-150
lines changed

_nuxthub/server/utils/blob.ts

+89-71
Original file line numberDiff line numberDiff line change
@@ -1,114 +1,116 @@
11
import type { R2Bucket, R2ListOptions } from '@cloudflare/workers-types/experimental'
22
import mime from 'mime'
33
// import { imageMeta } from 'image-meta'
4+
import type { H3Event } from 'h3'
45
import { defu } from 'defu'
56
import { randomUUID } from 'uncrypto'
67
import { parse } from 'pathe'
78
import { joinURL } from 'ufo'
89

9-
const _blobs: Record<string, R2Bucket> = {}
10+
const _r2_buckets: Record<string, R2Bucket> = {}
1011

11-
function _useBlob () {
12+
function _useBucket () {
1213
const name = 'BLOB'
13-
if (_blobs[name]) {
14-
return _blobs[name]
14+
if (_r2_buckets[name]) {
15+
return _r2_buckets[name]
1516
}
1617

1718
// @ts-ignore
1819
const binding = process.env[name] || globalThis.__env__?.[name] || globalThis[name]
1920
if (!binding) {
2021
throw createError(`Missing Cloudflare R2 binding ${name}`)
2122
}
22-
_blobs[name] = binding as R2Bucket
23+
_r2_buckets[name] = binding as R2Bucket
2324

24-
return _blobs[name]
25+
return _r2_buckets[name]
2526
}
2627

2728
export function useBlob () {
28-
const proxy = import.meta.dev && process.env.NUXT_HUB_URL
29+
const proxyURL = import.meta.dev && process.env.NUXT_HUB_URL
30+
let bucket: R2Bucket
31+
if (!proxyURL) {
32+
bucket = _useBucket()
33+
}
2934

3035
return {
31-
async list (options: R2ListOptions = {}) {
32-
if (proxy) {
33-
const query: Record<string, any> = {}
34-
35-
return $fetch<BlobObject[]>('/api/_hub/blob', { baseURL: proxy, method: 'GET', query })
36-
} else {
37-
const blob = _useBlob()
38-
39-
const resolvedOptions = defu(options, {
40-
limit: 500,
41-
include: ['httpMetadata' as const, 'customMetadata' as const],
36+
async list (options: BlobListOptions = { limit: 1000 }) {
37+
if (proxyURL) {
38+
return $fetch<BlobObject[]>('/api/_hub/blob', {
39+
baseURL: proxyURL,
40+
method: 'GET',
41+
query: options
4242
})
43+
}
44+
// Use R2 binding
45+
const resolvedOptions = defu(options, {
46+
limit: 500,
47+
include: ['httpMetadata' as const, 'customMetadata' as const],
48+
})
49+
50+
// https://developers.cloudflare.com/r2/api/workers/workers-api-reference/#r2listoptions
51+
const listed = await bucket.list(resolvedOptions)
52+
let truncated = listed.truncated
53+
let cursor = listed.truncated ? listed.cursor : undefined
54+
55+
while (truncated) {
56+
const next = await bucket.list({
57+
...options,
58+
cursor: cursor,
59+
})
60+
listed.objects.push(...next.objects)
4361

44-
// https://developers.cloudflare.com/r2/api/workers/workers-api-reference/#r2listoptions
45-
const listed = await blob.list(resolvedOptions)
46-
let truncated = listed.truncated
47-
let cursor = listed.truncated ? listed.cursor : undefined
48-
49-
while (truncated) {
50-
const next = await blob.list({
51-
...options,
52-
cursor: cursor,
53-
})
54-
listed.objects.push(...next.objects)
55-
56-
truncated = next.truncated
57-
cursor = next.truncated ? next.cursor : undefined
58-
}
59-
60-
return listed.objects.map(mapR2ObjectToBlob)
62+
truncated = next.truncated
63+
cursor = next.truncated ? next.cursor : undefined
6164
}
62-
},
63-
async get (key: string) {
64-
if (proxy) {
65-
const query: Record<string, any> = {}
6665

67-
return $fetch<ReadableStreamDefaultReader<any>>(`/api/_hub/blob/${key}`, { baseURL: proxy, method: 'GET', query })
68-
} else {
69-
const blob = _useBlob()
70-
const object = await blob.get(key)
66+
return listed.objects.map(mapR2ObjectToBlob)
67+
},
68+
async serve (event: H3Event, pathname: string) {
69+
if (proxyURL) {
70+
return $fetch<ReadableStreamDefaultReader<any>>(`/api/_hub/blob/${pathname}`, {
71+
baseURL: proxyURL,
72+
method: 'GET'
73+
})
74+
}
75+
// Use R2 binding
76+
const object = await bucket.get(pathname)
7177

72-
if (!object) {
73-
throw createError({ message: 'File not found', statusCode: 404 })
74-
}
78+
if (!object) {
79+
throw createError({ message: 'File not found', statusCode: 404 })
80+
}
7581

76-
// FIXME
77-
setHeader(useEvent(), 'Content-Type', object.httpMetadata!.contentType!)
78-
setHeader(useEvent(), 'Content-Length', object.size)
82+
setHeader(event, 'Content-Type', object.httpMetadata?.contentType || getContentType(pathname))
83+
setHeader(event, 'Content-Length', object.size)
7984

80-
return object.body.getReader()
81-
}
85+
return object.body
8286
},
8387
async put (pathname: string, body: string | ReadableStream<any> | ArrayBuffer | ArrayBufferView | Blob, options: { contentType?: string, addRandomSuffix?: boolean, [key: string]: any } = { addRandomSuffix: true }) {
84-
if (proxy) {
88+
if (proxyURL) {
8589
// TODO
86-
} else {
87-
const blob = _useBlob()
88-
const { contentType: optionsContentType, addRandomSuffix, ...customMetadata } = options
89-
const contentType = optionsContentType || (body as Blob).type || getContentType(pathname)
90-
91-
const { dir, ext, name: filename } = parse(pathname)
92-
let key = pathname
93-
if (addRandomSuffix) {
94-
key = joinURL(dir === '.' ? '' : dir, `${filename}-${randomUUID().split('-')[0]}${ext}`)
95-
}
90+
return console.warn('useBlob().put() Not implemented')
91+
}
92+
// Use R2 binding
93+
const { contentType: optionsContentType, addRandomSuffix, ...customMetadata } = options
94+
const contentType = optionsContentType || (body as Blob).type || getContentType(pathname)
95+
96+
const { dir, ext, name: filename } = parse(pathname)
97+
let key = pathname
98+
if (addRandomSuffix) {
99+
key = joinURL(dir === '.' ? '' : dir, `${filename}-${randomUUID().split('-')[0]}${ext}`)
100+
}
96101

97-
const object = await blob.put(key, body as any, { httpMetadata: { contentType }, customMetadata })
102+
const object = await bucket.put(key, body as any, { httpMetadata: { contentType }, customMetadata })
98103

99-
return mapR2ObjectToBlob(object)
100-
}
104+
return mapR2ObjectToBlob(object)
101105
},
102106
async delete (key: string) {
103-
if (proxy) {
107+
if (proxyURL) {
104108
const query: Record<string, any> = {}
105109

106-
return $fetch<void>(`/api/_hub/blob/${key}`, { baseURL: proxy, method: 'DELETE', query })
107-
} else {
108-
const blob = _useBlob()
109-
110-
return await blob.delete(key)
110+
return $fetch<void>(`/api/_hub/blob/${key}`, { baseURL: proxyURL, method: 'DELETE', query })
111111
}
112+
// Use R2 binding
113+
return await bucket.delete(key)
112114
}
113115
}
114116
}
@@ -140,6 +142,22 @@ function getContentType (pathOrExtension?: string) {
140142
// return metadata
141143
// }
142144

145+
// export async function readFiles (event: any) {
146+
// const files = (await readMultipartFormData(event) || [])
147+
148+
// // Filter only files
149+
// return files.filter((file) => Boolean(file.filename))
150+
// }
151+
152+
// export function toArrayBuffer (buffer: Buffer) {
153+
// const arrayBuffer = new ArrayBuffer(buffer.length)
154+
// const view = new Uint8Array(arrayBuffer)
155+
// for (let i = 0; i < buffer.length; ++i) {
156+
// view[i] = buffer[i]
157+
// }
158+
// return arrayBuffer
159+
// }
160+
143161
function mapR2ObjectToBlob (object: R2Object): BlobObject {
144162
return {
145163
pathname: object.key,

_nuxthub/types/index.d.ts

+10
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,13 @@ declare interface BlobObject {
44
size: number
55
uploadedAt: Date
66
}
7+
8+
declare interface BlobListOptions {
9+
/**
10+
* The maximum number of blobs to return per request.
11+
* @default 1000
12+
*/
13+
limit?: number
14+
prefix?: string
15+
cursor?: string
16+
}

nuxt.config.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export default defineNuxtConfig({
22
devtools: { enabled: true },
3+
typescript: { includeWorkspace: true },
34
// extends: '@nuxt/ui-pro',
45
extends: [
56
// '/Users/atinux/Projects/nuxt/ui-pro',

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@
3333
"@nuxt/devtools": "^1.0.8",
3434
"@nuxt/eslint-config": "^0.2.0",
3535
"eslint": "^8.56.0",
36-
"typescript": "5.3.3"
36+
"typescript": "5.3.3",
37+
"vue-tsc": "^1.8.27"
3738
},
3839
"resolutions": {
3940
"nitropack": "npm:nitropack-nightly@latest",

pages/storage.vue

+2-2
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,8 @@ const items = [[{
120120
}"
121121
class="overflow-hidden relative"
122122
>
123-
<!-- <img v-if="file.httpMetadata?.contentType?.startsWith('image/')" :src="`/api/storage/${file.key}`" class="h-36 w-full object-cover"> -->
124-
<div class="h-36 w-full flex items-center justify-center p-2 text-center">
123+
<img v-if="file.contentType?.startsWith('image/')" :src="`/api/storage/${file.pathname}`" class="h-36 w-full object-cover">
124+
<div v-else class="h-36 w-full flex items-center justify-center p-2 text-center">
125125
<UIcon name="i-heroicons-document" class="w-8 h-8" />
126126
</div>
127127
<div class="flex flex-col gap-1 p-2 border-t border-gray-200 dark:border-gray-800">

0 commit comments

Comments
 (0)