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