Skip to content

Commit

Permalink
feat(blob): add createFolder method (#751)
Browse files Browse the repository at this point in the history
* feat(blob): add createFolder method

And remove variadic calls from main put methods, which will allow us to more
easily upgrade to newer TS versions.

Related:
- #682
- #667

* changeset

* update desc

* update test

* update
  • Loading branch information
vvo committed Sep 13, 2024
1 parent 8d7e8b9 commit 8098803
Show file tree
Hide file tree
Showing 8 changed files with 66 additions and 31 deletions.
5 changes: 5 additions & 0 deletions .changeset/chilled-steaks-peel.md
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 3 additions & 1 deletion packages/blob/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
/**
Expand Down Expand Up @@ -515,3 +515,5 @@ export interface GenerateClientTokenOptions extends BlobCommandOptions {
addRandomSuffix?: boolean;
cacheControlMaxAge?: number;
}

export { createFolder } from './create-folder';
41 changes: 41 additions & 0 deletions packages/blob/src/create-folder.ts
Original file line number Diff line number Diff line change
@@ -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<CreateFolderResult> {
const path = pathname.endsWith('/') ? pathname : `${pathname}/`;

const headers: Record<string, string> = {};

headers[putOptionHeaderMap.addRandomSuffix] = '0';

const response = await requestApi<PutBlobApiResponse>(
`/${path}`,
{
method: 'PUT',
headers,
signal: options.abortSignal,
},
options,
);

return {
url: response.url,
pathname: response.pathname,
};
}
2 changes: 1 addition & 1 deletion packages/blob/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
/**
Expand Down
10 changes: 6 additions & 4 deletions packages/blob/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<PutCommandOptions>({
allowedOptions: ['cacheControlMaxAge', 'addRandomSuffix', 'contentType'],
Expand Down Expand Up @@ -93,3 +93,5 @@ export const completeMultipartUpload =
});

export type { Part, PartInput } from './multipart/helpers';

export { createFolder } from './create-folder';
2 changes: 1 addition & 1 deletion packages/blob/src/put-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
28 changes: 8 additions & 20 deletions packages/blob/src/put.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,43 +25,31 @@ export function createPutMethod<TOptions extends PutCommandOptions>({
getToken,
extraChecks,
}: CreatePutMethodOptions<TOptions>) {
return async function put<TPath extends string>(
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<PutBlobResult> {
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.",
);
}

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);
}

Expand Down
5 changes: 1 addition & 4 deletions test/next/src/app/vercel/blob/script.mts
Original file line number Diff line number Diff line change
Expand Up @@ -362,10 +362,7 @@ async function fetchExampleMultipart(): Promise<string> {
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)`);

Expand Down

0 comments on commit 8098803

Please sign in to comment.