2 changed files with 146 additions and 74 deletions
@ -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: <a href="${blobUrl}" target="_blank" download="${fileName}">${fileName}</a>`, |
|||
duration: 10000, |
|||
newWindow: true, |
|||
gravity: 'bottom', |
|||
position: 'right', |
|||
backgroundColor: '#fff', |
|||
stopOnFocus: true, |
|||
}).showToast(); |
|||
} |
|||
|
Loading…
Reference in new issue