From f86cadbf0e422cb4f240432293d7e466ec083999 Mon Sep 17 00:00:00 2001 From: Alex <7764119+AVaksman@users.noreply.github.com> Date: Thu, 30 May 2019 12:32:35 -0400 Subject: [PATCH] feat: add file.isPublic() function (#708) --- package.json | 1 + src/file.ts | 74 +++++++++++++++++++++++++++++++++++++++++- system-test/storage.ts | 38 ++++++++++------------ test/file.ts | 56 ++++++++++++++++++++++++++++++++ 4 files changed, 148 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index 0ca969bd1..af256c983 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "date-and-time": "^0.6.3", "duplexify": "^3.5.0", "extend": "^3.0.0", + "gaxios": "^2.0.1", "gcs-resumable-upload": "^2.0.0", "hash-stream-validation": "^0.2.1", "mime": "^2.2.0", diff --git a/src/file.ts b/src/file.ts index 46a8fa1f0..4e3f2dbac 100644 --- a/src/file.ts +++ b/src/file.ts @@ -57,7 +57,7 @@ import { } from '@google-cloud/common/build/src/util'; const duplexify: DuplexifyConstructor = require('duplexify'); import {normalize, objectEntries} from './util'; -import {Headers} from 'gaxios'; +import {GaxiosError, Headers, request as gaxiosRequest} from 'gaxios'; export type GetExpirationDateResponse = [Date]; export interface GetExpirationDateCallback { @@ -211,6 +211,12 @@ export type MakeFilePrivateResponse = [Metadata]; export interface MakeFilePrivateCallback extends SetFileMetadataCallback {} +export interface IsPublicCallback { + (err: Error | null, resp?: boolean): void; +} + +export type IsPublicResponse = [boolean]; + export type MakeFilePublicResponse = [Metadata]; export interface MakeFilePublicCallback { @@ -2595,6 +2601,72 @@ class File extends ServiceObject { }); } + isPublic(): Promise; + isPublic(callback: IsPublicCallback): void; + /** + * @callback IsPublicCallback + * @param {?Error} err Request error, if any. + * @param {boolean} resp Whether file is public or not. + */ + /** + * @typedef {array} IsPublicResponse + * @property {boolean} 0 Whether file is public or not. + */ + /** + * Check whether this file is public or not by sending + * a HEAD request without credentials. + * No errors from the server indicates that the current + * file is public. + * A 403-Forbidden error {@link https://cloud.google.com/storage/docs/json_api/v1/status-codes#403_Forbidden} + * indicates that file is private. + * Any other non 403 error is propagated to user. + * + * @param {IsPublicCallback} [callback] Callback function. + * @returns {Promise} + * + * @example + * const {Storage} = require('@google-cloud/storage'); + * const storage = new Storage(); + * const myBucket = storage.bucket('my-bucket'); + * + * const file = myBucket.file('my-file'); + * + * //- + * // Check whether the file is publicly accessible. + * //- + * file.isPublic(function(err, resp) { + * if (err) { + * console.error(err); + * return; + * } + * console.log(`the file ${file.id} is public: ${resp}`) ; + * }) + * //- + * // If the callback is omitted, we'll return a Promise. + * //- + * file.isPublic().then(function(data) { + * const resp = data[0]; + * }); + */ + + isPublic(callback?: IsPublicCallback): Promise | void { + gaxiosRequest({ + method: 'HEAD', + url: `http://${ + this.bucket.name + }.storage.googleapis.com/${encodeURIComponent(this.name)}`, + }).then( + () => callback!(null, true), + (err: GaxiosError) => { + if (err.code === '403') { + callback!(null, false); + } else { + callback!(err); + } + } + ); + } + makePrivate( options?: MakeFilePrivateOptions ): Promise; diff --git a/system-test/storage.ts b/system-test/storage.ts index 4f062ac0c..7e0ac9df4 100644 --- a/system-test/storage.ts +++ b/system-test/storage.ts @@ -209,17 +209,12 @@ describe('storage', () => { bucket = storageWithoutAuth.bucket('gcp-public-data-landsat'); }); - it('should list and download a file', done => { - bucket.getFiles( - { - autoPaginate: false, - }, - (err, files) => { - assert.ifError(err); - const file = files![0]; - file.download(done); - } - ); + it('should list and download a file', async () => { + const [files] = await bucket.getFiles({autoPaginate: false}); + const file = files[0]; + const [isPublic] = await file.isPublic(); + assert.strictEqual(isPublic, true); + assert.doesNotReject(file.download()); }); }); @@ -232,13 +227,14 @@ describe('storage', () => { file = bucket.file(privateFile.id!); }); - it('should not download a file', done => { - file.download(err => { - assert( - err!.message.indexOf('does not have storage.objects.get') > -1 - ); - done(); - }); + it('should not download a file', async () => { + const [isPublic] = await file.isPublic(); + assert.strictEqual(isPublic, false); + assert.rejects( + file.download(), + (err: Error) => + err.message.indexOf('does not have storage.objects.get') > -1 + ); }); it('should not upload a file', done => { @@ -390,7 +386,7 @@ describe('storage', () => { const resps = await Promise.all( files.map(file => isFilePublicAsync(file)) ); - resps.forEach(resp => assert.ok(resp)); + resps.forEach(resp => assert.strictEqual(resp, true)); await Promise.all([ bucket.acl.default.delete({entity: 'allUsers'}), bucket.deleteFiles(), @@ -422,7 +418,9 @@ describe('storage', () => { const resps = await Promise.all( files.map(file => isFilePublicAsync(file)) ); - resps.forEach(resp => assert.ok(!resp)); + resps.forEach(resp => { + assert.strictEqual(resp, false); + }); await bucket.deleteFiles(); }); }); diff --git a/test/file.ts b/test/file.ts index ed505a5e3..6ee1480ec 100644 --- a/test/file.ts +++ b/test/file.ts @@ -36,6 +36,7 @@ import * as through from 'through2'; import * as tmp from 'tmp'; import * as url from 'url'; import * as zlib from 'zlib'; +import * as gaxios from 'gaxios'; import { Bucket, @@ -3205,6 +3206,61 @@ describe('File', () => { }); }); + describe('isPublic', () => { + const sandbox = sinon.createSandbox(); + + afterEach(() => sandbox.restore()); + + it('should execute callback with `true` in response', done => { + sandbox.stub(gaxios, 'request').resolves(); + file.isPublic((err: gaxios.GaxiosError, resp: boolean) => { + assert.ifError(err); + assert.strictEqual(resp, true); + done(); + }); + }); + + it('should execute callback with `false` in response', done => { + sandbox.stub(gaxios, 'request').rejects({code: '403'}); + file.isPublic((err: gaxios.GaxiosError, resp: boolean) => { + assert.ifError(err); + assert.strictEqual(resp, false); + done(); + }); + }); + + it('should propagate non-403 errors to user', done => { + const error = {code: '400'}; + sandbox.stub(gaxios, 'request').rejects(error as gaxios.GaxiosError); + file.isPublic((err: gaxios.GaxiosError) => { + assert.strictEqual(err, error); + done(); + }); + }); + + it('should correctly send a HEAD request', done => { + const spy = sandbox.spy(gaxios, 'request'); + file.isPublic((err: gaxios.GaxiosError) => { + assert.ifError(err); + assert.strictEqual(spy.calledWithMatch({method: 'HEAD'}), true); + done(); + }); + }); + + it('should correctly format URL in the request', done => { + file = new File(BUCKET, 'my#file$.png'); + const expecterURL = `http://${ + BUCKET.name + }.storage.googleapis.com/${encodeURIComponent(file.name)}`; + const spy = sandbox.spy(gaxios, 'request'); + file.isPublic((err: gaxios.GaxiosError) => { + assert.ifError(err); + assert.strictEqual(spy.calledWithMatch({url: expecterURL}), true); + done(); + }); + }); + }); + describe('move', () => { describe('copy to destination', () => { function assertCopyFile(