| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765 | /** * @method FS.File * @namespace FS.File * @public * @constructor * @param {object|FS.File|data to attach} [ref] Another FS.File instance, a filerecord, or some data to pass to attachData */FS.File = function(ref, createdByTransform) {  var self = this;  self.createdByTransform = !!createdByTransform;  if (ref instanceof FS.File || isBasicObject(ref)) {    // Extend self with filerecord related data    FS.Utility.extend(self, FS.Utility.cloneFileRecord(ref, {full: true}));  } else if (ref) {    self.attachData(ref);  }};// An FS.File can emit eventsFS.File.prototype = new EventEmitter();/** * @method FS.File.prototype.attachData * @public * @param {File|Blob|Buffer|ArrayBuffer|Uint8Array|String} data The data that you want to attach to the file. * @param {Object} [options] Options * @param {String} [options.type] The data content (MIME) type, if known. * @param {String} [options.headers] When attaching a URL, headers to be used for the GET request (currently server only) * @param {String} [options.auth] When attaching a URL, "username:password" to be used for the GET request (currently server only) * @param {Function} [callback] Callback function, callback(error). On the client, a callback is required if data is a URL. * @returns {FS.File} This FS.File instance. * */FS.File.prototype.attachData = function fsFileAttachData(data, options, callback) {  var self = this;  if (!callback && typeof options === "function") {    callback = options;    options = {};  }  options = options || {};  if (!data) {    throw new Error('FS.File.attachData requires a data argument with some data');  }  var urlOpts;  // Set any other properties we can determine from the source data  // File  if (typeof File !== "undefined" && data instanceof File) {    self.name(data.name);    self.updatedAt(data.lastModifiedDate);    self.size(data.size);    setData(data.type);  }  // Blob  else if (typeof Blob !== "undefined" && data instanceof Blob) {    self.name(data.name);    self.updatedAt(new Date());    self.size(data.size);    setData(data.type);  }  // URL: we need to do a HEAD request to get the type because type  // is required for filtering to work.  else if (typeof data === "string" && (data.slice(0, 5) === "http:" || data.slice(0, 6) === "https:")) {    urlOpts = FS.Utility.extend({}, options);    if (urlOpts.type) {      delete urlOpts.type;    }    if (!callback) {      if (Meteor.isClient) {        throw new Error('FS.File.attachData requires a callback when attaching a URL on the client');      }      var result = Meteor.call('_cfs_getUrlInfo', data, urlOpts);      FS.Utility.extend(self, {original: result});      setData(result.type);    } else {      Meteor.call('_cfs_getUrlInfo', data, urlOpts, function (error, result) {        FS.debug && console.log("URL HEAD RESULT:", result);        if (error) {          callback(error);        } else {          var type = result.type || options.type;          if (! type) {            throw new Error('FS.File.attachData got a URL for which it could not determine the MIME type and none was provided using options.type');          }          FS.Utility.extend(self, {original: result});          setData(type);        }      });    }  }  // Everything else  else {    setData(options.type);  }  // Set the data  function setData(type) {    self.data = new DataMan(data, type, urlOpts);    // Update the type to match what the data is    self.type(self.data.type());    // Update the size to match what the data is.    // It's always safe to call self.data.size() without supplying a callback    // because it requires a callback only for URLs on the client, and we    // already added size for URLs when we got the result from '_cfs_getUrlInfo' method.    if (!self.size()) {      if (callback) {        self.data.size(function (error, size) {          if (error) {            callback && callback(error);          } else {            self.size(size);            setName();          }        });      } else {        self.size(self.data.size());        setName();      }    } else {      setName();    }  }  function setName() {    // See if we can extract a file name from URL or filepath    if (!self.name() && typeof data === "string") {      // name from URL      if (data.slice(0, 5) === "http:" || data.slice(0, 6) === "https:") {        if (FS.Utility.getFileExtension(data).length) {          // for a URL we assume the end is a filename only if it has an extension          self.name(FS.Utility.getFileName(data));        }      }      // name from filepath      else if (data.slice(0, 5) !== "data:") {        self.name(FS.Utility.getFileName(data));      }    }    callback && callback();  }  return self; //allow chaining};/** * @method FS.File.prototype.uploadProgress * @public * @returns {number} The server confirmed upload progress */FS.File.prototype.uploadProgress = function() {  var self = this;  // Make sure our file record is updated  self.getFileRecord();  // If fully uploaded, return 100  if (self.uploadedAt) {    return 100;  }  // Otherwise return the confirmed progress or 0  else {    return Math.round((self.chunkCount || 0) / (self.chunkSum || 1) * 100);  }};/** * @method FS.File.prototype.controlledByDeps * @public * @returns {FS.Collection} Returns true if this FS.File is reactive * * > Note: Returns true if this FS.File object was created by a FS.Collection * > and we are in a reactive computations. What does this mean? Well it should * > mean that our fileRecord is fully updated by Meteor and we are mounted on * > a collection */FS.File.prototype.controlledByDeps = function() {  var self = this;  return self.createdByTransform && Deps.active;};/** * @method FS.File.prototype.getCollection * @public * @returns {FS.Collection} Returns attached collection or undefined if not mounted */FS.File.prototype.getCollection = function() {  // Get the collection reference  var self = this;  // If we already made the link then do no more  if (self.collection) {    return self.collection;  }  // If we don't have a collectionName then there's not much to do, the file is  // not mounted yet  if (!self.collectionName) {    // Should not throw an error here - could be common that the file is not    // yet mounted into a collection    return;  }  // Link the collection to the file  self.collection = FS._collections[self.collectionName];  return self.collection; //possibly undefined, but that's desired behavior};/** * @method FS.File.prototype.isMounted * @public * @returns {FS.Collection} Returns attached collection or undefined if not mounted */FS.File.prototype.isMounted = FS.File.prototype.getCollection;/** * @method FS.File.prototype.getFileRecord Returns the fileRecord * @public * @returns {object} The filerecord */FS.File.prototype.getFileRecord = function() {  var self = this;  // Check if this file object fileRecord is kept updated by Meteor, if so  // return self  if (self.controlledByDeps()) {    return self;  }  // Go for manually updating the file record  if (self.isMounted()) {    FS.debug && console.log('GET FILERECORD: ' + self._id);    // Return the fileRecord or an empty object    var fileRecord = self.collection.files.findOne({_id: self._id}) || {};    FS.Utility.extend(self, fileRecord);    return fileRecord;  } else {    // We return an empty object, this way users can still do `getRecord().size`    // Without getting an error    return {};  }};/** * @method FS.File.prototype.update * @public * @param {modifier} modifier * @param {object} [options] * @param {function} [callback] * * Updates the fileRecord. */FS.File.prototype.update = function(modifier, options, callback) {  var self = this;  FS.debug && console.log('UPDATE: ' + JSON.stringify(modifier));  // Make sure we have options and callback  if (!callback && typeof options === 'function') {    callback = options;    options = {};  }  callback = callback || FS.Utility.defaultCallback;  if (!self.isMounted()) {    callback(new Error("Cannot update a file that is not associated with a collection"));    return;  }  // Call collection update - File record  return self.collection.files.update({_id: self._id}, modifier, options, function(err, count) {    // Update the fileRecord if it was changed and on the client    // The server-side methods will pull the fileRecord if needed    if (count > 0 && Meteor.isClient)      self.getFileRecord();    // Call callback    callback(err, count);  });};/** * @method FS.File.prototype._saveChanges * @private * @param {String} [what] "_original" to save original info, or a store name to save info for that store, or saves everything * * Updates the fileRecord from values currently set on the FS.File instance. */FS.File.prototype._saveChanges = function(what) {  var self = this;  if (!self.isMounted()) {    return;  }  FS.debug && console.log("FS.File._saveChanges:", what || "all");  var mod = {$set: {}};  if (what === "_original") {    mod.$set.original = self.original;  } else if (typeof what === "string") {    var info = self.copies[what];    if (info) {      mod.$set["copies." + what] = info;    }  } else {    mod.$set.original = self.original;    mod.$set.copies = self.copies;  }  self.update(mod);};/** * @method FS.File.prototype.remove * @public * @param {Function} [callback] * @returns {number} Count * * Remove the current file from its FS.Collection */FS.File.prototype.remove = function(callback) {  var self = this;  FS.debug && console.log('REMOVE: ' + self._id);  callback = callback || FS.Utility.defaultCallback;  if (!self.isMounted()) {    callback(new Error("Cannot remove a file that is not associated with a collection"));    return;  }  return self.collection.files.remove({_id: self._id}, function(err, res) {    if (!err) {      delete self._id;      delete self.collection;      delete self.collectionName;    }    callback(err, res);  });};/** * @method FS.File.prototype.moveTo * @param {FS.Collection} targetCollection * @private // Marked private until implemented * @todo Needs to be implemented * * Move the file from current collection to another collection * * > Note: Not yet implemented *//** * @method FS.File.prototype.getExtension Returns the lowercase file extension * @public * @deprecated Use the `extension` getter/setter method instead. * @param {Object} [options] * @param {String} [options.store] - Store name. Default is the original extension. * @returns {string} The extension eg.: `jpg` or if not found then an empty string '' */FS.File.prototype.getExtension = function(options) {  var self = this;  return self.extension(options);};function checkContentType(fsFile, storeName, startOfType) {  var type;  if (storeName && fsFile.hasStored(storeName)) {    type = fsFile.type({store: storeName});  } else {    type = fsFile.type();  }  if (typeof type === "string") {    return type.indexOf(startOfType) === 0;  }  return false;}/** * @method FS.File.prototype.isImage Is it an image file? * @public * @param {object} [options] * @param {string} [options.store] The store we're interested in * * Returns true if the copy of this file in the specified store has an image * content type. If the file object is unmounted or doesn't have a copy for * the specified store, or if you don't specify a store, this method checks * the content type of the original file. */FS.File.prototype.isImage = function(options) {  return checkContentType(this, (options || {}).store, 'image/');};/** * @method FS.File.prototype.isVideo Is it a video file? * @public * @param {object} [options] * @param {string} [options.store] The store we're interested in * * Returns true if the copy of this file in the specified store has a video * content type. If the file object is unmounted or doesn't have a copy for * the specified store, or if you don't specify a store, this method checks * the content type of the original file. */FS.File.prototype.isVideo = function(options) {  return checkContentType(this, (options || {}).store, 'video/');};/** * @method FS.File.prototype.isAudio Is it an audio file? * @public * @param {object} [options] * @param {string} [options.store] The store we're interested in * * Returns true if the copy of this file in the specified store has an audio * content type. If the file object is unmounted or doesn't have a copy for * the specified store, or if you don't specify a store, this method checks * the content type of the original file. */FS.File.prototype.isAudio = function(options) {  return checkContentType(this, (options || {}).store, 'audio/');};/** * @method FS.File.prototype.formattedSize * @public * @param  {Object} options * @param  {String} [options.store=none,display original file size] Which file do you want to get the size of? * @param  {String} [options.formatString='0.00 b'] The `numeral` format string to use. * @return {String} The file size formatted as a human readable string and reactively updated. * * * You must add the `numeral` package to your app before you can use this method. * * If info is not found or a size can't be determined, it will show 0. */FS.File.prototype.formattedSize = function fsFileFormattedSize(options) {  var self = this;  if (typeof numeral !== "function")    throw new Error("You must add the numeral package if you call FS.File.formattedSize");  options = options || {};  options = options.hash || options;  var size = self.size(options) || 0;  return numeral(size).format(options.formatString || '0.00 b');};/** * @method FS.File.prototype.isUploaded Is this file completely uploaded? * @public * @returns {boolean} True if the number of uploaded bytes is equal to the file size. */FS.File.prototype.isUploaded = function() {  var self = this;  // Make sure we use the updated file record  self.getFileRecord();  return !!self.uploadedAt;};/** * @method FS.File.prototype.hasStored * @public * @param {string} storeName Name of the store * @param {boolean} [optimistic=false] In case that the file record is not found, read below * @returns {boolean} Is a version of this file stored in the given store? * * > Note: If the file is not published to the client or simply not found: * this method cannot know for sure if it exists or not. The `optimistic` * param is the boolean value to return. Are we `optimistic` that the copy * could exist. This is the case in `FS.File.url` we are optimistic that the * copy supplied by the user exists. */FS.File.prototype.hasStored = function(storeName, optimistic) {  var self = this;  // Make sure we use the updated file record  self.getFileRecord();  // If we havent the published data then  if (FS.Utility.isEmpty(self.copies)) {    return !!optimistic;  }  if (typeof storeName === "string") {    // Return true only if the `key` property is present, which is not set until    // storage is complete.    return !!(self.copies && self.copies[storeName] && self.copies[storeName].key);  }  return false;};// Backwards compatibilityFS.File.prototype.hasCopy = FS.File.prototype.hasStored;/** * @method FS.File.prototype.getCopyInfo * @public * @deprecated Use individual methods with `store` option instead. * @param {string} storeName Name of the store for which to get copy info. * @returns {Object} The file details, e.g., name, size, key, etc., specific to the copy saved in this store. */FS.File.prototype.getCopyInfo = function(storeName) {  var self = this;  // Make sure we use the updated file record  self.getFileRecord();  return (self.copies && self.copies[storeName]) || null;};/** * @method FS.File.prototype._getInfo * @private * @param {String} [storeName] Name of the store for which to get file info. Omit for original file details. * @param {Object} [options] * @param {Boolean} [options.updateFileRecordFirst=false] Update this instance with data from the DB first? * @returns {Object} The file details, e.g., name, size, key, etc. If not found, returns an empty object. */FS.File.prototype._getInfo = function(storeName, options) {  var self = this;  options = options || {};  if (options.updateFileRecordFirst) {    // Make sure we use the updated file record    self.getFileRecord();  }  if (storeName) {    return (self.copies && self.copies[storeName]) || {};  } else {    return self.original || {};  }};/** * @method FS.File.prototype._setInfo * @private * @param {String} storeName - Name of the store for which to set file info. Non-string will set original file details. * @param {String} property - Property to set * @param {String} value - New value for property * @param {Boolean} save - Should the new value be saved to the DB, too, or just set in the FS.File properties? * @returns {undefined} */FS.File.prototype._setInfo = function(storeName, property, value, save) {  var self = this;  if (typeof storeName === "string") {    self.copies = self.copies || {};    self.copies[storeName] = self.copies[storeName] || {};    self.copies[storeName][property] = value;    save && self._saveChanges(storeName);  } else {    self.original = self.original || {};    self.original[property] = value;    save && self._saveChanges("_original");  }};/** * @method FS.File.prototype.name * @public * @param {String|null} [value] - If setting the name, specify the new name as the first argument. Otherwise the options argument should be first. * @param {Object} [options] * @param {Object} [options.store=none,original] - Get or set the name of the version of the file that was saved in this store. Default is the original file name. * @param {Boolean} [options.updateFileRecordFirst=false] Update this instance with data from the DB first? Applies to getter usage only. * @param {Boolean} [options.save=true] Save change to database? Applies to setter usage only. * @returns {String|undefined} If setting, returns `undefined`. If getting, returns the file name. */FS.File.prototype.name = function(value, options) {  var self = this;  if (!options && ((typeof value === "object" && value !== null) || typeof value === "undefined")) {    // GET    options = value || {};    options = options.hash || options; // allow use as UI helper    return self._getInfo(options.store, options).name;  } else {    // SET    options = options || {};    return self._setInfo(options.store, 'name', value, typeof options.save === "boolean" ? options.save : true);  }};/** * @method FS.File.prototype.extension * @public * @param {String|null} [value] - If setting the extension, specify the new extension (without period) as the first argument. Otherwise the options argument should be first. * @param {Object} [options] * @param {Object} [options.store=none,original] - Get or set the extension of the version of the file that was saved in this store. Default is the original file extension. * @param {Boolean} [options.updateFileRecordFirst=false] Update this instance with data from the DB first? Applies to getter usage only. * @param {Boolean} [options.save=true] Save change to database? Applies to setter usage only. * @returns {String|undefined} If setting, returns `undefined`. If getting, returns the file extension or an empty string if there isn't one. */FS.File.prototype.extension = function(value, options) {  var self = this;  if (!options && ((typeof value === "object" && value !== null) || typeof value === "undefined")) {    // GET    options = value || {};    return FS.Utility.getFileExtension(self.name(options) || '');  } else {    // SET    options = options || {};    var newName = FS.Utility.setFileExtension(self.name(options) || '', value);    return self._setInfo(options.store, 'name', newName, typeof options.save === "boolean" ? options.save : true);  }};/** * @method FS.File.prototype.size * @public * @param {Number} [value] - If setting the size, specify the new size in bytes as the first argument. Otherwise the options argument should be first. * @param {Object} [options] * @param {Object} [options.store=none,original] - Get or set the size of the version of the file that was saved in this store. Default is the original file size. * @param {Boolean} [options.updateFileRecordFirst=false] Update this instance with data from the DB first? Applies to getter usage only. * @param {Boolean} [options.save=true] Save change to database? Applies to setter usage only. * @returns {Number|undefined} If setting, returns `undefined`. If getting, returns the file size. */FS.File.prototype.size = function(value, options) {  var self = this;  if (!options && ((typeof value === "object" && value !== null) || typeof value === "undefined")) {    // GET    options = value || {};    options = options.hash || options; // allow use as UI helper    return self._getInfo(options.store, options).size;  } else {    // SET    options = options || {};    return self._setInfo(options.store, 'size', value, typeof options.save === "boolean" ? options.save : true);  }};/** * @method FS.File.prototype.type * @public * @param {String} [value] - If setting the type, specify the new type as the first argument. Otherwise the options argument should be first. * @param {Object} [options] * @param {Object} [options.store=none,original] - Get or set the type of the version of the file that was saved in this store. Default is the original file type. * @param {Boolean} [options.updateFileRecordFirst=false] Update this instance with data from the DB first? Applies to getter usage only. * @param {Boolean} [options.save=true] Save change to database? Applies to setter usage only. * @returns {String|undefined} If setting, returns `undefined`. If getting, returns the file type. */FS.File.prototype.type = function(value, options) {  var self = this;  if (!options && ((typeof value === "object" && value !== null) || typeof value === "undefined")) {    // GET    options = value || {};    options = options.hash || options; // allow use as UI helper    return self._getInfo(options.store, options).type;  } else {    // SET    options = options || {};    return self._setInfo(options.store, 'type', value, typeof options.save === "boolean" ? options.save : true);  }};/** * @method FS.File.prototype.updatedAt * @public * @param {String} [value] - If setting updatedAt, specify the new date as the first argument. Otherwise the options argument should be first. * @param {Object} [options] * @param {Object} [options.store=none,original] - Get or set the last updated date for the version of the file that was saved in this store. Default is the original last updated date. * @param {Boolean} [options.updateFileRecordFirst=false] Update this instance with data from the DB first? Applies to getter usage only. * @param {Boolean} [options.save=true] Save change to database? Applies to setter usage only. * @returns {String|undefined} If setting, returns `undefined`. If getting, returns the file's last updated date. */FS.File.prototype.updatedAt = function(value, options) {  var self = this;  if (!options && ((typeof value === "object" && value !== null && !(value instanceof Date)) || typeof value === "undefined")) {    // GET    options = value || {};    options = options.hash || options; // allow use as UI helper    return self._getInfo(options.store, options).updatedAt;  } else {    // SET    options = options || {};    return self._setInfo(options.store, 'updatedAt', value, typeof options.save === "boolean" ? options.save : true);  }};/** * @method FS.File.onStoredCallback * @summary Calls callback when the file is fully stored to the specify storeName * @public * @param {String} [storeName] - The name of the file store we want to get called when stored. * @param {function} [callback] */FS.File.prototype.onStoredCallback = function (storeName, callback) {  // Check file is not already stored  if (this.hasStored(storeName)) {    callback();    return;  }  if (Meteor.isServer) {    // Listen to file stored events    // TODO Require thinking whether it is better to use observer for case of using multiple application instances, Ask for same image url while upload is being done.    this.on('stored', function (newStoreName) {      // If stored is completed to the specified store call callback      if (storeName === newStoreName) {        // Remove the specified file stored listener        this.removeListener('stored', arguments.callee);        callback();      }    }.bind(this)    );  } else {    var fileId = this._id,        collectionName = this.collectionName;    // Wait for file to be fully uploaded    Tracker.autorun(function (c) {      Meteor.call('_cfs_returnWhenStored', collectionName, fileId, storeName, function (error, result) {        if (result && result === true) {          c.stop();          callback();        } else {          Meteor.setTimeout(function () {            c.invalidate();          }, 100);        }      });    });  }};/** * @method FS.File.onStored * @summary Function that returns when the file is fully stored to the specify storeName * @public * @param {String} storeName - The name of the file store we want to get called when stored. * * Function that returns when the file is fully stored to the specify storeName. * * For example needed if wanted to save the direct link to a file on s3 when fully uploaded. */FS.File.prototype.onStored = function (arguments) {  var onStoredSync = Meteor.wrapAsync(this.onStoredCallback);  return onStoredSync.call(this, arguments);};function isBasicObject(obj) {  return (obj === Object(obj) && Object.getPrototypeOf(obj) === Object.prototype);}// getPrototypeOf polyfillif (typeof Object.getPrototypeOf !== "function") {  if (typeof "".__proto__ === "object") {    Object.getPrototypeOf = function(object) {      return object.__proto__;    };  } else {    Object.getPrototypeOf = function(object) {      // May break if the constructor has been tampered with      return object.constructor.prototype;    };  }}
 |