Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add file.isPublic() function #708

Merged
merged 15 commits into from
May 30, 2019
Merged
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
74 changes: 73 additions & 1 deletion src/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -2595,6 +2601,72 @@ class File extends ServiceObject<File> {
});
}

isPublic(): Promise<IsPublicResponse>;
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<IsPublicResponse>}
*
* @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<IsPublicResponse> | void {
gaxiosRequest({
method: 'HEAD',
stephenplusplus marked this conversation as resolved.
Show resolved Hide resolved
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<MakeFilePrivateResponse>;
Expand Down
38 changes: 18 additions & 20 deletions system-test/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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());
});
});

Expand All @@ -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 => {
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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();
});
});
Expand Down
56 changes: 56 additions & 0 deletions test/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)}`;
stephenplusplus marked this conversation as resolved.
Show resolved Hide resolved
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(
Expand Down