Skip to content

Commit 437b0bd

Browse files
farnabazatinux
andauthored
feat: handleUpload() util and useUpload() composable (#99)
Co-authored-by: Sébastien Chopin <seb@nuxt.com>
1 parent 91b0910 commit 437b0bd

File tree

7 files changed

+246
-66
lines changed

7 files changed

+246
-66
lines changed

docs/content/docs/2.storage/3.blob.md

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,9 @@ export default eventHandler(async (event) => {
151151
::field{name="addRandomSuffix" type="Boolean"}
152152
If `true`, a random suffix will be added to the blob's name. Defaults to `false`.
153153
::
154+
::field{name="prefix" type="string"}
155+
The prefix to use for the blob pathname.
156+
::
154157
::
155158
::
156159

@@ -194,6 +197,56 @@ You can also use the `delete()` method as alias of `del()`.
194197

195198
Returns nothing.
196199

200+
### `handleUpload()`
201+
202+
`handleUpload` is a all in one function to validate a `Blob` by checking its size and type and upload it to the storage.
203+
It can used to handle file uploads in API routes.
204+
205+
```ts [server/api/blob.put.ts]
206+
export default eventHandler(async (event) => {
207+
return hubBlob().handleUpload(event, {
208+
formKey: 'file', // read file or files form the `formKey` field of request body (body should be a `FormData` object)
209+
multiple: true // when `true`, the `formKey` field will be an array of `Blob` objects
210+
})
211+
})
212+
```
213+
214+
#### Params
215+
216+
::field-group
217+
::field{name="formKey" type="string"}
218+
The form key to read the file from. Defaults to `'file'`.
219+
::
220+
::field{name="multiple" type="boolean"}
221+
When `true`, the `formKey` field will be an array of `Blob` objects.
222+
::
223+
::field{name="maxSize" type="BlobSize"}
224+
The maximum size of the file, should be: :br
225+
(`1` | `2` | `4` | `8` | `16` | `32` | `64` | `128` | `256` | `512` | `1024`) + (`B` | `KB` | `MB` | `GB`) :br
226+
e.g. `'512KB'`, `'1MB'`, `'2GB'`, etc.
227+
::
228+
::field{name="types" type="BlobType[]"}
229+
Allowed types of the file, e.g. `['image/jpeg']`.
230+
::
231+
::field{name="contentType" type="string"}
232+
The content type of the blob.
233+
::
234+
::field{name="contentLength" type="string"}
235+
The content length of the blob.
236+
::
237+
::field{name="addRandomSuffix" type="boolean"}
238+
If `true`, a random suffix will be added to the blob's name. Defaults to `false`.
239+
::
240+
::field{name="prefix" type="string"}
241+
The prefix to use for the blob pathname.
242+
::
243+
::
244+
245+
#### Return
246+
247+
Returns a [`BlobObject`](#blobobject) or an array of [`BlobObject`](#blobobject) if `multiple` is `true`.
248+
249+
Throws an error if `file` doesn't meet the requirements.
197250

198251
## `ensureBlob()`
199252

@@ -229,6 +282,51 @@ Returns nothing.
229282

230283
Throws an error if `file` doesn't meet the requirements.
231284

285+
## Composables
286+
287+
### `useUpload()`
288+
289+
`useUpload` is to handle file uploads in your Nuxt application.
290+
291+
```vue
292+
<script setup lang="ts">
293+
const upload = useUpload('/api/blob', { method: 'PUT' })
294+
295+
async function onFileSelect({ target }: Event) {
296+
const uploadedFiles = await upload(target as HTMLInputElement)
297+
298+
// file uploaded successfully
299+
}
300+
</script>
301+
302+
<template>
303+
<input
304+
accept="jpeg, png"
305+
type="file"
306+
name="file"
307+
multiple
308+
@change="onFileSelect"
309+
>
310+
</template>
311+
```
312+
313+
#### Params
314+
315+
::field-group
316+
::field{name="apiBase" type="string" required}
317+
The base URL of the upload API.
318+
::
319+
::field{name="options" type="Object" required}
320+
Optionally, you can pass Fetch options to the request. Read more about Fetch API [here](https://developer.mozilla.org/en-US/docs/Web/API/fetch#options).
321+
::field{name="formKey" type="string"}
322+
The key to add the file/files to the request form. Defaults to `'file'`.
323+
::
324+
::field{name="multiple" type="boolean"}
325+
Whether to allow multiple files to be uploaded. Defaults to `true`.
326+
::
327+
::
328+
::
329+
232330
## Types
233331

234332
### `BlobObject`

playground/pages/blob.vue

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<script setup lang="ts">
22
const loading = ref(false)
33
const newFilesValue = ref<File[]>([])
4-
const uploadRef = ref()
4+
const uploadRef = ref<HTMLInputElement>()
55
const folded = ref(false)
66
const prefixes = ref([])
77
const limit = ref(5)
@@ -20,7 +20,7 @@ const files = computed(() => blobData.value?.blobs || [])
2020
const folders = computed(() => blobData.value?.folders || [])
2121
2222
async function loadMore() {
23-
if (!blobData.value.hasMore) return
23+
if (!blobData.value?.hasMore) return
2424
const nextPage = await $fetch('/api/blob', {
2525
params: {
2626
folded: folded.value,
@@ -45,15 +45,8 @@ async function addFile() {
4545
loading.value = true
4646
4747
try {
48-
const formData = new FormData()
49-
newFilesValue.value.forEach(file => formData.append('files', file))
50-
const uploadedFiles = await $fetch('/api/blob', {
51-
method: 'PUT',
52-
params: {
53-
prefix: String(prefix.value || '')
54-
},
55-
body: formData
56-
})
48+
const uploadedFiles = await useUpload('/api/blob', { method: 'PUT', prefix: String(prefix.value || '') })(newFilesValue.value)
49+
5750
files.value!.push(...uploadedFiles)
5851
toast.add({ title: `File${uploadedFiles.length > 1 ? 's' : ''} uploaded.` })
5952
newFilesValue.value = []
@@ -80,7 +73,7 @@ async function deleteFile(pathname: string) {
8073
try {
8174
await $fetch(`/api/blob/${pathname}`, { method: 'DELETE' })
8275
83-
blobData.value.blobs = blobData.value.blobs!.filter(t => t.pathname !== pathname)
76+
blobData.value!.blobs = blobData.value!.blobs!.filter(t => t.pathname !== pathname)
8477
8578
toast.add({ title: `File "${pathname}" deleted.` })
8679
} catch (err: any) {
Lines changed: 5 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,8 @@
11
export default eventHandler(async (event) => {
22
const { prefix } = getQuery(event)
3-
const form = await readFormData(event)
4-
const files = form.getAll('files') as File[]
5-
if (!files) {
6-
throw createError({ statusCode: 400, message: 'Missing files' })
7-
}
8-
9-
const { put } = hubBlob()
10-
const objects: BlobObject[] = []
11-
try {
12-
for (const file of files) {
13-
// const object = await put(file.name, file, { addRandomSuffix: true })
14-
const object = await put(file.name, file, {
15-
prefix: String(prefix || '')
16-
})
17-
objects.push(object)
18-
}
19-
} catch (e: any) {
20-
throw createError({
21-
statusCode: 500,
22-
message: `Storage error: ${e.message}`
23-
})
24-
}
25-
26-
return objects
3+
return hubBlob().handleUpload(event, {
4+
formKey: 'file', // default
5+
multiple: true, // default
6+
prefix: String(prefix || '')
7+
})
278
})

src/module.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { mkdir, writeFile, readFile } from 'node:fs/promises'
22
import { execSync } from 'node:child_process'
33
import { argv } from 'node:process'
4-
import { defineNuxtModule, createResolver, logger, addServerScanDir, installModule, addServerImportsDir } from '@nuxt/kit'
4+
import { defineNuxtModule, createResolver, logger, addServerScanDir, installModule, addServerImportsDir, addImportsDir } from '@nuxt/kit'
55
import { join } from 'pathe'
66
import { defu } from 'defu'
77
import { findWorkspaceDir } from 'pkg-types'
@@ -160,9 +160,12 @@ export default defineNuxtModule<ModuleOptions>({
160160
? '#internal/nitro/routes/openapi'
161161
: resolve('./runtime/templates/openapi')
162162

163-
// Register composables
163+
// Register server utils
164164
addServerImportsDir(resolve('./runtime/server/utils'))
165165

166+
// Register client composables
167+
addImportsDir(resolve('./runtime/compsables'))
168+
166169
// nuxt prepare, stop here
167170
if (nuxt.options._prepare) {
168171
return

src/runtime/compsables/useUpload.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import type { FetchOptions } from 'ofetch'
2+
3+
interface UploadOptions extends FetchOptions {
4+
/**
5+
* The key to add the file/files to the request form.
6+
* @default 'file'
7+
*/
8+
formKey?: string
9+
10+
/**
11+
* Whether to allow multiple files to be uploaded.
12+
* @default true
13+
*/
14+
multiple?: boolean
15+
16+
}
17+
18+
export function useUpload(apiBase: string, options: UploadOptions & { multiple: false }): (data: FileList | HTMLInputElement | File[] | File) => Promise<BlobObject>
19+
export function useUpload(apiBase: string, options: UploadOptions): ((data: File) => Promise<BlobObject>) & ((data: FileList | HTMLInputElement | File[]) => Promise<BlobObject[]>)
20+
export function useUpload(apiBase: string, options: UploadOptions = {}) {
21+
const { formKey = 'file', multiple = true, method, ...fetchOptions } = options || {}
22+
23+
async function upload(data: File): Promise<BlobObject>
24+
async function upload(data: FileList | HTMLInputElement | File[] | File): Promise<BlobObject[] | BlobObject> {
25+
if (data instanceof HTMLInputElement) {
26+
data = data.files!
27+
}
28+
if (data instanceof File) {
29+
data = [data]
30+
}
31+
if (!data || !data.length) {
32+
throw createError({ statusCode: 400, message: 'Missing files' })
33+
}
34+
35+
const formData = new FormData()
36+
if (multiple) {
37+
for (const file of data) {
38+
formData.append(formKey, file)
39+
}
40+
} else {
41+
formData.append(formKey, data[0])
42+
}
43+
44+
return $fetch<BlobObject[]>(apiBase, {
45+
...fetchOptions,
46+
method: (method || 'POST') as any,
47+
body: formData
48+
}).then(result => (multiple === false || data instanceof File) ? result[0] : result)
49+
}
50+
51+
return upload
52+
}
Lines changed: 7 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,14 @@
1-
import { createError, eventHandler, readFormData } from 'h3'
2-
import { hubBlob, type BlobObject } from '../../../utils/blob'
1+
import { eventHandler } from 'h3'
2+
import { hubBlob } from '../../../utils/blob'
33
import { requireNuxtHubAuthorization } from '../../../utils/auth'
4-
import { requireNuxtHubFeature } from '../../../utils/features'
54

65
export default eventHandler(async (event) => {
76
await requireNuxtHubAuthorization(event)
8-
requireNuxtHubFeature('blob')
9-
107
const query = getQuery(event)
11-
const options = { ...query } as BlobPutOptions
12-
13-
const form = await readFormData(event)
14-
const files = form.getAll('files') as File[]
15-
if (!files) {
16-
throw createError({ statusCode: 400, message: 'Missing files' })
17-
}
18-
const blob = hubBlob()
19-
20-
const objects: BlobObject[] = []
21-
try {
22-
for (const file of files) {
23-
const object = await blob.put(file.name!, file, options)
24-
objects.push(object)
25-
}
26-
} catch (e: any) {
27-
throw createError({
28-
statusCode: 500,
29-
message: `Storage error: ${e.message}`
30-
})
31-
}
328

33-
return objects
9+
return hubBlob().handleUpload(event, {
10+
formKey: 'files',
11+
multiple: true,
12+
...query
13+
})
3414
})

0 commit comments

Comments
 (0)