// Copyright (C) 2018 The Android Open Source Project // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import {Draft, produce} from 'immer'; import * as uuidv4 from 'uuid/v4'; import {assertExists} from '../base/logging'; import {Actions} from '../common/actions'; import {EngineConfig, State} from '../common/state'; import {Controller} from './controller'; import {globals} from './globals'; export const BUCKET_NAME = 'perfetto-ui-data'; export class PermalinkController extends Controller<'main'> { private lastRequestId?: string; constructor() { super('main'); } run() { if (globals.state.permalink.requestId === undefined || globals.state.permalink.requestId === this.lastRequestId) { return; } const requestId = assertExists(globals.state.permalink.requestId); this.lastRequestId = requestId; // if the |link| is not set, this is a request to create a permalink. if (globals.state.permalink.hash === undefined) { PermalinkController.createPermalink().then(hash => { globals.dispatch(Actions.setPermalink({requestId, hash})); }); return; } // Otherwise, this is a request to load the permalink. PermalinkController.loadState(globals.state.permalink.hash).then(state => { globals.dispatch(Actions.setState({newState: state})); this.lastRequestId = state.permalink.requestId; }); } private static async createPermalink() { const state = globals.state; // Upload each loaded trace. const fileToUrl = new Map(); for (const engine of Object.values(state.engines)) { if (!(engine.source instanceof File)) continue; PermalinkController.updateStatus(`Uploading ${engine.source.name}`); const url = await this.saveTrace(engine.source); fileToUrl.set(engine.source, url); } // Convert state to use URLs and remove permalink. const uploadState = produce(state, draft => { for (const engine of Object.values>( draft.engines)) { if (!(engine.source instanceof File)) continue; engine.source = fileToUrl.get(engine.source)!; } draft.permalink = {}; }); // Upload state. PermalinkController.updateStatus(`Creating permalink...`); const hash = await this.saveState(uploadState); PermalinkController.updateStatus(`Permalink ready`); return hash; } private static async saveState(state: State): Promise { const text = JSON.stringify(state); const hash = await this.toSha256(text); const url = 'https://www.googleapis.com/upload/storage/v1/b/' + `${BUCKET_NAME}/o?uploadType=media` + `&name=${hash}&predefinedAcl=publicRead`; const response = await fetch(url, { method: 'post', headers: { 'Content-Type': 'application/json; charset=utf-8', }, body: text, }); await response.json(); return hash; } private static async saveTrace(trace: File): Promise { // TODO(hjd): This should probably also be a hash but that requires // trace processor support. const name = uuidv4(); const url = 'https://www.googleapis.com/upload/storage/v1/b/' + `${BUCKET_NAME}/o?uploadType=media` + `&name=${name}&predefinedAcl=publicRead`; const response = await fetch(url, { method: 'post', headers: {'Content-Type': 'application/octet-stream;'}, body: trace, }); await response.json(); return `https://storage.googleapis.com/${BUCKET_NAME}/${name}`; } private static async loadState(id: string): Promise { const url = `https://storage.googleapis.com/${BUCKET_NAME}/${id}`; const response = await fetch(url); const text = await response.text(); const stateHash = await this.toSha256(text); const state = JSON.parse(text); if (stateHash !== id) { throw new Error(`State hash does not match ${id} vs. ${stateHash}`); } return state; } private static async toSha256(str: string): Promise { // TODO(hjd): TypeScript bug with definition of TextEncoder. // tslint:disable-next-line no-any const buffer = new (TextEncoder as any)('utf-8').encode(str); const digest = await crypto.subtle.digest('SHA-256', buffer); return Array.from(new Uint8Array(digest)).map(x => x.toString(16)).join(''); } private static updateStatus(msg: string): void { // TODO(hjd): Unify loading updates. globals.dispatch(Actions.updateStatus({ msg, timestamp: Date.now() / 1000, })); } }