|
1 | 1 | import type { R2Bucket, R2ListOptions } from '@cloudflare/workers-types/experimental'
|
2 | 2 | import mime from 'mime'
|
3 | 3 | // import { imageMeta } from 'image-meta'
|
| 4 | +import type { H3Event } from 'h3' |
4 | 5 | import { defu } from 'defu'
|
5 | 6 | import { randomUUID } from 'uncrypto'
|
6 | 7 | import { parse } from 'pathe'
|
7 | 8 | import { joinURL } from 'ufo'
|
8 | 9 |
|
9 |
| -const _blobs: Record<string, R2Bucket> = {} |
| 10 | +const _r2_buckets: Record<string, R2Bucket> = {} |
10 | 11 |
|
11 |
| -function _useBlob () { |
| 12 | +function _useBucket () { |
12 | 13 | const name = 'BLOB'
|
13 |
| - if (_blobs[name]) { |
14 |
| - return _blobs[name] |
| 14 | + if (_r2_buckets[name]) { |
| 15 | + return _r2_buckets[name] |
15 | 16 | }
|
16 | 17 |
|
17 | 18 | // @ts-ignore
|
18 | 19 | const binding = process.env[name] || globalThis.__env__?.[name] || globalThis[name]
|
19 | 20 | if (!binding) {
|
20 | 21 | throw createError(`Missing Cloudflare R2 binding ${name}`)
|
21 | 22 | }
|
22 |
| - _blobs[name] = binding as R2Bucket |
| 23 | + _r2_buckets[name] = binding as R2Bucket |
23 | 24 |
|
24 |
| - return _blobs[name] |
| 25 | + return _r2_buckets[name] |
25 | 26 | }
|
26 | 27 |
|
27 | 28 | 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 | + } |
29 | 34 |
|
30 | 35 | 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 |
42 | 42 | })
|
| 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) |
43 | 61 |
|
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 |
61 | 64 | }
|
62 |
| - }, |
63 |
| - async get (key: string) { |
64 |
| - if (proxy) { |
65 |
| - const query: Record<string, any> = {} |
66 | 65 |
|
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) |
71 | 77 |
|
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 | + } |
75 | 81 |
|
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) |
79 | 84 |
|
80 |
| - return object.body.getReader() |
81 |
| - } |
| 85 | + return object.body |
82 | 86 | },
|
83 | 87 | 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) { |
85 | 89 | // 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 | + } |
96 | 101 |
|
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 }) |
98 | 103 |
|
99 |
| - return mapR2ObjectToBlob(object) |
100 |
| - } |
| 104 | + return mapR2ObjectToBlob(object) |
101 | 105 | },
|
102 | 106 | async delete (key: string) {
|
103 |
| - if (proxy) { |
| 107 | + if (proxyURL) { |
104 | 108 | const query: Record<string, any> = {}
|
105 | 109 |
|
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 }) |
111 | 111 | }
|
| 112 | + // Use R2 binding |
| 113 | + return await bucket.delete(key) |
112 | 114 | }
|
113 | 115 | }
|
114 | 116 | }
|
@@ -140,6 +142,22 @@ function getContentType (pathOrExtension?: string) {
|
140 | 142 | // return metadata
|
141 | 143 | // }
|
142 | 144 |
|
| 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 | + |
143 | 161 | function mapR2ObjectToBlob (object: R2Object): BlobObject {
|
144 | 162 | return {
|
145 | 163 | pathname: object.key,
|
|
0 commit comments