cfs_access-point.js 104 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914
  1. (function () {
  2. /* Imports */
  3. var Meteor = Package.meteor.Meteor;
  4. var global = Package.meteor.global;
  5. var meteorEnv = Package.meteor.meteorEnv;
  6. var FS = Package['cfs:base-package'].FS;
  7. var check = Package.check.check;
  8. var Match = Package.check.Match;
  9. var EJSON = Package.ejson.EJSON;
  10. var HTTP = Package['cfs:http-methods'].HTTP;
  11. /* Package-scope variables */
  12. var rootUrlPathPrefix, baseUrl, getHeaders, getHeadersByCollection, _existingMountPoints, mountUrls;
  13. (function(){
  14. ///////////////////////////////////////////////////////////////////////
  15. // //
  16. // packages/cfs_access-point/packages/cfs_access-point.js //
  17. // //
  18. ///////////////////////////////////////////////////////////////////////
  19. //
  20. (function () {
  21. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  22. // //
  23. // packages/cfs:access-point/access-point-common.js //
  24. // //
  25. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  26. //
  27. rootUrlPathPrefix = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX || ""; // 1
  28. // Adjust the rootUrlPathPrefix if necessary // 2
  29. if (rootUrlPathPrefix.length > 0) { // 3
  30. if (rootUrlPathPrefix.slice(0, 1) !== '/') { // 4
  31. rootUrlPathPrefix = '/' + rootUrlPathPrefix; // 5
  32. } // 6
  33. if (rootUrlPathPrefix.slice(-1) === '/') { // 7
  34. rootUrlPathPrefix = rootUrlPathPrefix.slice(0, -1); // 8
  35. } // 9
  36. } // 10
  37. // 11
  38. // prepend ROOT_URL when isCordova // 12
  39. if (Meteor.isCordova) { // 13
  40. rootUrlPathPrefix = Meteor.absoluteUrl(rootUrlPathPrefix.replace(/^\/+/, '')).replace(/\/+$/, ''); // 14
  41. } // 15
  42. // 16
  43. baseUrl = '/cfs'; // 17
  44. FS.HTTP = FS.HTTP || {}; // 18
  45. // 19
  46. // Note the upload URL so that client uploader packages know what it is // 20
  47. FS.HTTP.uploadUrl = rootUrlPathPrefix + baseUrl + '/files'; // 21
  48. // 22
  49. /** // 23
  50. * @method FS.HTTP.setBaseUrl // 24
  51. * @public // 25
  52. * @param {String} newBaseUrl - Change the base URL for the HTTP GET and DELETE endpoints. // 26
  53. * @returns {undefined} // 27
  54. */ // 28
  55. FS.HTTP.setBaseUrl = function setBaseUrl(newBaseUrl) { // 29
  56. // 30
  57. // Adjust the baseUrl if necessary // 31
  58. if (newBaseUrl.slice(0, 1) !== '/') { // 32
  59. newBaseUrl = '/' + newBaseUrl; // 33
  60. } // 34
  61. if (newBaseUrl.slice(-1) === '/') { // 35
  62. newBaseUrl = newBaseUrl.slice(0, -1); // 36
  63. } // 37
  64. // 38
  65. // Update the base URL // 39
  66. baseUrl = newBaseUrl; // 40
  67. // 41
  68. // Change the upload URL so that client uploader packages know what it is // 42
  69. FS.HTTP.uploadUrl = rootUrlPathPrefix + baseUrl + '/files'; // 43
  70. // 44
  71. // Remount URLs with the new baseUrl, unmounting the old, on the server only. // 45
  72. // If existingMountPoints is empty, then we haven't run the server startup // 46
  73. // code yet, so this new URL will be used at that point for the initial mount. // 47
  74. if (Meteor.isServer && !FS.Utility.isEmpty(_existingMountPoints)) { // 48
  75. mountUrls(); // 49
  76. } // 50
  77. }; // 51
  78. // 52
  79. /* // 53
  80. * FS.File extensions // 54
  81. */ // 55
  82. // 56
  83. /** // 57
  84. * @method FS.File.prototype.url Construct the file url // 58
  85. * @public // 59
  86. * @param {Object} [options] // 60
  87. * @param {String} [options.store] Name of the store to get from. If not defined, the first store defined in `options.stores` for the collection on the client is used.
  88. * @param {Boolean} [options.auth=null] Add authentication token to the URL query string? By default, a token for the current logged in user is added on the client. Set this to `false` to omit the token. Set this to a string to provide your own token. Set this to a number to specify an expiration time for the token in seconds.
  89. * @param {Boolean} [options.download=false] Should headers be set to force a download? Typically this means that clicking the link with this URL will download the file to the user's Downloads folder instead of displaying the file in the browser.
  90. * @param {Boolean} [options.brokenIsFine=false] Return the URL even if we know it's currently a broken link because the file hasn't been saved in the requested store yet.
  91. * @param {Boolean} [options.metadata=false] Return the URL for the file metadata access point rather than the file itself.
  92. * @param {String} [options.uploading=null] A URL to return while the file is being uploaded. // 66
  93. * @param {String} [options.storing=null] A URL to return while the file is being stored. // 67
  94. * @param {String} [options.filename=null] Override the filename that should appear at the end of the URL. By default it is the name of the file in the requested store.
  95. * // 69
  96. * Returns the HTTP URL for getting the file or its metadata. // 70
  97. */ // 71
  98. FS.File.prototype.url = function(options) { // 72
  99. var self = this; // 73
  100. options = options || {}; // 74
  101. options = FS.Utility.extend({ // 75
  102. store: null, // 76
  103. auth: null, // 77
  104. download: false, // 78
  105. metadata: false, // 79
  106. brokenIsFine: false, // 80
  107. uploading: null, // return this URL while uploading // 81
  108. storing: null, // return this URL while storing // 82
  109. filename: null // override the filename that is shown to the user // 83
  110. }, options.hash || options); // check for "hash" prop if called as helper // 84
  111. // 85
  112. // Primarily useful for displaying a temporary image while uploading an image // 86
  113. if (options.uploading && !self.isUploaded()) { // 87
  114. return options.uploading; // 88
  115. } // 89
  116. // 90
  117. if (self.isMounted()) { // 91
  118. // See if we've stored in the requested store yet // 92
  119. var storeName = options.store || self.collection.primaryStore.name; // 93
  120. if (!self.hasStored(storeName)) { // 94
  121. if (options.storing) { // 95
  122. return options.storing; // 96
  123. } else if (!options.brokenIsFine) { // 97
  124. // We want to return null if we know the URL will be a broken // 98
  125. // link because then we can avoid rendering broken links, broken // 99
  126. // images, etc. // 100
  127. return null; // 101
  128. } // 102
  129. } // 103
  130. // 104
  131. // Add filename to end of URL if we can determine one // 105
  132. var filename = options.filename || self.name({store: storeName}); // 106
  133. if (typeof filename === "string" && filename.length) { // 107
  134. filename = '/' + filename; // 108
  135. } else { // 109
  136. filename = ''; // 110
  137. } // 111
  138. // 112
  139. // TODO: Could we somehow figure out if the collection requires login? // 113
  140. var authToken = ''; // 114
  141. if (Meteor.isClient && typeof Accounts !== "undefined" && typeof Accounts._storedLoginToken === "function") { // 115
  142. if (options.auth !== false) { // 116
  143. // Add reactive deps on the user // 117
  144. Meteor.userId(); // 118
  145. // 119
  146. var authObject = { // 120
  147. authToken: Accounts._storedLoginToken() || '' // 121
  148. }; // 122
  149. // 123
  150. // If it's a number, we use that as the expiration time (in seconds) // 124
  151. if (options.auth === +options.auth) { // 125
  152. authObject.expiration = FS.HTTP.now() + options.auth * 1000; // 126
  153. } // 127
  154. // 128
  155. // Set the authToken // 129
  156. var authString = JSON.stringify(authObject); // 130
  157. authToken = FS.Utility.btoa(authString); // 131
  158. } // 132
  159. } else if (typeof options.auth === "string") { // 133
  160. // If the user supplies auth token the user will be responsible for // 134
  161. // updating // 135
  162. authToken = options.auth; // 136
  163. } // 137
  164. // 138
  165. // Construct query string // 139
  166. var params = {}; // 140
  167. if (authToken !== '') { // 141
  168. params.token = authToken; // 142
  169. } // 143
  170. if (options.download) { // 144
  171. params.download = true; // 145
  172. } // 146
  173. if (options.store) { // 147
  174. // We use options.store here instead of storeName because we want to omit the queryString // 148
  175. // whenever possible, allowing users to have "clean" URLs if they want. The server will // 149
  176. // assume the first store defined on the server, which means that we are assuming that // 150
  177. // the first on the client is also the first on the server. If that's not the case, the // 151
  178. // store option should be supplied. // 152
  179. params.store = options.store; // 153
  180. } // 154
  181. var queryString = FS.Utility.encodeParams(params); // 155
  182. if (queryString.length) { // 156
  183. queryString = '?' + queryString; // 157
  184. } // 158
  185. // 159
  186. // Determine which URL to use // 160
  187. var area; // 161
  188. if (options.metadata) { // 162
  189. area = '/record'; // 163
  190. } else { // 164
  191. area = '/files'; // 165
  192. } // 166
  193. // 167
  194. // Construct and return the http method url // 168
  195. return rootUrlPathPrefix + baseUrl + area + '/' + self.collection.name + '/' + self._id + filename + queryString; // 169
  196. } // 170
  197. // 171
  198. }; // 172
  199. // 173
  200. // 174
  201. // 175
  202. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  203. }).call(this);
  204. (function () {
  205. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  206. // //
  207. // packages/cfs:access-point/access-point-handlers.js //
  208. // //
  209. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  210. //
  211. getHeaders = []; // 1
  212. getHeadersByCollection = {}; // 2
  213. // 3
  214. FS.HTTP.Handlers = {}; // 4
  215. // 5
  216. /** // 6
  217. * @method FS.HTTP.Handlers.Del // 7
  218. * @public // 8
  219. * @returns {any} response // 9
  220. * // 10
  221. * HTTP DEL request handler // 11
  222. */ // 12
  223. FS.HTTP.Handlers.Del = function httpDelHandler(ref) { // 13
  224. var self = this; // 14
  225. var opts = FS.Utility.extend({}, self.query || {}, self.params || {}); // 15
  226. // 16
  227. // If DELETE request, validate with 'remove' allow/deny, delete the file, and return // 17
  228. FS.Utility.validateAction(ref.collection.files._validators['remove'], ref.file, self.userId); // 18
  229. // 19
  230. /* // 20
  231. * From the DELETE spec: // 21
  232. * A successful response SHOULD be 200 (OK) if the response includes an // 22
  233. * entity describing the status, 202 (Accepted) if the action has not // 23
  234. * yet been enacted, or 204 (No Content) if the action has been enacted // 24
  235. * but the response does not include an entity. // 25
  236. */ // 26
  237. self.setStatusCode(200); // 27
  238. // 28
  239. return { // 29
  240. deleted: !!ref.file.remove() // 30
  241. }; // 31
  242. }; // 32
  243. // 33
  244. /** // 34
  245. * @method FS.HTTP.Handlers.GetList // 35
  246. * @public // 36
  247. * @returns {Object} response // 37
  248. * // 38
  249. * HTTP GET file list request handler // 39
  250. */ // 40
  251. FS.HTTP.Handlers.GetList = function httpGetListHandler() { // 41
  252. // Not Yet Implemented // 42
  253. // Need to check publications and return file list based on // 43
  254. // what user is allowed to see // 44
  255. }; // 45
  256. // 46
  257. /* // 47
  258. requestRange will parse the range set in request header - if not possible it // 48
  259. will throw fitting errors and autofill range for both partial and full ranges // 49
  260. // 50
  261. throws error or returns the object: // 51
  262. { // 52
  263. start // 53
  264. end // 54
  265. length // 55
  266. unit // 56
  267. partial // 57
  268. } // 58
  269. */ // 59
  270. var requestRange = function(req, fileSize) { // 60
  271. if (req) { // 61
  272. if (req.headers) { // 62
  273. var rangeString = req.headers.range; // 63
  274. // 64
  275. // Make sure range is a string // 65
  276. if (rangeString === ''+rangeString) { // 66
  277. // 67
  278. // range will be in the format "bytes=0-32767" // 68
  279. var parts = rangeString.split('='); // 69
  280. var unit = parts[0]; // 70
  281. // 71
  282. // Make sure parts consists of two strings and range is of type "byte" // 72
  283. if (parts.length == 2 && unit == 'bytes') { // 73
  284. // Parse the range // 74
  285. var range = parts[1].split('-'); // 75
  286. var start = Number(range[0]); // 76
  287. var end = Number(range[1]); // 77
  288. // 78
  289. // Fix invalid ranges? // 79
  290. if (range[0] != start) start = 0; // 80
  291. if (range[1] != end || !end) end = fileSize - 1; // 81
  292. // 82
  293. // Make sure range consists of a start and end point of numbers and start is less than end // 83
  294. if (start < end) { // 84
  295. // 85
  296. var partSize = 0 - start + end + 1; // 86
  297. // 87
  298. // Return the parsed range // 88
  299. return { // 89
  300. start: start, // 90
  301. end: end, // 91
  302. length: partSize, // 92
  303. size: fileSize, // 93
  304. unit: unit, // 94
  305. partial: (partSize < fileSize) // 95
  306. }; // 96
  307. // 97
  308. } else { // 98
  309. throw new Meteor.Error(416, "Requested Range Not Satisfiable"); // 99
  310. } // 100
  311. // 101
  312. } else { // 102
  313. // The first part should be bytes // 103
  314. throw new Meteor.Error(416, "Requested Range Unit Not Satisfiable"); // 104
  315. } // 105
  316. // 106
  317. } else { // 107
  318. // No range found // 108
  319. } // 109
  320. // 110
  321. } else { // 111
  322. // throw new Error('No request headers set for _parseRange function'); // 112
  323. } // 113
  324. } else { // 114
  325. throw new Error('No request object passed to _parseRange function'); // 115
  326. } // 116
  327. // 117
  328. return { // 118
  329. start: 0, // 119
  330. end: fileSize - 1, // 120
  331. length: fileSize, // 121
  332. size: fileSize, // 122
  333. unit: 'bytes', // 123
  334. partial: false // 124
  335. }; // 125
  336. }; // 126
  337. // 127
  338. /** // 128
  339. * @method FS.HTTP.Handlers.Get // 129
  340. * @public // 130
  341. * @returns {any} response // 131
  342. * // 132
  343. * HTTP GET request handler // 133
  344. */ // 134
  345. FS.HTTP.Handlers.Get = function httpGetHandler(ref) { // 135
  346. var self = this; // 136
  347. // Once we have the file, we can test allow/deny validators // 137
  348. // XXX: pass on the "share" query eg. ?share=342hkjh23ggj for shared url access? // 138
  349. FS.Utility.validateAction(ref.collection._validators['download'], ref.file, self.userId /*, self.query.shareId*/); // 139
  350. // 140
  351. var storeName = ref.storeName; // 141
  352. // 142
  353. // If no storeName was specified, use the first defined storeName // 143
  354. if (typeof storeName !== "string") { // 144
  355. // No store handed, we default to primary store // 145
  356. storeName = ref.collection.primaryStore.name; // 146
  357. } // 147
  358. // 148
  359. // Get the storage reference // 149
  360. var storage = ref.collection.storesLookup[storeName]; // 150
  361. // 151
  362. if (!storage) { // 152
  363. throw new Meteor.Error(404, "Not Found", 'There is no store "' + storeName + '"'); // 153
  364. } // 154
  365. // 155
  366. // Get the file // 156
  367. var copyInfo = ref.file.copies[storeName]; // 157
  368. // 158
  369. if (!copyInfo) { // 159
  370. throw new Meteor.Error(404, "Not Found", 'This file was not stored in the ' + storeName + ' store'); // 160
  371. } // 161
  372. // 162
  373. // Set the content type for file // 163
  374. if (typeof copyInfo.type === "string") { // 164
  375. self.setContentType(copyInfo.type); // 165
  376. } else { // 166
  377. self.setContentType('application/octet-stream'); // 167
  378. } // 168
  379. // 169
  380. // Add 'Content-Disposition' header if requested a download/attachment URL // 170
  381. if (typeof ref.download !== "undefined") { // 171
  382. var filename = ref.filename || copyInfo.name; // 172
  383. self.addHeader('Content-Disposition', 'attachment; filename="' + filename + '"'); // 173
  384. } else { // 174
  385. self.addHeader('Content-Disposition', 'inline'); // 175
  386. } // 176
  387. // 177
  388. // Get the contents range from request // 178
  389. var range = requestRange(self.request, copyInfo.size); // 179
  390. // 180
  391. // Some browsers cope better if the content-range header is // 181
  392. // still included even for the full file being returned. // 182
  393. self.addHeader('Content-Range', range.unit + ' ' + range.start + '-' + range.end + '/' + range.size); // 183
  394. // 184
  395. // If a chunk/range was requested instead of the whole file, serve that' // 185
  396. if (range.partial) { // 186
  397. self.setStatusCode(206, 'Partial Content'); // 187
  398. } else { // 188
  399. self.setStatusCode(200, 'OK'); // 189
  400. } // 190
  401. // 191
  402. // Add any other global custom headers and collection-specific custom headers // 192
  403. FS.Utility.each(getHeaders.concat(getHeadersByCollection[ref.collection.name] || []), function(header) { // 193
  404. self.addHeader(header[0], header[1]); // 194
  405. }); // 195
  406. // 196
  407. // Inform clients about length (or chunk length in case of ranges) // 197
  408. self.addHeader('Content-Length', range.length); // 198
  409. // 199
  410. // Last modified header (updatedAt from file info) // 200
  411. self.addHeader('Last-Modified', copyInfo.updatedAt.toUTCString()); // 201
  412. // 202
  413. // Inform clients that we accept ranges for resumable chunked downloads // 203
  414. self.addHeader('Accept-Ranges', range.unit); // 204
  415. // 205
  416. if (FS.debug) console.log('Read file "' + (ref.filename || copyInfo.name) + '" ' + range.unit + ' ' + range.start + '-' + range.end + '/' + range.size);
  417. // 207
  418. var readStream = storage.adapter.createReadStream(ref.file, {start: range.start, end: range.end}); // 208
  419. // 209
  420. readStream.on('error', function(err) { // 210
  421. // Send proper error message on get error // 211
  422. if (err.message && err.statusCode) { // 212
  423. self.Error(new Meteor.Error(err.statusCode, err.message)); // 213
  424. } else { // 214
  425. self.Error(new Meteor.Error(503, 'Service unavailable')); // 215
  426. } // 216
  427. }); // 217
  428. // 218
  429. readStream.pipe(self.createWriteStream()); // 219
  430. }; // 220
  431. const originalHandler = FS.HTTP.Handlers.Get;
  432. FS.HTTP.Handlers.Get = function (ref) {
  433. //console.log(ref.filename);
  434. try {
  435. var userAgent = (this.requestHeaders['user-agent']||'').toLowerCase();
  436. if(userAgent.indexOf('msie') >= 0 || userAgent.indexOf('chrome') >= 0) {
  437. ref.filename = encodeURIComponent(ref.filename);
  438. } else if(userAgent.indexOf('firefox') >= 0) {
  439. ref.filename = new Buffer(ref.filename).toString('binary');
  440. } else {
  441. /* safari*/
  442. ref.filename = new Buffer(ref.filename).toString('binary');
  443. }
  444. } catch (ex){
  445. ref.filename = 'tempfix';
  446. }
  447. return originalHandler.call(this, ref);
  448. };
  449. // 221
  450. /** // 222
  451. * @method FS.HTTP.Handlers.PutInsert // 223
  452. * @public // 224
  453. * @returns {Object} response object with _id property // 225
  454. * // 226
  455. * HTTP PUT file insert request handler // 227
  456. */ // 228
  457. FS.HTTP.Handlers.PutInsert = function httpPutInsertHandler(ref) { // 229
  458. var self = this; // 230
  459. var opts = FS.Utility.extend({}, self.query || {}, self.params || {}); // 231
  460. // 232
  461. FS.debug && console.log("HTTP PUT (insert) handler"); // 233
  462. // 234
  463. // Create the nice FS.File // 235
  464. var fileObj = new FS.File(); // 236
  465. // 237
  466. // Set its name // 238
  467. fileObj.name(opts.filename || null); // 239
  468. // 240
  469. // Attach the readstream as the file's data // 241
  470. fileObj.attachData(self.createReadStream(), {type: self.requestHeaders['content-type'] || 'application/octet-stream'});
  471. // 243
  472. // Validate with insert allow/deny // 244
  473. FS.Utility.validateAction(ref.collection.files._validators['insert'], fileObj, self.userId); // 245
  474. // 246
  475. // Insert file into collection, triggering readStream storage // 247
  476. ref.collection.insert(fileObj); // 248
  477. // 249
  478. // Send response // 250
  479. self.setStatusCode(200); // 251
  480. // 252
  481. // Return the new file id // 253
  482. return {_id: fileObj._id}; // 254
  483. }; // 255
  484. // 256
  485. /** // 257
  486. * @method FS.HTTP.Handlers.PutUpdate // 258
  487. * @public // 259
  488. * @returns {Object} response object with _id and chunk properties // 260
  489. * // 261
  490. * HTTP PUT file update chunk request handler // 262
  491. */ // 263
  492. FS.HTTP.Handlers.PutUpdate = function httpPutUpdateHandler(ref) { // 264
  493. var self = this; // 265
  494. var opts = FS.Utility.extend({}, self.query || {}, self.params || {}); // 266
  495. // 267
  496. var chunk = parseInt(opts.chunk, 10); // 268
  497. if (isNaN(chunk)) chunk = 0; // 269
  498. // 270
  499. FS.debug && console.log("HTTP PUT (update) handler received chunk: ", chunk); // 271
  500. // 272
  501. // Validate with insert allow/deny; also mounts and retrieves the file // 273
  502. FS.Utility.validateAction(ref.collection.files._validators['insert'], ref.file, self.userId); // 274
  503. // 275
  504. self.createReadStream().pipe( FS.TempStore.createWriteStream(ref.file, chunk) ); // 276
  505. // 277
  506. // Send response // 278
  507. self.setStatusCode(200); // 279
  508. // 280
  509. return { _id: ref.file._id, chunk: chunk }; // 281
  510. }; // 282
  511. // 283
  512. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  513. }).call(this);
  514. (function () {
  515. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  516. // //
  517. // packages/cfs:access-point/access-point-server.js //
  518. // //
  519. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  520. //
  521. var path = Npm.require("path"); // 1
  522. // 2
  523. HTTP.publishFormats({ // 3
  524. fileRecordFormat: function (input) { // 4
  525. // Set the method scope content type to json // 5
  526. this.setContentType('application/json'); // 6
  527. if (FS.Utility.isArray(input)) { // 7
  528. return EJSON.stringify(FS.Utility.map(input, function (obj) { // 8
  529. return FS.Utility.cloneFileRecord(obj); // 9
  530. })); // 10
  531. } else { // 11
  532. return EJSON.stringify(FS.Utility.cloneFileRecord(input)); // 12
  533. } // 13
  534. } // 14
  535. }); // 15
  536. // 16
  537. /** // 17
  538. * @method FS.HTTP.setHeadersForGet // 18
  539. * @public // 19
  540. * @param {Array} headers - List of headers, where each is a two-item array in which item 1 is the header name and item 2 is the header value.
  541. * @param {Array|String} [collections] - Which collections the headers should be added for. Omit this argument to add the header for all collections.
  542. * @returns {undefined} // 22
  543. */ // 23
  544. FS.HTTP.setHeadersForGet = function setHeadersForGet(headers, collections) { // 24
  545. if (typeof collections === "string") { // 25
  546. collections = [collections]; // 26
  547. } // 27
  548. if (collections) { // 28
  549. FS.Utility.each(collections, function(collectionName) { // 29
  550. getHeadersByCollection[collectionName] = headers || []; // 30
  551. }); // 31
  552. } else { // 32
  553. getHeaders = headers || []; // 33
  554. } // 34
  555. }; // 35
  556. // 36
  557. /** // 37
  558. * @method FS.HTTP.publish // 38
  559. * @public // 39
  560. * @param {FS.Collection} collection // 40
  561. * @param {Function} func - Publish function that returns a cursor. // 41
  562. * @returns {undefined} // 42
  563. * // 43
  564. * Publishes all documents returned by the cursor at a GET URL // 44
  565. * with the format baseUrl/record/collectionName. The publish // 45
  566. * function `this` is similar to normal `Meteor.publish`. // 46
  567. */ // 47
  568. FS.HTTP.publish = function fsHttpPublish(collection, func) { // 48
  569. var name = baseUrl + '/record/' + collection.name; // 49
  570. // Mount collection listing URL using http-publish package // 50
  571. HTTP.publish({ // 51
  572. name: name, // 52
  573. defaultFormat: 'fileRecordFormat', // 53
  574. collection: collection, // 54
  575. collectionGet: true, // 55
  576. collectionPost: false, // 56
  577. documentGet: true, // 57
  578. documentPut: false, // 58
  579. documentDelete: false // 59
  580. }, func); // 60
  581. // 61
  582. FS.debug && console.log("Registered HTTP method GET URLs:\n\n" + name + '\n' + name + '/:id\n'); // 62
  583. }; // 63
  584. // 64
  585. /** // 65
  586. * @method FS.HTTP.unpublish // 66
  587. * @public // 67
  588. * @param {FS.Collection} collection // 68
  589. * @returns {undefined} // 69
  590. * // 70
  591. * Unpublishes a restpoint created by a call to `FS.HTTP.publish` // 71
  592. */ // 72
  593. FS.HTTP.unpublish = function fsHttpUnpublish(collection) { // 73
  594. // Mount collection listing URL using http-publish package // 74
  595. HTTP.unpublish(baseUrl + '/record/' + collection.name); // 75
  596. }; // 76
  597. // 77
  598. _existingMountPoints = {}; // 78
  599. // 79
  600. /** // 80
  601. * @method defaultSelectorFunction // 81
  602. * @private // 82
  603. * @returns { collection, file } // 83
  604. * // 84
  605. * This is the default selector function // 85
  606. */ // 86
  607. var defaultSelectorFunction = function() { // 87
  608. var self = this; // 88
  609. // Selector function // 89
  610. // // 90
  611. // This function will have to return the collection and the // 91
  612. // file. If file not found undefined is returned - if null is returned the // 92
  613. // search was not possible // 93
  614. var opts = FS.Utility.extend({}, self.query || {}, self.params || {}); // 94
  615. // 95
  616. // Get the collection name from the url // 96
  617. var collectionName = opts.collectionName; // 97
  618. // 98
  619. // Get the id from the url // 99
  620. var id = opts.id; // 100
  621. // 101
  622. // Get the collection // 102
  623. var collection = FS._collections[collectionName]; // 103
  624. // 104
  625. // Get the file if possible else return null // 105
  626. var file = (id && collection)? collection.findOne({ _id: id }): null; // 106
  627. // 107
  628. // Return the collection and the file // 108
  629. return { // 109
  630. collection: collection, // 110
  631. file: file, // 111
  632. storeName: opts.store, // 112
  633. download: opts.download, // 113
  634. filename: opts.filename // 114
  635. }; // 115
  636. }; // 116
  637. // 117
  638. /* // 118
  639. * @method FS.HTTP.mount // 119
  640. * @public // 120
  641. * @param {array of string} mountPoints mount points to map rest functinality on // 121
  642. * @param {function} selector_f [selector] function returns `{ collection, file }` for mount points to work with // 122
  643. * // 123
  644. */ // 124
  645. FS.HTTP.mount = function(mountPoints, selector_f) { // 125
  646. // We take mount points as an array and we get a selector function // 126
  647. var selectorFunction = selector_f || defaultSelectorFunction; // 127
  648. // 128
  649. var accessPoint = { // 129
  650. 'stream': true, // 130
  651. 'auth': expirationAuth, // 131
  652. 'post': function(data) { // 132
  653. // Use the selector for finding the collection and file reference // 133
  654. var ref = selectorFunction.call(this); // 134
  655. // 135
  656. // We dont support post - this would be normal insert eg. of filerecord? // 136
  657. throw new Meteor.Error(501, "Not implemented", "Post is not supported"); // 137
  658. }, // 138
  659. 'put': function(data) { // 139
  660. // Use the selector for finding the collection and file reference // 140
  661. var ref = selectorFunction.call(this); // 141
  662. // 142
  663. // Make sure we have a collection reference // 143
  664. if (!ref.collection) // 144
  665. throw new Meteor.Error(404, "Not Found", "No collection found"); // 145
  666. // 146
  667. // Make sure we have a file reference // 147
  668. if (ref.file === null) { // 148
  669. // No id supplied so we will create a new FS.File instance and // 149
  670. // insert the supplied data. // 150
  671. return FS.HTTP.Handlers.PutInsert.apply(this, [ref]); // 151
  672. } else { // 152
  673. if (ref.file) { // 153
  674. return FS.HTTP.Handlers.PutUpdate.apply(this, [ref]); // 154
  675. } else { // 155
  676. throw new Meteor.Error(404, "Not Found", 'No file found'); // 156
  677. } // 157
  678. } // 158
  679. }, // 159
  680. 'get': function(data) { // 160
  681. // Use the selector for finding the collection and file reference // 161
  682. var ref = selectorFunction.call(this); // 162
  683. // 163
  684. // Make sure we have a collection reference // 164
  685. if (!ref.collection) // 165
  686. throw new Meteor.Error(404, "Not Found", "No collection found"); // 166
  687. // 167
  688. // Make sure we have a file reference // 168
  689. if (ref.file === null) { // 169
  690. // No id supplied so we will return the published list of files ala // 170
  691. // http.publish in json format // 171
  692. return FS.HTTP.Handlers.GetList.apply(this, [ref]); // 172
  693. } else { // 173
  694. if (ref.file) { // 174
  695. return FS.HTTP.Handlers.Get.apply(this, [ref]); // 175
  696. } else { // 176
  697. throw new Meteor.Error(404, "Not Found", 'No file found'); // 177
  698. } // 178
  699. } // 179
  700. }, // 180
  701. 'delete': function(data) { // 181
  702. // Use the selector for finding the collection and file reference // 182
  703. var ref = selectorFunction.call(this); // 183
  704. // 184
  705. // Make sure we have a collection reference // 185
  706. if (!ref.collection) // 186
  707. throw new Meteor.Error(404, "Not Found", "No collection found"); // 187
  708. // 188
  709. // Make sure we have a file reference // 189
  710. if (ref.file) { // 190
  711. return FS.HTTP.Handlers.Del.apply(this, [ref]); // 191
  712. } else { // 192
  713. throw new Meteor.Error(404, "Not Found", 'No file found'); // 193
  714. } // 194
  715. } // 195
  716. }; // 196
  717. // 197
  718. var accessPoints = {}; // 198
  719. // 199
  720. // Add debug message // 200
  721. FS.debug && console.log('Registered HTTP method URLs:'); // 201
  722. // 202
  723. FS.Utility.each(mountPoints, function(mountPoint) { // 203
  724. // Couple mountpoint and accesspoint // 204
  725. accessPoints[mountPoint] = accessPoint; // 205
  726. // Remember our mountpoints // 206
  727. _existingMountPoints[mountPoint] = mountPoint; // 207
  728. // Add debug message // 208
  729. FS.debug && console.log(mountPoint); // 209
  730. }); // 210
  731. // 211
  732. // XXX: HTTP:methods should unmount existing mounts in case of overwriting? // 212
  733. HTTP.methods(accessPoints); // 213
  734. // 214
  735. }; // 215
  736. // 216
  737. /** // 217
  738. * @method FS.HTTP.unmount // 218
  739. * @public // 219
  740. * @param {string | array of string} [mountPoints] Optional, if not specified all mountpoints are unmounted // 220
  741. * // 221
  742. */ // 222
  743. FS.HTTP.unmount = function(mountPoints) { // 223
  744. // The mountPoints is optional, can be string or array if undefined then // 224
  745. // _existingMountPoints will be used // 225
  746. var unmountList; // 226
  747. // Container for the mount points to unmount // 227
  748. var unmountPoints = {}; // 228
  749. // 229
  750. if (typeof mountPoints === 'undefined') { // 230
  751. // Use existing mount points - unmount all // 231
  752. unmountList = _existingMountPoints; // 232
  753. } else if (mountPoints === ''+mountPoints) { // 233
  754. // Got a string // 234
  755. unmountList = [mountPoints]; // 235
  756. } else if (mountPoints.length) { // 236
  757. // Got an array // 237
  758. unmountList = mountPoints; // 238
  759. } // 239
  760. // 240
  761. // If we have a list to unmount // 241
  762. if (unmountList) { // 242
  763. // Iterate over each item // 243
  764. FS.Utility.each(unmountList, function(mountPoint) { // 244
  765. // Check _existingMountPoints to make sure the mount point exists in our // 245
  766. // context / was created by the FS.HTTP.mount // 246
  767. if (_existingMountPoints[mountPoint]) { // 247
  768. // Mark as unmount // 248
  769. unmountPoints[mountPoint] = false; // 249
  770. // Release // 250
  771. delete _existingMountPoints[mountPoint]; // 251
  772. } // 252
  773. }); // 253
  774. FS.debug && console.log('FS.HTTP.unmount:'); // 254
  775. FS.debug && console.log(unmountPoints); // 255
  776. // Complete unmount // 256
  777. HTTP.methods(unmountPoints); // 257
  778. } // 258
  779. }; // 259
  780. // 260
  781. // ### FS.Collection maps on HTTP pr. default on the following restpoints: // 261
  782. // * // 262
  783. // baseUrl + '/files/:collectionName/:id/:filename', // 263
  784. // baseUrl + '/files/:collectionName/:id', // 264
  785. // baseUrl + '/files/:collectionName' // 265
  786. // // 266
  787. // Change/ replace the existing mount point by: // 267
  788. // ```js // 268
  789. // // unmount all existing // 269
  790. // FS.HTTP.unmount(); // 270
  791. // // Create new mount point // 271
  792. // FS.HTTP.mount([ // 272
  793. // '/cfs/files/:collectionName/:id/:filename', // 273
  794. // '/cfs/files/:collectionName/:id', // 274
  795. // '/cfs/files/:collectionName' // 275
  796. // ]); // 276
  797. // ``` // 277
  798. // // 278
  799. mountUrls = function mountUrls() { // 279
  800. // We unmount first in case we are calling this a second time // 280
  801. FS.HTTP.unmount(); // 281
  802. // 282
  803. FS.HTTP.mount([ // 283
  804. baseUrl + '/files/:collectionName/:id/:filename', // 284
  805. baseUrl + '/files/:collectionName/:id', // 285
  806. baseUrl + '/files/:collectionName' // 286
  807. ]); // 287
  808. }; // 288
  809. // 289
  810. // Returns the userId from URL token // 290
  811. var expirationAuth = function expirationAuth() { // 291
  812. var self = this; // 292
  813. // 293
  814. // Read the token from '/hello?token=base64' // 294
  815. var encodedToken = self.query.token; // 295
  816. // 296
  817. FS.debug && console.log("token: "+encodedToken); // 297
  818. // 298
  819. if (!encodedToken || !Meteor.users) return false; // 299
  820. // 300
  821. // Check the userToken before adding it to the db query // 301
  822. // Set the this.userId // 302
  823. var tokenString = FS.Utility.atob(encodedToken); // 303
  824. // 304
  825. var tokenObject; // 305
  826. try { // 306
  827. tokenObject = JSON.parse(tokenString); // 307
  828. } catch(err) { // 308
  829. throw new Meteor.Error(400, 'Bad Request'); // 309
  830. } // 310
  831. // 311
  832. // XXX: Do some check here of the object // 312
  833. var userToken = tokenObject.authToken; // 313
  834. if (userToken !== ''+userToken) { // 314
  835. throw new Meteor.Error(400, 'Bad Request'); // 315
  836. } // 316
  837. // 317
  838. // If we have an expiration token we should check that it's still valid // 318
  839. if (tokenObject.expiration != null) { // 319
  840. // check if its too old // 320
  841. var now = Date.now(); // 321
  842. if (tokenObject.expiration < now) { // 322
  843. FS.debug && console.log('Expired token: ' + tokenObject.expiration + ' is less than ' + now); // 323
  844. throw new Meteor.Error(500, 'Expired token'); // 324
  845. } // 325
  846. } // 326
  847. // 327
  848. // We are not on a secure line - so we have to look up the user... // 328
  849. var user = Meteor.users.findOne({ // 329
  850. $or: [ // 330
  851. {'services.resume.loginTokens.hashedToken': Accounts._hashLoginToken(userToken)}, // 331
  852. {'services.resume.loginTokens.token': userToken} // 332
  853. ] // 333
  854. }); // 334
  855. // 335
  856. // Set the userId in the scope // 336
  857. return user && user._id; // 337
  858. }; // 338
  859. // 339
  860. HTTP.methods( // 340
  861. {'/cfs/servertime': { // 341
  862. get: function(data) { // 342
  863. return Date.now().toString(); // 343
  864. } // 344
  865. } // 345
  866. }); // 346
  867. // 347
  868. // Unify client / server api // 348
  869. FS.HTTP.now = function() { // 349
  870. return Date.now(); // 350
  871. }; // 351
  872. // 352
  873. // Start up the basic mount points // 353
  874. Meteor.startup(function () { // 354
  875. mountUrls(); // 355
  876. }); // 356
  877. // 357
  878. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  879. }).call(this);
  880. ///////////////////////////////////////////////////////////////////////
  881. }).call(this);
  882. /* Exports */
  883. if (typeof Package === 'undefined') Package = {};
  884. Package['cfs:access-point'] = {};
  885. })();