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 14 kB

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