Skip to content

Commit c97f5a9

Browse files
committed
chore(storage): move bucket crud logic in useBlob
1 parent a956284 commit c97f5a9

File tree

6 files changed

+91
-70
lines changed

6 files changed

+91
-70
lines changed

_nuxthub/server/utils/bucket.ts

+82-26
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import type { R2Bucket, R2ListOptions } from '@cloudflare/workers-types/experimental'
2+
import type { MultiPartData } from 'h3'
23
import mime from 'mime'
34
import { imageMeta } from 'image-meta'
45
import { defu } from 'defu'
6+
import { randomUUID } from 'uncrypto'
57

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

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

27-
export async function serveFiles (bucket: R2Bucket, options: R2ListOptions = {}) {
28-
const resolvedOptions = defu(options, {
29-
limit: 500,
30-
include: ['httpMetadata' as const, 'customMetadata' as const],
31-
})
32-
33-
// https://developers.cloudflare.com/r2/api/workers/workers-api-reference/#r2listoptions
34-
const listed = await bucket.list(resolvedOptions)
35-
let truncated = listed.truncated
36-
let cursor = listed.truncated ? listed.cursor : undefined
37-
38-
while (truncated) {
39-
const next = await bucket.list({
40-
...options,
41-
cursor: cursor,
42-
})
43-
listed.objects.push(...next.objects)
44-
45-
truncated = next.truncated
46-
cursor = next.truncated ? next.cursor : undefined
47-
}
29+
export function useBlob (name: string = '') {
30+
const isProxy = import.meta.dev && process.env.NUXT_HUB_URL
31+
32+
return {
33+
async list (options: R2ListOptions = {}) {
34+
if (isProxy) {
35+
// TODO
36+
} else {
37+
const bucket = useBucket(name)
38+
39+
const resolvedOptions = defu(options, {
40+
limit: 500,
41+
include: ['httpMetadata' as const, 'customMetadata' as const],
42+
})
43+
44+
// https://developers.cloudflare.com/r2/api/workers/workers-api-reference/#r2listoptions
45+
const listed = await bucket.list(resolvedOptions)
46+
let truncated = listed.truncated
47+
let cursor = listed.truncated ? listed.cursor : undefined
48+
49+
while (truncated) {
50+
const next = await bucket.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
61+
}
62+
},
63+
async get (key: string) {
64+
if (isProxy) {
65+
// TODO
66+
} else {
67+
const bucket = useBucket(name)
68+
const object = await bucket.get(key)
4869

49-
return listed.objects
70+
if (!object) {
71+
throw createError({ message: 'File not found', statusCode: 404 })
72+
}
73+
74+
setHeader(useEvent(), 'Content-Type', object.httpMetadata!.contentType!)
75+
setHeader(useEvent(), 'Content-Length', object.size)
76+
77+
return object.body.getReader()
78+
}
79+
},
80+
async put (file: MultiPartData) {
81+
if (isProxy) {
82+
// TODO
83+
} else {
84+
const bucket = useBucket(name)
85+
86+
const type = file.type || getContentType(file.filename)
87+
const extension = getExtension(type)
88+
// TODO: ensure filename unicity
89+
const filename = [randomUUID(), extension].filter(Boolean).join('.')
90+
const httpMetadata = { contentType: type }
91+
const customMetadata = getMetadata(type, file.data)
92+
93+
return await bucket.put(filename, toArrayBuffer(file.data), { httpMetadata, customMetadata })
94+
}
95+
},
96+
async delete (key: string) {
97+
if (isProxy) {
98+
// TODO
99+
} else {
100+
const bucket = useBucket(name)
101+
102+
return await bucket.delete(key)
103+
}
104+
}
105+
}
50106
}
51107

52-
export function getContentType (pathOrExtension?: string) {
108+
function getContentType (pathOrExtension?: string) {
53109
return (pathOrExtension && mime.getType(pathOrExtension)) || 'application/octet-stream'
54110
}
55111

56-
export function getExtension (type?: string) {
112+
function getExtension (type?: string) {
57113
return (type && mime.getExtension(type)) || ''
58114
}
59115

60-
export function getMetadata (type: string, buffer: Buffer) {
116+
function getMetadata (type: string, buffer: Buffer) {
61117
if (type.startsWith('image/')) {
62118
return imageMeta(buffer) as Record<string, any>
63119
} else {
64120
return {}
65121
}
66122
}
67123

68-
export function toArrayBuffer (buffer: Buffer) {
124+
function toArrayBuffer (buffer: Buffer) {
69125
const arrayBuffer = new ArrayBuffer(buffer.length)
70126
const view = new Uint8Array(arrayBuffer)
71127
for (let i = 0; i < buffer.length; ++i) {

server/api/bucket-test.ts

-11
This file was deleted.

server/api/storage/[key].delete.ts

+2-5
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,8 @@ import { useValidatedParams, z } from 'h3-zod'
33
export default eventHandler(async (event) => {
44
await requireUserSession(event)
55
const { key } = await useValidatedParams(event, {
6-
key: z.string().min(1).max(100)
6+
key: z.string().min(1)
77
})
88

9-
// Delete entry for the current user
10-
const bucket = useBucket()
11-
12-
return bucket.delete(key)
9+
return useBlob().delete(key)
1310
})

server/api/storage/[key].get.ts

+2-15
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,9 @@ import { useValidatedParams, z } from 'h3-zod'
22

33
export default eventHandler(async (event) => {
44
await requireUserSession(event)
5-
65
const { key } = await useValidatedParams(event, {
7-
key: z.string().min(1).max(100)
6+
key: z.string().min(1)
87
})
98

10-
// List files for the current user
11-
12-
const bucket = useBucket()
13-
const object = await bucket.get(key)
14-
15-
if (!object) {
16-
throw createError({ message: 'File not found', statusCode: 404 })
17-
}
18-
19-
setHeader(event, 'Content-Type', object.httpMetadata!.contentType!)
20-
setHeader(event, 'Content-Length', object.size)
21-
22-
return object.body.getReader()
9+
return useBlob().get(key)
2310
})

server/api/storage/index.get.ts

+1-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
export default eventHandler(async (event) => {
22
await requireUserSession(event)
33

4-
// List files for the current user
5-
6-
const bucket = useBucket()
7-
return serveFiles(bucket)
4+
return useBlob().list()
85
})

server/api/storage/index.put.ts

+4-9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { randomUUID } from 'uncrypto'
2-
31
export default eventHandler(async (event) => {
42
await requireUserSession(event)
53

@@ -10,17 +8,14 @@ export default eventHandler(async (event) => {
108
if (!files) {
119
throw createError({ statusCode: 400, message: 'Missing files' })
1210
}
11+
12+
const { put } = useBlob()
13+
1314
const objects = []
1415

1516
try {
1617
for (const file of files) {
17-
const type = file.type || getContentType(file.filename)
18-
const extension = getExtension(type)
19-
// TODO: ensure filename unicity
20-
const filename = [randomUUID(), extension].filter(Boolean).join('.')
21-
const httpMetadata = { contentType: type }
22-
const customMetadata = getMetadata(type, file.data)
23-
const object = await useBucket().put(filename, toArrayBuffer(file.data), { httpMetadata, customMetadata })
18+
const object = await put(file)
2419
objects.push(object)
2520
}
2621
} catch (e: any) {

0 commit comments

Comments
 (0)