form.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. const fs = require('fs');
  2. const path = require('path');
  3. const Readable = require('stream').Readable;
  4. const getCrc32 = require('crc32');
  5. const mime = require('mime');
  6. const FormStream = require('formstream');
  7. const conf = require('../conf');
  8. const util = require('../util');
  9. const rpc = require('../rpc');
  10. const { SERVICE_NAME } = require('../httpc/region');
  11. const { ResponseWrapper } = require('../httpc/responseWrapper');
  12. const { EndpointsRetryPolicy } = require('../httpc/endpointsRetryPolicy');
  13. const { RegionsRetryPolicy } = require('../httpc/regionsRetryPolicy');
  14. const { Retrier } = require('../retry');
  15. const {
  16. AccUnavailableRetryPolicy,
  17. handleReqCallback
  18. } = require('./internal');
  19. exports.FormUploader = FormUploader;
  20. exports.PutExtra = PutExtra;
  21. /**
  22. * @class
  23. * @param {conf.Config} [config]
  24. * @constructor
  25. */
  26. function FormUploader (config) {
  27. this.config = config || new conf.Config();
  28. }
  29. /**
  30. * @private
  31. * @param {Object} options
  32. * @param {string} options.accessKey
  33. * @param {string} options.bucketName
  34. * @param {boolean} [options.retryable]
  35. * @returns {Promise<Retrier>}
  36. */
  37. function _getRegionsRetrier (options) {
  38. const {
  39. bucketName,
  40. accessKey,
  41. retryable = true
  42. } = options;
  43. return this.config.getRegionsProvider({
  44. bucketName,
  45. accessKey
  46. })
  47. .then(regionsProvider => {
  48. let retryPolicies;
  49. if (this.config.accelerateUploading) {
  50. retryPolicies = [
  51. new AccUnavailableRetryPolicy(),
  52. new EndpointsRetryPolicy({
  53. skipInitContext: true
  54. }),
  55. new RegionsRetryPolicy({
  56. regionsProvider,
  57. serviceNames: [SERVICE_NAME.UP_ACC, SERVICE_NAME.UP]
  58. })
  59. ];
  60. } else {
  61. retryPolicies = [
  62. new EndpointsRetryPolicy({
  63. skipInitContext: true
  64. }),
  65. new RegionsRetryPolicy({
  66. regionsProvider,
  67. serviceNames: [SERVICE_NAME.UP]
  68. })
  69. ];
  70. }
  71. return new Retrier({
  72. retryPolicies,
  73. onBeforeRetry: (context, policy) => {
  74. if (context.error) {
  75. return retryable;
  76. }
  77. if (policy instanceof AccUnavailableRetryPolicy) {
  78. return true;
  79. }
  80. return retryable && context.result && context.result.needRetry();
  81. }
  82. });
  83. });
  84. };
  85. /**
  86. * 上传可选参数
  87. * @class
  88. * @constructor
  89. * @param {string} [fname] 请求体中的文件的名称
  90. * @param {Object} [params] 额外参数设置,参数名称必须以x:开头
  91. * @param {string} [mimeType] 指定文件的mimeType
  92. * @param {string} [crc32] 指定文件的crc32值
  93. * @param {number | boolean} [checkCrc] 指定是否检测文件的crc32值
  94. * @param {Object} [metadata] 元数据设置,参数名称必须以 x-qn-meta-${name}: 开头
  95. */
  96. function PutExtra (
  97. fname,
  98. params,
  99. mimeType,
  100. crc32,
  101. checkCrc,
  102. metadata
  103. ) {
  104. this.fname = fname || '';
  105. this.params = params || {};
  106. this.mimeType = mimeType || null;
  107. this.crc32 = crc32 || null;
  108. this.checkCrc = checkCrc || true;
  109. this.metadata = metadata || {};
  110. }
  111. /**
  112. * @callback reqCallback
  113. *
  114. * @param { Error } err
  115. * @param { Object } ret
  116. * @param { http.IncomingMessage } info
  117. */
  118. /**
  119. * @param {string} uploadToken
  120. * @param {string | null} key
  121. * @param {stream.Readable} fsStream
  122. * @param {PutExtra | null} putExtra
  123. * @param {reqCallback} callbackFunc
  124. * @returns {Promise<ResponseWrapper>}
  125. */
  126. FormUploader.prototype.putStream = function (
  127. uploadToken,
  128. key,
  129. fsStream,
  130. putExtra,
  131. callbackFunc
  132. ) {
  133. const preferredScheme = this.config.useHttpsDomain ? 'https' : 'http';
  134. // PutExtra
  135. putExtra = getDefaultPutExtra(
  136. putExtra,
  137. {
  138. key
  139. }
  140. );
  141. // Why need retrier even if retryable is false?
  142. // Because the retrier is used to get the endpoints,
  143. // which will be initialed by region policy.
  144. const result = _getRegionsRetrier.call(this, {
  145. bucketName: util.getBucketFromUptoken(uploadToken),
  146. accessKey: util.getAKFromUptoken(uploadToken),
  147. retryable: false
  148. })
  149. .then(retrier => Promise.all([
  150. retrier,
  151. retrier.initContext()
  152. ]))
  153. .then(([retrier, context]) => retrier.retry({
  154. func: context => putReq(
  155. context.endpoint.getValue({ scheme: preferredScheme }),
  156. uploadToken,
  157. key,
  158. fsStream,
  159. putExtra
  160. ),
  161. context
  162. }));
  163. handleReqCallback(result, callbackFunc);
  164. return result;
  165. };
  166. /**
  167. * @param {string} upDomain
  168. * @param {string} uploadToken
  169. * @param {string} key
  170. * @param {Readable} fsStream
  171. * @param {PutExtra} putExtra
  172. * @returns {Promise<any>}
  173. */
  174. function putReq (
  175. upDomain,
  176. uploadToken,
  177. key,
  178. fsStream,
  179. putExtra
  180. ) {
  181. const postForm = createMultipartForm(
  182. uploadToken,
  183. key,
  184. fsStream,
  185. putExtra
  186. );
  187. return new Promise((resolve, reject) => {
  188. rpc.postMultipart(
  189. upDomain,
  190. postForm,
  191. function (err, data, resp) {
  192. if (err) {
  193. err.resp = resp;
  194. reject(err);
  195. return;
  196. }
  197. resolve(new ResponseWrapper({
  198. data,
  199. resp
  200. }));
  201. }
  202. );
  203. });
  204. }
  205. /**
  206. * 上传字节
  207. * @param {string} uploadToken
  208. * @param {string | null} key
  209. * @param {any} body
  210. * @param {PutExtra | null} putExtra
  211. * @param {reqCallback} [callbackFunc]
  212. * @returns {Promise<UploadResult>}
  213. */
  214. FormUploader.prototype.put = function (
  215. uploadToken,
  216. key,
  217. body,
  218. putExtra,
  219. callbackFunc
  220. ) {
  221. const preferredScheme = this.config.useHttpsDomain ? 'https' : 'http';
  222. // initial PutExtra
  223. putExtra = getDefaultPutExtra(
  224. putExtra,
  225. {
  226. key
  227. }
  228. );
  229. // initial retrier and try upload
  230. const result = _getRegionsRetrier.call(this, {
  231. bucketName: util.getBucketFromUptoken(uploadToken),
  232. accessKey: util.getAKFromUptoken(uploadToken)
  233. })
  234. .then(retrier => Promise.all([
  235. retrier,
  236. retrier.initContext()
  237. ]))
  238. .then(([retrier, context]) => retrier.retry({
  239. func: context => {
  240. const fsStream = new Readable();
  241. fsStream.push(body);
  242. fsStream.push(null);
  243. return putReq(
  244. context.endpoint.getValue({ scheme: preferredScheme }),
  245. uploadToken,
  246. key,
  247. fsStream,
  248. putExtra
  249. );
  250. },
  251. context
  252. }));
  253. handleReqCallback(result, callbackFunc);
  254. return result;
  255. };
  256. /**
  257. * @param {string} uploadToken
  258. * @param {any} body
  259. * @param {PutExtra | null} putExtra
  260. * @param {reqCallback} callbackFunc
  261. * @returns {Promise<UploadResult>}
  262. */
  263. FormUploader.prototype.putWithoutKey = function (
  264. uploadToken,
  265. body,
  266. putExtra,
  267. callbackFunc
  268. ) {
  269. return this.put(uploadToken, null, body, putExtra, callbackFunc);
  270. };
  271. /**
  272. * @param {string} uploadToken
  273. * @param {string | null} key
  274. * @param {stream.Readable} fsStream
  275. * @param {PutExtra | null} putExtra
  276. * @returns {FormStream}
  277. */
  278. function createMultipartForm (uploadToken, key, fsStream, putExtra) {
  279. const postForm = new FormStream();
  280. postForm.field('token', uploadToken);
  281. if (key != null) {
  282. postForm.field('key', key);
  283. }
  284. // fix the bug of formstream
  285. // https://html.spec.whatwg.org/#multipart-form-data
  286. const escapeFname = putExtra.fname.replace(/"/g, '%22')
  287. .replace(/\r/g, '%0D')
  288. .replace(/\n/g, '%0A');
  289. postForm.stream(
  290. 'file',
  291. fsStream,
  292. escapeFname,
  293. putExtra.mimeType
  294. );
  295. // putExtra params
  296. for (const k in putExtra.params) {
  297. if (k.startsWith('x:')) {
  298. postForm.field(k, putExtra.params[k].toString());
  299. }
  300. }
  301. // putExtra metadata
  302. for (const metadataKey in putExtra.metadata) {
  303. if (metadataKey.startsWith('x-qn-meta-')) {
  304. postForm.field(metadataKey, putExtra.metadata[metadataKey].toString());
  305. }
  306. }
  307. let fileBody = [];
  308. fsStream.on('data', function (data) {
  309. fileBody.push(data);
  310. });
  311. fsStream.on('end', function () {
  312. if (putExtra.checkCrc) {
  313. if (putExtra.crc32 == null) {
  314. fileBody = Buffer.concat(fileBody);
  315. const bodyCrc32 = parseInt('0x' + getCrc32(fileBody));
  316. postForm.field('crc32', bodyCrc32.toString());
  317. } else {
  318. postForm.field('crc32', putExtra.crc32);
  319. }
  320. }
  321. });
  322. return postForm;
  323. }
  324. /** 上传本地文件
  325. * @param {string} uploadToken 上传凭证
  326. * @param {string | null} key 目标文件名
  327. * @param {string} localFile 本地文件路径
  328. * @param {PutExtra | null} putExtra 额外选项
  329. * @param {reqCallback} [callbackFunc] 回调函数
  330. * @returns {Promise<UploadResult>}
  331. */
  332. FormUploader.prototype.putFile = function (
  333. uploadToken,
  334. key,
  335. localFile,
  336. putExtra,
  337. callbackFunc
  338. ) {
  339. const preferredScheme = this.config.useHttpsDomain ? 'https' : 'http';
  340. // initial PutExtra
  341. putExtra = putExtra || new PutExtra();
  342. if (!putExtra.mimeType) {
  343. putExtra.mimeType = mime.getType(localFile);
  344. }
  345. if (!putExtra.fname) {
  346. putExtra.fname = path.basename(localFile);
  347. }
  348. putExtra = getDefaultPutExtra(
  349. putExtra,
  350. {
  351. key
  352. }
  353. );
  354. // initial retrier and try upload
  355. const result = _getRegionsRetrier.call(this, {
  356. bucketName: util.getBucketFromUptoken(uploadToken),
  357. accessKey: util.getAKFromUptoken(uploadToken)
  358. })
  359. .then(retrier => Promise.all([
  360. retrier,
  361. retrier.initContext()
  362. ]))
  363. .then(([retrier, context]) => retrier.retry({
  364. func: context => {
  365. const fsStream = fs.createReadStream(localFile);
  366. return putReq(
  367. context.endpoint.getValue({ scheme: preferredScheme }),
  368. uploadToken,
  369. key,
  370. fsStream,
  371. putExtra
  372. );
  373. },
  374. context
  375. }));
  376. handleReqCallback(result, callbackFunc);
  377. return result;
  378. };
  379. /** 上传本地文件
  380. * @param {string} uploadToken 上传凭证
  381. * @param {string} localFile 本地文件路径
  382. * @param {PutExtra | null} putExtra 额外选项
  383. * @param callbackFunc 回调函数
  384. * @returns {Promise<UploadResult>}
  385. */
  386. FormUploader.prototype.putFileWithoutKey = function (
  387. uploadToken,
  388. localFile,
  389. putExtra,
  390. callbackFunc
  391. ) {
  392. return this.putFile(uploadToken, null, localFile, putExtra, callbackFunc);
  393. };
  394. /**
  395. * @param {PutExtra} putExtra
  396. * @param {Object} options
  397. * @param {string} options.key
  398. * @returns {PutExtra}
  399. */
  400. function getDefaultPutExtra (putExtra, options) {
  401. putExtra = putExtra || new PutExtra();
  402. if (!putExtra.mimeType) {
  403. putExtra.mimeType = 'application/octet-stream';
  404. }
  405. if (!putExtra.fname) {
  406. putExtra.fname = options.key || 'fname';
  407. }
  408. return putExtra;
  409. }