| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442 |
- const fs = require('fs');
- const path = require('path');
- const Readable = require('stream').Readable;
- const getCrc32 = require('crc32');
- const mime = require('mime');
- const FormStream = require('formstream');
- const conf = require('../conf');
- const util = require('../util');
- const rpc = require('../rpc');
- const { SERVICE_NAME } = require('../httpc/region');
- const { ResponseWrapper } = require('../httpc/responseWrapper');
- const { EndpointsRetryPolicy } = require('../httpc/endpointsRetryPolicy');
- const { RegionsRetryPolicy } = require('../httpc/regionsRetryPolicy');
- const { Retrier } = require('../retry');
- const {
- AccUnavailableRetryPolicy,
- handleReqCallback
- } = require('./internal');
- exports.FormUploader = FormUploader;
- exports.PutExtra = PutExtra;
- /**
- * @class
- * @param {conf.Config} [config]
- * @constructor
- */
- function FormUploader (config) {
- this.config = config || new conf.Config();
- }
- /**
- * @private
- * @param {Object} options
- * @param {string} options.accessKey
- * @param {string} options.bucketName
- * @param {boolean} [options.retryable]
- * @returns {Promise<Retrier>}
- */
- function _getRegionsRetrier (options) {
- const {
- bucketName,
- accessKey,
- retryable = true
- } = options;
- return this.config.getRegionsProvider({
- bucketName,
- accessKey
- })
- .then(regionsProvider => {
- let retryPolicies;
- if (this.config.accelerateUploading) {
- retryPolicies = [
- new AccUnavailableRetryPolicy(),
- new EndpointsRetryPolicy({
- skipInitContext: true
- }),
- new RegionsRetryPolicy({
- regionsProvider,
- serviceNames: [SERVICE_NAME.UP_ACC, SERVICE_NAME.UP]
- })
- ];
- } else {
- retryPolicies = [
- new EndpointsRetryPolicy({
- skipInitContext: true
- }),
- new RegionsRetryPolicy({
- regionsProvider,
- serviceNames: [SERVICE_NAME.UP]
- })
- ];
- }
- return new Retrier({
- retryPolicies,
- onBeforeRetry: (context, policy) => {
- if (context.error) {
- return retryable;
- }
- if (policy instanceof AccUnavailableRetryPolicy) {
- return true;
- }
- return retryable && context.result && context.result.needRetry();
- }
- });
- });
- };
- /**
- * 上传可选参数
- * @class
- * @constructor
- * @param {string} [fname] 请求体中的文件的名称
- * @param {Object} [params] 额外参数设置,参数名称必须以x:开头
- * @param {string} [mimeType] 指定文件的mimeType
- * @param {string} [crc32] 指定文件的crc32值
- * @param {number | boolean} [checkCrc] 指定是否检测文件的crc32值
- * @param {Object} [metadata] 元数据设置,参数名称必须以 x-qn-meta-${name}: 开头
- */
- function PutExtra (
- fname,
- params,
- mimeType,
- crc32,
- checkCrc,
- metadata
- ) {
- this.fname = fname || '';
- this.params = params || {};
- this.mimeType = mimeType || null;
- this.crc32 = crc32 || null;
- this.checkCrc = checkCrc || true;
- this.metadata = metadata || {};
- }
- /**
- * @callback reqCallback
- *
- * @param { Error } err
- * @param { Object } ret
- * @param { http.IncomingMessage } info
- */
- /**
- * @param {string} uploadToken
- * @param {string | null} key
- * @param {stream.Readable} fsStream
- * @param {PutExtra | null} putExtra
- * @param {reqCallback} callbackFunc
- * @returns {Promise<ResponseWrapper>}
- */
- FormUploader.prototype.putStream = function (
- uploadToken,
- key,
- fsStream,
- putExtra,
- callbackFunc
- ) {
- const preferredScheme = this.config.useHttpsDomain ? 'https' : 'http';
- // PutExtra
- putExtra = getDefaultPutExtra(
- putExtra,
- {
- key
- }
- );
- // Why need retrier even if retryable is false?
- // Because the retrier is used to get the endpoints,
- // which will be initialed by region policy.
- const result = _getRegionsRetrier.call(this, {
- bucketName: util.getBucketFromUptoken(uploadToken),
- accessKey: util.getAKFromUptoken(uploadToken),
- retryable: false
- })
- .then(retrier => Promise.all([
- retrier,
- retrier.initContext()
- ]))
- .then(([retrier, context]) => retrier.retry({
- func: context => putReq(
- context.endpoint.getValue({ scheme: preferredScheme }),
- uploadToken,
- key,
- fsStream,
- putExtra
- ),
- context
- }));
- handleReqCallback(result, callbackFunc);
- return result;
- };
- /**
- * @param {string} upDomain
- * @param {string} uploadToken
- * @param {string} key
- * @param {Readable} fsStream
- * @param {PutExtra} putExtra
- * @returns {Promise<any>}
- */
- function putReq (
- upDomain,
- uploadToken,
- key,
- fsStream,
- putExtra
- ) {
- const postForm = createMultipartForm(
- uploadToken,
- key,
- fsStream,
- putExtra
- );
- return new Promise((resolve, reject) => {
- rpc.postMultipart(
- upDomain,
- postForm,
- function (err, data, resp) {
- if (err) {
- err.resp = resp;
- reject(err);
- return;
- }
- resolve(new ResponseWrapper({
- data,
- resp
- }));
- }
- );
- });
- }
- /**
- * 上传字节
- * @param {string} uploadToken
- * @param {string | null} key
- * @param {any} body
- * @param {PutExtra | null} putExtra
- * @param {reqCallback} [callbackFunc]
- * @returns {Promise<UploadResult>}
- */
- FormUploader.prototype.put = function (
- uploadToken,
- key,
- body,
- putExtra,
- callbackFunc
- ) {
- const preferredScheme = this.config.useHttpsDomain ? 'https' : 'http';
- // initial PutExtra
- putExtra = getDefaultPutExtra(
- putExtra,
- {
- key
- }
- );
- // initial retrier and try upload
- const result = _getRegionsRetrier.call(this, {
- bucketName: util.getBucketFromUptoken(uploadToken),
- accessKey: util.getAKFromUptoken(uploadToken)
- })
- .then(retrier => Promise.all([
- retrier,
- retrier.initContext()
- ]))
- .then(([retrier, context]) => retrier.retry({
- func: context => {
- const fsStream = new Readable();
- fsStream.push(body);
- fsStream.push(null);
- return putReq(
- context.endpoint.getValue({ scheme: preferredScheme }),
- uploadToken,
- key,
- fsStream,
- putExtra
- );
- },
- context
- }));
- handleReqCallback(result, callbackFunc);
- return result;
- };
- /**
- * @param {string} uploadToken
- * @param {any} body
- * @param {PutExtra | null} putExtra
- * @param {reqCallback} callbackFunc
- * @returns {Promise<UploadResult>}
- */
- FormUploader.prototype.putWithoutKey = function (
- uploadToken,
- body,
- putExtra,
- callbackFunc
- ) {
- return this.put(uploadToken, null, body, putExtra, callbackFunc);
- };
- /**
- * @param {string} uploadToken
- * @param {string | null} key
- * @param {stream.Readable} fsStream
- * @param {PutExtra | null} putExtra
- * @returns {FormStream}
- */
- function createMultipartForm (uploadToken, key, fsStream, putExtra) {
- const postForm = new FormStream();
- postForm.field('token', uploadToken);
- if (key != null) {
- postForm.field('key', key);
- }
- // fix the bug of formstream
- // https://html.spec.whatwg.org/#multipart-form-data
- const escapeFname = putExtra.fname.replace(/"/g, '%22')
- .replace(/\r/g, '%0D')
- .replace(/\n/g, '%0A');
- postForm.stream(
- 'file',
- fsStream,
- escapeFname,
- putExtra.mimeType
- );
- // putExtra params
- for (const k in putExtra.params) {
- if (k.startsWith('x:')) {
- postForm.field(k, putExtra.params[k].toString());
- }
- }
- // putExtra metadata
- for (const metadataKey in putExtra.metadata) {
- if (metadataKey.startsWith('x-qn-meta-')) {
- postForm.field(metadataKey, putExtra.metadata[metadataKey].toString());
- }
- }
- let fileBody = [];
- fsStream.on('data', function (data) {
- fileBody.push(data);
- });
- fsStream.on('end', function () {
- if (putExtra.checkCrc) {
- if (putExtra.crc32 == null) {
- fileBody = Buffer.concat(fileBody);
- const bodyCrc32 = parseInt('0x' + getCrc32(fileBody));
- postForm.field('crc32', bodyCrc32.toString());
- } else {
- postForm.field('crc32', putExtra.crc32);
- }
- }
- });
- return postForm;
- }
- /** 上传本地文件
- * @param {string} uploadToken 上传凭证
- * @param {string | null} key 目标文件名
- * @param {string} localFile 本地文件路径
- * @param {PutExtra | null} putExtra 额外选项
- * @param {reqCallback} [callbackFunc] 回调函数
- * @returns {Promise<UploadResult>}
- */
- FormUploader.prototype.putFile = function (
- uploadToken,
- key,
- localFile,
- putExtra,
- callbackFunc
- ) {
- const preferredScheme = this.config.useHttpsDomain ? 'https' : 'http';
- // initial PutExtra
- putExtra = putExtra || new PutExtra();
- if (!putExtra.mimeType) {
- putExtra.mimeType = mime.getType(localFile);
- }
- if (!putExtra.fname) {
- putExtra.fname = path.basename(localFile);
- }
- putExtra = getDefaultPutExtra(
- putExtra,
- {
- key
- }
- );
- // initial retrier and try upload
- const result = _getRegionsRetrier.call(this, {
- bucketName: util.getBucketFromUptoken(uploadToken),
- accessKey: util.getAKFromUptoken(uploadToken)
- })
- .then(retrier => Promise.all([
- retrier,
- retrier.initContext()
- ]))
- .then(([retrier, context]) => retrier.retry({
- func: context => {
- const fsStream = fs.createReadStream(localFile);
- return putReq(
- context.endpoint.getValue({ scheme: preferredScheme }),
- uploadToken,
- key,
- fsStream,
- putExtra
- );
- },
- context
- }));
- handleReqCallback(result, callbackFunc);
- return result;
- };
- /** 上传本地文件
- * @param {string} uploadToken 上传凭证
- * @param {string} localFile 本地文件路径
- * @param {PutExtra | null} putExtra 额外选项
- * @param callbackFunc 回调函数
- * @returns {Promise<UploadResult>}
- */
- FormUploader.prototype.putFileWithoutKey = function (
- uploadToken,
- localFile,
- putExtra,
- callbackFunc
- ) {
- return this.putFile(uploadToken, null, localFile, putExtra, callbackFunc);
- };
- /**
- * @param {PutExtra} putExtra
- * @param {Object} options
- * @param {string} options.key
- * @returns {PutExtra}
- */
- function getDefaultPutExtra (putExtra, options) {
- putExtra = putExtra || new PutExtra();
- if (!putExtra.mimeType) {
- putExtra.mimeType = 'application/octet-stream';
- }
- if (!putExtra.fname) {
- putExtra.fname = options.key || 'fname';
- }
- return putExtra;
- }
|