data-man-api.js 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. /**
  2. * @method DataMan
  3. * @public
  4. * @constructor
  5. * @param {File|Blob|ArrayBuffer|Uint8Array|String} data The data that you want to manipulate.
  6. * @param {String} [type] The data content (MIME) type, if known. Required if the first argument is an ArrayBuffer, Uint8Array, or URL
  7. */
  8. DataMan = function DataMan(data, type) {
  9. var self = this;
  10. if (!data) {
  11. throw new Error("DataMan constructor requires a data argument");
  12. }
  13. // The end result of all this is that we will have one of the following set:
  14. // - self.blob
  15. // - self.url
  16. // Unless we already have in-memory data, we don't load anything into memory
  17. // and instead rely on obtaining a read stream when the time comes.
  18. if (typeof File !== "undefined" && data instanceof File) {
  19. self.blob = data; // File inherits from Blob so this is OK
  20. self._type = data.type;
  21. } else if (typeof Blob !== "undefined" && data instanceof Blob) {
  22. self.blob = data;
  23. self._type = data.type;
  24. } else if (typeof ArrayBuffer !== "undefined" && data instanceof ArrayBuffer || EJSON.isBinary(data)) {
  25. if (typeof Blob === "undefined") {
  26. throw new Error("Browser must support Blobs to handle an ArrayBuffer or Uint8Array");
  27. }
  28. if (!type) {
  29. throw new Error("DataMan constructor requires a type argument when passed an ArrayBuffer or Uint8Array");
  30. }
  31. self.blob = new Blob([data], {type: type});
  32. self._type = type;
  33. } else if (typeof data === "string") {
  34. if (data.slice(0, 5) === "data:") {
  35. self._type = data.slice(5, data.indexOf(';'));
  36. self.blob = dataURItoBlob(data, self._type);
  37. } else if (data.slice(0, 5) === "http:" || data.slice(0, 6) === "https:") {
  38. if (!type) {
  39. throw new Error("DataMan constructor requires a type argument when passed a URL");
  40. }
  41. self.url = data;
  42. self._type = type;
  43. } else {
  44. throw new Error("DataMan constructor received unrecognized data string");
  45. }
  46. } else {
  47. throw new Error("DataMan constructor received data that it doesn't support");
  48. }
  49. };
  50. /**
  51. * @method DataMan.prototype.getBlob
  52. * @public
  53. * @param {Function} [callback] - callback(error, blob)
  54. * @returns {undefined|Blob}
  55. *
  56. * Passes a Blob representing this data to a callback or returns
  57. * the Blob if no callback is provided. A callback is required
  58. * if getting a Blob for a URL.
  59. */
  60. DataMan.prototype.getBlob = function dataManGetBlob(callback) {
  61. var self = this;
  62. if (callback) {
  63. if (self.blob) {
  64. callback(null, self.blob);
  65. } else if (self.url) {
  66. var xhr = new XMLHttpRequest();
  67. xhr.open('GET', self.url, true);
  68. xhr.responseType = "blob";
  69. xhr.onload = function(data) {
  70. self.blob = xhr.response;
  71. callback(null, self.blob);
  72. };
  73. xhr.onerror = function(err) {
  74. callback(err);
  75. };
  76. xhr.send();
  77. }
  78. } else {
  79. if (self.url)
  80. throw new Error('DataMan.getBlob requires a callback when managing a URL');
  81. return self.blob;
  82. }
  83. };
  84. /**
  85. * @method DataMan.prototype.getBinary
  86. * @public
  87. * @param {Number} [start] - First byte position to read.
  88. * @param {Number} [end] - Last byte position to read.
  89. * @param {Function} callback - callback(error, binaryData)
  90. * @returns {undefined}
  91. *
  92. * Passes a Uint8Array representing this data to a callback.
  93. */
  94. DataMan.prototype.getBinary = function dataManGetBinary(start, end, callback) {
  95. var self = this;
  96. if (typeof start === "function") {
  97. callback = start;
  98. }
  99. callback = callback || defaultCallback;
  100. function read(blob) {
  101. if (typeof FileReader === "undefined") {
  102. callback(new Error("Browser does not support FileReader"));
  103. return;
  104. }
  105. var reader = new FileReader();
  106. reader.onload = function(evt) {
  107. callback(null, new Uint8Array(evt.target.result));
  108. };
  109. reader.onerror = function(err) {
  110. callback(err);
  111. };
  112. reader.readAsArrayBuffer(blob);
  113. }
  114. self.getBlob(function (error, blob) {
  115. if (error) {
  116. callback(error);
  117. } else {
  118. if (typeof start === "number" && typeof end === "number") {
  119. var size = blob.size;
  120. // Return the requested chunk of binary data
  121. if (start >= size) {
  122. callback(new Error("DataMan.getBinary: start position beyond end of data (" + size + ")"));
  123. return;
  124. }
  125. end = Math.min(size, end);
  126. var slice = blob.slice || blob.webkitSlice || blob.mozSlice;
  127. if (typeof slice === 'undefined') {
  128. callback(new Error('Browser does not support File.slice'));
  129. return;
  130. }
  131. read(slice.call(blob, start, end, self._type));
  132. } else {
  133. // Return the entire binary data
  134. read(blob);
  135. }
  136. }
  137. });
  138. };
  139. /** @method DataMan.prototype.saveAs
  140. * @public
  141. * @param {String} [filename]
  142. * @return {undefined}
  143. *
  144. * Tells the browser to save the data like a normal downloaded file,
  145. * using the provided filename.
  146. *
  147. */
  148. DataMan.prototype.saveAs = function dataManSaveAs(filename) {
  149. var self = this;
  150. if (typeof window === "undefined")
  151. throw new Error("window must be defined to use saveLocal");
  152. if (!window.saveAs) {
  153. console.warn('DataMan.saveAs: window.saveAs not supported by this browser - add cfs-filesaver package');
  154. return;
  155. }
  156. self.getBlob(function (error, blob) {
  157. if (error) {
  158. throw error;
  159. } else {
  160. window.saveAs(blob, filename);
  161. }
  162. });
  163. };
  164. /**
  165. * @method DataMan.prototype.getDataUri
  166. * @public
  167. * @param {function} callback callback(err, dataUri)
  168. */
  169. DataMan.prototype.getDataUri = function dataManGetDataUri(callback) {
  170. // XXX: We could consider using: URL.createObjectURL(blob);
  171. // This will create a reference to the blob data instead of a clone
  172. // This is part of the File API - as the rest - Not sure how to generally
  173. // support from IE10, FF26, Chrome 31, safari 7, opera 19, ios 6, android 4
  174. var self = this;
  175. if (typeof callback !== 'function')
  176. throw new Error("getDataUri requires callback function");
  177. if (typeof FileReader === "undefined") {
  178. callback(new Error("Browser does not support FileReader"));
  179. return;
  180. }
  181. var fileReader = new FileReader();
  182. fileReader.onload = function(event) {
  183. var dataUri = event.target.result;
  184. callback(null, dataUri);
  185. };
  186. fileReader.onerror = function(err) {
  187. callback(err);
  188. };
  189. self.getBlob(function (error, blob) {
  190. if (error) {
  191. callback(error);
  192. } else {
  193. fileReader.readAsDataURL(blob);
  194. }
  195. });
  196. };
  197. /**
  198. * @method DataMan.prototype.size
  199. * @public
  200. * @param {function} [callback] callback(err, size)
  201. *
  202. * Passes the size of the data to the callback, if provided,
  203. * or returns it. A callback is required to get the size of a URL on the client.
  204. */
  205. DataMan.prototype.size = function dataManSize(callback) {
  206. var self = this;
  207. if (callback) {
  208. if (typeof self._size === "number") {
  209. callback(null, self._size);
  210. } else {
  211. self.getBlob(function (error, blob) {
  212. if (error) {
  213. callback(error);
  214. } else {
  215. self._size = blob.size;
  216. callback(null, self._size);
  217. }
  218. });
  219. }
  220. } else {
  221. if (self.url) {
  222. throw new Error("On the client, DataMan.size requires a callback when getting size for a URL on the client");
  223. } else if (typeof self._size === "number") {
  224. return self._size;
  225. } else {
  226. var blob = self.getBlob();
  227. self._size = blob.size;
  228. return self._size;
  229. }
  230. }
  231. };
  232. /**
  233. * @method DataMan.prototype.type
  234. * @public
  235. *
  236. * Returns the type of the data.
  237. */
  238. DataMan.prototype.type = function dataManType() {
  239. return this._type;
  240. };
  241. /**
  242. * @method dataURItoBlob
  243. * @private
  244. * @param {String} dataURI The data URI
  245. * @param {String} dataTYPE The content type
  246. * @returns {Blob} A new Blob instance
  247. *
  248. * Converts a data URI to a Blob.
  249. */
  250. function dataURItoBlob(dataURI, dataTYPE) {
  251. var str = atob(dataURI.split(',')[1]), array = [];
  252. for(var i = 0; i < str.length; i++) array.push(str.charCodeAt(i));
  253. return new Blob([new Uint8Array(array)], {type: dataTYPE});
  254. }
  255. /**
  256. * @method defaultCallback
  257. * @private
  258. * @param {Error} [err]
  259. * @returns {undefined}
  260. *
  261. * Can be used as a default callback for client methods that need a callback.
  262. * Simply throws the provided error if there is one.
  263. */
  264. function defaultCallback(err) {
  265. if (err) {
  266. // Show gentle error if Meteor error
  267. if (err instanceof Meteor.Error) {
  268. console.error(err.message);
  269. } else {
  270. // Normal error, just throw error
  271. throw err;
  272. }
  273. }
  274. }