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', () => {