bigFileUpload.html 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta http-equiv="X-UA-Compatible" content="IE=edge">
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7. <title>BigFileUpload</title>
  8. </head>
  9. <body>
  10. <div class="upload-box">
  11. <h2>big file upload</h2>
  12. <div class="get-file">
  13. <input type="file" name="file" id="file">
  14. </div>
  15. <div class="upload-file">
  16. <button id="upload">上传文件</button>
  17. </div>
  18. </div>
  19. <script crossorigin="anonymous" integrity="sha512-bZS47S7sPOxkjU/4Bt0zrhEtWx0y0CRkhEp8IckzK+ltifIIE9EMIMTuT/mEzoIMewUINruDBIR/jJnbguonqQ==" src="https://lib.baomitu.com/axios/0.21.1/axios.min.js"></script>
  20. <script crossorigin="anonymous" integrity="sha384-45XT1VzQggQADTAenPH2Ecf0gLIwfiZ1J+nlE27AA9qXjtUXaplXshIamSqaco/e" src="https://lib.baomitu.com/spark-md5/3.0.0/spark-md5.js"></script>
  21. <script type="module">
  22. const file = document.querySelector('#file')
  23. const uploadBtn = document.querySelector('#upload')
  24. const DEFAULT_CHUNK_SIZE = 20 * 1024 * 1024; // 20MB
  25. const DEFAULT_OPTIONS = {
  26. chunkSize: DEFAULT_CHUNK_SIZE,
  27. };
  28. let uploadId
  29. class FileUploader {
  30. constructor(options) {
  31. this.fileUploaderClientOptions = Object.assign(DEFAULT_OPTIONS, options);
  32. }
  33. /**
  34. * 将file对象进行切片,然后根据切片计算md5
  35. * @param file 要上传的文件
  36. * @returns 返回md5和切片列表
  37. */
  38. async getChunkListAndFileMd5(
  39. file,
  40. ) {
  41. return new Promise((resolve, reject) => {
  42. let currentChunk = 0;
  43. const chunkSize = this.fileUploaderClientOptions.chunkSize;
  44. const chunks = Math.ceil(file.size / chunkSize);
  45. const spark = new SparkMD5.ArrayBuffer();
  46. const fileReader = new FileReader();
  47. const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
  48. const chunkList = [];
  49. fileReader.onload = function (e) {
  50. if (e.target.result instanceof ArrayBuffer) {
  51. spark.append(e.target.result);
  52. }
  53. currentChunk++;
  54. if (currentChunk < chunks) {
  55. loadNextChunk();
  56. } else {
  57. const computedHash = spark.end();
  58. resolve({ md5: computedHash, chunkList });
  59. }
  60. };
  61. fileReader.onerror = function (e) {
  62. console.warn('read file error', e);
  63. reject(e);
  64. };
  65. function loadNextChunk() {
  66. const start = currentChunk * chunkSize;
  67. const end = (start + chunkSize) >= file.size ? file.size : start + chunkSize;
  68. const chunk = blobSlice.call(file, start, end);
  69. chunkList.push(chunk);
  70. fileReader.readAsArrayBuffer(chunk);
  71. }
  72. loadNextChunk();
  73. });
  74. }
  75. /**
  76. * 上传文件方法,当FileUploaderClient的配置项中传入了requestOptions才能使用
  77. * 会依次执行getChunkListAndFileMd5、配置项中的initFilePartUploadFunc、配置项中的uploadPartFileFunc、配置项中的finishFilePartUploadFunc
  78. * 执行完成后返回上传结果,若有分片上传失败,则会自动重试
  79. * @param file 要上传的文件
  80. * @returns finishFilePartUploadFunc函数Promise resolve的值
  81. */
  82. async uploadFile(file) {
  83. const requestOptions = this.fileUploaderClientOptions;
  84. const { md5, chunkList } = await this.getChunkListAndFileMd5(file);
  85. const retryList = [];
  86. if (
  87. requestOptions.retryTimes === undefined ||
  88. !requestOptions.initFilePartUploadFunc ||
  89. !requestOptions.uploadPartFileFunc ||
  90. !requestOptions.finishFilePartUploadFunc
  91. ) {
  92. throw Error(
  93. 'invalid request options, need retryTimes, initFilePartUploadFunc, uploadPartFileFunc and finishFilePartUploadFunc',
  94. );
  95. }
  96. await requestOptions.initFilePartUploadFunc();
  97. // await requestOptions.uploadPartFileFunc(chunkList[0], 0);
  98. for (let index = 0; index < chunkList.length; index++) {
  99. try {
  100. await requestOptions.uploadPartFileFunc(chunkList[index], index);
  101. } catch (e) {
  102. console.warn(`${index} part upload failed`);
  103. retryList.push(index);
  104. }
  105. }
  106. for (let retry = 0; retry < requestOptions.retryTimes; retry++) {
  107. if (retryList.length > 0) {
  108. console.log(`retry start, times: ${retry}`);
  109. for (let a = 0; a < retryList.length; a++) {
  110. const blobIndex = retryList[a];
  111. try {
  112. await requestOptions.uploadPartFileFunc(
  113. chunkList[blobIndex],
  114. blobIndex,
  115. );
  116. retryList.splice(a, 1);
  117. } catch (e) {
  118. console.warn(
  119. `${blobIndex} part retry upload failed, times: ${retry}`,
  120. );
  121. }
  122. }
  123. }
  124. }
  125. if (retryList.length === 0) {
  126. return await requestOptions.finishFilePartUploadFunc(md5);
  127. } else {
  128. throw Error(
  129. `upload failed, some chunks upload failed: ${JSON.stringify(
  130. retryList,
  131. )}`,
  132. );
  133. }
  134. }
  135. }
  136. const fileUpload = new FileUploader({
  137. retryTimes: 2,
  138. initFilePartUploadFunc: async () => {
  139. const fileName = file.files[0].name
  140. const { data: { data } } = await axios.get('/api/upload-init', {
  141. params: {
  142. name: fileName,
  143. }
  144. })
  145. uploadId = data.uploadId
  146. console.log('初始化上传完成')
  147. },
  148. uploadPartFileFunc: async (chunk, index) => {
  149. const formData = new FormData()
  150. formData.append('uploadId', uploadId)
  151. formData.append('partIndex', index.toString())
  152. formData.append('partFile', chunk)
  153. const { data: { success, data } } = await axios.post('/api/upload-part', formData, {
  154. headers: { 'Content-Type': 'multipart/form-data' },
  155. })
  156. if (success) {
  157. console.log(data)
  158. console.log(`上传分片${data.index}完成`)
  159. }
  160. },
  161. finishFilePartUploadFunc: async (md5) => {
  162. const fileName = file.files[0].name
  163. const { data: { success, data } } = await axios.get('/api/upload-finish', {
  164. params: {
  165. name: fileName,
  166. uploadId,
  167. md5,
  168. }
  169. })
  170. if (success) {
  171. console.log(`上传完成,存储地址为:${data.path}`)
  172. const p = document.createElement('p')
  173. p.textContent = `文件地址:${data.path}`
  174. document.body.appendChild(p)
  175. }
  176. },
  177. })
  178. uploadBtn.addEventListener('click', () => {
  179. fileUpload.uploadFile(file.files[0])
  180. })
  181. </script>
  182. </body>
  183. </html>