You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

MinioUploader.vue 13 kB

4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  1. <template>
  2. <div class="dropzone-wrapper dataset-files">
  3. <div
  4. id="dataset"
  5. class="dropzone"
  6. />
  7. <p class="upload-info">
  8. {{ file_status_text }}
  9. <span class="success">{{ status }}</span>
  10. </p>
  11. </div>
  12. </template>
  13. <script>
  14. /* eslint-disable eqeqeq */
  15. // import Dropzone from 'dropzone/dist/dropzone.js';
  16. // import 'dropzone/dist/dropzone.css'
  17. import SparkMD5 from 'spark-md5';
  18. import axios from 'axios';
  19. import qs from 'qs';
  20. import createDropzone from '../features/dropzone.js';
  21. const {_AppSubUrl, _StaticUrlPrefix, csrf} = window.config;
  22. export default {
  23. data() {
  24. return {
  25. dropzoneUploader: null,
  26. maxFiles: 1,
  27. maxFilesize: 1 * 1024 * 1024 * 1024 * 1024,
  28. acceptedFiles: '*/*',
  29. progress: 0,
  30. status: '',
  31. dropzoneParams: {},
  32. file_status_text: ''
  33. };
  34. },
  35. async mounted() {
  36. let previewTemplate = '';
  37. previewTemplate += '<div class="dz-preview dz-file-preview">\n ';
  38. previewTemplate += ' <div class="dz-details">\n ';
  39. previewTemplate += ' <div class="dz-filename">';
  40. previewTemplate +=
  41. ' <span data-dz-name data-dz-thumbnail></span>';
  42. previewTemplate += ' </div>\n ';
  43. previewTemplate += ' <div class="dz-size" data-dz-size></div>\n ';
  44. previewTemplate += ' </div>\n ';
  45. previewTemplate += ' <div class="dz-progress ui active progress">';
  46. previewTemplate +=
  47. ' <div class="dz-upload bar" data-dz-uploadprogress><div class="progress"></div></div>\n ';
  48. previewTemplate += ' </div>\n ';
  49. previewTemplate += ' <div class="dz-success-mark">';
  50. previewTemplate += ' <span>上传成功</span>';
  51. previewTemplate += ' </div>\n ';
  52. previewTemplate += ' <div class="dz-error-mark">';
  53. previewTemplate += ' <span>上传失败</span>';
  54. previewTemplate += ' </div>\n ';
  55. previewTemplate += ' <div class="dz-error-message">';
  56. previewTemplate += ' <span data-dz-errormessage></span>';
  57. previewTemplate += ' </div>\n';
  58. previewTemplate += '</div>';
  59. this.dropzoneParams = $('div#minioUploader-params');
  60. this.file_status_text = this.dropzoneParams.data('file-status');
  61. this.status = this.dropzoneParams.data('file-init-status');
  62. const $dropzone = $('div#dataset');
  63. const dropzoneUploader = await createDropzone($dropzone[0], {
  64. url: '/todouploader',
  65. maxFiles: this.maxFiles,
  66. maxFilesize: this.maxFileSize,
  67. timeout: 0,
  68. autoQueue: false,
  69. dictDefaultMessage: this.dropzoneParams.data('default-message'),
  70. dictInvalidFileType: this.dropzoneParams.data('invalid-input-type'),
  71. dictFileTooBig: this.dropzoneParams.data('file-too-big'),
  72. dictRemoveFile: this.dropzoneParams.data('remove-file'),
  73. previewTemplate
  74. });
  75. dropzoneUploader.on('addedfile', (file) => {
  76. setTimeout(() => {
  77. // eslint-disable-next-line no-unused-expressions
  78. file.accepted && this.onFileAdded(file);
  79. }, 200);
  80. });
  81. dropzoneUploader.on('maxfilesexceeded', function (file) {
  82. if (this.files[0].status !== 'success') {
  83. alert(this.dropzoneParams.data('waitting-uploading'));
  84. this.removeFile(file);
  85. return;
  86. }
  87. this.removeAllFiles();
  88. this.addFile(file);
  89. });
  90. this.dropzoneUploader = dropzoneUploader;
  91. },
  92. methods: {
  93. resetStatus() {
  94. this.progress = 0;
  95. this.status = '';
  96. },
  97. updateProgress(file, progress) {
  98. file.previewTemplate.querySelector(
  99. '.dz-upload'
  100. ).style.width = `${progress}%`;
  101. },
  102. emitDropzoneSuccess(file) {
  103. file.status = 'success';
  104. this.dropzoneUploader.emit('success', file);
  105. this.dropzoneUploader.emit('complete', file);
  106. },
  107. onFileAdded(file) {
  108. file.datasetId = document
  109. .getElementById('datasetId')
  110. .getAttribute('datasetId');
  111. this.resetStatus();
  112. this.computeMD5(file);
  113. },
  114. finishUpload(file) {
  115. this.emitDropzoneSuccess(file);
  116. setTimeout(() => {
  117. window.location.reload();
  118. }, 1000);
  119. },
  120. computeMD5(file) {
  121. this.resetStatus();
  122. const blobSlice =
  123. File.prototype.slice ||
  124. File.prototype.mozSlice ||
  125. File.prototype.webkitSlice,
  126. chunkSize = 1024 * 1024 * 64,
  127. chunks = Math.ceil(file.size / chunkSize),
  128. spark = new SparkMD5.ArrayBuffer(),
  129. fileReader = new FileReader();
  130. let currentChunk = 0;
  131. const time = new Date().getTime();
  132. // console.log('计算MD5...')
  133. this.status = this.dropzoneParams.data('md5-computing');
  134. file.totalChunkCounts = chunks;
  135. loadNext();
  136. fileReader.onload = (e) => {
  137. fileLoaded.call(this, e);
  138. };
  139. fileReader.onerror = (err) => {
  140. console.warn('oops, something went wrong.', err);
  141. file.cancel();
  142. };
  143. function fileLoaded(e) {
  144. spark.append(e.target.result); // Append array buffer
  145. currentChunk++;
  146. if (currentChunk < chunks) {
  147. // console.log(`第${currentChunk}分片解析完成, 开始第${currentChunk +1}/${chunks}分片解析`);
  148. this.status = `${this.dropzoneParams.data('loading-file')} ${(
  149. (currentChunk / chunks) *
  150. 100
  151. ).toFixed(2)}% (${currentChunk}/${chunks})`;
  152. this.updateProgress(file, ((currentChunk / chunks) * 100).toFixed(2));
  153. loadNext();
  154. return;
  155. }
  156. const md5 = spark.end();
  157. console.log(
  158. `MD5计算完成:${file.name} \nMD5:${md5} \n分片:${chunks} 大小:${
  159. file.size
  160. } 用时:${(new Date().getTime() - time) / 1000} s`
  161. );
  162. spark.destroy(); // 释放缓存
  163. file.uniqueIdentifier = md5; // 将文件md5赋值给文件唯一标识
  164. file.cmd5 = false; // 取消计算md5状态
  165. this.computeMD5Success(file);
  166. }
  167. function loadNext() {
  168. const start = currentChunk * chunkSize;
  169. const end =
  170. start + chunkSize >= file.size ? file.size : start + chunkSize;
  171. fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
  172. }
  173. },
  174. async computeMD5Success(md5edFile) {
  175. const file = await this.getSuccessChunks(md5edFile);
  176. if (file.uploadID == '' || file.uuid == '') {
  177. // 未上传过
  178. await this.newMultiUpload(file);
  179. if (file.uploadID != '' && file.uuid != '') {
  180. file.chunks = '';
  181. this.multipartUpload(file);
  182. } else {
  183. // 失败如何处理
  184. return;
  185. }
  186. return;
  187. }
  188. if (file.uploaded == '1') {
  189. // 已上传成功
  190. // 秒传
  191. if (file.attachID == '0') {
  192. // 删除数据集记录,未删除文件
  193. await addAttachment(file);
  194. }
  195. console.log('文件已上传完成');
  196. this.progress = 100;
  197. this.status = this.dropzoneParams.data('upload-complete');
  198. this.finishUpload(file);
  199. } else {
  200. // 断点续传
  201. this.multipartUpload(file);
  202. }
  203. async function addAttachment(file) {
  204. return await axios.post(
  205. '/attachments/add',
  206. qs.stringify({
  207. uuid: file.uuid,
  208. file_name: file.name,
  209. size: file.size,
  210. dataset_id: file.datasetId,
  211. _csrf: csrf
  212. })
  213. );
  214. }
  215. },
  216. async getSuccessChunks(file) {
  217. const params = {
  218. params: {
  219. md5: file.uniqueIdentifier,
  220. _csrf: csrf
  221. }
  222. };
  223. try {
  224. const response = await axios.get('/attachments/get_chunks', params);
  225. file.uploadID = response.data.uploadID;
  226. file.uuid = response.data.uuid;
  227. file.uploaded = response.data.uploaded;
  228. file.chunks = response.data.chunks;
  229. file.attachID = response.data.attachID;
  230. return file;
  231. } catch (error) {
  232. console.log('getSuccessChunks catch: ', error);
  233. return null;
  234. }
  235. },
  236. async newMultiUpload(file) {
  237. const res = await axios.get('/attachments/new_multipart', {
  238. params: {
  239. totalChunkCounts: file.totalChunkCounts,
  240. md5: file.uniqueIdentifier,
  241. size: file.size,
  242. fileType: file.type,
  243. _csrf: csrf
  244. }
  245. });
  246. file.uploadID = res.data.uploadID;
  247. file.uuid = res.data.uuid;
  248. },
  249. multipartUpload(file) {
  250. const blobSlice =
  251. File.prototype.slice ||
  252. File.prototype.mozSlice ||
  253. File.prototype.webkitSlice,
  254. chunkSize = 1024 * 1024 * 64,
  255. chunks = Math.ceil(file.size / chunkSize),
  256. fileReader = new FileReader(),
  257. time = new Date().getTime();
  258. let currentChunk = 0;
  259. function loadNext() {
  260. const start = currentChunk * chunkSize;
  261. const end =
  262. start + chunkSize >= file.size ? file.size : start + chunkSize;
  263. fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
  264. }
  265. function checkSuccessChunks() {
  266. const index = successChunks.indexOf((currentChunk + 1).toString());
  267. if (index == -1) {
  268. return false;
  269. }
  270. return true;
  271. }
  272. async function getUploadChunkUrl(currentChunk, partSize) {
  273. const res = await axios.get('/attachments/get_multipart_url', {
  274. params: {
  275. uuid: file.uuid,
  276. uploadID: file.uploadID,
  277. size: partSize,
  278. chunkNumber: currentChunk + 1,
  279. _csrf: csrf
  280. }
  281. });
  282. console.log('getUploadChunkUrl: ', res);
  283. urls[currentChunk] = res.data.url;
  284. }
  285. async function uploadMinio(url, e) {
  286. const res = await axios.put(url, e.target.result);
  287. etags[currentChunk] = res.headers.etag;
  288. }
  289. async function updateChunk(currentChunk) {
  290. await axios.post(
  291. '/attachments/update_chunk',
  292. qs.stringify({
  293. uuid: file.uuid,
  294. chunkNumber: currentChunk + 1,
  295. etag: etags[currentChunk],
  296. _csrf: csrf
  297. })
  298. );
  299. }
  300. async function uploadChunk(e) {
  301. if (!checkSuccessChunks()) {
  302. const start = currentChunk * chunkSize;
  303. const partSize =
  304. start + chunkSize >= file.size ? file.size - start : chunkSize;
  305. // 获取分片上传url
  306. await getUploadChunkUrl(currentChunk, partSize);
  307. if (urls[currentChunk] != '') {
  308. // 上传到minio
  309. await uploadMinio(urls[currentChunk], e);
  310. if (etags[currentChunk] != '') {
  311. // 更新数据库:分片上传结果
  312. await updateChunk(currentChunk);
  313. } else {
  314. console.log("上传到minio uploadChunk etags[currentChunk] == ''");// TODO
  315. }
  316. } else {
  317. console.log("uploadChunk urls[currentChunk] != ''");// TODO
  318. }
  319. }
  320. }
  321. async function completeUpload() {
  322. return await axios.post(
  323. '/attachments/complete_multipart',
  324. qs.stringify({
  325. uuid: file.uuid,
  326. uploadID: file.uploadID,
  327. file_name: file.name,
  328. size: file.size,
  329. dataset_id: file.datasetId,
  330. _csrf: csrf
  331. })
  332. );
  333. }
  334. const successChunks = [];
  335. let successParts = [];
  336. successParts = file.chunks.split(',');
  337. for (let i = 0; i < successParts.length; i++) {
  338. successChunks[i] = successParts[i].split('-')[0].split('"')[1];
  339. }
  340. const urls = []; // TODO const ?
  341. const etags = [];
  342. console.log('上传分片...');
  343. this.status = this.dropzoneParams.data('uploading');
  344. loadNext();
  345. fileReader.onload = async (e) => {
  346. await uploadChunk(e);
  347. currentChunk++;
  348. if (currentChunk < chunks) {
  349. console.log(
  350. `第${currentChunk}个分片上传完成, 开始第${currentChunk +
  351. 1}/${chunks}个分片上传`
  352. );
  353. this.progress = Math.ceil((currentChunk / chunks) * 100);
  354. this.updateProgress(file, ((currentChunk / chunks) * 100).toFixed(2));
  355. this.status = `${this.dropzoneParams.data('uploading')} ${(
  356. (currentChunk / chunks) *
  357. 100
  358. ).toFixed(2)}%`;
  359. await loadNext();
  360. } else {
  361. await completeUpload();
  362. console.log(
  363. `文件上传完成:${file.name} \n分片:${chunks} 大小:${
  364. file.size
  365. } 用时:${(new Date().getTime() - time) / 1000} s`
  366. );
  367. this.progress = 100;
  368. this.status = this.dropzoneParams.data('upload-complete');
  369. this.finishUpload(file);
  370. }
  371. };
  372. }
  373. }
  374. };
  375. </script>
  376. <style>
  377. .dropzone-wrapper {
  378. margin: 2em auto;
  379. }
  380. .ui .dropzone {
  381. border: 2px dashed #0087f5;
  382. box-shadow: none !important;
  383. padding: 0;
  384. min-height: 5rem;
  385. border-radius: 4px;
  386. }
  387. .dataset .dataset-files #dataset .dz-preview.dz-file-preview,
  388. .dataset .dataset-files #dataset .dz-preview.dz-processing {
  389. display: flex;
  390. align-items: center;
  391. }
  392. .dataset .dataset-files #dataset .dz-preview {
  393. border-bottom: 1px solid #dadce0;
  394. min-height: 0;
  395. }
  396. </style>