diff --git a/docs/content/docs/2.features/0.blob.md b/docs/content/docs/2.features/0.blob.md index f7d59c9b..537a61b3 100644 --- a/docs/content/docs/2.features/0.blob.md +++ b/docs/content/docs/2.features/0.blob.md @@ -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. :: diff --git a/playground/server/api/blob/multipart/[action]/[...pathname].ts b/playground/server/api/blob/multipart/[action]/[...pathname].ts index 22196a37..68042b9d 100644 --- a/playground/server/api/blob/multipart/[action]/[...pathname].ts +++ b/playground/server/api/blob/multipart/[action]/[...pathname].ts @@ -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' }) }) diff --git a/src/blob/lib/drivers/cloudflare-r2.ts b/src/blob/lib/drivers/cloudflare-r2.ts index 6cd0dac1..b99d295f 100644 --- a/src/blob/lib/drivers/cloudflare-r2.ts +++ b/src/blob/lib/drivers/cloudflare-r2.ts @@ -86,6 +86,9 @@ export function createDriver(options: CloudflareDriverOptions): BlobDriver { // 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' }) @@ -188,6 +186,11 @@ export function createDriver(options: S3DriverOptions): BlobDriver { 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', diff --git a/src/blob/lib/storage.ts b/src/blob/lib/storage.ts index aaf217f0..a16b00bb 100644 --- a/src/blob/lib/storage.ts +++ b/src/blob/lib/storage.ts @@ -55,7 +55,7 @@ export function createBlobStorage(driver: BlobDriver): BlobStorage { async put(pathname: string, body: string | ReadableStream | 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) @@ -72,7 +72,8 @@ export function createBlobStorage(driver: BlobDriver): BlobStorage { return driver.put(pathname, body, { contentType, contentLength, - customMetadata + customMetadata, + access }) }, diff --git a/src/blob/setup.ts b/src/blob/setup.ts index c9c8ea8c..d816d4c1 100644 --- a/src/blob/setup.ts +++ b/src/blob/setup.ts @@ -24,7 +24,7 @@ export function resolveBlobConfig(hub: HubConfig, deps: Record): // 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') } @@ -32,8 +32,8 @@ export function resolveBlobConfig(hub: HubConfig, deps: Record): 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 } diff --git a/src/blob/types/index.ts b/src/blob/types/index.ts index 54189a11..c2785631 100644 --- a/src/blob/types/index.ts +++ b/src/blob/types/index.ts @@ -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. */ @@ -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. */