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
67 changes: 66 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,65 @@ 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.
Copy link
Contributor

Choose a reason for hiding this comment

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

We should explain how we are doing this. It is possible the server has a hiccup and returns an error code, even though the file is public.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done!
Thanks

* @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