Commit 54cad2d8 authored by Mohamed Heikal's avatar Mohamed Heikal Committed by Commit Bot

Supersize viewer can now use cloud storage API to access private files

Supersize tiger viewer until now used just a regular fetch since it only
dealt with public files. This cl allows it to use Google Cloud Storage'
json api to access private files which the user has access to.

Bug: 1048299
Change-Id: If4f4015efddea771c0de9fa36193d861a72902e8
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2088639
Commit-Queue: Mohamed Heikal <mheikal@chromium.org>
Reviewed-by: default avatarSamuel Huang <huangs@chromium.org>
Cr-Commit-Position: refs/heads/master@{#747419}
parent ce47e50c
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// @ts-check
'use strict';
const AUTH_API_KEY = 'AIzaSyAeKy_bJvcqYoLG02gCVF23an0kx8KzMng';
const AUTH_CLIENT_ID = '84462612899-hsikvugfjv36k8nt8459b7at62hi9sba.apps.googleusercontent.com';
const AUTH_SCOPE = 'https://www.googleapis.com/auth/devstorage.read_only';
const AUTH_DISCOVERY_URL = 'https://www.googleapis.com/discovery/v1/apis/storage/v1/rest';
const STORAGE_API_ENDPOINT = 'https://storage.googleapis.com/storage/v1';
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// @ts-check
'use strict';
window.googleAuth = null;
window.googleAuthPromise = new Promise((resolve, reject) => {
googleAuthPromiseResolve = resolve;
});
let _googleAuthPromiseResolve = null;
function handleClientLoad() {
if (requiresAuthentication()) {
gapi.load('client:auth2', initClient);
}
}
function initClient() {
return gapi.client.init({
'apiKey': AUTH_API_KEY,
'clientId': AUTH_CLIENT_ID,
'discoveryDocs': [AUTH_DISCOVERY_URL],
'scope': AUTH_SCOPE,
}).then(function () {
window.googleAuth = gapi.auth2.getAuthInstance();
if (!window.googleAuth.isSignedIn.get()) {
window.googleAuth.signIn().then(setSigninStatus);
} else {
setSigninStatus();
}
});
}
function setSigninStatus() {
let user = window.googleAuth.currentUser.get();
_googleAuthPromiseResolve(user.getAuthResponse());
}
function requiresAuthentication() {
let urlParams = new URLSearchParams(window.location.search);
return !!urlParams.get('authenticate');
}
...@@ -3,6 +3,10 @@ ...@@ -3,6 +3,10 @@
// found in the LICENSE file. // found in the LICENSE file.
const DO_NOT_DIFF = 'Don\'t diff'; const DO_NOT_DIFF = 'Don\'t diff';
// Domain hosting the viewer.html
const FIREBASE_HOST = 'https://chrome-supersize.firebaseapp.com'
// Storage bucket hosting the size diffs.
const SIZE_FILEHOST = 'https://storage.googleapis.com/chrome-supersize'
function buildOptions(options) { function buildOptions(options) {
const fragment = document.createDocumentFragment(); const fragment = document.createDocumentFragment();
...@@ -25,19 +29,20 @@ function setSubmitListener(form, fetchDataUrl) { ...@@ -25,19 +29,20 @@ function setSubmitListener(form, fetchDataUrl) {
form.addEventListener('submit', event => { form.addEventListener('submit', event => {
event.preventDefault(); event.preventDefault();
const dataUrl = fetchDataUrl(); const dataUrl = fetchDataUrl();
window.open(`viewer.html?load_url=${dataUrl}`); window.open(`${FIREBASE_HOST}/viewer.html?load_url=${dataUrl}`);
}); });
} }
// Milestones. // Milestones.
(async () => { (async () => {
// Milestones. // Milestones.
const milestoneResponse = await fetch('milestones/milestones.json'); const milestoneResponse = await fetch(
`${SIZE_FILEHOST}/milestones/milestones.json`);
const milestonesPushed = (await milestoneResponse.json())['pushed']; const milestonesPushed = (await milestoneResponse.json())['pushed'];
// Official Builds // Official Builds
const officialBuildsResponse = const officialBuildsResponse =
await fetch('official_builds/canary_reports.json'); await fetch(`${SIZE_FILEHOST}/official_builds/canary_reports.json`);
const officialBuildsPushed = (await officialBuildsResponse.json())['pushed']; const officialBuildsPushed = (await officialBuildsResponse.json())['pushed'];
if (document.readyState === 'loading') { if (document.readyState === 'loading') {
...@@ -169,10 +174,10 @@ function setSubmitListener(form, fetchDataUrl) { ...@@ -169,10 +174,10 @@ function setSubmitListener(form, fetchDataUrl) {
function sizeUrlFor(value) { function sizeUrlFor(value) {
if (value.indexOf('canary') != -1) { if (value.indexOf('canary') != -1) {
const strippedVersion = value.replace(/[^\d.]/g, ''); const strippedVersion = value.replace(/[^\d.]/g, '');
return `official_builds/reports/${strippedVersion}/${ return `${SIZE_FILEHOST}/official_builds/reports/${strippedVersion}/${
selApk.value}.size`; selApk.value}.size`;
} }
return `milestones/${value}/${selApk.value}.size`; return `${SIZE_FILEHOST}/milestones/${value}/${selApk.value}.size`;
} }
let ret = sizeUrlFor(selVersion1.value); let ret = sizeUrlFor(selVersion1.value);
if (selVersion2.value !== DO_NOT_DIFF) { if (selVersion2.value !== DO_NOT_DIFF) {
......
...@@ -76,37 +76,50 @@ class TreeWorker { ...@@ -76,37 +76,50 @@ class TreeWorker {
* Loads the tree data given on a worker thread and replaces the tree view in * Loads the tree data given on a worker thread and replaces the tree view in
* the UI once complete. Uses query string as state for the options. * the UI once complete. Uses query string as state for the options.
* Use `onProgress` before calling `loadTree`. * Use `onProgress` before calling `loadTree`.
* @param {string} input * @param {?string=} input
* @param {?string=} accessToken
* @returns {Promise<TreeProgress>} * @returns {Promise<TreeProgress>}
*/ */
loadTree(input = null) { loadTree(input = null, accessToken = null) {
return this._waitForResponse('load', { return this._waitForResponse('load', {
input, input,
accessToken,
options: location.search.slice(1), options: location.search.slice(1),
}); });
} }
} }
let _innerWorker = null; window.supersize = {
let worker = null; worker: null,
treeReady: null,
};
// .size files and .ndjson files require different web workers. // .size files and .ndjson files require different web workers.
// Switch between the two dynamically. // Switch between the two dynamically.
function startWorkerForFileName(fileName) { function startWorkerForFileName(fileName) {
let innerWorker = null;
if (fileName && if (fileName &&
(fileName.endsWith('.size') || fileName.endsWith('.sizediff'))) { (fileName.endsWith('.size') || fileName.endsWith('.sizediff'))) {
console.log('Using WebAssembly web worker'); console.log('Using WebAssembly web worker');
_innerWorker = new Worker('tree-worker-wasm.js'); innerWorker = new Worker('tree-worker-wasm.js');
} else { } else {
console.log('Using JavaScript web worker'); console.log('Using JavaScript web worker');
_innerWorker = new Worker('tree-worker.js'); innerWorker = new Worker('tree-worker.js');
} }
worker = new TreeWorker(_innerWorker); window.supersize.worker = new TreeWorker(innerWorker);
} }
const urlParams = new URLSearchParams(window.location.search); (function() {
startWorkerForFileName(urlParams.get('load_url')); const urlParams = new URLSearchParams(window.location.search);
const url = urlParams.get('load_url');
startWorkerForFileName(url);
// Kick off the worker ASAP so it can start parsing data faster. if (requiresAuthentication()) {
// Subsequent calls will just use a worker locally. window.supersize.treeReady = window.googleAuthPromise.then((authResponse) =>
const treeReady = worker.loadTree('from-url://'); window.supersize.worker.loadTree('from-url://',
authResponse.access_token));
} else {
window.supersize.treeReady = window.supersize.worker.loadTree('from-url://');
}
})()
...@@ -102,7 +102,7 @@ const newTreeElement = (() => { ...@@ -102,7 +102,7 @@ const newTreeElement = (() => {
/** @type {HTMLSpanElement} */ /** @type {HTMLSpanElement} */
const symbolName = link.querySelector('.symbol-name'); const symbolName = link.querySelector('.symbol-name');
const idPath = symbolName.title; const idPath = symbolName.title;
data = await worker.openNode(idPath); data = await window.supersize.worker.openNode(idPath);
_uiNodeData.set(link, data); _uiNodeData.set(link, data);
} }
...@@ -451,8 +451,8 @@ const newTreeElement = (() => { ...@@ -451,8 +451,8 @@ const newTreeElement = (() => {
); );
} }
treeReady.then(displayTree); window.supersize.treeReady.then(displayTree);
worker.setOnProgressHandler(displayTree); window.supersize.worker.setOnProgressHandler(displayTree);
_fileUpload.addEventListener('change', event => { _fileUpload.addEventListener('change', event => {
const input = /** @type {HTMLInputElement} */ (event.currentTarget); const input = /** @type {HTMLInputElement} */ (event.currentTarget);
...@@ -463,7 +463,7 @@ const newTreeElement = (() => { ...@@ -463,7 +463,7 @@ const newTreeElement = (() => {
_dataUrlInput.value = ''; _dataUrlInput.value = '';
_dataUrlInput.dispatchEvent(new Event('change')); _dataUrlInput.dispatchEvent(new Event('change'));
worker.loadTree(fileUrl).then(displayTree); window.supersize.worker.loadTree(fileUrl).then(displayTree);
// Clean up afterwards so new files trigger event // Clean up afterwards so new files trigger event
input.value = ''; input.value = '';
}); });
...@@ -474,12 +474,12 @@ const newTreeElement = (() => { ...@@ -474,12 +474,12 @@ const newTreeElement = (() => {
// options (marked by `data-dynamic`) are changed. // options (marked by `data-dynamic`) are changed.
if (!event.target.dataset.hasOwnProperty('dynamic')) { if (!event.target.dataset.hasOwnProperty('dynamic')) {
_progress.setValue(0); _progress.setValue(0);
worker.loadTree().then(displayTree); window.supersize.worker.loadTree().then(displayTree);
} }
}); });
form.addEventListener('submit', event => { form.addEventListener('submit', event => {
event.preventDefault(); event.preventDefault();
_progress.setValue(0); _progress.setValue(0);
worker.loadTree().then(displayTree); window.supersize.worker.loadTree().then(displayTree);
}); });
} }
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
importScripts('./shared.js'); importScripts('./shared.js');
importScripts('./caspian_web.js'); importScripts('./caspian_web.js');
importScripts('./auth-consts.js');
const LoadWasm = new Promise(function(resolve, reject) { const LoadWasm = new Promise(function(resolve, reject) {
Module['onRuntimeInitialized'] = function() { Module['onRuntimeInitialized'] = function() {
...@@ -48,14 +49,43 @@ class DataFetcher { ...@@ -48,14 +49,43 @@ class DataFetcher {
this._input = input; this._input = input;
} }
/**
* Sets the access token to be used for authenticated requests. If accessToken
* is non-null and the URL is a google storage URL, an authenticated request
* is performed instead.
* @param {?string} accessToken
*/
setAccessToken(accessToken) {
this._accessToken = accessToken;
}
/** /**
* Starts a new request and aborts the previous one. * Starts a new request and aborts the previous one.
* @param {string | Request} url * @param {string | Request} url
*/ */
async fetch(url) { async fetchUrl(url) {
if (this._accessToken && looksLikeGoogleCloudStorage(url)) {
return this._fetchFromGoogleCloudStorage(url);
} else {
return this._doFetch(url);
}
}
async _fetchFromGoogleCloudStorage(url) {
const {bucket, file} = parseGoogleCloudStorageUrl(url);
const params = `alt=media&key=${AUTH_API_KEY}`;
const api_url = `${STORAGE_API_ENDPOINT}/b/${bucket}/o/${file}?${params}`;
const headers = new Headers();
headers.append('Authorization', `Bearer ${this._accessToken}`);
return this._doFetch(api_url, headers);
}
async _doFetch(url, headers) {
if (this._controller) this._controller.abort(); if (this._controller) this._controller.abort();
this._controller = new AbortController(); this._controller = new AbortController();
const headers = new Headers(); if (!headers) {
headers = new Headers();
}
headers.append('cache-control', 'no-cache'); headers.append('cache-control', 'no-cache');
return fetch(url, { return fetch(url, {
headers, headers,
...@@ -69,13 +99,25 @@ class DataFetcher { ...@@ -69,13 +99,25 @@ class DataFetcher {
*/ */
async loadSizeBuffer() { async loadSizeBuffer() {
if (!this._cache) { if (!this._cache) {
const response = await this.fetch(this._input); const response = await this.fetchUrl(this._input);
this._cache = new Uint8Array(await response.arrayBuffer()); this._cache = new Uint8Array(await response.arrayBuffer());
} }
return this._cache; return this._cache;
} }
} }
function looksLikeGoogleCloudStorage(url) {
return url.startsWith('https://storage.googleapis.com/');
}
function parseGoogleCloudStorageUrl(url) {
const re = /^https:\/\/storage\.googleapis\.com\/(?<bucket>[^\/]+)\/(?<file>.+)/;
const match = re.exec(url);
const bucket = encodeURIComponent(match.groups['bucket']);
const file = encodeURIComponent(match.groups['file']);
return {bucket, file};
}
function mallocBuffer(buf) { function mallocBuffer(buf) {
var dataPtr = Module._malloc(buf.byteLength); var dataPtr = Module._malloc(buf.byteLength);
var dataHeap = new Uint8Array(Module.HEAPU8.buffer, dataPtr, buf.byteLength); var dataHeap = new Uint8Array(Module.HEAPU8.buffer, dataPtr, buf.byteLength);
...@@ -195,8 +237,8 @@ function parseOptions(options) { ...@@ -195,8 +237,8 @@ function parseOptions(options) {
} }
const actions = { const actions = {
/** @param {{input:string|null,options:string}} param0 */ /** @param {{input:string|null,accessToken:string|null,options:string}} param0 */
load({input, options}) { load({input, accessToken, options}) {
const { const {
groupBy, groupBy,
includeRegex, includeRegex,
...@@ -208,6 +250,9 @@ const actions = { ...@@ -208,6 +250,9 @@ const actions = {
url, url,
beforeUrl, beforeUrl,
} = parseOptions(options); } = parseOptions(options);
if (accessToken) {
fetcher.setAccessToken(accessToken);
}
if (input === 'from-url://' && url) { if (input === 'from-url://' && url) {
// Display the data from the `load_url` query parameter // Display the data from the `load_url` query parameter
console.info('Displaying data from', url); console.info('Displaying data from', url);
......
...@@ -8,6 +8,8 @@ ...@@ -8,6 +8,8 @@
<head> <head>
<title>Super Size Tiger View</title> <title>Super Size Tiger View</title>
<script src="auth-consts.js"></script>
<script src="auth.js"></script>
<script src="start-worker.js"></script> <script src="start-worker.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#4285f4"> <meta name="theme-color" content="#4285f4">
...@@ -20,11 +22,15 @@ ...@@ -20,11 +22,15 @@
<script defer src="state.js"></script> <script defer src="state.js"></script>
<script defer src="infocard-ui.js"></script> <script defer src="infocard-ui.js"></script>
<script defer src="tree-ui.js"></script> <script defer src="tree-ui.js"></script>
<script async defer src="https://apis.google.com/js/api.js"
onload="this.onload=function(){};handleClientLoad()"
onreadystatechange="if (this.readyState === 'complete') this.onload()">
</script>
<script defer async> <script defer async>
if ('serviceWorker' in navigator) { if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('sw.js') navigator.serviceWorker.register('sw.js')
.catch(() => console.warn('ServiceWorker not loaded.')); .catch(() => console.warn('ServiceWorker not loaded.'));
} }
</script> </script>
</head> </head>
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment