Skip to content

Commit bc4a844

Browse files
authored
Storage List API (#1610)
1 parent 2e6c4aa commit bc4a844

File tree

13 files changed

+570
-17
lines changed

13 files changed

+570
-17
lines changed

packages/firebase/index.d.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5484,6 +5484,85 @@ declare namespace firebase.storage {
54845484
* including if the object did not exist.
54855485
*/
54865486
updateMetadata(metadata: firebase.storage.SettableMetadata): Promise<any>;
5487+
/**
5488+
* List all items (files) and prefixes (folders) under this storage reference.
5489+
*
5490+
* This is a helper method for calling list() repeatedly until there are
5491+
* no more results. The default pagination size is 1000.
5492+
*
5493+
* Note: The results may not be consistent if objects are changed while this
5494+
* operation is running.
5495+
*
5496+
* Warning: listAll may potentially consume too many resources if there are
5497+
* too many results.
5498+
*
5499+
* @return A Promise that resolves with all the items and prefixes under
5500+
* the current storage reference. `prefixes` contains references to
5501+
* sub-directories and `items` contains references to objects in this
5502+
* folder. `nextPageToken` is never returned.
5503+
*/
5504+
listAll(): Promise<ListResult>;
5505+
/**
5506+
* List items (files) and prefixes (folders) under this storage reference.
5507+
*
5508+
* List API is only available for Firebase Rules Version 2.
5509+
*
5510+
* GCS is a key-blob store. Firebase Storage imposes the semantic of '/'
5511+
* delimited folder structure.
5512+
* Refer to GCS's List API if you want to learn more.
5513+
*
5514+
* To adhere to Firebase Rules's Semantics, Firebase Storage does not
5515+
* support objects whose paths end with "/" or contain two consecutive
5516+
* "/"s. Firebase Storage List API will filter these unsupported objects.
5517+
* list() may fail if there are too many unsupported objects in the bucket.
5518+
*
5519+
* @param options See ListOptions for details.
5520+
* @return A Promise that resolves with the items and prefixes.
5521+
* `prefixes` contains references to sub-folders and `items`
5522+
* contains references to objects in this folder. `nextPageToken`
5523+
* can be used to get the rest of the results.
5524+
*/
5525+
list(options?: ListOptions): Promise<ListResult>;
5526+
}
5527+
5528+
/**
5529+
* Result returned by list().
5530+
*/
5531+
interface ListResult {
5532+
/**
5533+
* References to prefixes (sub-folders). You can call list() on them to
5534+
* get its contents.
5535+
*
5536+
* Folders are implicit based on '/' in the object paths.
5537+
* For example, if a bucket has two objects '/a/b/1' and '/a/b/2', list('/a')
5538+
* will return '/a/b' as a prefix.
5539+
*/
5540+
prefixes: Reference[];
5541+
/**
5542+
* Objects in this directory.
5543+
* You can call getMetadate() and getDownloadUrl() on them.
5544+
*/
5545+
items: Reference[];
5546+
/**
5547+
* If set, there might be more results for this list. Use this token to resume the list.
5548+
*/
5549+
nextPageToken: string | null;
5550+
}
5551+
5552+
/**
5553+
* The options list() accepts.
5554+
*/
5555+
interface ListOptions {
5556+
/**
5557+
* If set, limits the total number of `prefixes` and `items` to return.
5558+
* The default and maximum maxResults is 1000.
5559+
*/
5560+
maxResults?: number | null;
5561+
/**
5562+
* The `nextPageToken` from a previous call to list(). If provided,
5563+
* listing is resumed from the previous position.
5564+
*/
5565+
pageToken?: string | null;
54875566
}
54885567

54895568
/**

packages/storage-types/index.d.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,18 @@ export interface Reference {
5151
storage: Storage;
5252
toString(): string;
5353
updateMetadata(metadata: SettableMetadata): Promise<FullMetadata>;
54+
list(options?: ListOptions): Promise<ListResult>;
55+
}
56+
57+
export interface ListResult {
58+
prefixes: Reference[];
59+
items: Reference[];
60+
nextPageToken: string | null;
61+
}
62+
63+
export interface ListOptions {
64+
maxResults?: number | null;
65+
pageToken?: string | null;
5466
}
5567

5668
export interface SettableMetadata {

packages/storage/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
- [Feature] Added the support for List API.
2+

packages/storage/src/implementation/args.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
* limitations under the License.
1616
*/
1717
import * as errorsExports from './error';
18-
import { errors } from './error';
1918
import * as MetadataUtils from './metadata';
19+
import * as ListOptionsUtils from './list';
2020
import * as type from './type';
2121

2222
/**
@@ -117,6 +117,10 @@ export function metadataSpec(opt_optional?: boolean): ArgSpec {
117117
return new ArgSpec(MetadataUtils.metadataValidator, opt_optional);
118118
}
119119

120+
export function listOptionSpec(opt_optional?: boolean): ArgSpec {
121+
return new ArgSpec(ListOptionsUtils.listOptionsValidator, opt_optional);
122+
}
123+
120124
export function nonNegativeNumberSpec(): ArgSpec {
121125
function validator(p: any) {
122126
let valid = type.isNumber(p) && p >= 0;
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/**
2+
* @license
3+
* Copyright 2019 Google Inc.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
/**
19+
* @fileoverview Documentation for the listOptions and listResult format
20+
*/
21+
import { AuthWrapper } from './authwrapper';
22+
import { Location } from './location';
23+
import * as json from './json';
24+
import * as type from './type';
25+
import { ListResult } from '../list';
26+
27+
/**
28+
* Represents the simplified object metadata returned by List API.
29+
* Other fields are filtered because list in Firebase Rules does not grant
30+
* the permission to read the metadata.
31+
*/
32+
interface ListMetadataResponse {
33+
name: string;
34+
bucket: string;
35+
}
36+
37+
/**
38+
* Represents the JSON response of List API.
39+
*/
40+
interface ListResultResponse {
41+
prefixes: string[];
42+
items: ListMetadataResponse[];
43+
nextPageToken?: string;
44+
}
45+
46+
const MAX_RESULTS_KEY = 'maxResults';
47+
const MAX_MAX_RESULTS = 1000;
48+
const PAGE_TOKEN_KEY = 'pageToken';
49+
const PREFIXES_KEY = 'prefixes';
50+
const ITEMS_KEY = 'items';
51+
52+
function fromBackendResponse(
53+
authWrapper: AuthWrapper,
54+
resource: ListResultResponse
55+
): ListResult {
56+
const listResult: ListResult = {
57+
prefixes: [],
58+
items: [],
59+
nextPageToken: resource['nextPageToken']
60+
};
61+
if (resource[PREFIXES_KEY]) {
62+
for (const path of resource[PREFIXES_KEY]) {
63+
const pathWithoutTrailingSlash = path.replace(/\/$/, '');
64+
const reference = authWrapper.makeStorageReference(
65+
new Location(authWrapper.bucket(), pathWithoutTrailingSlash)
66+
);
67+
listResult.prefixes.push(reference);
68+
}
69+
}
70+
71+
if (resource[ITEMS_KEY]) {
72+
for (const item of resource[ITEMS_KEY]) {
73+
const reference = authWrapper.makeStorageReference(
74+
new Location(authWrapper.bucket(), item['name'])
75+
);
76+
listResult.items.push(reference);
77+
}
78+
}
79+
return listResult;
80+
}
81+
82+
export function fromResponseString(
83+
authWrapper: AuthWrapper,
84+
resourceString: string
85+
): ListResult | null {
86+
const obj = json.jsonObjectOrNull(resourceString);
87+
if (obj === null) {
88+
return null;
89+
}
90+
const resource = obj as ListResultResponse;
91+
return fromBackendResponse(authWrapper, resource);
92+
}
93+
94+
export function listOptionsValidator(p: any) {
95+
const validType = p && type.isObject(p);
96+
if (!validType) {
97+
throw 'Expected ListOptions object.';
98+
}
99+
for (const key in p) {
100+
if (key === MAX_RESULTS_KEY) {
101+
if (!type.isInteger(p[MAX_RESULTS_KEY]) || p[MAX_RESULTS_KEY] <= 0) {
102+
throw 'Expected maxResults to be a positive number.';
103+
}
104+
if (p[MAX_RESULTS_KEY] > 1000) {
105+
throw `Expected maxResults to be less than or equal to ${MAX_MAX_RESULTS}.`;
106+
}
107+
} else if (key === PAGE_TOKEN_KEY) {
108+
if (p[PAGE_TOKEN_KEY] && !type.isString(p[PAGE_TOKEN_KEY])) {
109+
throw 'Expected pageToken to be string.';
110+
}
111+
} else {
112+
throw 'Unknown option: ' + key;
113+
}
114+
}
115+
}

packages/storage/src/implementation/location.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ export class Location {
3636
return this.path_;
3737
}
3838

39+
get isRoot(): boolean {
40+
return this.path.length === 0;
41+
}
42+
3943
fullServerUrl(): string {
4044
let encode = encodeURIComponent;
4145
return '/b/' + encode(this.bucket) + '/o/' + encode(this.path);

packages/storage/src/implementation/requests.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
*/
2121

2222
import { Metadata } from '../metadata';
23+
import { ListResult } from '../list';
2324

2425
import * as array from './array';
2526
import { AuthWrapper } from './authwrapper';
@@ -28,6 +29,7 @@ import * as errorsExports from './error';
2829
import { FirebaseStorageError } from './error';
2930
import { Location } from './location';
3031
import * as MetadataUtils from './metadata';
32+
import * as ListResultUtils from './list';
3133
import * as object from './object';
3234
import { RequestInfo } from './requestinfo';
3335
import * as type from './type';
@@ -59,6 +61,17 @@ export function metadataHandler(
5961
return handler;
6062
}
6163

64+
export function listHandler(
65+
authWrapper: AuthWrapper
66+
): (p1: XhrIo, p2: string) => ListResult {
67+
function handler(xhr: XhrIo, text: string): ListResult {
68+
const listResult = ListResultUtils.fromResponseString(authWrapper, text);
69+
handlerCheck(listResult !== null);
70+
return listResult as ListResult;
71+
}
72+
return handler;
73+
}
74+
6275
export function downloadUrlHandler(
6376
authWrapper: AuthWrapper,
6477
mappings: MetadataUtils.Mappings
@@ -143,6 +156,43 @@ export function getMetadata(
143156
return requestInfo;
144157
}
145158

159+
export function list(
160+
authWrapper: AuthWrapper,
161+
location: Location,
162+
delimiter?: string,
163+
pageToken?: string,
164+
maxResults?: number
165+
): RequestInfo<ListResult> {
166+
let urlParams = {};
167+
if (location.isRoot) {
168+
urlParams['prefix'] = '';
169+
} else {
170+
urlParams['prefix'] = location.path + '/';
171+
}
172+
if (delimiter && delimiter.length > 0) {
173+
urlParams['delimiter'] = delimiter;
174+
}
175+
if (pageToken) {
176+
urlParams['pageToken'] = pageToken;
177+
}
178+
if (maxResults) {
179+
urlParams['maxResults'] = maxResults;
180+
}
181+
const urlPart = location.bucketOnlyServerUrl();
182+
const url = UrlUtils.makeUrl(urlPart);
183+
const method = 'GET';
184+
const timeout = authWrapper.maxOperationRetryTime();
185+
const requestInfo = new RequestInfo(
186+
url,
187+
method,
188+
listHandler(authWrapper),
189+
timeout
190+
);
191+
requestInfo.urlParams = urlParams;
192+
requestInfo.errorHandler = sharedErrorHandler(location);
193+
return requestInfo;
194+
}
195+
146196
export function getDownloadUrl(
147197
authWrapper: AuthWrapper,
148198
location: Location,

packages/storage/src/implementation/type.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ export function isString(p: any): boolean {
4646
return typeof p === 'string' || p instanceof String;
4747
}
4848

49+
export function isInteger(p: any): boolean {
50+
return isNumber(p) && Number.isInteger(p);
51+
}
52+
4953
export function isNumber(p: any): boolean {
5054
return typeof p === 'number' || p instanceof Number;
5155
}

packages/storage/src/list.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/**
2+
* @license
3+
* Copyright 2019 Google Inc.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
import { Reference } from './reference';
18+
19+
/**
20+
* @fileoverview Documentation for ListOptions and ListResult format.
21+
*/
22+
export type ListOptions = {
23+
maxResults?: number | null;
24+
pageToken?: string | null;
25+
};
26+
27+
export type ListResult = {
28+
prefixes: Reference[];
29+
items: Reference[];
30+
nextPageToken?: string | null;
31+
};

0 commit comments

Comments
 (0)