github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/ui/app/components/fs/file.js (about)

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