diff --git a/src/Adapters/Files/GridStoreAdapter.js b/src/Adapters/Files/GridStoreAdapter.js index d7844a0b5f..da7754a8fc 100644 --- a/src/Adapters/Files/GridStoreAdapter.js +++ b/src/Adapters/Files/GridStoreAdapter.js @@ -67,6 +67,15 @@ export class GridStoreAdapter extends FilesAdapter { getFileLocation(config, filename) { return (config.mount + '/files/' + config.applicationId + '/' + encodeURIComponent(filename)); } + + getFileStream(filename: string) { + return this._connect().then(database => { + return GridStore.exist(database, filename).then(() => { + let gridStore = new GridStore(database, filename, 'r'); + return gridStore.open(); + }); + }); + } } export default GridStoreAdapter; diff --git a/src/Controllers/FilesController.js b/src/Controllers/FilesController.js index a355396e14..6bbeb4377d 100644 --- a/src/Controllers/FilesController.js +++ b/src/Controllers/FilesController.js @@ -82,6 +82,10 @@ export class FilesController extends AdaptableController { expectedAdapterType() { return FilesAdapter; } + + getFileStream(config, filename) { + return this.adapter.getFileStream(filename); + } } export default FilesController; diff --git a/src/Routers/FilesRouter.js b/src/Routers/FilesRouter.js index ef0375f559..d116776dfc 100644 --- a/src/Routers/FilesRouter.js +++ b/src/Routers/FilesRouter.js @@ -36,17 +36,27 @@ export class FilesRouter { const config = new Config(req.params.appId); const filesController = config.filesController; const filename = req.params.filename; - filesController.getFileData(config, filename).then((data) => { - res.status(200); - var contentType = mime.lookup(filename); - res.set('Content-Type', contentType); - res.set('Content-Length', data.length); - res.end(data); - }).catch((err) => { - res.status(404); - res.set('Content-Type', 'text/plain'); - res.end('File not found.'); - }); + const contentType = mime.lookup(filename); + if (isFileStreamable(req, filesController)) { + filesController.getFileStream(config, filename).then((stream) => { + handleFileStream(stream, req, res, contentType); + }).catch((err) => { + res.status(404); + res.set('Content-Type', 'text/plain'); + res.end('File not found.'); + }); + } else { + filesController.getFileData(config, filename).then((data) => { + res.status(200); + res.set('Content-Type', contentType); + res.set('Content-Length', data.length); + res.end(data); + }).catch((err) => { + res.status(404); + res.set('Content-Type', 'text/plain'); + res.end('File not found.'); + }); + } } createHandler(req, res, next) { @@ -94,3 +104,97 @@ export class FilesRouter { }); } } + +function isFileStreamable(req, filesController){ + if (req.get['Range']) { + if (!(typeof filesController.adapter.getFileStream === 'function')) { + return false; + } + if (typeof filesController.adapter.constructor.name !== 'undefined') { + if (filesController.adapter.constructor.name == 'GridStoreAdapter') { + return true; + } + } + } + return false; +} + +// handleFileStream is licenced under Creative Commons Attribution 4.0 International License (https://creativecommons.org/licenses/by/4.0/). +// Author: LEROIB at weightingformypizza (https://weightingformypizza.wordpress.com/2015/06/24/stream-html5-media-content-like-video-audio-from-mongodb-using-express-and-gridstore/). +function handleFileStream(stream, req, res, contentType) { + var buffer_size = 1024 * 1024;//1024Kb + // Range request, partiall stream the file + var parts = req.get["Range"].replace(/bytes=/, "").split("-"); + var partialstart = parts[0]; + var partialend = parts[1]; + var start = partialstart ? parseInt(partialstart, 10) : 0; + var end = partialend ? parseInt(partialend, 10) : stream.length - 1; + var chunksize = (end - start) + 1; + + if (chunksize == 1) { + start = 0; + partialend = false; + } + + if (!partialend) { + if (((stream.length-1) - start) < (buffer_size)) { + end = stream.length - 1; + }else{ + end = start + (buffer_size); + } + chunksize = (end - start) + 1; + } + + if (start == 0 && end == 2) { + chunksize = 1; + } + + res.writeHead(206, { + 'Content-Range': 'bytes ' + start + '-' + end + '/' + stream.length, + 'Accept-Ranges': 'bytes', + 'Content-Length': chunksize, + 'Content-Type': contentType, + }); + + stream.seek(start, function () { + // get gridFile stream + var gridFileStream = stream.stream(true); + var ended = false; + var bufferIdx = 0; + var bufferAvail = 0; + var range = (end - start) + 1; + var totalbyteswanted = (end - start) + 1; + var totalbyteswritten = 0; + // write to response + gridFileStream.on('data', function (buff) { + bufferAvail += buff.length; + //Ok check if we have enough to cover our range + if (bufferAvail < range) { + //Not enough bytes to satisfy our full range + if (bufferAvail > 0) { + //Write full buffer + res.write(buff); + totalbyteswritten += buff.length; + range -= buff.length; + bufferIdx += buff.length; + bufferAvail -= buff.length; + } + } else { + //Enough bytes to satisfy our full range! + if (bufferAvail > 0) { + const buffer = buff.slice(0,range); + res.write(buffer); + totalbyteswritten += buffer.length; + bufferIdx += range; + bufferAvail -= range; + } + } + if (totalbyteswritten >= totalbyteswanted) { + //totalbytes = 0; + stream.close(); + res.end(); + this.destroy(); + } + }); + }); +}