Browse Source
* buffer files on a character basis * Add tests and initial github workflow * Bump version * Pull request feedback from @butlerx * ESlint ignore download.spec.ts filepull/225/head
committed by
Cian Butler
6 changed files with 972 additions and 229 deletions
@ -0,0 +1,22 @@ |
|||||
|
--- |
||||
|
on: [pull_request] |
||||
|
|
||||
|
name: Run tests |
||||
|
jobs: |
||||
|
build_and_test: |
||||
|
runs-on: ubuntu-latest |
||||
|
steps: |
||||
|
- name: Checkout |
||||
|
uses: actions/checkout@v1 |
||||
|
- name: Setup env |
||||
|
uses: actions/setup-node@v1 |
||||
|
with: |
||||
|
node-version: 12 |
||||
|
- run: yarn |
||||
|
name: Install dependencies |
||||
|
- name: ESLint checks |
||||
|
run: yarn lint |
||||
|
- run: yarn build |
||||
|
name: Compile Typescript |
||||
|
- run: yarn test |
||||
|
name: Run tests |
@ -1,55 +1,96 @@ |
|||||
import * as fileType from 'file-type'; |
const DEFAULT_FILE_BEGIN = '\u001b[5i'; |
||||
import Toastify from 'toastify-js'; |
const DEFAULT_FILE_END = '\u001b[4i'; |
||||
|
|
||||
export const FILE_BEGIN = '\u001b[5i'; |
export class FileDownloader { |
||||
export const FILE_END = '\u001b[4i'; |
constructor( |
||||
export let fileBuffer = []; |
onCompleteFileCallback: (file: string) => any, |
||||
|
fileBegin: string = DEFAULT_FILE_BEGIN, |
||||
export function onCompleteFile() { |
fileEnd: string = DEFAULT_FILE_END |
||||
let bufferCharacters = fileBuffer.join(''); |
) { |
||||
bufferCharacters = bufferCharacters.substring( |
this.fileBuffer = []; |
||||
bufferCharacters.lastIndexOf(FILE_BEGIN) + FILE_BEGIN.length, |
this.onCompleteFileCallback = onCompleteFileCallback; |
||||
bufferCharacters.lastIndexOf(FILE_END) |
this.fileBegin = fileBegin; |
||||
); |
this.fileEnd = fileEnd; |
||||
|
this.partialFileBegin = ''; |
||||
// Try to decode it as base64, if it fails we assume it's not base64
|
} |
||||
try { |
|
||||
bufferCharacters = window.atob(bufferCharacters); |
bufferCharacter(character: string): string { |
||||
} catch (err) { |
// If we are not currently buffering a file.
|
||||
// Assuming it's not base64...
|
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); |
buffer(data: string): string { |
||||
for (let i = 0; i < bufferCharacters.length; i += 1) { |
// This is a optimization to quickly return if we know for
|
||||
bytes[i] = bufferCharacters.charCodeAt(i); |
// 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; |
||||
|
} |
||||
|
return data.split('').map(this.bufferCharacter.bind(this)).join('') |
||||
} |
} |
||||
|
|
||||
let mimeType = 'application/octet-stream'; |
onCompleteFile(bufferCharacters: string) { |
||||
let fileExt = ''; |
this.onCompleteFileCallback(bufferCharacters); |
||||
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); |
|
||||
|
|
||||
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(); |
|
||||
} |
} |
||||
|
@ -0,0 +1,195 @@ |
|||||
|
/* eslint-disable */ |
||||
|
|
||||
|
import { expect } from 'chai'; |
||||
|
import 'mocha'; |
||||
|
import * as sinon from 'sinon'; |
||||
|
|
||||
|
import { JSDOM } from 'jsdom'; |
||||
|
import { FileDownloader } from '../download'; |
||||
|
|
||||
|
const { window } = new JSDOM(`...`); |
||||
|
|
||||
|
describe('FileDownloader', () => { |
||||
|
const FILE_BEGIN = 'BEGIN'; |
||||
|
const FILE_END = 'END'; |
||||
|
let fileDownloader: any; |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
fileDownloader = new FileDownloader(() => { }, FILE_BEGIN, FILE_END); |
||||
|
}); |
||||
|
|
||||
|
afterEach(() => { |
||||
|
sinon.restore(); |
||||
|
}); |
||||
|
|
||||
|
it('should return data before file markers', () => { |
||||
|
const onCompleteFileStub = sinon.stub(fileDownloader, 'onCompleteFile'); |
||||
|
expect( |
||||
|
fileDownloader.buffer(`DATA AT THE LEFT${FILE_BEGIN}FILE${FILE_END}`) |
||||
|
).to.equal('DATA AT THE LEFT'); |
||||
|
expect(onCompleteFileStub.calledOnce).to.be.true; |
||||
|
expect(onCompleteFileStub.getCall(0).args[0]).to.equal('FILE'); |
||||
|
}); |
||||
|
|
||||
|
it('should return data after file markers', () => { |
||||
|
const onCompleteFileStub = sinon.stub(fileDownloader, 'onCompleteFile'); |
||||
|
expect( |
||||
|
fileDownloader.buffer(`${FILE_BEGIN}FILE${FILE_END}DATA AT THE RIGHT`) |
||||
|
).to.equal('DATA AT THE RIGHT'); |
||||
|
expect(onCompleteFileStub.calledOnce).to.be.true; |
||||
|
expect(onCompleteFileStub.getCall(0).args[0]).to.equal('FILE'); |
||||
|
}); |
||||
|
|
||||
|
it('should return data before and after file markers', () => { |
||||
|
const onCompleteFileStub = sinon.stub(fileDownloader, 'onCompleteFile'); |
||||
|
expect( |
||||
|
fileDownloader.buffer( |
||||
|
`DATA AT THE LEFT${FILE_BEGIN}FILE${FILE_END}DATA AT THE RIGHT` |
||||
|
) |
||||
|
).to.equal('DATA AT THE LEFTDATA AT THE RIGHT'); |
||||
|
expect(onCompleteFileStub.calledOnce).to.be.true; |
||||
|
expect(onCompleteFileStub.getCall(0).args[0]).to.equal('FILE'); |
||||
|
}); |
||||
|
|
||||
|
it('should return data before a beginning marker found', () => { |
||||
|
const onCompleteFileStub = sinon.stub(fileDownloader, 'onCompleteFile'); |
||||
|
expect(fileDownloader.buffer(`DATA AT THE LEFT${FILE_BEGIN}FILE`)).to.equal( |
||||
|
'DATA AT THE LEFT' |
||||
|
); |
||||
|
}); |
||||
|
|
||||
|
it('should return data after an ending marker found', () => { |
||||
|
const onCompleteFileStub = sinon.stub(fileDownloader, 'onCompleteFile'); |
||||
|
expect(fileDownloader.buffer(`${FILE_BEGIN}FI`)).to.equal(''); |
||||
|
expect(fileDownloader.buffer(`LE${FILE_END}DATA AT THE RIGHT`)).to.equal( |
||||
|
'DATA AT THE RIGHT' |
||||
|
); |
||||
|
expect(onCompleteFileStub.calledOnce).to.be.true; |
||||
|
expect(onCompleteFileStub.getCall(0).args[0]).to.equal('FILE'); |
||||
|
}); |
||||
|
|
||||
|
it('should buffer across incomplete file begin marker sequence on two calls', () => { |
||||
|
fileDownloader = new FileDownloader(() => { }, 'BEGIN', 'END'); |
||||
|
const onCompleteFileStub = sinon.stub(fileDownloader, 'onCompleteFile'); |
||||
|
|
||||
|
expect(fileDownloader.buffer('BEG')).to.equal(''); |
||||
|
expect(fileDownloader.buffer('INFILEEND')).to.equal(''); |
||||
|
expect(onCompleteFileStub.calledOnce).to.be.true; |
||||
|
expect(onCompleteFileStub.getCall(0).args[0]).to.equal('FILE'); |
||||
|
}); |
||||
|
|
||||
|
it('should buffer across incomplete file begin marker sequence on n calls', () => { |
||||
|
fileDownloader = new FileDownloader(() => { }, 'BEGIN', 'END'); |
||||
|
const onCompleteFileStub = sinon.stub(fileDownloader, 'onCompleteFile'); |
||||
|
|
||||
|
expect(fileDownloader.buffer('B')).to.equal(''); |
||||
|
expect(fileDownloader.buffer('E')).to.equal(''); |
||||
|
expect(fileDownloader.buffer('G')).to.equal(''); |
||||
|
expect(fileDownloader.buffer('I')).to.equal(''); |
||||
|
expect(fileDownloader.buffer('NFILE' + 'END')).to.equal(''); |
||||
|
expect(onCompleteFileStub.calledOnce).to.be.true; |
||||
|
expect(onCompleteFileStub.getCall(0).args[0]).to.equal('FILE'); |
||||
|
}); |
||||
|
|
||||
|
it('should buffer across incomplete file begin marker sequence with data on the left and right on multiple calls', () => { |
||||
|
fileDownloader = new FileDownloader(() => { }, 'BEGIN', 'END'); |
||||
|
const onCompleteFileStub = sinon.stub(fileDownloader, 'onCompleteFile'); |
||||
|
|
||||
|
expect(fileDownloader.buffer('DATA AT THE LEFT' + 'B')).to.equal( |
||||
|
'DATA AT THE LEFT' |
||||
|
); |
||||
|
expect(fileDownloader.buffer('E')).to.equal(''); |
||||
|
expect(fileDownloader.buffer('G')).to.equal(''); |
||||
|
expect(fileDownloader.buffer('I')).to.equal(''); |
||||
|
expect(fileDownloader.buffer('NFILE' + 'ENDDATA AT THE RIGHT')).to.equal( |
||||
|
'DATA AT THE RIGHT' |
||||
|
); |
||||
|
expect(onCompleteFileStub.calledOnce).to.be.true; |
||||
|
expect(onCompleteFileStub.getCall(0).args[0]).to.equal('FILE'); |
||||
|
}); |
||||
|
|
||||
|
it('should buffer across incomplete file begin marker sequence then handle false positive', () => { |
||||
|
fileDownloader = new FileDownloader(() => { }, 'BEGIN', 'END'); |
||||
|
const onCompleteFileStub = sinon.stub(fileDownloader, 'onCompleteFile'); |
||||
|
|
||||
|
expect(fileDownloader.buffer('DATA AT THE LEFT' + 'B')).to.equal( |
||||
|
'DATA AT THE LEFT' |
||||
|
); |
||||
|
expect(fileDownloader.buffer('E')).to.equal(''); |
||||
|
expect(fileDownloader.buffer('G')).to.equal(''); |
||||
|
// This isn't part of the file_begin marker and should trigger the partial
|
||||
|
// file begin marker to be returned with the normal data
|
||||
|
expect(fileDownloader.buffer('ZDATA AT THE RIGHT')).to.equal( |
||||
|
'BEGZDATA AT THE RIGHT' |
||||
|
); |
||||
|
expect(onCompleteFileStub.called).to.be.false; |
||||
|
}); |
||||
|
|
||||
|
it('should buffer across incomplete file end marker sequence on two calls', () => { |
||||
|
fileDownloader = new FileDownloader(() => { }, 'BEGIN', 'END'); |
||||
|
const mockFilePart1 = 'DATA AT THE LEFTBEGINFILEE'; |
||||
|
const mockFilePart2 = 'NDDATA AT THE RIGHT'; |
||||
|
|
||||
|
const onCompleteFileStub = sinon.stub(fileDownloader, 'onCompleteFile'); |
||||
|
expect(fileDownloader.buffer(mockFilePart1)).to.equal('DATA AT THE LEFT'); |
||||
|
expect(fileDownloader.buffer(mockFilePart2)).to.equal('DATA AT THE RIGHT'); |
||||
|
|
||||
|
expect(onCompleteFileStub.calledOnce).to.be.true; |
||||
|
expect(onCompleteFileStub.getCall(0).args[0]).to.equal('FILE'); |
||||
|
}); |
||||
|
|
||||
|
it('should buffer across incomplete file end and file begin marker sequence with data on the left and right on multiple calls', () => { |
||||
|
fileDownloader = new FileDownloader(() => { }, 'BEGIN', 'END'); |
||||
|
const onCompleteFileStub = sinon.stub(fileDownloader, 'onCompleteFile'); |
||||
|
|
||||
|
expect(fileDownloader.buffer('DATA AT THE LEFT' + 'BE')).to.equal( |
||||
|
'DATA AT THE LEFT' |
||||
|
); |
||||
|
expect(fileDownloader.buffer('G')).to.equal(''); |
||||
|
expect(fileDownloader.buffer('I')).to.equal(''); |
||||
|
expect(fileDownloader.buffer('NFILEE')).to.equal(''); |
||||
|
expect(fileDownloader.buffer('N')).to.equal(''); |
||||
|
expect(fileDownloader.buffer('DDATA AT THE RIGHT')).to.equal( |
||||
|
'DATA AT THE RIGHT' |
||||
|
); |
||||
|
expect(onCompleteFileStub.calledOnce).to.be.true; |
||||
|
expect(onCompleteFileStub.getCall(0).args[0]).to.equal('FILE'); |
||||
|
}); |
||||
|
|
||||
|
it('should be able to handle multiple files', () => { |
||||
|
fileDownloader = new FileDownloader(() => { }, 'BEGIN', 'END'); |
||||
|
const onCompleteFileStub = sinon.stub(fileDownloader, 'onCompleteFile'); |
||||
|
|
||||
|
expect( |
||||
|
fileDownloader.buffer( |
||||
|
'DATA AT THE LEFT' + 'BEGIN' + 'FILE1' + 'END' + 'SECOND DATA' + 'BEGIN' |
||||
|
) |
||||
|
).to.equal('DATA AT THE LEFT' + 'SECOND DATA'); |
||||
|
expect(onCompleteFileStub.calledOnce).to.be.true; |
||||
|
expect(onCompleteFileStub.getCall(0).args[0]).to.equal('FILE1'); |
||||
|
|
||||
|
expect(fileDownloader.buffer('FILE2')).to.equal(''); |
||||
|
expect(fileDownloader.buffer('E')).to.equal(''); |
||||
|
expect(fileDownloader.buffer('NDRIGHT')).to.equal('RIGHT'); |
||||
|
expect(onCompleteFileStub.calledTwice).to.be.true; |
||||
|
expect(onCompleteFileStub.getCall(1).args[0]).to.equal('FILE2'); |
||||
|
}); |
||||
|
|
||||
|
it('should be able to handle multiple files with an ending marker', () => { |
||||
|
fileDownloader = new FileDownloader(() => { }, 'BEGIN', 'END'); |
||||
|
const onCompleteFileStub = sinon.stub(fileDownloader, 'onCompleteFile'); |
||||
|
|
||||
|
expect( |
||||
|
fileDownloader.buffer('DATA AT THE LEFT' + 'BEGIN' + 'FILE1' + 'EN') |
||||
|
).to.equal('DATA AT THE LEFT'); |
||||
|
expect(onCompleteFileStub.calledOnce).to.be.false; |
||||
|
expect( |
||||
|
fileDownloader.buffer('D' + 'SECOND DATA' + 'BEGIN' + 'FILE2' + 'EN') |
||||
|
).to.equal('SECOND DATA'); |
||||
|
expect(onCompleteFileStub.calledOnce).to.be.true; |
||||
|
expect(onCompleteFileStub.getCall(0).args[0]).to.equal('FILE1'); |
||||
|
expect(fileDownloader.buffer('D')).to.equal(''); |
||||
|
expect(onCompleteFileStub.calledTwice).to.be.true; |
||||
|
expect(onCompleteFileStub.getCall(1).args[0]).to.equal('FILE2'); |
||||
|
}); |
||||
|
}); |
File diff suppressed because it is too large
Loading…
Reference in new issue