1// Copyright (C) 2018 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15import {Draft, produce} from 'immer'; 16import * as uuidv4 from 'uuid/v4'; 17 18import {assertExists} from '../base/logging'; 19import {Actions} from '../common/actions'; 20import {EngineConfig, State} from '../common/state'; 21 22import {Controller} from './controller'; 23import {globals} from './globals'; 24 25export const BUCKET_NAME = 'perfetto-ui-data'; 26 27export class PermalinkController extends Controller<'main'> { 28 private lastRequestId?: string; 29 constructor() { 30 super('main'); 31 } 32 33 run() { 34 if (globals.state.permalink.requestId === undefined || 35 globals.state.permalink.requestId === this.lastRequestId) { 36 return; 37 } 38 const requestId = assertExists(globals.state.permalink.requestId); 39 this.lastRequestId = requestId; 40 41 // if the |link| is not set, this is a request to create a permalink. 42 if (globals.state.permalink.hash === undefined) { 43 PermalinkController.createPermalink().then(hash => { 44 globals.dispatch(Actions.setPermalink({requestId, hash})); 45 }); 46 return; 47 } 48 49 // Otherwise, this is a request to load the permalink. 50 PermalinkController.loadState(globals.state.permalink.hash).then(state => { 51 globals.dispatch(Actions.setState({newState: state})); 52 this.lastRequestId = state.permalink.requestId; 53 }); 54 } 55 56 private static async createPermalink() { 57 const state = globals.state; 58 59 // Upload each loaded trace. 60 const fileToUrl = new Map<File, string>(); 61 for (const engine of Object.values<EngineConfig>(state.engines)) { 62 if (!(engine.source instanceof File)) continue; 63 PermalinkController.updateStatus(`Uploading ${engine.source.name}`); 64 const url = await this.saveTrace(engine.source); 65 fileToUrl.set(engine.source, url); 66 } 67 68 // Convert state to use URLs and remove permalink. 69 const uploadState = produce(state, draft => { 70 for (const engine of Object.values<Draft<EngineConfig>>( 71 draft.engines)) { 72 if (!(engine.source instanceof File)) continue; 73 engine.source = fileToUrl.get(engine.source)!; 74 } 75 draft.permalink = {}; 76 }); 77 78 // Upload state. 79 PermalinkController.updateStatus(`Creating permalink...`); 80 const hash = await this.saveState(uploadState); 81 PermalinkController.updateStatus(`Permalink ready`); 82 return hash; 83 } 84 85 private static async saveState(state: State): Promise<string> { 86 const text = JSON.stringify(state); 87 const hash = await this.toSha256(text); 88 const url = 'https://www.googleapis.com/upload/storage/v1/b/' + 89 `${BUCKET_NAME}/o?uploadType=media` + 90 `&name=${hash}&predefinedAcl=publicRead`; 91 const response = await fetch(url, { 92 method: 'post', 93 headers: { 94 'Content-Type': 'application/json; charset=utf-8', 95 }, 96 body: text, 97 }); 98 await response.json(); 99 100 return hash; 101 } 102 103 private static async saveTrace(trace: File): Promise<string> { 104 // TODO(hjd): This should probably also be a hash but that requires 105 // trace processor support. 106 const name = uuidv4(); 107 const url = 'https://www.googleapis.com/upload/storage/v1/b/' + 108 `${BUCKET_NAME}/o?uploadType=media` + 109 `&name=${name}&predefinedAcl=publicRead`; 110 const response = await fetch(url, { 111 method: 'post', 112 headers: {'Content-Type': 'application/octet-stream;'}, 113 body: trace, 114 }); 115 await response.json(); 116 return `https://storage.googleapis.com/${BUCKET_NAME}/${name}`; 117 } 118 119 private static async loadState(id: string): Promise<State> { 120 const url = `https://storage.googleapis.com/${BUCKET_NAME}/${id}`; 121 const response = await fetch(url); 122 const text = await response.text(); 123 const stateHash = await this.toSha256(text); 124 const state = JSON.parse(text); 125 if (stateHash !== id) { 126 throw new Error(`State hash does not match ${id} vs. ${stateHash}`); 127 } 128 return state; 129 } 130 131 private static async toSha256(str: string): Promise<string> { 132 // TODO(hjd): TypeScript bug with definition of TextEncoder. 133 // tslint:disable-next-line no-any 134 const buffer = new (TextEncoder as any)('utf-8').encode(str); 135 const digest = await crypto.subtle.digest('SHA-256', buffer); 136 return Array.from(new Uint8Array(digest)).map(x => x.toString(16)).join(''); 137 } 138 139 private static updateStatus(msg: string): void { 140 // TODO(hjd): Unify loading updates. 141 globals.dispatch(Actions.updateStatus({ 142 msg, 143 timestamp: Date.now() / 1000, 144 })); 145 } 146} 147