fsFile-server.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. /**
  2. * Notes a details about a storage adapter failure within the file record
  3. * @param {string} storeName
  4. * @param {number} maxTries
  5. * @return {undefined}
  6. * @todo deprecate this
  7. */
  8. FS.File.prototype.logCopyFailure = function(storeName, maxTries) {
  9. var self = this;
  10. // hasStored will update from the fileRecord
  11. if (self.hasStored(storeName)) {
  12. throw new Error("logCopyFailure: invalid storeName");
  13. }
  14. // Make sure we have a temporary file saved since we will be
  15. // trying the save again.
  16. FS.TempStore.ensureForFile(self);
  17. var now = new Date();
  18. var currentCount = (self.failures && self.failures.copies && self.failures.copies[storeName] && typeof self.failures.copies[storeName].count === "number") ? self.failures.copies[storeName].count : 0;
  19. maxTries = maxTries || 5;
  20. var modifier = {};
  21. modifier.$set = {};
  22. modifier.$set['failures.copies.' + storeName + '.lastAttempt'] = now;
  23. if (currentCount === 0) {
  24. modifier.$set['failures.copies.' + storeName + '.firstAttempt'] = now;
  25. }
  26. modifier.$set['failures.copies.' + storeName + '.count'] = currentCount + 1;
  27. modifier.$set['failures.copies.' + storeName + '.doneTrying'] = (currentCount + 1 >= maxTries);
  28. self.update(modifier);
  29. };
  30. /**
  31. * Has this store permanently failed?
  32. * @param {String} storeName The name of the store
  33. * @return {boolean} Has this store failed permanently?
  34. * @todo deprecate this
  35. */
  36. FS.File.prototype.failedPermanently = function(storeName) {
  37. var self = this;
  38. return !!(self.failures &&
  39. self.failures.copies &&
  40. self.failures.copies[storeName] &&
  41. self.failures.copies[storeName].doneTrying);
  42. };
  43. /**
  44. * @method FS.File.prototype.createReadStream
  45. * @public
  46. * @param {String} [storeName]
  47. * @returns {stream.Readable} Readable NodeJS stream
  48. *
  49. * Returns a readable stream. Where the stream reads from depends on the FS.File instance and whether you pass a store name.
  50. *
  51. * * If you pass a `storeName`, a readable stream for the file data saved in that store is returned.
  52. * * If you don't pass a `storeName` and data is attached to the FS.File instance (on `data` property, which must be a DataMan instance), then a readable stream for the attached data is returned.
  53. * * If you don't pass a `storeName` and there is no data attached to the FS.File instance, a readable stream for the file data currently in the temporary store (`FS.TempStore`) is returned.
  54. *
  55. */
  56. FS.File.prototype.createReadStream = function(storeName) {
  57. var self = this;
  58. // If we dont have a store name but got Buffer data?
  59. if (!storeName && self.data) {
  60. FS.debug && console.log("fileObj.createReadStream creating read stream for attached data");
  61. // Stream from attached data if present
  62. return self.data.createReadStream();
  63. } else if (!storeName && FS.TempStore && FS.TempStore.exists(self)) {
  64. FS.debug && console.log("fileObj.createReadStream creating read stream for temp store");
  65. // Stream from temp store - its a bit slower than regular streams?
  66. return FS.TempStore.createReadStream(self);
  67. } else {
  68. // Stream from the store using storage adapter
  69. if (self.isMounted()) {
  70. var storage = self.collection.storesLookup[storeName] || self.collection.primaryStore;
  71. FS.debug && console.log("fileObj.createReadStream creating read stream for store", storage.name);
  72. // return stream
  73. return storage.adapter.createReadStream(self);
  74. } else {
  75. throw new Meteor.Error('File not mounted');
  76. }
  77. }
  78. };
  79. /**
  80. * @method FS.File.prototype.createWriteStream
  81. * @public
  82. * @param {String} [storeName]
  83. * @returns {stream.Writeable} Writeable NodeJS stream
  84. *
  85. * Returns a writeable stream. Where the stream writes to depends on whether you pass in a store name.
  86. *
  87. * * If you pass a `storeName`, a writeable stream for (over)writing the file data in that store is returned.
  88. * * If you don't pass a `storeName`, a writeable stream for writing to the temp store for this file is returned.
  89. *
  90. */
  91. FS.File.prototype.createWriteStream = function(storeName) {
  92. var self = this;
  93. // We have to have a mounted file in order for this to work
  94. if (self.isMounted()) {
  95. if (!storeName && FS.TempStore && FS.FileWorker) {
  96. // If we have worker installed - we pass the file to FS.TempStore
  97. // We dont need the storeName since all stores will be generated from
  98. // TempStore.
  99. // This should trigger FS.FileWorker at some point?
  100. FS.TempStore.createWriteStream(self);
  101. } else {
  102. // Stream directly to the store using storage adapter
  103. var storage = self.collection.storesLookup[storeName] || self.collection.primaryStore;
  104. return storage.adapter.createWriteStream(self);
  105. }
  106. } else {
  107. throw new Meteor.Error('File not mounted');
  108. }
  109. };
  110. /**
  111. * @method FS.File.prototype.copy Makes a copy of the file and underlying data in all stores.
  112. * @public
  113. * @returns {FS.File} The new FS.File instance
  114. */
  115. FS.File.prototype.copy = function() {
  116. var self = this;
  117. if (!self.isMounted()) {
  118. throw new Error("Cannot copy a file that is not associated with a collection");
  119. }
  120. // Get the file record
  121. var fileRecord = self.collection.files.findOne({_id: self._id}, {transform: null}) || {};
  122. // Remove _id and copy keys from the file record
  123. delete fileRecord._id;
  124. // Insert directly; we don't have access to "original" in this case
  125. var newId = self.collection.files.insert(fileRecord);
  126. var newFile = self.collection.findOne(newId);
  127. // Copy underlying files in the stores
  128. var mod, oldKey;
  129. for (var name in newFile.copies) {
  130. if (newFile.copies.hasOwnProperty(name)) {
  131. oldKey = newFile.copies[name].key;
  132. if (oldKey) {
  133. // We need to ask the adapter for the true oldKey because
  134. // right now gridfs does some extra stuff.
  135. // TODO GridFS should probably set the full key object
  136. // (with _id and filename) into `copies.key`
  137. // so that copies.key can be passed directly to
  138. // createReadStreamForFileKey
  139. var sourceFileStorage = self.collection.storesLookup[name];
  140. if (!sourceFileStorage) {
  141. throw new Error(name + " is not a valid store name");
  142. }
  143. oldKey = sourceFileStorage.adapter.fileKey(self);
  144. // delete so that new fileKey will be generated in copyStoreData
  145. delete newFile.copies[name].key;
  146. mod = mod || {};
  147. mod["copies." + name + ".key"] = copyStoreData(newFile, name, oldKey);
  148. }
  149. }
  150. }
  151. // Update keys in the filerecord
  152. if (mod) {
  153. newFile.update({$set: mod});
  154. }
  155. return newFile;
  156. };
  157. Meteor.methods({
  158. // Does a HEAD request to URL to get the type, updatedAt,
  159. // and size prior to actually downloading the data.
  160. // That way we can do filter checks without actually downloading.
  161. '_cfs_getUrlInfo': function (url, options) {
  162. check(url, String);
  163. check(options, Object);
  164. this.unblock();
  165. var response = HTTP.call("HEAD", url, options);
  166. var headers = response.headers;
  167. var result = {};
  168. if (headers['content-type']) {
  169. result.type = headers['content-type'];
  170. }
  171. if (headers['content-length']) {
  172. result.size = +headers['content-length'];
  173. }
  174. if (headers['last-modified']) {
  175. result.updatedAt = new Date(headers['last-modified']);
  176. }
  177. return result;
  178. },
  179. // Helper function that checks whether given fileId from collectionName
  180. // Is fully uploaded to specify storeName.
  181. '_cfs_returnWhenStored' : function (collectionName, fileId, storeName) {
  182. check(collectionName, String);
  183. check(fileId, String);
  184. check(storeName, String);
  185. var collection = FS._collections[collectionName];
  186. if (!collection) {
  187. return Meteor.Error('_cfs_returnWhenStored: FSCollection name not exists');
  188. }
  189. var file = collection.findOne({_id: fileId});
  190. if (!file) {
  191. return Meteor.Error('_cfs_returnWhenStored: FSFile not exists');
  192. }
  193. return file.hasStored(storeName);
  194. }
  195. });
  196. // TODO maybe this should be in cfs-storage-adapter
  197. function _copyStoreData(fileObj, storeName, sourceKey, callback) {
  198. if (!fileObj.isMounted()) {
  199. throw new Error("Cannot copy store data for a file that is not associated with a collection");
  200. }
  201. var storage = fileObj.collection.storesLookup[storeName];
  202. if (!storage) {
  203. throw new Error(storeName + " is not a valid store name");
  204. }
  205. // We want to prevent beforeWrite and transformWrite from running, so
  206. // we interact directly with the store.
  207. var destinationKey = storage.adapter.fileKey(fileObj);
  208. var readStream = storage.adapter.createReadStreamForFileKey(sourceKey);
  209. var writeStream = storage.adapter.createWriteStreamForFileKey(destinationKey);
  210. writeStream.once('stored', function(result) {
  211. callback(null, result.fileKey);
  212. });
  213. writeStream.once('error', function(error) {
  214. callback(error);
  215. });
  216. readStream.pipe(writeStream);
  217. }
  218. var copyStoreData = Meteor.wrapAsync(_copyStoreData);
  219. /**
  220. * @method FS.File.prototype.copyData Copies the content of a store directly into another store.
  221. * @public
  222. * @param {string} sourceStoreName
  223. * @param {string} targetStoreName
  224. * @param {boolean=} move
  225. */
  226. FS.File.prototype.copyData = function(sourceStoreName, targetStoreName, move){
  227. move = !!move;
  228. /**
  229. * @type {Object.<string,*>}
  230. */
  231. var sourceStoreValues = this.copies[sourceStoreName];
  232. /**
  233. * @type {string}
  234. */
  235. var copyKey = cloneDataToStore(this, sourceStoreName, targetStoreName, move);
  236. /**
  237. * @type {Object.<string,*>}
  238. */
  239. var targetStoreValues = {};
  240. for (var v in sourceStoreValues) {
  241. if (sourceStoreValues.hasOwnProperty(v)) {
  242. targetStoreValues[v] = sourceStoreValues[v]
  243. }
  244. }
  245. targetStoreValues.key = copyKey;
  246. targetStoreValues.createdAt = new Date();
  247. targetStoreValues.updatedAt = new Date();
  248. /**
  249. *
  250. * @type {modifier}
  251. */
  252. var modifier = {};
  253. modifier.$set = {};
  254. modifier.$set["copies."+targetStoreName] = targetStoreValues;
  255. if(move){
  256. modifier.$unset = {};
  257. modifier.$unset["copies."+sourceStoreName] = "";
  258. }
  259. this.update(modifier);
  260. };
  261. /**
  262. * @method FS.File.prototype.moveData Moves the content of a store directly into another store.
  263. * @public
  264. * @param {string} sourceStoreName
  265. * @param {string} targetStoreName
  266. */
  267. FS.File.prototype.moveData = function(sourceStoreName, targetStoreName){
  268. this.copyData(sourceStoreName, targetStoreName, true);
  269. };
  270. // TODO maybe this should be in cfs-storage-adapter
  271. /**
  272. *
  273. * @param {FS.File} fileObj
  274. * @param {string} sourceStoreName
  275. * @param {string} targetStoreName
  276. * @param {boolean} move
  277. * @param callback
  278. * @private
  279. */
  280. function _copyDataFromStoreToStore(fileObj, sourceStoreName, targetStoreName, move, callback) {
  281. if (!fileObj.isMounted()) {
  282. throw new Error("Cannot copy store data for a file that is not associated with a collection");
  283. }
  284. /**
  285. * @type {FS.StorageAdapter}
  286. */
  287. var sourceStorage = fileObj.collection.storesLookup[sourceStoreName];
  288. /**
  289. * @type {FS.StorageAdapter}
  290. */
  291. var targetStorage = fileObj.collection.storesLookup[targetStoreName];
  292. if (!sourceStorage) {
  293. throw new Error(sourceStoreName + " is not a valid store name");
  294. }
  295. if (!targetStorage) {
  296. throw new Error(targetStorage + " is not a valid store name");
  297. }
  298. // We want to prevent beforeWrite and transformWrite from running, so
  299. // we interact directly with the store.
  300. var sourceKey = sourceStorage.adapter.fileKey(fileObj);
  301. var targetKey = targetStorage.adapter.fileKey(fileObj);
  302. var readStream = sourceStorage.adapter.createReadStreamForFileKey(sourceKey);
  303. var writeStream = targetStorage.adapter.createWriteStreamForFileKey(targetKey);
  304. writeStream.safeOnce('stored', function(result) {
  305. if(move && sourceStorage.adapter.remove(fileObj)===false){
  306. callback("Copied to store:" + targetStoreName
  307. + " with fileKey: "
  308. + result.fileKey
  309. + ", but could not delete from source store: "
  310. + sourceStoreName);
  311. }else{
  312. callback(null, result.fileKey);
  313. }
  314. });
  315. writeStream.once('error', function(error) {
  316. callback(error);
  317. });
  318. readStream.pipe(writeStream);
  319. }
  320. var cloneDataToStore = Meteor.wrapAsync(_copyDataFromStoreToStore);