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
Ben Letchford
5 years ago
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'; |
|||
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) |
|||
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 ''; |
|||
} |
|||
|
|||
// 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 bytes = new Uint8Array(bufferCharacters.length); |
|||
for (let i = 0; i < bufferCharacters.length; i += 1) { |
|||
bytes[i] = bufferCharacters.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); |
|||
|
|||
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(); |
|||
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; |
|||
} |
|||
return data.split('').map(this.bufferCharacter.bind(this)).join('') |
|||
} |
|||
|
|||
onCompleteFile(bufferCharacters: string) { |
|||
this.onCompleteFileCallback(bufferCharacters); |
|||
} |
|||
} |
|||
|
@ -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