diff --git a/.changeset/chilled-steaks-peel.md b/.changeset/chilled-steaks-peel.md new file mode 100644 index 000000000..2970121f9 --- /dev/null +++ b/.changeset/chilled-steaks-peel.md @@ -0,0 +1,5 @@ +--- +'@vercel/blob': minor +--- + +Add createFolder method. Warning, if you were using the standard put() method to created fodlers, this will now fail and you must move to createFolder() instead. diff --git a/packages/blob/src/client.ts b/packages/blob/src/client.ts index dde953def..6b20676a4 100644 --- a/packages/blob/src/client.ts +++ b/packages/blob/src/client.ts @@ -20,7 +20,7 @@ import { createCreateMultipartUploaderMethod } from './multipart/create-uploader // This types omits all options that are encoded in the client token. export interface ClientCommonCreateBlobOptions { /** - * Whether the blob should be publicly accessible. Support for private blobs is planned. + * Whether the blob should be publicly accessible. */ access: 'public'; /** @@ -515,3 +515,5 @@ export interface GenerateClientTokenOptions extends BlobCommandOptions { addRandomSuffix?: boolean; cacheControlMaxAge?: number; } + +export { createFolder } from './create-folder'; diff --git a/packages/blob/src/create-folder.ts b/packages/blob/src/create-folder.ts new file mode 100644 index 000000000..2182ee5bf --- /dev/null +++ b/packages/blob/src/create-folder.ts @@ -0,0 +1,41 @@ +import { requestApi } from './api'; +import type { BlobCommandOptions } from './helpers'; +import { putOptionHeaderMap, type PutBlobApiResponse } from './put-helpers'; + +export interface CreateFolderResult { + pathname: string; + url: string; +} + +/** + * Creates a folder in your store. Vercel Blob has no real concept of folders, our file browser on Vercel.com displays folders based on the presence of trailing slashes in the pathname. Unless you are building a file browser system, you probably don't need to use this method. + * + * Use the resulting `url` to delete the folder, just like you would delete a blob. + * @param pathname - Can be user1/ or user1/avatars/ + * @param options - Additional options like `token` + */ +export async function createFolder( + pathname: string, + options: BlobCommandOptions = {}, +): Promise { + const path = pathname.endsWith('/') ? pathname : `${pathname}/`; + + const headers: Record = {}; + + headers[putOptionHeaderMap.addRandomSuffix] = '0'; + + const response = await requestApi( + `/${path}`, + { + method: 'PUT', + headers, + signal: options.abortSignal, + }, + options, + ); + + return { + url: response.url, + pathname: response.pathname, + }; +} diff --git a/packages/blob/src/helpers.ts b/packages/blob/src/helpers.ts index 89ccdea9f..ca51bcc8a 100644 --- a/packages/blob/src/helpers.ts +++ b/packages/blob/src/helpers.ts @@ -16,7 +16,7 @@ export interface BlobCommandOptions { // shared interface for put, copy and multipartUpload export interface CommonCreateBlobOptions extends BlobCommandOptions { /** - * Whether the blob should be publicly accessible. Support for private blobs is planned. + * Whether the blob should be publicly accessible. */ access: 'public'; /** diff --git a/packages/blob/src/index.ts b/packages/blob/src/index.ts index c241040fe..4519945d8 100644 --- a/packages/blob/src/index.ts +++ b/packages/blob/src/index.ts @@ -32,11 +32,11 @@ export type { PutCommandOptions }; * Uploads a blob into your store from your server. * Detailed documentation can be found here: https://vercel.com/docs/storage/vercel-blob/using-blob-sdk#upload-a-blob * - * If you want to upload from the browser directly, check out the documentation for client uploads: https://vercel.com/docs/storage/vercel-blob/using-blob-sdk#client-uploads + * If you want to upload from the browser directly, check out the documentation forAclient uploads: https://vercel.com/docs/storage/vercel-blob/using-blob-sdk#client-uploads * - * @param pathname - The pathname to upload the blob to. For file upload this includes the filename. Pathnames that end with a slash are treated as folder creations. - * @param bodyOrOptions - Either the contents of your blob or the options object. For file uploads this has to be a supported fetch body type https://developer.mozilla.org/en-US/docs/Web/API/fetch#body. For folder creations this is the options object since no body is required. - * @param options - Additional options like `token` or `contentType` for file uploads. For folder creations this argument can be ommited. + * @param pathname - The pathname to upload the blob to, including the extension. This will influence the url of your blob like https://$storeId.public.blob.vercel-storage.com/$pathname. + * @param body - The content of your blob, can be a: string, File, Blob, Buffer or Stream. We support everything fetch supports: https://developer.mozilla.org/en-US/docs/Web/API/RequestInit#body. + * @param options - Additional options like `token` or `contentType`. */ export const put = createPutMethod({ allowedOptions: ['cacheControlMaxAge', 'addRandomSuffix', 'contentType'], @@ -93,3 +93,5 @@ export const completeMultipartUpload = }); export type { Part, PartInput } from './multipart/helpers'; + +export { createFolder } from './create-folder'; diff --git a/packages/blob/src/put-helpers.ts b/packages/blob/src/put-helpers.ts index afbd57da3..f36abacf6 100644 --- a/packages/blob/src/put-helpers.ts +++ b/packages/blob/src/put-helpers.ts @@ -5,7 +5,7 @@ import type { CommonCreateBlobOptions } from './helpers'; import { BlobError } from './helpers'; import { MAXIMUM_PATHNAME_LENGTH } from './api'; -const putOptionHeaderMap = { +export const putOptionHeaderMap = { cacheControlMaxAge: 'x-cache-control-max-age', addRandomSuffix: 'x-add-random-suffix', contentType: 'x-content-type', diff --git a/packages/blob/src/put.ts b/packages/blob/src/put.ts index 3ac8d9a27..2a2b66aa7 100644 --- a/packages/blob/src/put.ts +++ b/packages/blob/src/put.ts @@ -25,27 +25,16 @@ export function createPutMethod({ getToken, extraChecks, }: CreatePutMethodOptions) { - return async function put( - pathname: TPath, - bodyOrOptions: TPath extends `${string}/` ? TOptions : PutBody, - optionsInput?: TPath extends `${string}/` ? never : TOptions, + return async function put( + pathname: string, + body: PutBody, + optionsInput: TOptions, ): Promise { - const isFolderCreation = pathname.endsWith('/'); - - // prevent empty bodies for files - if (!bodyOrOptions && !isFolderCreation) { + if (!body) { throw new BlobError('body is required'); } - // runtime check for non TS users that provide all three args - if (bodyOrOptions && optionsInput && isFolderCreation) { - throw new BlobError('body is not allowed for creating empty folders'); - } - - // avoid using the options as body - const body = isFolderCreation ? undefined : bodyOrOptions; - - if (body !== undefined && isPlainObject(body)) { + if (isPlainObject(body)) { throw new BlobError( "Body must be a string, buffer or stream. You sent a plain JavaScript object, double check what you're trying to upload.", ); @@ -53,15 +42,14 @@ export function createPutMethod({ const options = await createPutOptions({ pathname, - // when no body is required (for folder creations) options are the second argument - options: isFolderCreation ? (bodyOrOptions as TOptions) : optionsInput, + options: optionsInput, extraChecks, getToken, }); const headers = createPutHeaders(allowedOptions, options); - if (options.multipart === true && body) { + if (options.multipart === true) { return uncontrolledMultipartUpload(pathname, body, headers, options); } diff --git a/test/next/src/app/vercel/blob/script.mts b/test/next/src/app/vercel/blob/script.mts index d420aaa97..e8dbb6131 100644 --- a/test/next/src/app/vercel/blob/script.mts +++ b/test/next/src/app/vercel/blob/script.mts @@ -362,10 +362,7 @@ async function fetchExampleMultipart(): Promise { async function createFolder() { const start = Date.now(); - const blob = await vercelBlob.put(`foolder${Date.now()}/`, { - access: 'public', - addRandomSuffix: false, - }); + const blob = await vercelBlob.createFolder(`foolder${Date.now()}/`); console.log('create folder example:', blob, `(${Date.now() - start}ms)`);