Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/content/docs/2.features/0.blob.md
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,9 @@ async function uploadImage (e: Event) {
::field{name="options" type="Object"}
The put options. Any other provided field will be stored in the blob's metadata.
::collapsible
::field{name="access" type="'public' | 'private'"}
The access level of the blob. Can be `'public'` or `'private'`. Note that only S3 driver supports this option currently.
::
::field{name="contentType" type="String"}
The content type of the blob. If not given, it will be inferred from the Blob or the file extension.
::
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { blob } from 'hub:blob'

export default eventHandler(async (event) => {
return await blob.handleMultipartUpload(event, {
addRandomSuffix: true
addRandomSuffix: true,
access: 'public'
})
})
3 changes: 3 additions & 0 deletions src/blob/lib/drivers/cloudflare-r2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ export function createDriver(options: CloudflareDriverOptions): BlobDriver<Cloud

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

if (options?.access) {
console.warn('Setting access level for blob in Cloudflare R2 is not supported, it will be ignored')
}
const r2Object = await bucket.put(pathname, body as any, {
httpMetadata: {
contentType
Expand Down
13 changes: 8 additions & 5 deletions src/blob/lib/drivers/s3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,15 +78,13 @@ function mapS3ObjectToBlob(object: S3Object): BlobObject {
export function createDriver(options: S3DriverOptions): BlobDriver<S3DriverOptions> {
// Use path-style for custom endpoints (S3-compatible services like MinIO, R2, etc.)
// Use virtual-hosted style for AWS S3
const region = options.region || 'auto'
const usePathStyle = !!options.endpoint
const baseEndpoint = options.endpoint ?? `https://${options.bucket}.s3.${region}.amazonaws.com`
const bucketUrl = usePathStyle ? `${baseEndpoint}/${options.bucket}` : baseEndpoint
const baseEndpoint = options.endpoint ?? `https://${options.bucket}.s3.${options.region}.amazonaws.com`
const bucketUrl = options.endpoint && options.bucket ? `${baseEndpoint}/${options.bucket}` : baseEndpoint

const aws = new AwsClient({
accessKeyId: options.accessKeyId,
secretAccessKey: options.secretAccessKey,
region,
region: options.region,
service: 's3'
})

Expand Down Expand Up @@ -188,6 +186,11 @@ export function createDriver(options: S3DriverOptions): BlobDriver<S3DriverOptio
}
}

// Add support for public/private access
if (putOptions?.access === 'public') {
headers['x-amz-acl'] = 'public-read'
}

const res = await aws.fetch(`${bucketUrl}/${encodeURI(decodeURIComponent(pathname))}`, {
method: 'PUT',
headers,
Expand Down
8 changes: 7 additions & 1 deletion src/blob/lib/drivers/vercel-blob.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
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'
import { handleUpload, type HandleUploadBody } from '@vercel/blob/client'
import type { PutBlobResult, ListBlobResultBlob } from '@vercel/blob'
import { readBody, type H3Event } from 'h3'
import { createError, readBody, type H3Event } from 'h3'
import type { BlobDriver, BlobPutBody } from './types'
import type { BlobListOptions, BlobListResult, BlobMultipartOptions, BlobMultipartUpload, BlobObject, BlobPutOptions, BlobUploadedPart, HandleMPUResponse } from '../../types'
import { getContentType } from '../utils'
Expand Down Expand Up @@ -81,6 +81,12 @@ export function createDriver(options: VercelDriverOptions = {}): BlobDriver<Verc
async put(pathname: string, body: BlobPutBody, putOptions?: BlobPutOptions): Promise<BlobObject> {
const contentType = putOptions?.contentType || (body instanceof Blob ? body.type : undefined) || getContentType(pathname)

if (putOptions?.access === 'private') {
throw createError({
statusCode: 400,
statusMessage: 'Private access is not yet supported for Vercel Blob'
})
}
const result = await vercelPut(pathname, body as any, {
token,
access: access as 'public',
Expand Down
5 changes: 3 additions & 2 deletions src/blob/lib/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export function createBlobStorage(driver: BlobDriver): BlobStorage {

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

const { dir, ext, name: filename } = parse(pathname)
Expand All @@ -72,7 +72,8 @@ export function createBlobStorage(driver: BlobDriver): BlobStorage {
return driver.put(pathname, body, {
contentType,
contentLength,
customMetadata
customMetadata,
access
})
},

Expand Down
6 changes: 3 additions & 3 deletions src/blob/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,16 @@ export function resolveBlobConfig(hub: HubConfig, deps: Record<string, string>):

// Otherwise hub.blob is set to true, so we need to resolve the config
// AWS S3
if (process.env.S3_ACCESS_KEY_ID && process.env.S3_SECRET_ACCESS_KEY && process.env.S3_BUCKET && process.env.S3_REGION) {
if (process.env.S3_ACCESS_KEY_ID && process.env.S3_SECRET_ACCESS_KEY && (process.env.S3_BUCKET || process.env.S3_ENDPOINT)) {
if (!deps['aws4fetch']) {
log.error('Please run `npx nypm i aws4fetch` to use S3')
}
return defu(hub.blob, {
driver: 's3',
accessKeyId: process.env.S3_ACCESS_KEY_ID,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
bucket: process.env.S3_BUCKET,
region: process.env.S3_REGION,
bucket: process.env.S3_BUCKET || '',
region: process.env.S3_REGION || 'auto',
endpoint: process.env.S3_ENDPOINT
}) as ResolvedBlobConfig
}
Expand Down
8 changes: 8 additions & 0 deletions src/blob/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ export interface BlobListOptions {
}

export interface BlobPutOptions {
/**
* The access level of the blob.
*/
access?: 'public' | 'private'
/**
* The content type of the blob.
*/
Expand Down Expand Up @@ -134,6 +138,10 @@ export interface BlobPutOptions {
}

export interface BlobMultipartOptions {
/**
* The access level of the blob.
*/
access?: 'public' | 'private'
/**
* The content type of the blob.
*/
Expand Down
Loading