Skip to content

Commit

Permalink
feat(blob): add list mode param to merge blob folder (#476)
Browse files Browse the repository at this point in the history
Adds a new `mode: folded | expanded (default)` parameter to the list command options. When `folded` is passed to `mode`, all files belonging to the same folder are automatically folded into a single folder entry.
  • Loading branch information
luismeyer authored Nov 7, 2023
1 parent ea6ecfd commit d57df99
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 8 deletions.
5 changes: 5 additions & 0 deletions .changeset/metal-pigs-hear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@vercel/blob": minor
---

Adds a new `mode: folded | expanded (default)` parameter to the list command options. When you pass `folded` to `mode`, then we automatically fold all files belonging to the same folder into a single folder entry. This allows you to build file browsers using the Vercel Blob API.
39 changes: 39 additions & 0 deletions packages/blob/src/index.node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,45 @@ describe('blob client', () => {
),
);
});

it('list should pass the mode param and return folders array', async () => {
let path: string | null = null;

mockClient
.intercept({
path: () => true,
method: 'GET',
})
.reply(200, (req) => {
path = req.path;
return {
blobs: [mockedFileMetaList],
folders: ['foo', 'bar'],
hasMore: false,
};
});

await expect(list({ mode: 'folded' })).resolves.toMatchInlineSnapshot(`
{
"blobs": [
{
"pathname": "foo.txt",
"size": 12345,
"uploadedAt": 2023-05-04T15:12:07.818Z,
"url": "https://storeId.public.blob.vercel-storage.com/foo-id.txt",
},
],
"cursor": undefined,
"folders": [
"foo",
"bar",
],
"hasMore": false,
}
`);

expect(path).toBe('/?mode=folded');
});
});

describe('put', () => {
Expand Down
1 change: 1 addition & 0 deletions packages/blob/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export type {
ListBlobResultBlob,
ListBlobResult,
ListCommandOptions,
ListFoldedBlobResult,
} from './list';
export { list } from './list';

Expand Down
48 changes: 40 additions & 8 deletions packages/blob/src/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,40 +20,59 @@ export interface ListBlobResult {
hasMore: boolean;
}

export interface ListFoldedBlobResult extends ListBlobResult {
folders: string[];
}

interface ListBlobApiResponseBlob
extends Omit<ListBlobResultBlob, 'uploadedAt'> {
uploadedAt: string;
}

interface ListBlobApiResponse extends Omit<ListBlobResult, 'blobs'> {
blobs: ListBlobApiResponseBlob[];
folders?: string[];
}

export interface ListCommandOptions extends BlobCommandOptions {
export interface ListCommandOptions<
M extends 'expanded' | 'folded' | undefined = undefined,
> extends BlobCommandOptions {
/**
* The maximum number of blobs to return.
* @defaultvalue 1000
*/
limit?: number;
/**
* Filters the result to only include blobs located in a certain folder inside your store.
* Filters the result to only include blobs that start with this prefix.
* If used together with `mode: 'folded'`, make sure to include a trailing slash after the foldername.
*/
prefix?: string;
/**
* The cursor to use for pagination. Can be obtained from the response of a previous `list` request.
*/
cursor?: string;
/**
* Defines how the blobs are listed
* - `expanded` the blobs property contains all blobs.
* - `folded` the blobs property contains only the blobs at the root level of your store. Blobs that are located inside a folder get merged into a single entry in the folder response property.
* @defaultvalue 'expanded'
*/
mode?: M;
}

type ListCommandResult<
M extends 'expanded' | 'folded' | undefined = undefined,
> = M extends 'folded' ? ListFoldedBlobResult : ListBlobResult;

/**
* Fetches a paginated list of blob objects from your store.
* Detailed documentation can be found here: https://vercel.com/docs/storage/vercel-blob/using-blob-sdk#list-blobs
*
* @param options - Additional options for the request.
*/
export async function list(
options?: ListCommandOptions,
): Promise<ListBlobResult> {
export async function list<
M extends 'expanded' | 'folded' | undefined = undefined,
>(options?: ListCommandOptions<M>): Promise<ListCommandResult<M>> {
const listApiUrl = new URL(getApiUrl());
if (options?.limit) {
listApiUrl.searchParams.set('limit', options.limit.toString());
Expand All @@ -64,6 +83,10 @@ export async function list(
if (options?.cursor) {
listApiUrl.searchParams.set('cursor', options.cursor);
}
if (options?.mode) {
listApiUrl.searchParams.set('mode', options.mode);
}

const blobApiResponse = await fetch(listApiUrl, {
method: 'GET',
headers: {
Expand All @@ -76,13 +99,22 @@ export async function list(

const results = (await blobApiResponse.json()) as ListBlobApiResponse;

if (options?.mode === 'folded') {
return {
folders: results.folders ?? [],
cursor: results.cursor,
hasMore: results.hasMore,
blobs: results.blobs.map(mapBlobResult),
} as ListCommandResult<M>;
}

return {
...results,
cursor: results.cursor,
hasMore: results.hasMore,
blobs: results.blobs.map(mapBlobResult),
};
} as ListCommandResult<M>;
}

function mapBlobResult(blobResult: ListBlobApiResponseBlob): ListBlobResultBlob;
function mapBlobResult(
blobResult: ListBlobApiResponseBlob,
): ListBlobResultBlob {
Expand Down
17 changes: 17 additions & 0 deletions test/next/src/app/vercel/blob/script.mts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ async function run(): Promise<void> {
noExtensionExample(),
weirdCharactersExample(),
copyTextFile(),
listFolders(),
]);

await Promise.all(
Expand Down Expand Up @@ -274,3 +275,19 @@ async function copyTextFile() {

return copiedBlob.url;
}

async function listFolders() {
const start = Date.now();

const blob = await vercelBlob.put('foo/bar.txt', 'Hello, world!', {
access: 'public',
});

const response = await vercelBlob.list({
mode: 'folded',
});

console.log('fold blobs example:', response, `(${Date.now() - start}ms)`);

return blob.url;
}

1 comment on commit d57df99

@vercel
Copy link

@vercel vercel bot commented on d57df99 Nov 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.