From e81966cd5c460184eea389ab4b31b7618b3f3b31 Mon Sep 17 00:00:00 2001 From: Ben Letchford Date: Wed, 27 Nov 2019 16:52:34 +1100 Subject: [PATCH] buffer files on a character basis --- src/client/download.ts | 143 +++++++++++++++++++++++++++-------------- src/client/index.ts | 77 +++++++++++++++------- 2 files changed, 146 insertions(+), 74 deletions(-) diff --git a/src/client/download.ts b/src/client/download.ts index 60525eb..5050025 100644 --- a/src/client/download.ts +++ b/src/client/download.ts @@ -1,55 +1,100 @@ -import * as fileType from 'file-type'; -import Toastify from 'toastify-js'; - -export const FILE_BEGIN = '\u001b[5i'; -export const FILE_END = '\u001b[4i'; -export let fileBuffer = []; - -export function onCompleteFile() { - let bufferCharacters = fileBuffer.join(''); - bufferCharacters = bufferCharacters.substring( - bufferCharacters.lastIndexOf(FILE_BEGIN) + FILE_BEGIN.length, - bufferCharacters.lastIndexOf(FILE_END) - ); - - // Try to decode it as base64, if it fails we assume it's not base64 - try { - bufferCharacters = window.atob(bufferCharacters); - } catch (err) { - // Assuming it's not base64... +const DEFAULT_FILE_BEGIN = '\u001b[5i'; +const DEFAULT_FILE_END = '\u001b[4i'; + +export class FileDownloader { + constructor( + onCompleteFileCallback: (file: string) => any, + fileBegin: string = DEFAULT_FILE_BEGIN, + fileEnd: string = DEFAULT_FILE_END + ) { + this.fileBuffer = []; + this.onCompleteFileCallback = onCompleteFileCallback; + this.fileBegin = fileBegin; + this.fileEnd = fileEnd; + this.partialFileBegin = ''; + } + + bufferCharacter(character: string): string { + // If we are not currently buffering a file. + if (this.fileBuffer.length === 0) { + // If we are not currently expecting the rest of the fileBegin sequences. + if (this.partialFileBegin.length === 0) { + // If the character is the first character of fileBegin we know to start + // expecting the rest of the fileBegin sequence. + if (character === this.fileBegin[0]) { + this.partialFileBegin = character; + return ''; + } + // Otherwise, we just return the character for printing to the terminal. + + return character; + } + // We're currently in the state of buffering a beginner marker... + + const nextExpectedCharacter = this.fileBegin[ + this.partialFileBegin.length + ]; + // If the next character *is* the next character in the fileBegin sequence. + if (character === nextExpectedCharacter) { + this.partialFileBegin += character; + // Do we now have the complete fileBegin sequence. + if (this.partialFileBegin === this.fileBegin) { + this.partialFileBegin = ''; + this.fileBuffer = this.fileBuffer.concat(this.fileBegin.split('')); + return ''; + } + // Otherwise, we just wait until the next character. + + return ''; + } + // If the next expected character wasn't found for the fileBegin sequence, + // we need to return all the data that was bufferd in the partialFileBegin + // back to the terminal. + + const dataToReturn = this.partialFileBegin + character; + this.partialFileBegin = ''; + return dataToReturn; + } + // If we are currently in the state of buffering a file. + + this.fileBuffer.push(character); + // If we now have an entire fileEnd marker, we have a complete file! + if ( + this.fileBuffer.length >= this.fileBegin.length + this.fileEnd.length && + this.fileBuffer.slice(-this.fileEnd.length).join('') === this.fileEnd + ) { + this.onCompleteFile( + this.fileBuffer + .slice( + this.fileBegin.length, + this.fileBuffer.length - this.fileEnd.length + ) + .join('') + ); + this.fileBuffer = []; + } + + return ''; } - const bytes = new Uint8Array(bufferCharacters.length); - for (let i = 0; i < bufferCharacters.length; i += 1) { - bytes[i] = bufferCharacters.charCodeAt(i); + buffer(data: string): string { + // This is a optimization to quickly return if we know for + // sure we don't need to loop over each character. + if ( + this.fileBuffer.length === 0 && + this.partialFileBegin.length === 0 && + data.indexOf(this.fileBegin[0]) === -1 + ) { + return data; + } + let newData = ''; + for (let i = 0; i < data.length; i += 1) { + newData += this.bufferCharacter(data[i]); + } + return newData; } - let mimeType = 'application/octet-stream'; - let fileExt = ''; - const typeData = fileType(bytes); - if (typeData) { - mimeType = typeData.mime; - fileExt = typeData.ext; + onCompleteFile(bufferCharacters: string) { + this.onCompleteFileCallback(bufferCharacters); } - const fileName = `file-${new Date() - .toISOString() - .split('.')[0] - .replace(/-/g, '') - .replace('T', '') - .replace(/:/g, '')}${fileExt ? `.${fileExt}` : ''}`; - - const blob = new Blob([new Uint8Array(bytes.buffer)], { type: mimeType }); - const blobUrl = URL.createObjectURL(blob); - - fileBuffer = []; - - Toastify({ - text: `Download ready: ${fileName}`, - duration: 10000, - newWindow: true, - gravity: 'bottom', - position: 'right', - backgroundColor: '#fff', - stopOnFocus: true, - }).showToast(); } diff --git a/src/client/index.ts b/src/client/index.ts index 0101d6c..eee0940 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -3,9 +3,11 @@ import { isNull } from 'lodash'; import { dom, library } from "@fortawesome/fontawesome-svg-core"; import { faCogs } from "@fortawesome/free-solid-svg-icons/faCogs"; +import Toastify from 'toastify-js'; +import * as fileType from 'file-type'; import { socket } from './socket'; import { overlay, terminal } from './elements'; -import { FILE_BEGIN, FILE_END, fileBuffer, onCompleteFile } from './download'; +import { FileDownloader } from './download'; import verifyPrompt from './verify'; import disconnect from './disconnect'; import mobileKeyboard from './mobile'; @@ -76,6 +78,52 @@ socket.on('connect', () => { term.focus(); mobileKeyboard(); + const fileDownloader = new FileDownloader(function ( + bufferCharacters: string + ) { + let fileCharacters = bufferCharacters; + // Try to decode it as base64, if it fails we assume it's not base64 + try { + fileCharacters = window.atob(fileCharacters); + } catch (err) { + // Assuming it's not base64... + } + + const bytes = new Uint8Array(fileCharacters.length); + for (let i = 0; i < fileCharacters.length; i += 1) { + bytes[i] = fileCharacters.charCodeAt(i); + } + + let mimeType = 'application/octet-stream'; + let fileExt = ''; + const typeData = fileType(bytes); + if (typeData) { + mimeType = typeData.mime; + fileExt = typeData.ext; + } + const fileName = `file-${new Date() + .toISOString() + .split('.')[0] + .replace(/-/g, '') + .replace('T', '') + .replace(/:/g, '')}${fileExt ? `.${fileExt}` : ''}`; + + const blob = new Blob([new Uint8Array(bytes.buffer)], { + type: mimeType, + }); + const blobUrl = URL.createObjectURL(blob); + + Toastify({ + text: `Download ready: ${fileName}`, + duration: 10000, + newWindow: true, + gravity: 'bottom', + position: 'right', + backgroundColor: '#fff', + stopOnFocus: true, + }).showToast(); + }); + term.on('data', data => { socket.emit('input', data); }); @@ -84,30 +132,9 @@ socket.on('connect', () => { }); socket .on('data', (data: string) => { - const indexOfFileBegin = data.indexOf(FILE_BEGIN); - const indexOfFileEnd = data.indexOf(FILE_END); - - // If we've got the entire file in one chunk - if (indexOfFileBegin !== -1 && indexOfFileEnd !== -1) { - fileBuffer.push(data); - onCompleteFile(); - } - // If we've found a beginning marker - else if (indexOfFileBegin !== -1) { - fileBuffer.push(data); - } - // If we've found an ending marker - else if (indexOfFileEnd !== -1) { - fileBuffer.push(data); - onCompleteFile(); - } - // If we've found the continuation of a file - else if (fileBuffer.length > 0) { - fileBuffer.push(data); - } - // Just treat it as normal data - else { - term.write(data); + const remainingData = fileDownloader.buffer(data); + if (remainingData) { + term.write(remainingData); } }) .on('login', () => {