github.com/hernad/nomad@v1.6.112/ui/app/components/fs/file.js (about) 1 /** 2 * Copyright (c) HashiCorp, Inc. 3 * SPDX-License-Identifier: MPL-2.0 4 */ 5 6 import Ember from 'ember'; 7 import { inject as service } from '@ember/service'; 8 import Component from '@ember/component'; 9 import { action, computed } from '@ember/object'; 10 import { equal, gt } from '@ember/object/computed'; 11 import RSVP from 'rsvp'; 12 import Log from 'nomad-ui/utils/classes/log'; 13 import timeout from 'nomad-ui/utils/timeout'; 14 import { classNames, attributeBindings } from '@ember-decorators/component'; 15 import classic from 'ember-classic-decorator'; 16 17 @classic 18 @classNames('boxed-section', 'task-log') 19 @attributeBindings('data-test-file-viewer') 20 export default class File extends Component { 21 @service token; 22 @service system; 23 24 'data-test-file-viewer' = true; 25 26 allocation = null; 27 taskState = null; 28 file = null; 29 stat = null; // { Name, IsDir, Size, FileMode, ModTime, ContentType } 30 31 // When true, request logs from the server agent 32 useServer = false; 33 34 // When true, logs cannot be fetched from either the client or the server 35 noConnection = false; 36 37 clientTimeout = 1000; 38 serverTimeout = 5000; 39 40 mode = 'head'; 41 42 @computed('stat.ContentType') 43 get fileComponent() { 44 const contentType = this.stat.ContentType || ''; 45 46 if (contentType.startsWith('image/')) { 47 return 'image'; 48 } else if ( 49 contentType.startsWith('text/') || 50 contentType.startsWith('application/json') 51 ) { 52 return 'stream'; 53 } else { 54 return 'unknown'; 55 } 56 } 57 58 @gt('stat.Size', 50000) isLarge; 59 60 @equal('fileComponent', 'unknown') fileTypeIsUnknown; 61 @equal('fileComponent', 'stream') isStreamable; 62 isStreaming = false; 63 64 @computed('allocation.id', 'taskState.name', 'file') 65 get catUrlWithoutRegion() { 66 const taskUrlPrefix = this.taskState ? `${this.taskState.name}/` : ''; 67 const encodedPath = encodeURIComponent(`${taskUrlPrefix}${this.file}`); 68 return `/v1/client/fs/cat/${this.allocation.id}?path=${encodedPath}`; 69 } 70 71 @computed('catUrlWithoutRegion', 'system.{activeRegion,shouldIncludeRegion}') 72 get catUrl() { 73 let apiPath = this.catUrlWithoutRegion; 74 if (this.system.shouldIncludeRegion) { 75 apiPath += `®ion=${this.system.activeRegion}`; 76 } 77 return apiPath; 78 } 79 80 @computed('isLarge', 'mode') 81 get fetchMode() { 82 if (this.mode === 'streaming') { 83 return 'stream'; 84 } 85 86 if (!this.isLarge) { 87 return 'cat'; 88 } else if (this.mode === 'head' || this.mode === 'tail') { 89 return 'readat'; 90 } 91 92 return undefined; 93 } 94 95 @computed('allocation.{id,node.httpAddr}', 'fetchMode', 'useServer') 96 get fileUrl() { 97 const address = this.get('allocation.node.httpAddr'); 98 const url = `/v1/client/fs/${this.fetchMode}/${this.allocation.id}`; 99 return this.useServer ? url : `//${address}${url}`; 100 } 101 102 @computed('file', 'mode', 'stat.Size', 'taskState.name') 103 get fileParams() { 104 // The Log class handles encoding query params 105 const taskUrlPrefix = this.taskState ? `${this.taskState.name}/` : ''; 106 const path = `${taskUrlPrefix}${this.file}`; 107 108 switch (this.mode) { 109 case 'head': 110 return { path, offset: 0, limit: 50000 }; 111 case 'tail': 112 return { path, offset: this.stat.Size - 50000, limit: 50000 }; 113 case 'streaming': 114 return { path, offset: 50000, origin: 'end' }; 115 default: 116 return { path }; 117 } 118 } 119 120 @computed( 121 'clientTimeout', 122 'fileParams', 123 'fileUrl', 124 'mode', 125 'serverTimeout', 126 'useServer' 127 ) 128 get logger() { 129 // The cat and readat APIs are in plainText while the stream API is always encoded. 130 const plainText = this.mode === 'head' || this.mode === 'tail'; 131 132 // If the file request can't settle in one second, the client 133 // must be unavailable and the server should be used instead 134 const timing = this.useServer ? this.serverTimeout : this.clientTimeout; 135 const logFetch = (url) => 136 RSVP.race([this.token.authorizedRequest(url), timeout(timing)]).then( 137 (response) => { 138 if (!response || !response.ok) { 139 this.nextErrorState(response); 140 } 141 return response; 142 }, 143 (error) => this.nextErrorState(error) 144 ); 145 146 return Log.create({ 147 logFetch, 148 plainText, 149 params: this.fileParams, 150 url: this.fileUrl, 151 }); 152 } 153 154 nextErrorState(error) { 155 if (this.useServer) { 156 this.set('noConnection', true); 157 } else { 158 this.send('failoverToServer'); 159 } 160 throw error; 161 } 162 163 @action 164 toggleStream() { 165 this.set('mode', 'streaming'); 166 this.toggleProperty('isStreaming'); 167 } 168 169 @action 170 gotoHead() { 171 this.set('mode', 'head'); 172 this.set('isStreaming', false); 173 } 174 175 @action 176 gotoTail() { 177 this.set('mode', 'tail'); 178 this.set('isStreaming', false); 179 } 180 181 @action 182 failoverToServer() { 183 this.set('useServer', true); 184 } 185 186 @action 187 async downloadFile() { 188 const timing = this.useServer ? this.serverTimeout : this.clientTimeout; 189 190 try { 191 const response = await RSVP.race([ 192 this.token.authorizedRequest(this.catUrlWithoutRegion), 193 timeout(timing), 194 ]); 195 196 if (!response || !response.ok) throw new Error('file download timeout'); 197 198 // Don't download in tests. Unfortunately, since the download is triggered 199 // by the download attribute of the ephemeral anchor element, there's no 200 // way to stub this in tests. 201 if (Ember.testing) return; 202 203 const blob = await response.blob(); 204 const url = window.URL.createObjectURL(blob); 205 const downloadAnchor = document.createElement('a'); 206 207 downloadAnchor.href = url; 208 downloadAnchor.target = '_blank'; 209 downloadAnchor.rel = 'noopener noreferrer'; 210 downloadAnchor.download = this.file; 211 212 // Appending the element to the DOM is required for Firefox support 213 document.body.appendChild(downloadAnchor); 214 downloadAnchor.click(); 215 downloadAnchor.remove(); 216 217 window.URL.revokeObjectURL(url); 218 } catch (err) { 219 this.nextErrorState(err); 220 } 221 } 222 }