1
1
import type { R2Bucket , R2ListOptions } from '@cloudflare/workers-types/experimental'
2
- import type { MultiPartData } from 'h3'
2
+ import type { EventHandlerRequest , H3Event } from 'h3'
3
3
import mime from 'mime'
4
4
import { imageMeta } from 'image-meta'
5
5
import { defu } from 'defu'
6
6
import { randomUUID } from 'uncrypto'
7
+ import { parse } from 'pathe'
8
+ import { joinURL } from 'ufo'
7
9
8
10
const _buckets : Record < string , R2Bucket > = { }
9
11
10
- export function useBucket ( name : string = '' ) {
11
- const bucketName = name ? `BUCKET_ ${ name . toUpperCase ( ) } ` : 'BUCKET'
12
+ function useBucket ( ) {
13
+ const bucketName = 'BUCKET'
12
14
if ( _buckets [ bucketName ] ) {
13
15
return _buckets [ bucketName ]
14
16
}
@@ -26,18 +28,17 @@ export function useBucket (name: string = '') {
26
28
return _buckets [ bucketName ]
27
29
}
28
30
29
- export function useBlob ( name : string = '' ) {
31
+ export function useBlob ( ) {
30
32
const proxy = import . meta. dev && process . env . NUXT_HUB_URL
31
33
32
34
return {
33
35
async list ( options : R2ListOptions = { } ) {
34
36
if ( proxy ) {
35
37
const query : Record < string , any > = { }
36
- if ( name ) { query . name = name }
37
38
38
39
return $fetch < R2Object [ ] > ( '/api/_hub/bucket' , { baseURL : proxy , method : 'GET' , query } )
39
40
} else {
40
- const bucket = useBucket ( name )
41
+ const bucket = useBucket ( )
41
42
42
43
const resolvedOptions = defu ( options , {
43
44
limit : 500 ,
@@ -60,55 +61,55 @@ export function useBlob (name: string = '') {
60
61
cursor = next . truncated ? next . cursor : undefined
61
62
}
62
63
63
- return listed . objects
64
+ return listed . objects . map ( mapR2ObjectToBlob )
64
65
}
65
66
} ,
66
67
async get ( key : string ) {
67
68
if ( proxy ) {
68
69
const query : Record < string , any > = { }
69
- if ( name ) { query . name = name }
70
70
71
71
return $fetch < ReadableStreamDefaultReader < any > > ( `/api/_hub/bucket/${ key } ` , { baseURL : proxy , method : 'GET' , query } )
72
72
} else {
73
- const bucket = useBucket ( name )
73
+ const bucket = useBucket ( )
74
74
const object = await bucket . get ( key )
75
75
76
76
if ( ! object ) {
77
77
throw createError ( { message : 'File not found' , statusCode : 404 } )
78
78
}
79
79
80
- // setHeader(useEvent(), 'Content-Type', object.httpMetadata!.contentType!)
81
- // setHeader(useEvent(), 'Content-Length', object.size)
80
+ // FIXME
81
+ setHeader ( useEvent ( ) , 'Content-Type' , object . httpMetadata ! . contentType ! )
82
+ setHeader ( useEvent ( ) , 'Content-Length' , object . size )
82
83
83
84
return object . body . getReader ( )
84
85
}
85
86
} ,
86
- async put ( file : MultiPartData ) {
87
+ async put ( pathname : string , body : string | ReadableStream < any > | ArrayBuffer | ArrayBufferView | Blob , options : { contentType ?: string , addRandomSuffix ?: boolean , [ key : string ] : any } = { addRandomSuffix : true } ) {
87
88
if ( proxy ) {
88
89
// TODO
89
90
} else {
90
- const bucket = useBucket ( name )
91
-
92
- const type = file . type || getContentType ( file . filename )
93
- // TODO: ensure key unicity
94
- const key = randomUUID ( )
95
- const httpMetadata = { contentType : type }
96
- const customMetadata : Record < string , any > = {
97
- ...getMetadata ( type , file . data ) ,
98
- filename : file . filename
91
+ const bucket = useBucket ( )
92
+ const fileContentType = ( body as Blob ) . type || getContentType ( pathname )
93
+ const { contentType, addRandomSuffix, ...customMetadata } = options
94
+
95
+ const { dir, ext, name : filename } = parse ( pathname )
96
+ let key = pathname
97
+ if ( addRandomSuffix ) {
98
+ key = joinURL ( dir === '.' ? '' : dir , `${ filename } -${ randomUUID ( ) . split ( '-' ) [ 0 ] } ${ ext } ` )
99
99
}
100
100
101
- return await bucket . put ( key , toArrayBuffer ( file . data ) , { httpMetadata, customMetadata } )
101
+ const object = await bucket . put ( key , body as any , { httpMetadata : { contentType : contentType || fileContentType } , customMetadata } )
102
+
103
+ return mapR2ObjectToBlob ( object )
102
104
}
103
105
} ,
104
106
async delete ( key : string ) {
105
107
if ( proxy ) {
106
108
const query : Record < string , any > = { }
107
- if ( name ) { query . name = name }
108
109
109
110
return $fetch < void > ( `/api/_hub/bucket/${ key } ` , { baseURL : proxy , method : 'DELETE' , query } )
110
111
} else {
111
- const bucket = useBucket ( name )
112
+ const bucket = useBucket ( )
112
113
113
114
return await bucket . delete ( key )
114
115
}
@@ -131,15 +132,19 @@ function getContentType (pathOrExtension?: string) {
131
132
return ( pathOrExtension && mime . getType ( pathOrExtension ) ) || 'application/octet-stream'
132
133
}
133
134
134
- function getMetadata ( type : string , buffer : Buffer ) {
135
- if ( type . startsWith ( 'image/' ) ) {
136
- return imageMeta ( buffer ) as Record < string , any >
137
- } else {
138
- return { }
135
+ export function getMetadata ( filename : string , buffer : Buffer ) {
136
+ const metadata : Record < string , any > = {
137
+ contentType : getContentType ( filename )
138
+ }
139
+
140
+ if ( metadata . contentType . startsWith ( 'image/' ) ) {
141
+ Object . assign ( metadata , imageMeta ( buffer ) )
139
142
}
143
+
144
+ return metadata
140
145
}
141
146
142
- function toArrayBuffer ( buffer : Buffer ) {
147
+ export function toArrayBuffer ( buffer : Buffer ) {
143
148
const arrayBuffer = new ArrayBuffer ( buffer . length )
144
149
const view = new Uint8Array ( arrayBuffer )
145
150
for ( let i = 0 ; i < buffer . length ; ++ i ) {
@@ -153,4 +158,13 @@ export async function readFiles (event: H3Event<EventHandlerRequest>) {
153
158
154
159
// Filter only files
155
160
return files . filter ( ( file ) => Boolean ( file . filename ) )
156
- }
161
+ }
162
+
163
+ function mapR2ObjectToBlob ( object : R2Object ) {
164
+ return {
165
+ pathname : object . key ,
166
+ contentType : object . httpMetadata ?. contentType ,
167
+ size : object . size ,
168
+ uploadedAt : object . uploaded ,
169
+ }
170
+ }
0 commit comments