From 40dbe6cc4268e4ad1939cebf4bec0dc6dc5b19fe Mon Sep 17 00:00:00 2001 From: Sam Carlton Date: Tue, 19 Jul 2022 21:24:30 -0500 Subject: [PATCH] Add FileApi FileReader --- helpers/scanner/file-api.js | 289 ++++++++++++++++++++++++++++++++++++ 1 file changed, 289 insertions(+) diff --git a/helpers/scanner/file-api.js b/helpers/scanner/file-api.js index a5bc95b..dbc1bc0 100644 --- a/helpers/scanner/file-api.js +++ b/helpers/scanner/file-api.js @@ -1,3 +1,5 @@ +import { EventEmitter } from 'events' + export function File (input) { var self = this; @@ -42,3 +44,290 @@ export function File (input) { // }); // } } + + +function doop(fn, args, context) { + if ('function' === typeof fn) { + fn.apply(context, args); + } +} + +function toDataUrl(data, type) { + // var data = self.result; + var dataUrl = 'data:'; + + if (type) { + dataUrl += type + ';'; + } + + if (/text/i.test(type)) { + dataUrl += 'charset=utf-8,'; + dataUrl += data.toString('utf8'); + } else { + dataUrl += 'base64,'; + dataUrl += data.toString('base64'); + } + + return dataUrl; +} + +function mapDataToFormat(file, data, format, encoding) { + // var data = self.result; + + switch (format) { + case 'buffer': + return data; + break; + case 'binary': + return data.toString('binary'); + break; + case 'dataUrl': + return toDataUrl(data, file.type); + break; + case 'text': + return data.toString(encoding || 'utf8'); + break; + } +} + +export function FileReader () { + var self = this, + emitter = new EventEmitter, + file; + + self.addEventListener = function(on, callback) { + emitter.on(on, callback); + }; + self.removeEventListener = function(callback) { + emitter.removeListener(callback); + } + self.dispatchEvent = function(on) { + emitter.emit(on); + } + + self.EMPTY = 0; + self.LOADING = 1; + self.DONE = 2; + + self.error = undefined; // Read only + self.readyState = self.EMPTY; // Read only + self.result = undefined; // Road only + + // non-standard + self.on = function() { + emitter.on.apply(emitter, arguments); + } + self.nodeChunkedEncoding = false; + self.setNodeChunkedEncoding = function(val) { + self.nodeChunkedEncoding = val; + }; + // end non-standard + + + + // Whatever the file object is, turn it into a Node.JS File.Stream + function createFileStream() { + var stream = new EventEmitter(), + chunked = self.nodeChunkedEncoding; + + // attempt to make the length computable + // if (!file.size && chunked && file.path) { + // fs.stat(file.path, function(err, stat) { + // file.size = stat.size; + // file.lastModifiedDate = stat.mtime; + // }); + // } + + + // The stream exists, do nothing more + if (file.stream) { + return; + } + + + // Create a read stream from a buffer + if (file.buffer) { + process.nextTick(function() { + stream.emit('data', file.buffer); + stream.emit('end'); + }); + file.stream = stream; + return; + } + + + // Create a read stream from a file + // if (file.path) { + // // TODO url + // if (!chunked) { + // fs.readFile(file.path, function(err, data) { + // if (err) { + // stream.emit('error', err); + // } + // if (data) { + // stream.emit('data', data); + // stream.emit('end'); + // } + // }); + + // file.stream = stream; + // return; + // } + + // // TODO don't duplicate this code here, + // // expose a method in File instead + // file.stream = fs.createReadStream(file.path); + // } + } + + + + // before any other listeners are added + emitter.on('abort', function() { + self.readyState = self.DONE; + }); + + + + // Map `error`, `progress`, `load`, and `loadend` + function mapStreamToEmitter(format, encoding) { + var stream = file.stream, + buffers = [], + chunked = self.nodeChunkedEncoding; + + buffers.dataLength = 0; + + stream.on('error', function(err) { + if (self.DONE === self.readyState) { + return; + } + + self.readyState = self.DONE; + self.error = err; + emitter.emit('error', err); + }); + + stream.on('data', function(data) { + if (self.DONE === self.readyState) { + return; + } + + buffers.dataLength += data.length; + buffers.push(data); + + emitter.emit('progress', { + // fs.stat will probably complete before this + // but possibly it will not, hence the check + lengthComputable: (!isNaN(file.size)) ? true : false, + loaded: buffers.dataLength, + total: file.size + }); + + emitter.emit('data', data); + }); + + stream.on('end', function() { + if (self.DONE === self.readyState) { + return; + } + + var data; + + if (buffers.length > 1) { + data = Buffer.concat(buffers); + } else { + data = buffers[0]; + } + + self.readyState = self.DONE; + self.result = mapDataToFormat(file, data, format, encoding); + emitter.emit('load', { + target: { + // non-standard + nodeBufferResult: data, + result: self.result + } + }); + + emitter.emit('loadend'); + }); + } + + + // Abort is overwritten by readAsXyz + self.abort = function() { + if (self.readState == self.DONE) { + return; + } + self.readyState = self.DONE; + emitter.emit('abort'); + }; + + + + // + function mapUserEvents() { + emitter.on('start', function() { + doop(self.onloadstart, arguments); + }); + emitter.on('progress', function() { + doop(self.onprogress, arguments); + }); + emitter.on('error', function(err) { + // TODO translate to FileError + if (self.onerror) { + self.onerror(err); + } else { + if (!emitter.listeners.error || !emitter.listeners.error.length) { + throw err; + } + } + }); + emitter.on('load', function() { + doop(self.onload, arguments); + }); + emitter.on('end', function() { + doop(self.onloadend, arguments); + }); + emitter.on('abort', function() { + doop(self.onabort, arguments); + }); + } + + + + function readFile(_file, format, encoding) { + file = _file; + if (!file || !file.name || !(file.path || file.stream || file.buffer)) { + throw new Error("cannot read as File: " + JSON.stringify(file)); + } + if (0 !== self.readyState) { + console.log("already loading, request to change format ignored"); + return; + } + + // 'process.nextTick' does not ensure order, (i.e. an fs.stat queued later may return faster) + // but `onloadstart` must come before the first `data` event and must be asynchronous. + // Hence we waste a single tick waiting + process.nextTick(function() { + self.readyState = self.LOADING; + emitter.emit('loadstart'); + createFileStream(); + mapStreamToEmitter(format, encoding); + mapUserEvents(); + }); + } + + self.readAsArrayBuffer = function(file) { + readFile(file, 'buffer'); + }; + self.readAsBinaryString = function(file) { + readFile(file, 'binary'); + }; + self.readAsDataURL = function(file) { + readFile(file, 'dataUrl'); + }; + self.readAsText = function(file, encoding) { + readFile(file, 'text', encoding); + }; +}