Skip to content

Commit f86cadb

Browse files
authored
feat: add file.isPublic() function (#708)
1 parent b2f8a74 commit f86cadb

File tree

4 files changed

+148
-21
lines changed

4 files changed

+148
-21
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
"date-and-time": "^0.6.3",
6161
"duplexify": "^3.5.0",
6262
"extend": "^3.0.0",
63+
"gaxios": "^2.0.1",
6364
"gcs-resumable-upload": "^2.0.0",
6465
"hash-stream-validation": "^0.2.1",
6566
"mime": "^2.2.0",

src/file.ts

+73-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ import {
5757
} from '@google-cloud/common/build/src/util';
5858
const duplexify: DuplexifyConstructor = require('duplexify');
5959
import {normalize, objectEntries} from './util';
60-
import {Headers} from 'gaxios';
60+
import {GaxiosError, Headers, request as gaxiosRequest} from 'gaxios';
6161

6262
export type GetExpirationDateResponse = [Date];
6363
export interface GetExpirationDateCallback {
@@ -211,6 +211,12 @@ export type MakeFilePrivateResponse = [Metadata];
211211

212212
export interface MakeFilePrivateCallback extends SetFileMetadataCallback {}
213213

214+
export interface IsPublicCallback {
215+
(err: Error | null, resp?: boolean): void;
216+
}
217+
218+
export type IsPublicResponse = [boolean];
219+
214220
export type MakeFilePublicResponse = [Metadata];
215221

216222
export interface MakeFilePublicCallback {
@@ -2595,6 +2601,72 @@ class File extends ServiceObject<File> {
25952601
});
25962602
}
25972603

2604+
isPublic(): Promise<IsPublicResponse>;
2605+
isPublic(callback: IsPublicCallback): void;
2606+
/**
2607+
* @callback IsPublicCallback
2608+
* @param {?Error} err Request error, if any.
2609+
* @param {boolean} resp Whether file is public or not.
2610+
*/
2611+
/**
2612+
* @typedef {array} IsPublicResponse
2613+
* @property {boolean} 0 Whether file is public or not.
2614+
*/
2615+
/**
2616+
* Check whether this file is public or not by sending
2617+
* a HEAD request without credentials.
2618+
* No errors from the server indicates that the current
2619+
* file is public.
2620+
* A 403-Forbidden error {@link https://cloud.google.com/storage/docs/json_api/v1/status-codes#403_Forbidden}
2621+
* indicates that file is private.
2622+
* Any other non 403 error is propagated to user.
2623+
*
2624+
* @param {IsPublicCallback} [callback] Callback function.
2625+
* @returns {Promise<IsPublicResponse>}
2626+
*
2627+
* @example
2628+
* const {Storage} = require('@google-cloud/storage');
2629+
* const storage = new Storage();
2630+
* const myBucket = storage.bucket('my-bucket');
2631+
*
2632+
* const file = myBucket.file('my-file');
2633+
*
2634+
* //-
2635+
* // Check whether the file is publicly accessible.
2636+
* //-
2637+
* file.isPublic(function(err, resp) {
2638+
* if (err) {
2639+
* console.error(err);
2640+
* return;
2641+
* }
2642+
* console.log(`the file ${file.id} is public: ${resp}`) ;
2643+
* })
2644+
* //-
2645+
* // If the callback is omitted, we'll return a Promise.
2646+
* //-
2647+
* file.isPublic().then(function(data) {
2648+
* const resp = data[0];
2649+
* });
2650+
*/
2651+
2652+
isPublic(callback?: IsPublicCallback): Promise<IsPublicResponse> | void {
2653+
gaxiosRequest({
2654+
method: 'HEAD',
2655+
url: `http://${
2656+
this.bucket.name
2657+
}.storage.googleapis.com/${encodeURIComponent(this.name)}`,
2658+
}).then(
2659+
() => callback!(null, true),
2660+
(err: GaxiosError) => {
2661+
if (err.code === '403') {
2662+
callback!(null, false);
2663+
} else {
2664+
callback!(err);
2665+
}
2666+
}
2667+
);
2668+
}
2669+
25982670
makePrivate(
25992671
options?: MakeFilePrivateOptions
26002672
): Promise<MakeFilePrivateResponse>;

system-test/storage.ts

+18-20
Original file line numberDiff line numberDiff line change
@@ -209,17 +209,12 @@ describe('storage', () => {
209209
bucket = storageWithoutAuth.bucket('gcp-public-data-landsat');
210210
});
211211

212-
it('should list and download a file', done => {
213-
bucket.getFiles(
214-
{
215-
autoPaginate: false,
216-
},
217-
(err, files) => {
218-
assert.ifError(err);
219-
const file = files![0];
220-
file.download(done);
221-
}
222-
);
212+
it('should list and download a file', async () => {
213+
const [files] = await bucket.getFiles({autoPaginate: false});
214+
const file = files[0];
215+
const [isPublic] = await file.isPublic();
216+
assert.strictEqual(isPublic, true);
217+
assert.doesNotReject(file.download());
223218
});
224219
});
225220

@@ -232,13 +227,14 @@ describe('storage', () => {
232227
file = bucket.file(privateFile.id!);
233228
});
234229

235-
it('should not download a file', done => {
236-
file.download(err => {
237-
assert(
238-
err!.message.indexOf('does not have storage.objects.get') > -1
239-
);
240-
done();
241-
});
230+
it('should not download a file', async () => {
231+
const [isPublic] = await file.isPublic();
232+
assert.strictEqual(isPublic, false);
233+
assert.rejects(
234+
file.download(),
235+
(err: Error) =>
236+
err.message.indexOf('does not have storage.objects.get') > -1
237+
);
242238
});
243239

244240
it('should not upload a file', done => {
@@ -390,7 +386,7 @@ describe('storage', () => {
390386
const resps = await Promise.all(
391387
files.map(file => isFilePublicAsync(file))
392388
);
393-
resps.forEach(resp => assert.ok(resp));
389+
resps.forEach(resp => assert.strictEqual(resp, true));
394390
await Promise.all([
395391
bucket.acl.default.delete({entity: 'allUsers'}),
396392
bucket.deleteFiles(),
@@ -422,7 +418,9 @@ describe('storage', () => {
422418
const resps = await Promise.all(
423419
files.map(file => isFilePublicAsync(file))
424420
);
425-
resps.forEach(resp => assert.ok(!resp));
421+
resps.forEach(resp => {
422+
assert.strictEqual(resp, false);
423+
});
426424
await bucket.deleteFiles();
427425
});
428426
});

test/file.ts

+56
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import * as through from 'through2';
3636
import * as tmp from 'tmp';
3737
import * as url from 'url';
3838
import * as zlib from 'zlib';
39+
import * as gaxios from 'gaxios';
3940

4041
import {
4142
Bucket,
@@ -3205,6 +3206,61 @@ describe('File', () => {
32053206
});
32063207
});
32073208

3209+
describe('isPublic', () => {
3210+
const sandbox = sinon.createSandbox();
3211+
3212+
afterEach(() => sandbox.restore());
3213+
3214+
it('should execute callback with `true` in response', done => {
3215+
sandbox.stub(gaxios, 'request').resolves();
3216+
file.isPublic((err: gaxios.GaxiosError, resp: boolean) => {
3217+
assert.ifError(err);
3218+
assert.strictEqual(resp, true);
3219+
done();
3220+
});
3221+
});
3222+
3223+
it('should execute callback with `false` in response', done => {
3224+
sandbox.stub(gaxios, 'request').rejects({code: '403'});
3225+
file.isPublic((err: gaxios.GaxiosError, resp: boolean) => {
3226+
assert.ifError(err);
3227+
assert.strictEqual(resp, false);
3228+
done();
3229+
});
3230+
});
3231+
3232+
it('should propagate non-403 errors to user', done => {
3233+
const error = {code: '400'};
3234+
sandbox.stub(gaxios, 'request').rejects(error as gaxios.GaxiosError);
3235+
file.isPublic((err: gaxios.GaxiosError) => {
3236+
assert.strictEqual(err, error);
3237+
done();
3238+
});
3239+
});
3240+
3241+
it('should correctly send a HEAD request', done => {
3242+
const spy = sandbox.spy(gaxios, 'request');
3243+
file.isPublic((err: gaxios.GaxiosError) => {
3244+
assert.ifError(err);
3245+
assert.strictEqual(spy.calledWithMatch({method: 'HEAD'}), true);
3246+
done();
3247+
});
3248+
});
3249+
3250+
it('should correctly format URL in the request', done => {
3251+
file = new File(BUCKET, 'my#file$.png');
3252+
const expecterURL = `http://${
3253+
BUCKET.name
3254+
}.storage.googleapis.com/${encodeURIComponent(file.name)}`;
3255+
const spy = sandbox.spy(gaxios, 'request');
3256+
file.isPublic((err: gaxios.GaxiosError) => {
3257+
assert.ifError(err);
3258+
assert.strictEqual(spy.calledWithMatch({url: expecterURL}), true);
3259+
done();
3260+
});
3261+
});
3262+
});
3263+
32083264
describe('move', () => {
32093265
describe('copy to destination', () => {
32103266
function assertCopyFile(

0 commit comments

Comments
 (0)