getHeaders = []; getHeadersByCollection = {}; var contentDisposition = Npm.require('content-disposition'); FS.HTTP.Handlers = {}; /** * @method FS.HTTP.Handlers.Del * @public * @returns {any} response * * HTTP DEL request handler */ FS.HTTP.Handlers.Del = function httpDelHandler(ref) { var self = this; var opts = FS.Utility.extend({}, self.query || {}, self.params || {}); // If DELETE request, validate with 'remove' allow/deny, delete the file, and return FS.Utility.validateAction(ref.collection.files._validators['remove'], ref.file, self.userId); /* * From the DELETE spec: * A successful response SHOULD be 200 (OK) if the response includes an * entity describing the status, 202 (Accepted) if the action has not * yet been enacted, or 204 (No Content) if the action has been enacted * but the response does not include an entity. */ self.setStatusCode(200); return { deleted: !!ref.file.remove() }; }; /** * @method FS.HTTP.Handlers.GetList * @public * @returns {Object} response * * HTTP GET file list request handler */ FS.HTTP.Handlers.GetList = function httpGetListHandler() { // Not Yet Implemented // Need to check publications and return file list based on // what user is allowed to see }; /* requestRange will parse the range set in request header - if not possible it will throw fitting errors and autofill range for both partial and full ranges throws error or returns the object: { start end length unit partial } */ var requestRange = function(req, fileSize) { if (req) { if (req.headers) { var rangeString = req.headers.range; // Make sure range is a string if (rangeString === ''+rangeString) { // range will be in the format "bytes=0-32767" var parts = rangeString.split('='); var unit = parts[0]; // Make sure parts consists of two strings and range is of type "byte" if (parts.length == 2 && unit == 'bytes') { // Parse the range var range = parts[1].split('-'); var start = Number(range[0]); var end = Number(range[1]); // Fix invalid ranges? if (range[0] != start) start = 0; if (range[1] != end || !end) end = fileSize - 1; // Make sure range consists of a start and end point of numbers and start is less than end if (start < end) { var partSize = 0 - start + end + 1; // Return the parsed range return { start: start, end: end, length: partSize, size: fileSize, unit: unit, partial: (partSize < fileSize) }; } else { throw new Meteor.Error(416, "Requested Range Not Satisfiable"); } } else { // The first part should be bytes throw new Meteor.Error(416, "Requested Range Unit Not Satisfiable"); } } else { // No range found } } else { // throw new Error('No request headers set for _parseRange function'); } } else { throw new Error('No request object passed to _parseRange function'); } return { start: 0, end: fileSize - 1, length: fileSize, size: fileSize, unit: 'bytes', partial: false }; }; /** * @method FS.HTTP.Handlers.Get * @public * @returns {any} response * * HTTP GET request handler */ FS.HTTP.Handlers.Get = function httpGetHandler(ref) { var self = this; // Once we have the file, we can test allow/deny validators // XXX: pass on the "share" query eg. ?share=342hkjh23ggj for shared url access? FS.Utility.validateAction(ref.collection._validators['download'], ref.file, self.userId /*, self.query.shareId*/); var storeName = ref.storeName; // If no storeName was specified, use the first defined storeName if (typeof storeName !== "string") { // No store handed, we default to primary store storeName = ref.collection.primaryStore.name; } // Get the storage reference var storage = ref.collection.storesLookup[storeName]; if (!storage) { throw new Meteor.Error(404, "Not Found", 'There is no store "' + storeName + '"'); } // Get the file var copyInfo = ref.file.copies[storeName]; if (!copyInfo) { throw new Meteor.Error(404, "Not Found", 'This file was not stored in the ' + storeName + ' store'); } // Set the content type for file if (typeof copyInfo.type === "string") { self.setContentType(copyInfo.type); } else { self.setContentType('application/octet-stream'); } // Add 'Content-Disposition' header if requested a download/attachment URL if (typeof ref.download !== "undefined") { var filename = ref.filename || copyInfo.name; self.addHeader('Content-Disposition', contentDisposition(filename)); } else { self.addHeader('Content-Disposition', 'inline'); } // Get the contents range from request var range = requestRange(self.request, copyInfo.size); // Some browsers cope better if the content-range header is // still included even for the full file being returned. self.addHeader('Content-Range', range.unit + ' ' + range.start + '-' + range.end + '/' + range.size); // If a chunk/range was requested instead of the whole file, serve that' if (range.partial) { self.setStatusCode(206, 'Partial Content'); } else { self.setStatusCode(200, 'OK'); } // Add any other global custom headers and collection-specific custom headers FS.Utility.each(getHeaders.concat(getHeadersByCollection[ref.collection.name] || []), function(header) { self.addHeader(header[0], header[1]); }); // Inform clients about length (or chunk length in case of ranges) self.addHeader('Content-Length', range.length); // Last modified header (updatedAt from file info) self.addHeader('Last-Modified', copyInfo.updatedAt.toUTCString()); // Inform clients that we accept ranges for resumable chunked downloads self.addHeader('Accept-Ranges', range.unit); if (FS.debug) console.log('Read file "' + (ref.filename || copyInfo.name) + '" ' + range.unit + ' ' + range.start + '-' + range.end + '/' + range.size); var readStream = storage.adapter.createReadStream(ref.file, {start: range.start, end: range.end}); readStream.on('error', function(err) { // Send proper error message on get error if (err.message && err.statusCode) { self.Error(new Meteor.Error(err.statusCode, err.message)); } else { self.Error(new Meteor.Error(503, 'Service unavailable')); } }); readStream.pipe(self.createWriteStream()); }; // File with unicode or other encodings filename can upload to server susscessfully, // but when download, the HTTP header "Content-Disposition" cannot accept // characters other than ASCII, the filename should be converted to binary or URI encoded. // https://github.com/wekan/wekan/issues/784 const originalHandler = FS.HTTP.Handlers.Get; FS.HTTP.Handlers.Get = function (ref) { try { var userAgent = (this.requestHeaders['user-agent']||'').toLowerCase(); if(userAgent.indexOf('msie') >= 0 || userAgent.indexOf('chrome') >= 0) { ref.filename = encodeURIComponent(ref.filename); } else if(userAgent.indexOf('firefox') >= 0) { ref.filename = new Buffer(ref.filename).toString('binary'); } else { /* safari*/ ref.filename = new Buffer(ref.filename).toString('binary'); } } catch (ex){ ref.filename = ref.filename; } return originalHandler.call(this, ref); }; /** * @method FS.HTTP.Handlers.PutInsert * @public * @returns {Object} response object with _id property * * HTTP PUT file insert request handler */ FS.HTTP.Handlers.PutInsert = function httpPutInsertHandler(ref) { var self = this; var opts = FS.Utility.extend({}, self.query || {}, self.params || {}); FS.debug && console.log("HTTP PUT (insert) handler"); // Create the nice FS.File var fileObj = new FS.File(); // Set its name fileObj.name(opts.filename || null); // Attach the readstream as the file's data fileObj.attachData(self.createReadStream(), {type: self.requestHeaders['content-type'] || 'application/octet-stream'}); // Validate with insert allow/deny FS.Utility.validateAction(ref.collection.files._validators['insert'], fileObj, self.userId); // Insert file into collection, triggering readStream storage ref.collection.insert(fileObj); // Send response self.setStatusCode(200); // Return the new file id return {_id: fileObj._id}; }; /** * @method FS.HTTP.Handlers.PutUpdate * @public * @returns {Object} response object with _id and chunk properties * * HTTP PUT file update chunk request handler */ FS.HTTP.Handlers.PutUpdate = function httpPutUpdateHandler(ref) { var self = this; var opts = FS.Utility.extend({}, self.query || {}, self.params || {}); var chunk = parseInt(opts.chunk, 10); if (isNaN(chunk)) chunk = 0; FS.debug && console.log("HTTP PUT (update) handler received chunk: ", chunk); // Validate with insert allow/deny; also mounts and retrieves the file FS.Utility.validateAction(ref.collection.files._validators['insert'], ref.file, self.userId); self.createReadStream().pipe( FS.TempStore.createWriteStream(ref.file, chunk) ); // Send response self.setStatusCode(200); return { _id: ref.file._id, chunk: chunk }; };