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 @@
// found in the LICENSE file.
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) {
const fragment = document.createDocumentFragment();
......@@ -25,19 +29,20 @@ function setSubmitListener(form, fetchDataUrl) {
form.addEventListener('submit', event => {
event.preventDefault();
const dataUrl = fetchDataUrl();
window.open(`viewer.html?load_url=${dataUrl}`);
window.open(`${FIREBASE_HOST}/viewer.html?load_url=${dataUrl}`);
});
}
// Milestones.
(async () => {
// Milestones.
const milestoneResponse = await fetch('milestones/milestones.json');
const milestoneResponse = await fetch(
`${SIZE_FILEHOST}/milestones/milestones.json`);
const milestonesPushed = (await milestoneResponse.json())['pushed'];
// Official Builds
const officialBuildsResponse =
await fetch('official_builds/canary_reports.json');
await fetch(`${SIZE_FILEHOST}/official_builds/canary_reports.json`);
const officialBuildsPushed = (await officialBuildsResponse.json())['pushed'];
if (document.readyState === 'loading') {
......@@ -169,10 +174,10 @@ function setSubmitListener(form, fetchDataUrl) {
function sizeUrlFor(value) {
if (value.indexOf('canary') != -1) {
const strippedVersion = value.replace(/[^\d.]/g, '');
return `official_builds/reports/${strippedVersion}/${
return `${SIZE_FILEHOST}/official_builds/reports/${strippedVersion}/${
selApk.value}.size`;
}
return `milestones/${value}/${selApk.value}.size`;
return `${SIZE_FILEHOST}/milestones/${value}/${selApk.value}.size`;
}
let ret = sizeUrlFor(selVersion1.value);
if (selVersion2.value !== DO_NOT_DIFF) {
......
......@@ -76,37 +76,50 @@ class TreeWorker {
* 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.
* Use `onProgress` before calling `loadTree`.
* @param {string} input
* @param {?string=} input
* @param {?string=} accessToken
* @returns {Promise<TreeProgress>}
*/
loadTree(input = null) {
loadTree(input = null, accessToken = null) {
return this._waitForResponse('load', {
input,
accessToken,
options: location.search.slice(1),
});
}
}
let _innerWorker = null;
let worker = null;
window.supersize = {
worker: null,
treeReady: null,
};
// .size files and .ndjson files require different web workers.
// Switch between the two dynamically.
function startWorkerForFileName(fileName) {
let innerWorker = null;
if (fileName &&
(fileName.endsWith('.size') || fileName.endsWith('.sizediff'))) {
console.log('Using WebAssembly web worker');
_innerWorker = new Worker('tree-worker-wasm.js');
innerWorker = new Worker('tree-worker-wasm.js');
} else {
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);
startWorkerForFileName(urlParams.get('load_url'));
(function() {
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.
// Subsequent calls will just use a worker locally.
const treeReady = worker.loadTree('from-url://');
if (requiresAuthentication()) {
window.supersize.treeReady = window.googleAuthPromise.then((authResponse) =>
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 = (() => {
/** @type {HTMLSpanElement} */
const symbolName = link.querySelector('.symbol-name');
const idPath = symbolName.title;
data = await worker.openNode(idPath);
data = await window.supersize.worker.openNode(idPath);
_uiNodeData.set(link, data);
}
......@@ -451,8 +451,8 @@ const newTreeElement = (() => {
);
}
treeReady.then(displayTree);
worker.setOnProgressHandler(displayTree);
window.supersize.treeReady.then(displayTree);
window.supersize.worker.setOnProgressHandler(displayTree);
_fileUpload.addEventListener('change', event => {
const input = /** @type {HTMLInputElement} */ (event.currentTarget);
......@@ -463,7 +463,7 @@ const newTreeElement = (() => {
_dataUrlInput.value = '';
_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
input.value = '';
});
......@@ -474,12 +474,12 @@ const newTreeElement = (() => {
// options (marked by `data-dynamic`) are changed.
if (!event.target.dataset.hasOwnProperty('dynamic')) {
_progress.setValue(0);
worker.loadTree().then(displayTree);
window.supersize.worker.loadTree().then(displayTree);
}
});
form.addEventListener('submit', event => {
event.preventDefault();
_progress.setValue(0);
worker.loadTree().then(displayTree);
window.supersize.worker.loadTree().then(displayTree);
});
}
......@@ -6,6 +6,7 @@
importScripts('./shared.js');
importScripts('./caspian_web.js');
importScripts('./auth-consts.js');
const LoadWasm = new Promise(function(resolve, reject) {
Module['onRuntimeInitialized'] = function() {
......@@ -48,14 +49,43 @@ class DataFetcher {
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.
* @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();
this._controller = new AbortController();
const headers = new Headers();
if (!headers) {
headers = new Headers();
}
headers.append('cache-control', 'no-cache');
return fetch(url, {
headers,
......@@ -69,13 +99,25 @@ class DataFetcher {
*/
async loadSizeBuffer() {
if (!this._cache) {
const response = await this.fetch(this._input);
const response = await this.fetchUrl(this._input);
this._cache = new Uint8Array(await response.arrayBuffer());
}
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) {
var dataPtr = Module._malloc(buf.byteLength);
var dataHeap = new Uint8Array(Module.HEAPU8.buffer, dataPtr, buf.byteLength);
......@@ -195,8 +237,8 @@ function parseOptions(options) {
}
const actions = {
/** @param {{input:string|null,options:string}} param0 */
load({input, options}) {
/** @param {{input:string|null,accessToken:string|null,options:string}} param0 */
load({input, accessToken, options}) {
const {
groupBy,
includeRegex,
......@@ -208,6 +250,9 @@ const actions = {
url,
beforeUrl,
} = parseOptions(options);
if (accessToken) {
fetcher.setAccessToken(accessToken);
}
if (input === 'from-url://' && url) {
// Display the data from the `load_url` query parameter
console.info('Displaying data from', url);
......
......@@ -8,6 +8,8 @@
<head>
<title>Super Size Tiger View</title>
<script src="auth-consts.js"></script>
<script src="auth.js"></script>
<script src="start-worker.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#4285f4">
......@@ -20,11 +22,15 @@
<script defer src="state.js"></script>
<script defer src="infocard-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>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('sw.js')
.catch(() => console.warn('ServiceWorker not loaded.'));
}
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('sw.js')
.catch(() => console.warn('ServiceWorker not loaded.'));
}
</script>
</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