upload.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683
  1. import Vue from 'vue'
  2. import axios from 'axios'
  3. import sparkMd5 from 'spark-md5'
  4. import {
  5. MessageBox,
  6. Message
  7. } from 'element-ui'
  8. import { addOrg } from '@/api/base'
  9. import request, { tenantRequest } from '@/utils/request'
  10. import { AssetType } from '@/constant'
  11. const CancelToken = axios.CancelToken
  12. const EventBus = new Vue()
  13. export const State = {
  14. INIT: 0,
  15. CALC: 4,
  16. CHECK: 8,
  17. READY: 10,
  18. UPLOADING: 50,
  19. MERGING: 90,
  20. SUCCESS: 100,
  21. ABORT: 999
  22. }
  23. // 计算线程数的阀值,大于1时本地校验同文件将可能失效
  24. const CALC_MAX = 1
  25. // 上传线程数的阀值
  26. const IDLE_MAX = 5
  27. // 单片的大小
  28. const CHUNK_SIZE = 10 * 1024 * 1024
  29. // 完成度优先
  30. // 开启后,空闲线程优先满足单个文件
  31. // 关闭时,多线程数为同时上传的文件数最大数,当一个块上传完时会启用空闲线程协助上传
  32. const COMPLETE_FIRST = false
  33. const ANALYSIS_FILE = true
  34. const files = []
  35. const analyzeQueue = []
  36. const hashQueue = []
  37. const pendingQueue = []
  38. let idle = IDLE_MAX
  39. let calc = CALC_MAX
  40. const noop = () => {}
  41. export function appendFile (file) {
  42. const fileType = getType(file)
  43. if (!fileType) {
  44. return
  45. }
  46. const { name, size } = file
  47. const obj = {
  48. id: Math.random().toString(16).slice(2),
  49. name, file,
  50. type: fileType,
  51. totalSize: size,
  52. uploaded: 0,
  53. valid: 1,
  54. state: State.INIT,
  55. url: fileType === AssetType.IMAGE ? URL.createObjectURL(file) : null
  56. }
  57. console.log(`添加文件${obj.name}, ${obj.totalSize}, ${file.type}`)
  58. files.push(obj)
  59. emitChange()
  60. if (ANALYSIS_FILE) {
  61. analyzeQueue.push(obj)
  62. analyzeMediaInfo()
  63. } else {
  64. startCalculate(obj)
  65. }
  66. }
  67. let analyzing = false
  68. function analyzeMediaInfo () {
  69. if (analyzing) {
  70. return
  71. }
  72. const obj = analyzeQueue.shift()
  73. if (!obj) {
  74. return
  75. }
  76. analyzing = true
  77. analyzeByWorker(obj).then(() => {
  78. startCalculate(obj)
  79. }, () => {
  80. remove(obj)
  81. }).finally(() => {
  82. analyzing = false
  83. analyzeMediaInfo()
  84. })
  85. }
  86. function analyzeByWorker (obj) {
  87. return new Promise((resolve, reject) => {
  88. console.log(`开始解析文件${obj.name}`)
  89. const worker = new Worker('/mediainfo.js')
  90. worker.onmessage = e => {
  91. let isNeedConfirm = false
  92. const { error, media } = e.data
  93. if (error) {
  94. console.log(`解析文件${obj.name}失败`, error)
  95. } else {
  96. console.log(media)
  97. if (obj.type === AssetType.IMAGE) {
  98. const imageTrack = media.track.find(track => track['@type'] === 'Image')
  99. if (imageTrack) {
  100. obj.resolutionRatio = `${imageTrack.Width}x${imageTrack.Height}`
  101. }
  102. } else {
  103. const generalTrack = media.track.find(track => track['@type'] === 'General')
  104. obj.duration = generalTrack ? parseInt(generalTrack.Duration) | 0 : 0
  105. if (obj.type === AssetType.VIDEO) {
  106. const videoTrack = media.track.find(track => track['@type'] === 'Video')
  107. if (videoTrack && videoTrack.Format !== 'AVC') {
  108. isNeedConfirm = true
  109. }
  110. }
  111. }
  112. }
  113. if (isNeedConfirm) {
  114. MessageBox.confirm(
  115. `视频 ${obj.name} 非H264编码将无法预览`,
  116. '继续上传',
  117. { type: 'warning' }
  118. ).then(resolve, reject)
  119. } else {
  120. resolve()
  121. }
  122. worker.terminate()
  123. }
  124. worker.onmessageerror = worker.onerror = e => {
  125. console.log(`解析视频${obj.name}失败`, e)
  126. resolve()
  127. worker.terminate()
  128. }
  129. worker.postMessage(obj)
  130. })
  131. }
  132. export function removeFile (id) {
  133. const index = files.findIndex(obj => obj.id === id)
  134. if (~index) {
  135. const obj = files[index]
  136. if (isResolved(obj)) {
  137. files.splice(index, 1)
  138. emitChange()
  139. } else {
  140. MessageBox.confirm(
  141. `取消上传文件 ${obj.name} ?`,
  142. { type: 'warning' }
  143. ).then(() => {
  144. console.log(`${obj.name}已取消`)
  145. files.splice(index, 1)
  146. obj.source?.cancel('abort')
  147. setState(obj, State.ABORT)
  148. emitChange()
  149. })
  150. }
  151. }
  152. }
  153. export function retry (id) {
  154. const obj = files.find(obj => obj.id === id)
  155. if (obj && !obj.valid) {
  156. obj.valid = true
  157. switch (obj.state) {
  158. case State.CALC:
  159. startCalculate(obj)
  160. break
  161. case State.CHECK:
  162. startCheck(obj)
  163. break
  164. case State.UPLOADING:
  165. startUpload(obj)
  166. break
  167. case State.MERGING:
  168. startMerge(obj)
  169. break
  170. default:
  171. break
  172. }
  173. emitChange()
  174. }
  175. }
  176. export function addListener (eventName, fn) {
  177. EventBus.$on(eventName, fn)
  178. if (eventName === 'change') {
  179. fn(getFiles())
  180. }
  181. }
  182. export function removeListener (...args) {
  183. EventBus.$off(...args)
  184. }
  185. function emit (...args) {
  186. EventBus.$emit(...args)
  187. }
  188. function getType ({ name, type }) {
  189. switch (true) {
  190. case /png|jpg|jpeg|gif/i.test(type):
  191. return AssetType.IMAGE
  192. case /mp4/i.test(type):
  193. return AssetType.VIDEO
  194. case /audio\/mpeg/.test(type):
  195. return AssetType.AUDIO
  196. case /application\/(vnd.ms-powerpoint|vnd.openxmlformats-officedocument.presentationml.presentation)/.test(type):
  197. return AssetType.PPT
  198. case /application\/pdf/.test(type):
  199. return AssetType.PDF
  200. case /application\/(msword|vnd.openxmlformats-officedocument.wordprocessingml.document)/i.test(type):
  201. return AssetType.DOC
  202. default:
  203. Message({
  204. type: 'warning',
  205. message: `暂不支持${name}该类型文件`
  206. })
  207. return null
  208. }
  209. }
  210. let pending = false
  211. function emitChange () {
  212. if (!pending) {
  213. pending = true
  214. Vue.nextTick(() => {
  215. pending = false
  216. emit('change', getFiles())
  217. })
  218. }
  219. }
  220. let count = 0
  221. function setState (obj, state) {
  222. if (state >= State.SUCCESS) {
  223. count -= 1
  224. } else if (obj.state === State.INIT || state === obj.state) {
  225. count += 1
  226. }
  227. obj.state = state
  228. emitChange()
  229. }
  230. function setInvalid (obj, message, stack) {
  231. count -= 1
  232. console.warn(message, stack || '')
  233. Message({
  234. type: 'warning',
  235. message
  236. })
  237. obj.valid = false
  238. emitChange()
  239. }
  240. export function isUploading () {
  241. return count > 0
  242. }
  243. function transformFile ({ id, type, name, state, valid, uploaded, totalChunks, url }) {
  244. return {
  245. id,
  246. type,
  247. name,
  248. state,
  249. valid,
  250. url,
  251. percentage: state === State.UPLOADING ? 10 + uploaded * 80 / totalChunks | 0 : state
  252. }
  253. }
  254. export function getFiles () {
  255. return files.map(transformFile)
  256. }
  257. function remove (obj, message) {
  258. if (message) {
  259. console.log(message)
  260. Message({
  261. type: 'warning',
  262. message
  263. })
  264. }
  265. setState(obj, State.ABORT)
  266. removeFile(obj.id)
  267. }
  268. function isResolved ({ state, valid }) {
  269. return state === State.SUCCESS || state === State.ABORT || !valid
  270. }
  271. function startCalculate (obj) {
  272. hashQueue.push(obj)
  273. shuntCalculate()
  274. }
  275. function shuntCalculate () {
  276. if (!calc) {
  277. return
  278. }
  279. const obj = hashQueue.shift()
  280. if (!obj) {
  281. return
  282. }
  283. if (isResolved(obj)) {
  284. shuntCalculate()
  285. return
  286. }
  287. console.log(`正在计算${obj.name}的HASH`)
  288. calc -= 1
  289. calculate(obj)
  290. .finally(() => {
  291. obj.source = null
  292. calc += 1
  293. shuntCalculate()
  294. })
  295. .then(({ hash, chunks, totalChunks }) => {
  296. console.log(`${obj.name}计算HASH结束`, hash)
  297. obj.hash = hash
  298. obj.chunks = chunks
  299. obj.totalChunks = totalChunks
  300. startCheck(obj)
  301. }, e => {
  302. if (!isResolved(obj)) {
  303. setInvalid(obj, `${obj.name}计算HASH异常`, e)
  304. }
  305. })
  306. }
  307. function calculate (obj) {
  308. setState(obj, State.CALC)
  309. if (typeof Worker !== 'undefined') {
  310. return calculateHashByWorker(obj)
  311. }
  312. if (window.requestIdleCallback) {
  313. return calculateHashByIdle(obj)
  314. }
  315. return calculateSimple(obj)
  316. }
  317. function calculateSimple (obj) {
  318. return new Promise((resolve, reject) => {
  319. obj.source = { cancel: reason => reject(reason) }
  320. const { file, totalSize } = obj
  321. file.arrayBuffer().then(arrayBuffer => {
  322. const spark = new sparkMd5.ArrayBuffer()
  323. spark.append(arrayBuffer)
  324. resolve({
  325. hash: spark.end(),
  326. chunks: [{ index: 0, raw: file, size: totalSize }],
  327. totalChunks: 1
  328. })
  329. }, reject)
  330. })
  331. }
  332. function createWorkerCancel (reject) {
  333. let worker
  334. return {
  335. cancel (reason) {
  336. if (worker !== null) {
  337. worker.terminate()
  338. worker = null
  339. reject(reason)
  340. }
  341. },
  342. token (val) {
  343. worker = val
  344. }
  345. }
  346. }
  347. function calculateHashByWorker (obj, options = {}) {
  348. return new Promise((resolve, reject) => {
  349. obj.source = createWorkerCancel(reject)
  350. const { progress = noop } = options
  351. const { token } = obj.source
  352. const chunks = createChunks(obj.file)
  353. const worker = new Worker('/hash.js')
  354. token(worker)
  355. worker.onmessage = e => {
  356. const { hash, index, total } = e.data
  357. if (hash) {
  358. token(null)
  359. worker.terminate()
  360. resolve({
  361. hash,
  362. chunks,
  363. totalChunks: chunks.length
  364. })
  365. } else {
  366. progress(index, total)
  367. }
  368. }
  369. worker.onmessageerror = worker.onerror = e => {
  370. obj.source?.cancel(e)
  371. }
  372. worker.postMessage({ chunks })
  373. })
  374. }
  375. function createIdleCancel (reject) {
  376. let id = null
  377. return {
  378. cancel (reason) {
  379. if (id !== null) {
  380. window.cancelIdleCallback(id)
  381. reject(reason)
  382. }
  383. },
  384. token (val) {
  385. id = val
  386. }
  387. }
  388. }
  389. function calculateHashByIdle (obj, options = {}) {
  390. return new Promise((resolve, reject) => {
  391. obj.source = createIdleCancel(reject)
  392. const { progress = noop } = options
  393. const { token } = obj.source
  394. const chunks = createChunks(obj.file)
  395. const total = chunks.length
  396. const spark = new sparkMd5.ArrayBuffer()
  397. const appendToSpark = blob => blob.arrayBuffer().then(arrayBuffer => spark.append(arrayBuffer))
  398. let count = 0
  399. const workLoop = async deadline => {
  400. while (count < total && deadline.timeRemaining() > 1) {
  401. await appendToSpark(chunks[count].raw)
  402. count++
  403. progress(count, total)
  404. if (count === total) {
  405. token(null)
  406. resolve({
  407. hash: spark.end(),
  408. chunks,
  409. totalChunks: chunks.length
  410. })
  411. }
  412. }
  413. count < total && token(window.requestIdleCallback(workLoop))
  414. }
  415. progress(count, total)
  416. token(window.requestIdleCallback(workLoop))
  417. })
  418. }
  419. function createChunks (blob, chunkSize = CHUNK_SIZE) {
  420. const chunks = []
  421. try {
  422. const { size } = blob
  423. let cur = 0
  424. let index = 0
  425. while (cur < size) {
  426. chunks.push({
  427. index,
  428. raw: blob.slice(cur, cur + chunkSize),
  429. size: Math.min(chunkSize, size - cur)
  430. })
  431. cur += chunkSize
  432. index += 1
  433. }
  434. } catch (e) {
  435. console.warn(e)
  436. return []
  437. }
  438. return chunks
  439. }
  440. function isExists (obj) {
  441. const { id, hash } = obj
  442. const file = files.find(file => file.id !== id && file.hash === hash)
  443. if (file) {
  444. return {
  445. result: true,
  446. message: obj.name === file.name ? `${obj.name}已在上传列表中` : `${obj.name}在上传列表中存在同内容文件${file.name}`
  447. }
  448. }
  449. console.log(`${obj.name}服务器校验`)
  450. obj.source = CancelToken.source()
  451. return request({
  452. url: '/minio-data/integral/check',
  453. method: 'post',
  454. data: { identifier: hash },
  455. cancelToken: obj.source.token,
  456. custom: true,
  457. background: true
  458. }).finally(() => {
  459. obj.source = null
  460. }).then(
  461. ({ data }) => {
  462. return {
  463. result: true,
  464. message: `${obj.name}已上传过,文件名为${data.originalName}`
  465. }
  466. },
  467. ({ errCode, data }) => {
  468. if (errCode === '666') {
  469. return {
  470. result: false,
  471. message: data
  472. }
  473. }
  474. return Promise.reject()
  475. }
  476. )
  477. }
  478. async function startCheck (obj) {
  479. try {
  480. setState(obj, State.CHECK)
  481. const { result, message } = await isExists(obj)
  482. if (result) {
  483. remove(obj, message)
  484. return
  485. }
  486. message.forEach(chunkNumber => {
  487. const index = obj.chunks.findIndex(({ index }) => index === chunkNumber)
  488. if (~index) {
  489. obj.chunks.splice(index, 1)
  490. obj.uploaded += 1
  491. }
  492. })
  493. } catch (e) {
  494. if (!isResolved(obj)) {
  495. setInvalid(obj, `${obj.name}校验失败`, e)
  496. }
  497. return
  498. }
  499. if (obj.chunks.length) {
  500. setState(obj, State.READY)
  501. startUpload(obj)
  502. } else {
  503. startMerge(obj)
  504. }
  505. }
  506. function startUpload (obj) {
  507. console.log(`准备分流${obj.name}`)
  508. pendingQueue.push(obj)
  509. shuntTask()
  510. }
  511. function shuntTask () {
  512. console.log(`分流,当前剩余文件${pendingQueue.length}、线程${idle}`)
  513. if (!idle || !pendingQueue.length) {
  514. return
  515. }
  516. const obj = pendingQueue.shift()
  517. if (isResolved(obj)) {
  518. shuntTask()
  519. return
  520. }
  521. obj.source = CancelToken.source()
  522. setState(obj, State.UPLOADING)
  523. startChunkTask(obj)
  524. }
  525. function idleTask () {
  526. idle += 1
  527. shuntTask()
  528. }
  529. function finish (obj) {
  530. const { id, hash, name, type } = obj
  531. Message({
  532. type: 'success',
  533. message: `${name}上传成功`
  534. })
  535. setState(obj, State.SUCCESS)
  536. emit('uploaded', { id, hash, name, type })
  537. }
  538. function transformName (name) {
  539. return name.replace(/\.[^.]+$/, '')
  540. }
  541. function startChunkTask (obj) {
  542. idle -= 1
  543. const { name, hash, source, chunks, totalChunks } = obj
  544. console.log(`开始上传${name},共${totalChunks}个切片`)
  545. start()
  546. // 抢占
  547. if (COMPLETE_FIRST) {
  548. checkIdle()
  549. }
  550. function checkIdle () {
  551. while (idle && chunks.length) {
  552. idle -= 1
  553. start(true)
  554. }
  555. }
  556. async function start (sub) {
  557. const chunk = chunks.shift()
  558. const { index, raw, size } = chunk
  559. try {
  560. console.log(`上传${name}第${index + 1}个切片, ${size}`)
  561. const formData = new FormData()
  562. formData.append('identifier', hash)
  563. formData.append('chunkNumber', index)
  564. formData.append('file', raw)
  565. await request({
  566. url: '/minio-data/chunk/upload',
  567. method: 'post',
  568. data: formData,
  569. timeout: 0,
  570. cancelToken: source.token,
  571. custom: true,
  572. background: true
  573. })
  574. console.log(`${name}第${index + 1}个切片上传成功`)
  575. obj.uploaded += 1
  576. emitChange()
  577. if (obj.uploaded === totalChunks) {
  578. idleTask()
  579. startMerge(obj)
  580. } else if (!chunks.length || (sub && !COMPLETE_FIRST && pendingQueue.length)) {
  581. idleTask()
  582. } else {
  583. start(sub)
  584. // 利用空闲线程
  585. checkIdle()
  586. }
  587. } catch (e) {
  588. chunks.unshift(chunk)
  589. if (isResolved(obj)) {
  590. chunk.error = 0
  591. idleTask()
  592. return
  593. }
  594. chunk.error = (chunk.error || 0) + 1
  595. console.log(`${name}第${index + 1}个切片上传失败第${chunk.error}次`, e)
  596. if (chunk.error === 3) {
  597. chunk.error = 0
  598. obj.source?.cancel(e)
  599. obj.source = null
  600. setInvalid(obj, `${name}上传失败`)
  601. idleTask()
  602. } else {
  603. start(sub)
  604. }
  605. }
  606. }
  607. }
  608. function startMerge (obj) {
  609. const { hash, name, type, totalSize, totalChunks, resolutionRatio, duration } = obj
  610. console.log(`开始合并${name}`)
  611. setState(obj, State.MERGING)
  612. tenantRequest({
  613. url: '/minio-data/chunk/reduce',
  614. method: 'post',
  615. data: addOrg({
  616. identifier: hash,
  617. filename: transformName(name),
  618. type,
  619. totalSize,
  620. totalChunks,
  621. resolutionRatio,
  622. duration
  623. }),
  624. timeout: 0,
  625. custom: true,
  626. background: true
  627. }).then(({ data, errMessage }) => {
  628. if (data) {
  629. finish(obj, State.SUCCESS)
  630. } else {
  631. setInvalid(obj, `${name}合并文件失败`, errMessage)
  632. }
  633. }, e => {
  634. if (!isResolved(obj)) {
  635. setInvalid(obj, `${name}合并文件失败`, e)
  636. }
  637. })
  638. }