UNPKG

leancloud-storage

Version:
711 lines (666 loc) 23.5 kB
'use strict'; var _ = require('underscore'); var cos = require('./uploader/cos'); var qiniu = require('./uploader/qiniu'); var s3 = require('./uploader/s3'); var AVError = require('./error'); var AVRequest = require('./request').request; var Promise = require('./promise'); module.exports = function (AV) { // 挂载一些配置 var avConfig = AV._config; // port from browserify path module // since react-native packager won't shim node modules. var extname = function extname(path) { return path.match(/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/)[4]; }; var b64Digit = function b64Digit(number) { if (number < 26) { return String.fromCharCode(65 + number); } if (number < 52) { return String.fromCharCode(97 + (number - 26)); } if (number < 62) { return String.fromCharCode(48 + (number - 52)); } if (number === 62) { return '+'; } if (number === 63) { return '/'; } throw new Error('Tried to encode large digit ' + number + ' in base64.'); }; var encodeBase64 = function encodeBase64(array) { var chunks = []; chunks.length = Math.ceil(array.length / 3); _.times(chunks.length, function (i) { var b1 = array[i * 3]; var b2 = array[i * 3 + 1] || 0; var b3 = array[i * 3 + 2] || 0; var has2 = i * 3 + 1 < array.length; var has3 = i * 3 + 2 < array.length; chunks[i] = [b64Digit(b1 >> 2 & 0x3F), b64Digit(b1 << 4 & 0x30 | b2 >> 4 & 0x0F), has2 ? b64Digit(b2 << 2 & 0x3C | b3 >> 6 & 0x03) : "=", has3 ? b64Digit(b3 & 0x3F) : "="].join(""); }); return chunks.join(""); }; // 取出 dataURL 中 base64 的部分 var dataURLToBase64 = function dataURLToBase64(base64) { if (base64.split(',')[0] && base64.split(',')[0].indexOf('base64') >= 0) { base64 = base64.split(',')[1]; } return base64; }; // 判断是否是国内节点 var isCnNode = function isCnNode() { return avConfig.region === 'cn'; }; // A list of file extensions to mime types as found here: // http://stackoverflow.com/questions/58510/using-net-how-can-you-find-the- // mime-type-of-a-file-based-on-the-file-signature var mimeTypes = { ai: "application/postscript", aif: "audio/x-aiff", aifc: "audio/x-aiff", aiff: "audio/x-aiff", asc: "text/plain", atom: "application/atom+xml", au: "audio/basic", avi: "video/x-msvideo", bcpio: "application/x-bcpio", bin: "application/octet-stream", bmp: "image/bmp", cdf: "application/x-netcdf", cgm: "image/cgm", "class": "application/octet-stream", cpio: "application/x-cpio", cpt: "application/mac-compactpro", csh: "application/x-csh", css: "text/css", dcr: "application/x-director", dif: "video/x-dv", dir: "application/x-director", djv: "image/vnd.djvu", djvu: "image/vnd.djvu", dll: "application/octet-stream", dmg: "application/octet-stream", dms: "application/octet-stream", doc: "application/msword", docx: "application/vnd.openxmlformats-officedocument.wordprocessingml." + "document", dotx: "application/vnd.openxmlformats-officedocument.wordprocessingml." + "template", docm: "application/vnd.ms-word.document.macroEnabled.12", dotm: "application/vnd.ms-word.template.macroEnabled.12", dtd: "application/xml-dtd", dv: "video/x-dv", dvi: "application/x-dvi", dxr: "application/x-director", eps: "application/postscript", etx: "text/x-setext", exe: "application/octet-stream", ez: "application/andrew-inset", gif: "image/gif", gram: "application/srgs", grxml: "application/srgs+xml", gtar: "application/x-gtar", hdf: "application/x-hdf", hqx: "application/mac-binhex40", htm: "text/html", html: "text/html", ice: "x-conference/x-cooltalk", ico: "image/x-icon", ics: "text/calendar", ief: "image/ief", ifb: "text/calendar", iges: "model/iges", igs: "model/iges", jnlp: "application/x-java-jnlp-file", jp2: "image/jp2", jpe: "image/jpeg", jpeg: "image/jpeg", jpg: "image/jpeg", js: "application/x-javascript", kar: "audio/midi", latex: "application/x-latex", lha: "application/octet-stream", lzh: "application/octet-stream", m3u: "audio/x-mpegurl", m4a: "audio/mp4a-latm", m4b: "audio/mp4a-latm", m4p: "audio/mp4a-latm", m4u: "video/vnd.mpegurl", m4v: "video/x-m4v", mac: "image/x-macpaint", man: "application/x-troff-man", mathml: "application/mathml+xml", me: "application/x-troff-me", mesh: "model/mesh", mid: "audio/midi", midi: "audio/midi", mif: "application/vnd.mif", mov: "video/quicktime", movie: "video/x-sgi-movie", mp2: "audio/mpeg", mp3: "audio/mpeg", mp4: "video/mp4", mpe: "video/mpeg", mpeg: "video/mpeg", mpg: "video/mpeg", mpga: "audio/mpeg", ms: "application/x-troff-ms", msh: "model/mesh", mxu: "video/vnd.mpegurl", nc: "application/x-netcdf", oda: "application/oda", ogg: "application/ogg", pbm: "image/x-portable-bitmap", pct: "image/pict", pdb: "chemical/x-pdb", pdf: "application/pdf", pgm: "image/x-portable-graymap", pgn: "application/x-chess-pgn", pic: "image/pict", pict: "image/pict", png: "image/png", pnm: "image/x-portable-anymap", pnt: "image/x-macpaint", pntg: "image/x-macpaint", ppm: "image/x-portable-pixmap", ppt: "application/vnd.ms-powerpoint", pptx: "application/vnd.openxmlformats-officedocument.presentationml." + "presentation", potx: "application/vnd.openxmlformats-officedocument.presentationml." + "template", ppsx: "application/vnd.openxmlformats-officedocument.presentationml." + "slideshow", ppam: "application/vnd.ms-powerpoint.addin.macroEnabled.12", pptm: "application/vnd.ms-powerpoint.presentation.macroEnabled.12", potm: "application/vnd.ms-powerpoint.template.macroEnabled.12", ppsm: "application/vnd.ms-powerpoint.slideshow.macroEnabled.12", ps: "application/postscript", qt: "video/quicktime", qti: "image/x-quicktime", qtif: "image/x-quicktime", ra: "audio/x-pn-realaudio", ram: "audio/x-pn-realaudio", ras: "image/x-cmu-raster", rdf: "application/rdf+xml", rgb: "image/x-rgb", rm: "application/vnd.rn-realmedia", roff: "application/x-troff", rtf: "text/rtf", rtx: "text/richtext", sgm: "text/sgml", sgml: "text/sgml", sh: "application/x-sh", shar: "application/x-shar", silo: "model/mesh", sit: "application/x-stuffit", skd: "application/x-koan", skm: "application/x-koan", skp: "application/x-koan", skt: "application/x-koan", smi: "application/smil", smil: "application/smil", snd: "audio/basic", so: "application/octet-stream", spl: "application/x-futuresplash", src: "application/x-wais-source", sv4cpio: "application/x-sv4cpio", sv4crc: "application/x-sv4crc", svg: "image/svg+xml", swf: "application/x-shockwave-flash", t: "application/x-troff", tar: "application/x-tar", tcl: "application/x-tcl", tex: "application/x-tex", texi: "application/x-texinfo", texinfo: "application/x-texinfo", tif: "image/tiff", tiff: "image/tiff", tr: "application/x-troff", tsv: "text/tab-separated-values", txt: "text/plain", ustar: "application/x-ustar", vcd: "application/x-cdlink", vrml: "model/vrml", vxml: "application/voicexml+xml", wav: "audio/x-wav", wbmp: "image/vnd.wap.wbmp", wbmxl: "application/vnd.wap.wbxml", wml: "text/vnd.wap.wml", wmlc: "application/vnd.wap.wmlc", wmls: "text/vnd.wap.wmlscript", wmlsc: "application/vnd.wap.wmlscriptc", wrl: "model/vrml", xbm: "image/x-xbitmap", xht: "application/xhtml+xml", xhtml: "application/xhtml+xml", xls: "application/vnd.ms-excel", xml: "application/xml", xpm: "image/x-xpixmap", xsl: "application/xml", xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", xltx: "application/vnd.openxmlformats-officedocument.spreadsheetml." + "template", xlsm: "application/vnd.ms-excel.sheet.macroEnabled.12", xltm: "application/vnd.ms-excel.template.macroEnabled.12", xlam: "application/vnd.ms-excel.addin.macroEnabled.12", xlsb: "application/vnd.ms-excel.sheet.binary.macroEnabled.12", xslt: "application/xslt+xml", xul: "application/vnd.mozilla.xul+xml", xwd: "image/x-xwindowdump", xyz: "chemical/x-xyz", zip: "application/zip" }; /** * An AV.File is a local representation of a file that is saved to the AV * cloud. * @param name {String} The file's name. This will change to a unique value * once the file has finished saving. * @param data {Array} The data for the file, as either: * 1. an Array of byte value Numbers, or * 2. an Object like { base64: "..." } with a base64-encoded String. * 3. a File object selected with a file upload control. (3) only works * in Firefox 3.6+, Safari 6.0.2+, Chrome 7+, and IE 10+. * 4.a Buffer object in Node.js runtime. * * For example:<pre> * var fileUploadControl = $("#profilePhotoFileUpload")[0]; * if (fileUploadControl.files.length > 0) { * var file = fileUploadControl.files[0]; * var name = "photo.jpg"; * var file = new AV.File(name, file); * file.save().then(function() { * // The file has been saved to AV. * }, function(error) { * // The file either could not be read, or could not be saved to AV. * }); * }</pre> * * @class * @param [type] {String} Content-Type header to use for the file. If * this is omitted, the content type will be inferred from the name's * extension. */ AV.File = function (name, data, type) { this.attributes = { name: name, url: '', metaData: {}, // 用来存储转换后要上传的 base64 String base64: '' }; var owner = void 0; if (data && data.owner) { owner = data.owner; } else if (!AV._config.disableCurrentUser) { try { owner = AV.User.current(); } catch (error) { if ('SYNC_API_NOT_AVAILABLE' === error.code) { console.warn('Get current user failed. It seems this runtime use an async storage system, please create AV.File in the callback of AV.User.currentAsync().'); } else { throw error; } } } this.attributes.metaData = { owner: owner ? owner.id : 'unknown' }; // Guess the content type from the extension if we need to. var extension = /\.([^.]*)$/.exec(name); if (extension) { extension = extension[1].toLowerCase(); } var guessedType = type || mimeTypes[extension] || "text/plain"; this._guessedType = guessedType; if (_.isArray(data)) { this.attributes.metaData.size = data.length; data = { base64: encodeBase64(data) }; } if (data && data.base64) { var parseBase64 = require('./browserify-wrapper/parse-base64'); var dataBase64 = parseBase64(data.base64, guessedType); this.attributes.base64 = dataURLToBase64(data.base64); this._source = Promise.resolve({ data: dataBase64, type: guessedType }); } else if (data && data.blob) { if (!data.blob.type) { data.blob.type = guessedType; } this._source = Promise.resolve({ data: data.blob, type: guessedType }); } else if (typeof File !== "undefined" && data instanceof global.File) { if (data.size) { this.attributes.metaData.size = data.size; } this._source = Promise.resolve({ data: data, type: guessedType }); } else if (typeof global.Buffer !== "undefined" && global.Buffer.isBuffer(data)) { // use global.Buffer to prevent browserify pack Buffer module this.attributes.metaData.size = data.length; this._source = Promise.resolve({ data: data, type: guessedType }); } else if (_.isString(data)) { throw new Error("Creating a AV.File from a String is not yet supported."); } }; /** * Creates a fresh AV.File object with exists url for saving to AVOS Cloud. * @param {String} name the file name * @param {String} url the file url. * @param {Object} [metaData] the file metadata object. * @param {String} [type] Content-Type header to use for the file. If * this is omitted, the content type will be inferred from the name's * extension. * @return {AV.File} the file object */ AV.File.withURL = function (name, url, metaData, type) { if (!name || !url) { throw "Please provide file name and url"; } var file = new AV.File(name, null, type); //copy metaData properties to file. if (metaData) { for (var prop in metaData) { if (!file.attributes.metaData[prop]) file.attributes.metaData[prop] = metaData[prop]; } } file.attributes.url = url; //Mark the file is from external source. file.attributes.metaData.__source = 'external'; return file; }; /** * Creates a file object with exists objectId. * @param {String} objectId The objectId string * @return {AV.File} the file object */ AV.File.createWithoutData = function (objectId) { var file = new AV.File(); file.id = objectId; return file; }; AV.File.prototype = { className: '_File', toJSON: function toJSON() { return AV._encode(this); }, /** * Returns the ACL for this file. * @returns {AV.ACL} An instance of AV.ACL. */ getACL: function getACL() { return this._acl; }, /** * Sets the ACL to be used for this file. * @param {AV.ACL} acl An instance of AV.ACL. */ setACL: function setACL(acl) { if (!(acl instanceof AV.ACL)) { return new AVError(AVError.OTHER_CAUSE, 'ACL must be a AV.ACL.'); } this._acl = acl; }, /** * Gets the name of the file. Before save is called, this is the filename * given by the user. After save is called, that name gets prefixed with a * unique identifier. */ name: function name() { return this.get('name'); }, /** * Gets the url of the file. It is only available after you save the file or * after you get the file from a AV.Object. * @return {String} */ url: function url() { return this.get('url'); }, /** * Gets the attributs of the file object. * @param {String} The attribute name which want to get. * @returns {Any} */ get: function get(attrName) { switch (attrName) { case 'objectId': return this.id; case 'url': case 'name': case 'metaData': case 'createdAt': case 'updatedAt': return this.attributes[attrName]; default: return this.attributes.metaData[attrName]; } }, /** * Set the metaData of the file object. * @param {Object} Object is an key value Object for setting metaData. * @param {String} attr is an optional metadata key. * @param {Object} value is an optional metadata value. * @returns {String|Number|Array|Object} */ set: function set() { var _this = this; var set = function set(attrName, value) { switch (attrName) { case 'name': case 'url': case 'base64': case 'metaData': _this.attributes[attrName] = value; break; default: // File 并非一个 AVObject,不能完全自定义其他属性,所以只能都放在 metaData 上面 _this.attributes.metaData[attrName] = value; break; } }; for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } switch (args.length) { case 1: // 传入一个 Object for (var k in args[0]) { set(k, args[0][k]); } break; case 2: set(args[0], args[1]); break; } }, /** * <p>Returns the file's metadata JSON object if no arguments is given.Returns the * metadata value if a key is given.Set metadata value if key and value are both given.</p> * <p><pre> * var metadata = file.metaData(); //Get metadata JSON object. * var size = file.metaData('size'); // Get the size metadata value. * file.metaData('format', 'jpeg'); //set metadata attribute and value. *</pre></p> * @return {Object} The file's metadata JSON object. * @param {String} attr an optional metadata key. * @param {Object} value an optional metadata value. **/ metaData: function metaData(attr, value) { if (attr && value) { this.attributes.metaData[attr] = value; return this; } else if (attr && !value) { return this.attributes.metaData[attr]; } else { return this.attributes.metaData; } }, /** * 如果文件是图片,获取图片的缩略图URL。可以传入宽度、高度、质量、格式等参数。 * @return {String} 缩略图URL * @param {Number} width 宽度,单位:像素 * @param {Number} heigth 高度,单位:像素 * @param {Number} quality 质量,1-100的数字,默认100 * @param {Number} scaleToFit 是否将图片自适应大小。默认为true。 * @param {String} fmt 格式,默认为png,也可以为jpeg,gif等格式。 */ thumbnailURL: function thumbnailURL(width, height, quality, scaleToFit, fmt) { var url = this.attributes.url; if (!url) { throw new Error('Invalid url.'); } if (!width || !height || width <= 0 || height <= 0) { throw new Error('Invalid width or height value.'); } quality = quality || 100; scaleToFit = !scaleToFit ? true : scaleToFit; if (quality <= 0 || quality > 100) { throw new Error('Invalid quality value.'); } fmt = fmt || 'png'; var mode = scaleToFit ? 2 : 1; return url + '?imageView/' + mode + '/w/' + width + '/h/' + height + '/q/' + quality + '/format/' + fmt; }, /** * Returns the file's size. * @return {Number} The file's size in bytes. **/ size: function size() { return this.metaData().size; }, /** * Returns the file's owner. * @return {String} The file's owner id. */ ownerId: function ownerId() { return this.metaData().owner; }, /** * Destroy the file. * @param {AuthOptions} options * @return {Promise} A promise that is fulfilled when the destroy * completes. */ destroy: function destroy(options) { if (!this.id) { return Promise.reject(new Error('The file id is not eixsts.')); } var request = AVRequest("files", null, this.id, 'DELETE', null, options); return request; }, /** * Request Qiniu upload token * @param {string} type * @return {Promise} Resolved with the response * @private */ _fileToken: function _fileToken(type) { var route = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'fileTokens'; var name = this.attributes.name; // Create 16-bits uuid as qiniu key. var extName = extname(name); var hexOctet = function hexOctet() { return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1); }; var key = hexOctet() + hexOctet() + hexOctet() + hexOctet() + hexOctet() + extName; var data = { key: key, name: name, ACL: this._acl, mime_type: type, metaData: this.attributes.metaData }; if (type && !this.attributes.metaData.mime_type) { this.attributes.metaData.mime_type = type; } this._qiniu_key = key; return AVRequest(route, null, null, 'POST', data); }, /** * @callback UploadProgressCallback * @param {XMLHttpRequestProgressEvent} event - The progress event with 'loaded' and 'total' attributes */ /** * Saves the file to the AV cloud. * @param {Object} [options] * @param {UploadProgressCallback} [options.onProgress] * @return {Promise} Promise that is resolved when the save finishes. */ save: function save(options) { var _this2 = this; if (this.id) { throw new Error('File already saved. If you want to manipulate a file, use AV.Query to get it.'); } if (!this._previousSave) { if (this._source) { this._previousSave = this._source.then(function (_ref) { var data = _ref.data, type = _ref.type; return _this2._fileToken(type).then(function (uploadInfo) { var uploadPromise = void 0; switch (uploadInfo.provider) { case 's3': uploadPromise = s3(uploadInfo, data, _this2, options); break; case 'qcloud': uploadPromise = cos(uploadInfo, data, _this2, options); break; case 'qiniu': default: uploadPromise = qiniu(uploadInfo, data, _this2, options); break; } return uploadPromise.catch(function (err) { // destroy this file object when upload fails. _this2.destroy(); throw err; }); }); }); } else if (this.attributes.url && this.attributes.metaData.__source === 'external') { // external link file. var data = { name: this.attributes.name, ACL: this._acl, metaData: this.attributes.metaData, mime_type: this._guessedType, url: this.attributes.url }; this._previousSave = AVRequest('files', this.attributes.name, null, 'post', data).then(function (response) { _this2.attributes.name = response.name; _this2.attributes.url = response.url; _this2.id = response.objectId; if (response.size) { _this2.attributes.metaData.size = response.size; } return _this2; }); } } return this._previousSave; }, /** * fetch the file from server. If the server's representation of the * model differs from its current attributes, they will be overriden, * @param {AuthOptions} options AuthOptions plus 'keys' and 'include' option. * @return {Promise} A promise that is fulfilled when the fetch * completes. */ fetch: function fetch(options) { var _this3 = this; var options = null; var request = AVRequest('files', null, this.id, 'GET', options); return request.then(function (response) { var value = AV.Object.prototype.parse(response); value.attributes = { name: value.name, url: value.url }; value.attributes.metaData = value.metaData || {}; // clean delete value.objectId; delete value.metaData; delete value.url; delete value.name; _.extend(_this3, value); return _this3; }); } }; };