diff --git a/src/Adapters/Files/GridFSBucketAdapter.js b/src/Adapters/Files/GridFSBucketAdapter.js index 3281fe93bb..bc1b4ae920 100644 --- a/src/Adapters/Files/GridFSBucketAdapter.js +++ b/src/Adapters/Files/GridFSBucketAdapter.js @@ -59,7 +59,7 @@ export class GridFSBucketAdapter extends FilesAdapter { async deleteFile(filename: string) { const bucket = await this._getBucket(); - const documents = await bucket.find({ filename: filename }).toArray(); + const documents = await bucket.find({ filename }).toArray(); if (documents.length === 0) { throw new Error('FileNotFound'); } @@ -71,7 +71,8 @@ export class GridFSBucketAdapter extends FilesAdapter { } async getFileData(filename: string) { - const stream = await this.getDownloadStream(filename); + const bucket = await this._getBucket(); + const stream = bucket.openDownloadStreamByName(filename); stream.read(); return new Promise((resolve, reject) => { const chunks = []; @@ -97,9 +98,39 @@ export class GridFSBucketAdapter extends FilesAdapter { ); } - async getDownloadStream(filename: string) { + async handleFileStream(filename: string, req, res, contentType) { const bucket = await this._getBucket(); - return bucket.openDownloadStreamByName(filename); + const files = await bucket.find({ filename }).toArray(); + if (files.length === 0) { + throw new Error('FileNotFound'); + } + const parts = req + .get('Range') + .replace(/bytes=/, '') + .split('-'); + const partialstart = parts[0]; + const partialend = parts[1]; + + const start = parseInt(partialstart, 10); + const end = partialend ? parseInt(partialend, 10) : files[0].length - 1; + + res.writeHead(206, { + 'Accept-Ranges': 'bytes', + 'Content-Length': end - start + 1, + 'Content-Range': 'bytes ' + start + '-' + end + '/' + files[0].length, + 'Content-Type': contentType, + }); + const stream = bucket.openDownloadStreamByName(filename); + stream.start(start); + stream.on('data', chunk => { + res.write(chunk); + }); + stream.on('error', () => { + res.sendStatus(404); + }); + stream.on('end', () => { + res.end(); + }); } handleShutdown() { diff --git a/src/Adapters/Files/GridStoreAdapter.js b/src/Adapters/Files/GridStoreAdapter.js index 23f1668b19..3eb50ee561 100644 --- a/src/Adapters/Files/GridStoreAdapter.js +++ b/src/Adapters/Files/GridStoreAdapter.js @@ -94,13 +94,14 @@ export class GridStoreAdapter extends FilesAdapter { ); } - getFileStream(filename: string) { - return this._connect().then(database => { + async handleFileStream(filename: string, req, res, contentType) { + const stream = await this._connect().then(database => { return GridStore.exist(database, filename).then(() => { const gridStore = new GridStore(database, filename, 'r'); return gridStore.open(); }); }); + handleRangeRequest(stream, req, res, contentType); } handleShutdown() { @@ -111,4 +112,73 @@ export class GridStoreAdapter extends FilesAdapter { } } +// handleRangeRequest is licensed 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 handleRangeRequest(stream, req, res, contentType) { + const buffer_size = 1024 * 1024; //1024Kb + // Range request, partial stream the file + const parts = req + .get('Range') + .replace(/bytes=/, '') + .split('-'); + let [start, end] = parts; + const notEnded = !end && end !== 0; + const notStarted = !start && start !== 0; + // No end provided, we want all bytes + if (notEnded) { + end = stream.length - 1; + } + // No start provided, we're reading backwards + if (notStarted) { + start = stream.length - end; + end = start + end - 1; + } + + // Data exceeds the buffer_size, cap + if (end - start >= buffer_size) { + end = start + buffer_size - 1; + } + + const contentLength = end - start + 1; + + res.writeHead(206, { + 'Content-Range': 'bytes ' + start + '-' + end + '/' + stream.length, + 'Accept-Ranges': 'bytes', + 'Content-Length': contentLength, + 'Content-Type': contentType, + }); + + stream.seek(start, function() { + // Get gridFile stream + const gridFileStream = stream.stream(true); + let bufferAvail = 0; + let remainingBytesToWrite = contentLength; + let totalBytesWritten = 0; + // Write to response + gridFileStream.on('data', function(data) { + bufferAvail += data.length; + if (bufferAvail > 0) { + // slice returns the same buffer if overflowing + // safe to call in any case + const buffer = data.slice(0, remainingBytesToWrite); + // Write the buffer + res.write(buffer); + // Increment total + totalBytesWritten += buffer.length; + // Decrement remaining + remainingBytesToWrite -= data.length; + // Decrement the available buffer + bufferAvail -= buffer.length; + } + // In case of small slices, all values will be good at that point + // we've written enough, end... + if (totalBytesWritten >= contentLength) { + stream.close(); + res.end(); + this.destroy(); + } + }); + }); +} + export default GridStoreAdapter; diff --git a/src/Controllers/FilesController.js b/src/Controllers/FilesController.js index d0e42368e8..579e3e43d6 100644 --- a/src/Controllers/FilesController.js +++ b/src/Controllers/FilesController.js @@ -92,8 +92,8 @@ export class FilesController extends AdaptableController { return FilesAdapter; } - getFileStream(config, filename) { - return this.adapter.getFileStream(filename); + handleFileStream(config, filename, req, res, contentType) { + return this.adapter.handleFileStream(filename, req, res, contentType); } } diff --git a/src/Routers/FilesRouter.js b/src/Routers/FilesRouter.js index 6fb1386af5..72d032afa7 100644 --- a/src/Routers/FilesRouter.js +++ b/src/Routers/FilesRouter.js @@ -45,10 +45,7 @@ export class FilesRouter { const contentType = mime.getType(filename); if (isFileStreamable(req, filesController)) { filesController - .getFileStream(config, filename) - .then(stream => { - handleFileStream(stream, req, res, contentType); - }) + .handleFileStream(config, filename, req, res, contentType) .catch(() => { res.status(404); res.set('Content-Type', 'text/plain'); @@ -142,80 +139,6 @@ export class FilesRouter { function isFileStreamable(req, filesController) { return ( req.get('Range') && - typeof filesController.adapter.getFileStream === 'function' + typeof filesController.adapter.handleFileStream === 'function' ); } - -function getRange(req) { - const parts = req - .get('Range') - .replace(/bytes=/, '') - .split('-'); - return { start: parseInt(parts[0], 10), end: parseInt(parts[1], 10) }; -} - -// 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) { - const buffer_size = 1024 * 1024; //1024Kb - // Range request, partiall stream the file - let { start, end } = getRange(req); - - const notEnded = !end && end !== 0; - const notStarted = !start && start !== 0; - // No end provided, we want all bytes - if (notEnded) { - end = stream.length - 1; - } - // No start provided, we're reading backwards - if (notStarted) { - start = stream.length - end; - end = start + end - 1; - } - - // Data exceeds the buffer_size, cap - if (end - start >= buffer_size) { - end = start + buffer_size - 1; - } - - const contentLength = end - start + 1; - - res.writeHead(206, { - 'Content-Range': 'bytes ' + start + '-' + end + '/' + stream.length, - 'Accept-Ranges': 'bytes', - 'Content-Length': contentLength, - 'Content-Type': contentType, - }); - - stream.seek(start, function() { - // get gridFile stream - const gridFileStream = stream.stream(true); - let bufferAvail = 0; - let remainingBytesToWrite = contentLength; - let totalBytesWritten = 0; - // write to response - gridFileStream.on('data', function(data) { - bufferAvail += data.length; - if (bufferAvail > 0) { - // slice returns the same buffer if overflowing - // safe to call in any case - const buffer = data.slice(0, remainingBytesToWrite); - // write the buffer - res.write(buffer); - // increment total - totalBytesWritten += buffer.length; - // decrement remaining - remainingBytesToWrite -= data.length; - // decrement the avaialbe buffer - bufferAvail -= buffer.length; - } - // in case of small slices, all values will be good at that point - // we've written enough, end... - if (totalBytesWritten >= contentLength) { - stream.close(); - res.end(); - this.destroy(); - } - }); - }); -}