Skip to content

Commit 03cc324

Browse files
authored
feat(blob): add support for access and improve s3 driver (#709)
1 parent 2a37e09 commit 03cc324

File tree

8 files changed

+37
-12
lines changed

8 files changed

+37
-12
lines changed

docs/content/docs/2.features/0.blob.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,9 @@ async function uploadImage (e: Event) {
372372
::field{name="options" type="Object"}
373373
The put options. Any other provided field will be stored in the blob's metadata.
374374
::collapsible
375+
::field{name="access" type="'public' | 'private'"}
376+
The access level of the blob. Can be `'public'` or `'private'`. Note that only S3 driver supports this option currently.
377+
::
375378
::field{name="contentType" type="String"}
376379
The content type of the blob. If not given, it will be inferred from the Blob or the file extension.
377380
::

playground/server/api/blob/multipart/[action]/[...pathname].ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { blob } from 'hub:blob'
22

33
export default eventHandler(async (event) => {
44
return await blob.handleMultipartUpload(event, {
5-
addRandomSuffix: true
5+
addRandomSuffix: true,
6+
access: 'public'
67
})
78
})

src/blob/lib/drivers/cloudflare-r2.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,9 @@ export function createDriver(options: CloudflareDriverOptions): BlobDriver<Cloud
8686

8787
const contentType = options?.contentType || (body instanceof Blob ? body.type : undefined) || getContentType(pathname)
8888

89+
if (options?.access) {
90+
console.warn('Setting access level for blob in Cloudflare R2 is not supported, it will be ignored')
91+
}
8992
const r2Object = await bucket.put(pathname, body as any, {
9093
httpMetadata: {
9194
contentType

src/blob/lib/drivers/s3.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,15 +78,13 @@ function mapS3ObjectToBlob(object: S3Object): BlobObject {
7878
export function createDriver(options: S3DriverOptions): BlobDriver<S3DriverOptions> {
7979
// Use path-style for custom endpoints (S3-compatible services like MinIO, R2, etc.)
8080
// Use virtual-hosted style for AWS S3
81-
const region = options.region || 'auto'
82-
const usePathStyle = !!options.endpoint
83-
const baseEndpoint = options.endpoint ?? `https://${options.bucket}.s3.${region}.amazonaws.com`
84-
const bucketUrl = usePathStyle ? `${baseEndpoint}/${options.bucket}` : baseEndpoint
81+
const baseEndpoint = options.endpoint ?? `https://${options.bucket}.s3.${options.region}.amazonaws.com`
82+
const bucketUrl = options.endpoint && options.bucket ? `${baseEndpoint}/${options.bucket}` : baseEndpoint
8583

8684
const aws = new AwsClient({
8785
accessKeyId: options.accessKeyId,
8886
secretAccessKey: options.secretAccessKey,
89-
region,
87+
region: options.region,
9088
service: 's3'
9189
})
9290

@@ -188,6 +186,11 @@ export function createDriver(options: S3DriverOptions): BlobDriver<S3DriverOptio
188186
}
189187
}
190188

189+
// Add support for public/private access
190+
if (putOptions?.access === 'public') {
191+
headers['x-amz-acl'] = 'public-read'
192+
}
193+
191194
const res = await aws.fetch(`${bucketUrl}/${encodeURI(decodeURIComponent(pathname))}`, {
192195
method: 'PUT',
193196
headers,

src/blob/lib/drivers/vercel-blob.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { put as vercelPut, del as vercelDel, head as vercelHead, list as vercelList, createMultipartUpload as vercelCreateMultipartUpload, uploadPart as vercelUploadPart, completeMultipartUpload as vercelCompleteMultipartUpload } from '@vercel/blob'
22
import { handleUpload, type HandleUploadBody } from '@vercel/blob/client'
33
import type { PutBlobResult, ListBlobResultBlob } from '@vercel/blob'
4-
import { readBody, type H3Event } from 'h3'
4+
import { createError, readBody, type H3Event } from 'h3'
55
import type { BlobDriver, BlobPutBody } from './types'
66
import type { BlobListOptions, BlobListResult, BlobMultipartOptions, BlobMultipartUpload, BlobObject, BlobPutOptions, BlobUploadedPart, HandleMPUResponse } from '../../types'
77
import { getContentType } from '../utils'
@@ -81,6 +81,12 @@ export function createDriver(options: VercelDriverOptions = {}): BlobDriver<Verc
8181
async put(pathname: string, body: BlobPutBody, putOptions?: BlobPutOptions): Promise<BlobObject> {
8282
const contentType = putOptions?.contentType || (body instanceof Blob ? body.type : undefined) || getContentType(pathname)
8383

84+
if (putOptions?.access === 'private') {
85+
throw createError({
86+
statusCode: 400,
87+
statusMessage: 'Private access is not yet supported for Vercel Blob'
88+
})
89+
}
8490
const result = await vercelPut(pathname, body as any, {
8591
token,
8692
access: access as 'public',

src/blob/lib/storage.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export function createBlobStorage(driver: BlobDriver): BlobStorage {
5555

5656
async put(pathname: string, body: string | ReadableStream<any> | ArrayBuffer | ArrayBufferView | Blob, options: BlobPutOptions = {}) {
5757
pathname = decodeURIComponent(pathname)
58-
const { contentType: optionsContentType, contentLength, addRandomSuffix, prefix, customMetadata } = options
58+
const { contentType: optionsContentType, contentLength, addRandomSuffix, prefix, customMetadata, access } = options
5959
const contentType = optionsContentType || (body as Blob).type || getContentType(pathname)
6060

6161
const { dir, ext, name: filename } = parse(pathname)
@@ -72,7 +72,8 @@ export function createBlobStorage(driver: BlobDriver): BlobStorage {
7272
return driver.put(pathname, body, {
7373
contentType,
7474
contentLength,
75-
customMetadata
75+
customMetadata,
76+
access
7677
})
7778
},
7879

src/blob/setup.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,16 @@ export function resolveBlobConfig(hub: HubConfig, deps: Record<string, string>):
2424

2525
// Otherwise hub.blob is set to true, so we need to resolve the config
2626
// AWS S3
27-
if (process.env.S3_ACCESS_KEY_ID && process.env.S3_SECRET_ACCESS_KEY && process.env.S3_BUCKET && process.env.S3_REGION) {
27+
if (process.env.S3_ACCESS_KEY_ID && process.env.S3_SECRET_ACCESS_KEY && (process.env.S3_BUCKET || process.env.S3_ENDPOINT)) {
2828
if (!deps['aws4fetch']) {
2929
log.error('Please run `npx nypm i aws4fetch` to use S3')
3030
}
3131
return defu(hub.blob, {
3232
driver: 's3',
3333
accessKeyId: process.env.S3_ACCESS_KEY_ID,
3434
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
35-
bucket: process.env.S3_BUCKET,
36-
region: process.env.S3_REGION,
35+
bucket: process.env.S3_BUCKET || '',
36+
region: process.env.S3_REGION || 'auto',
3737
endpoint: process.env.S3_ENDPOINT
3838
}) as ResolvedBlobConfig
3939
}

src/blob/types/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,10 @@ export interface BlobListOptions {
106106
}
107107

108108
export interface BlobPutOptions {
109+
/**
110+
* The access level of the blob.
111+
*/
112+
access?: 'public' | 'private'
109113
/**
110114
* The content type of the blob.
111115
*/
@@ -134,6 +138,10 @@ export interface BlobPutOptions {
134138
}
135139

136140
export interface BlobMultipartOptions {
141+
/**
142+
* The access level of the blob.
143+
*/
144+
access?: 'public' | 'private'
137145
/**
138146
* The content type of the blob.
139147
*/

0 commit comments

Comments
 (0)