access-point-handlers.js 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. getHeaders = [];
  2. getHeadersByCollection = {};
  3. var contentDisposition = Npm.require('content-disposition');
  4. FS.HTTP.Handlers = {};
  5. /**
  6. * @method FS.HTTP.Handlers.Del
  7. * @public
  8. * @returns {any} response
  9. *
  10. * HTTP DEL request handler
  11. */
  12. FS.HTTP.Handlers.Del = function httpDelHandler(ref) {
  13. var self = this;
  14. var opts = FS.Utility.extend({}, self.query || {}, self.params || {});
  15. // If DELETE request, validate with 'remove' allow/deny, delete the file, and return
  16. FS.Utility.validateAction(ref.collection.files._validators['remove'], ref.file, self.userId);
  17. /*
  18. * From the DELETE spec:
  19. * A successful response SHOULD be 200 (OK) if the response includes an
  20. * entity describing the status, 202 (Accepted) if the action has not
  21. * yet been enacted, or 204 (No Content) if the action has been enacted
  22. * but the response does not include an entity.
  23. */
  24. self.setStatusCode(200);
  25. return {
  26. deleted: !!ref.file.remove()
  27. };
  28. };
  29. /**
  30. * @method FS.HTTP.Handlers.GetList
  31. * @public
  32. * @returns {Object} response
  33. *
  34. * HTTP GET file list request handler
  35. */
  36. FS.HTTP.Handlers.GetList = function httpGetListHandler() {
  37. // Not Yet Implemented
  38. // Need to check publications and return file list based on
  39. // what user is allowed to see
  40. };
  41. /*
  42. requestRange will parse the range set in request header - if not possible it
  43. will throw fitting errors and autofill range for both partial and full ranges
  44. throws error or returns the object:
  45. {
  46. start
  47. end
  48. length
  49. unit
  50. partial
  51. }
  52. */
  53. var requestRange = function(req, fileSize) {
  54. if (req) {
  55. if (req.headers) {
  56. var rangeString = req.headers.range;
  57. // Make sure range is a string
  58. if (rangeString === ''+rangeString) {
  59. // range will be in the format "bytes=0-32767"
  60. var parts = rangeString.split('=');
  61. var unit = parts[0];
  62. // Make sure parts consists of two strings and range is of type "byte"
  63. if (parts.length == 2 && unit == 'bytes') {
  64. // Parse the range
  65. var range = parts[1].split('-');
  66. var start = Number(range[0]);
  67. var end = Number(range[1]);
  68. // Fix invalid ranges?
  69. if (range[0] != start) start = 0;
  70. if (range[1] != end || !end) end = fileSize - 1;
  71. // Make sure range consists of a start and end point of numbers and start is less than end
  72. if (start < end) {
  73. var partSize = 0 - start + end + 1;
  74. // Return the parsed range
  75. return {
  76. start: start,
  77. end: end,
  78. length: partSize,
  79. size: fileSize,
  80. unit: unit,
  81. partial: (partSize < fileSize)
  82. };
  83. } else {
  84. throw new Meteor.Error(416, "Requested Range Not Satisfiable");
  85. }
  86. } else {
  87. // The first part should be bytes
  88. throw new Meteor.Error(416, "Requested Range Unit Not Satisfiable");
  89. }
  90. } else {
  91. // No range found
  92. }
  93. } else {
  94. // throw new Error('No request headers set for _parseRange function');
  95. }
  96. } else {
  97. throw new Error('No request object passed to _parseRange function');
  98. }
  99. return {
  100. start: 0,
  101. end: fileSize - 1,
  102. length: fileSize,
  103. size: fileSize,
  104. unit: 'bytes',
  105. partial: false
  106. };
  107. };
  108. /**
  109. * @method FS.HTTP.Handlers.Get
  110. * @public
  111. * @returns {any} response
  112. *
  113. * HTTP GET request handler
  114. */
  115. FS.HTTP.Handlers.Get = function httpGetHandler(ref) {
  116. var self = this;
  117. // Once we have the file, we can test allow/deny validators
  118. // XXX: pass on the "share" query eg. ?share=342hkjh23ggj for shared url access?
  119. FS.Utility.validateAction(ref.collection._validators['download'], ref.file, self.userId /*, self.query.shareId*/);
  120. var storeName = ref.storeName;
  121. // If no storeName was specified, use the first defined storeName
  122. if (typeof storeName !== "string") {
  123. // No store handed, we default to primary store
  124. storeName = ref.collection.primaryStore.name;
  125. }
  126. // Get the storage reference
  127. var storage = ref.collection.storesLookup[storeName];
  128. if (!storage) {
  129. throw new Meteor.Error(404, "Not Found", 'There is no store "' + storeName + '"');
  130. }
  131. // Get the file
  132. var copyInfo = ref.file.copies[storeName];
  133. if (!copyInfo) {
  134. throw new Meteor.Error(404, "Not Found", 'This file was not stored in the ' + storeName + ' store');
  135. }
  136. // Set the content type for file
  137. if (typeof copyInfo.type === "string") {
  138. self.setContentType(copyInfo.type);
  139. } else {
  140. self.setContentType('application/octet-stream');
  141. }
  142. // Add 'Content-Disposition' header if requested a download/attachment URL
  143. if (typeof ref.download !== "undefined") {
  144. var filename = ref.filename || copyInfo.name;
  145. self.addHeader('Content-Disposition', contentDisposition(filename));
  146. } else {
  147. self.addHeader('Content-Disposition', 'inline');
  148. }
  149. // Get the contents range from request
  150. var range = requestRange(self.request, copyInfo.size);
  151. // Some browsers cope better if the content-range header is
  152. // still included even for the full file being returned.
  153. self.addHeader('Content-Range', range.unit + ' ' + range.start + '-' + range.end + '/' + range.size);
  154. // If a chunk/range was requested instead of the whole file, serve that'
  155. if (range.partial) {
  156. self.setStatusCode(206, 'Partial Content');
  157. } else {
  158. self.setStatusCode(200, 'OK');
  159. }
  160. // Add any other global custom headers and collection-specific custom headers
  161. FS.Utility.each(getHeaders.concat(getHeadersByCollection[ref.collection.name] || []), function(header) {
  162. self.addHeader(header[0], header[1]);
  163. });
  164. // Inform clients about length (or chunk length in case of ranges)
  165. self.addHeader('Content-Length', range.length);
  166. // Last modified header (updatedAt from file info)
  167. self.addHeader('Last-Modified', copyInfo.updatedAt.toUTCString());
  168. // Inform clients that we accept ranges for resumable chunked downloads
  169. self.addHeader('Accept-Ranges', range.unit);
  170. if (FS.debug) console.log('Read file "' + (ref.filename || copyInfo.name) + '" ' + range.unit + ' ' + range.start + '-' + range.end + '/' + range.size);
  171. var readStream = storage.adapter.createReadStream(ref.file, {start: range.start, end: range.end});
  172. readStream.on('error', function(err) {
  173. // Send proper error message on get error
  174. if (err.message && err.statusCode) {
  175. self.Error(new Meteor.Error(err.statusCode, err.message));
  176. } else {
  177. self.Error(new Meteor.Error(503, 'Service unavailable'));
  178. }
  179. });
  180. readStream.pipe(self.createWriteStream());
  181. };
  182. // File with unicode or other encodings filename can upload to server susscessfully,
  183. // but when download, the HTTP header "Content-Disposition" cannot accept
  184. // characters other than ASCII, the filename should be converted to binary or URI encoded.
  185. // https://github.com/wekan/wekan/issues/784
  186. const originalHandler = FS.HTTP.Handlers.Get;
  187. FS.HTTP.Handlers.Get = function (ref) {
  188. try {
  189. var userAgent = (this.requestHeaders['user-agent']||'').toLowerCase();
  190. if(userAgent.indexOf('msie') >= 0 || userAgent.indexOf('chrome') >= 0) {
  191. ref.filename = encodeURIComponent(ref.filename);
  192. } else if(userAgent.indexOf('firefox') >= 0) {
  193. ref.filename = new Buffer(ref.filename).toString('binary');
  194. } else {
  195. /* safari*/
  196. ref.filename = new Buffer(ref.filename).toString('binary');
  197. }
  198. } catch (ex){
  199. ref.filename = ref.filename;
  200. }
  201. return originalHandler.call(this, ref);
  202. };
  203. /**
  204. * @method FS.HTTP.Handlers.PutInsert
  205. * @public
  206. * @returns {Object} response object with _id property
  207. *
  208. * HTTP PUT file insert request handler
  209. */
  210. FS.HTTP.Handlers.PutInsert = function httpPutInsertHandler(ref) {
  211. var self = this;
  212. var opts = FS.Utility.extend({}, self.query || {}, self.params || {});
  213. FS.debug && console.log("HTTP PUT (insert) handler");
  214. // Create the nice FS.File
  215. var fileObj = new FS.File();
  216. // Set its name
  217. fileObj.name(opts.filename || null);
  218. // Attach the readstream as the file's data
  219. fileObj.attachData(self.createReadStream(), {type: self.requestHeaders['content-type'] || 'application/octet-stream'});
  220. // Validate with insert allow/deny
  221. FS.Utility.validateAction(ref.collection.files._validators['insert'], fileObj, self.userId);
  222. // Insert file into collection, triggering readStream storage
  223. ref.collection.insert(fileObj);
  224. // Send response
  225. self.setStatusCode(200);
  226. // Return the new file id
  227. return {_id: fileObj._id};
  228. };
  229. /**
  230. * @method FS.HTTP.Handlers.PutUpdate
  231. * @public
  232. * @returns {Object} response object with _id and chunk properties
  233. *
  234. * HTTP PUT file update chunk request handler
  235. */
  236. FS.HTTP.Handlers.PutUpdate = function httpPutUpdateHandler(ref) {
  237. var self = this;
  238. var opts = FS.Utility.extend({}, self.query || {}, self.params || {});
  239. var chunk = parseInt(opts.chunk, 10);
  240. if (isNaN(chunk)) chunk = 0;
  241. FS.debug && console.log("HTTP PUT (update) handler received chunk: ", chunk);
  242. // Validate with insert allow/deny; also mounts and retrieves the file
  243. FS.Utility.validateAction(ref.collection.files._validators['insert'], ref.file, self.userId);
  244. self.createReadStream().pipe( FS.TempStore.createWriteStream(ref.file, chunk) );
  245. // Send response
  246. self.setStatusCode(200);
  247. return { _id: ref.file._id, chunk: chunk };
  248. };