大檔案上傳、斷點續傳、秒傳、beego、vue

賜我白日夢發表於2020-06-23

大檔案上傳

0、專案原始碼地址

原始碼地址 :https://github.com/zhuchangwu/large-file-upload

它是個demo,僅供參考

前端基於 vue-simple-uploader (感謝這個大佬)實現: https://github.com/simple-uploader/vue-uploader/blob/master/README_zh-CN.md

vue-simple-uploader底層封裝了uploader.js : https://github.com/simple-uploader/Uploader/blob/develop/README_zh-CN.md

1、如何唯一標識一個檔案?

檔案的資訊後端會儲存在mysql資料庫表中。

在上傳之前,前端通過 spark-md5.js 計算檔案的md5值以此去唯一的標示一個檔案。

spark-md5.js 地址:https://github.com/satazor/js-spark-md5

README.md中有spark-md5.js的使用demo,可以去看看。

2、斷點續傳是如何實現的?

斷點續傳可以實現這樣的功能,比如使用者上傳200M的檔案,當使用者上傳完199M時,斷網了,有了斷點續傳的功能,我們允許RD再次上傳時,能從第199M的位置重新上傳。

實現原理:

實現斷點續傳的前提是,大檔案切片上傳。然後前端得問後端哪些chunk曾經上傳過,讓前端跳過這些上傳過的chunk就好了。

前端的上傳器(uploader.js)在上傳時會先傳送一個GET請求,這個請求不會攜帶任何chunk資料,作用就是向後端詢問哪些chunk曾經上傳過。 後端會將這些資料儲存在mysql資料庫表中。比如按這種格式:1:2:3:5表示,曾經上傳過的分片有1,2,3,5。第四片沒有被上傳,前端會跳過1,2,3,5。 僅僅會將第四個chunk傳送給後端。

3、秒傳是如何實現的?

秒傳實現的功能是:當RD重複上傳一份相同的檔案時,除了第一次上傳會正常傳送上傳請求後,其他的上傳都會跳過真正的上傳,直接顯示秒成功。

實現方式:

後端儲存著當前檔案的相關資訊。為了實現秒傳,我們需要搞一個欄位(isUploaded)表示當前md5對應的檔案是否曾經上傳過。 後端在處理 前端的上傳器(uploader.js)傳送的第一個GET請求時,會將這個欄位傳送給前端,比如 isUploaded = true。前端看到這個資訊後,直接跳過上傳,顯示上傳成功。

4、上傳暫停是如何實現的?

上傳的暫停:並不是去暫停一個已經傳送出去的正在進行資料傳輸的http請求~

而是暫停傳送起傳送下一個http請求。

就我們的專案而言,因為我們的檔案本來就是先切片,對於我們來說,暫停檔案的上傳,本質上就是暫停傳送下一個chunk。

5、前端上傳併發數是多少?

前端的uploader.js中預設會三條執行緒啟動併發上傳,前端會在同一時刻併發 傳送3個chunk,後端就會相應的為每個請求開啟三個協程處理上傳的過來的chunk。

在我們的專案中,會將前端併發數調整成了1。原因如下:

因為考慮到了斷點續傳的實現,後端需要記錄下曾經上傳過哪些切片。(這個記錄在mysql的資料庫表中,以 ”1:2:3:4:5“ )這種格式記錄。

Mysql5.7預設的儲存引擎是innoDB,預設的隔離級別是RR。如果我們將前端的併發數調大,就會出現下面的異常情況:

1. goroutine1 獲取開啟事物,讀取當前上傳到記錄是 1:2 (未提交事物)
2. goroutine1 在現有的記錄上加上自己處理的分片3,並和現有的1:2拼接在一起成1:2:3 (未提交事物)
3. goroutine2 獲取開啟事物,(因為RR,所以它讀不到1:2:3)讀取當前上傳到記錄是 1:2 (未提交事物)
4. goroutine1 提交事物,將1:2:3寫回到mysql
5. goroutine2 在現有的記錄上加上自己處理的分片4,並和現有的1:2拼接在一起成1:2:4 (提交事物)

可以看到,如果前端併發上傳,後端就會出現分片丟失的問題。 故前端將併發數置為1。

6、單個chunk上傳失敗怎麼辦?

前端會重傳chunk?

由於網路問題,或者時後端處理chunk時出現的其他未知的錯誤,會導致chunk上傳失敗。

uploaded.js 中有如下的配置項, 每次uploader.js 在上傳每一個切片實際上都是在傳送一次post請求,後端根據這個post請求是會給前端一個狀態嗎。 uploader.js 就是根據這個狀態碼去判斷是失敗了還是成功了,如果失敗了就會重新傳送這個上傳的請求。

那uploader.js是如何知道有哪些狀態嗎是它應該重傳chunk的標記呢? 看看下面uploader.js需要的options 就明白了,其中的permantErrors中配置的狀態碼標示:當遇到這個狀態碼時整個上傳直接失敗~

successStatuses中配置的狀態碼錶示chunk是上傳成功的~。 其他的狀態嗎uploader.js 就會任務chunk上傳的有問題,於是重新上傳~

        options: {
          target: 'http://localhost:8081/file/upload',
          maxChunkRetries: 3,
          permanentErrors:[502], // 永久性的上傳失敗~,會認為整個檔案都上傳失敗了
          successStatuses:[200], // 當前chunk上傳成功後的狀態嗎
          ...
        }

7、超過重傳次數後,怎麼辦?

比如我們設定出錯後重傳的次數為3,那麼無論當前分片是第幾片,整個檔案的上傳狀態被標記為false,這就意味著會終止所有的上傳。

肯定不會出現這種情況:chunk1重傳3次後失敗了,chunk2還能再去上傳,這樣的話資料肯定不一致了。

8、如何控制上傳多大的檔案?

目前瞭解到nginx端的限制上單次上傳不能超過1M。

前端會對大檔案進行切片突破nginx的限制。

        options: {
          target: 'http://localhost:8081/file/upload',
          chunkSize: 512000, // 單次上傳 512KB 
        }     

如果後續和nginx負責的同學達成一致,可以把這個值進行調整。前端可以後續將這個chunk的閾值加大。

9、如何保證上傳檔案的百分百正確?

在上傳檔案前,前端會計算出當前RD選擇的這個檔案的 md5 值。

當後端檢測到所有的分片全部上傳完畢,這時會merge所有分片匯聚成單個檔案。計算這個檔案的md5 同 RD在前端提供的檔案的md5值比對。 比對結果一致說明RD正確的完成了上傳。結果不一致,說明檔案上傳失敗了~返回給前端任務失敗,提示RD重新上傳。

10、其他細節問題:

如何判斷檔案上傳失敗了,給RD展示紅色?

如何控制上傳什麼型別的檔案?

如何控制不能上傳空檔案?

上面說過了,當 uploader.js 遇到了permanentErrors這種狀態碼時會認為檔案上傳失敗了。

前端想在上傳失敗後,將進度條轉換成紅色,其實改一下CSS樣式就好了,問題就在於,根據什麼去修改?在哪裡去修改?

前端會將每一個file封裝成一個元件:如下圖中的files就是file的集合

image-20200621214536333

整個的fileList會將會被渲染成下面這樣。

image-20200621214718940


我們上傳的檔案被vue-simple-uploader的作者封裝成一個file.vue元件,這個物件中會有個配置引數, 比如它會長下面這樣。

     options: {
        target: 'http://localhost:8081/file/upload',
        statusText: {
          success: '上傳成功',
          error: '上傳出錯,請重試',
          typeError: '暫不支援上傳您新增的檔案格式',
          uploading: '上傳中',
          emptyError:'不能上傳空檔案',
          paused: '請確認檔案後點選上傳',
          waiting: '等待中'
        }
      }
    },

我們將上面的配置新增給Uploader.js

      const uploader = new Uploader(this.options)

在file元件中有如下計算屬性的,分別是status和statusText

    computed: {
      // 計算出一個狀態資訊
      status () {
        const isUploading = this.isUploading // 是否正在上傳
        const isComplete = this.isComplete // 是否已經上傳完成
        const isError = this.error // 是否出錯了
        const isTypeError = this.typeError // 是否出錯了
        const paused = this.paused // 是否暫停了
        const isEmpty = this.emptyError // 是否暫停了
        // 哪個屬性先不為空,就返回哪個屬性
        if (isComplete) {
          return 'success'
        } else if (isError) {
          return 'error'
        } else if (isUploading) {
          return 'uploading'
        } else if (isTypeError) {
          return 'typeError'
        } else if (isEmpty) {
          return 'emptyError'
        } else if (paused) {
          return 'paused'
        } else {
          return 'waiting'
        }
      },
      // 狀態文字提示資訊
      statusText () {
        // 獲取到計算出的status屬性(相當於是個key,具體的值在下面的fileStatusText中獲取到)
        const status = this.status
        // 從file的uploader物件中獲取到 fileStatusText,也就是用自己定義的名字
        const fileStatusText = this.file.uploader.fileStatusText
        let txt = status
        if (typeof fileStatusText === 'function') {
          txt = fileStatusText(status, this.response)
        } else {
          txt = fileStatusText[status]
        }
        return txt || status
      },
    },

status繫結在html上

	<div class="uploader-file" :status="status">

對應的CSS樣式入下:

  .uploader-file[status="error"] .uploader-file-progress {
    background: #ffe0e0;
  }

綜上:有了上面程式碼的編寫,我們可以直接像下面這樣控制就好了

  file.typeError = true // 表示檔案的型別不符合我們的預期,不允許RD上傳
  file.error = true // 表示檔案上傳失敗了
  file.emptyError = true // 表示檔案為空,不允許上傳

11、後端資料庫表設計

CREATE TABLE `file_upload_detail` (                                                                               
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵',                                                           
  `username` varchar(64) NOT NULL COMMENT '上傳檔案的使用者賬號',                                                            
  `file_name` varchar(64) NOT NULL COMMENT '上傳檔名',                                                               
  `md5` varchar(255) NOT NULL COMMENT '上傳檔案的MD5值',                                                                
  `is_uploaded` int(11) DEFAULT '0' COMMENT '是否完整上傳過 \n0:否\n1:是',                                                 
  `has_been_uploaded` varchar(1024) DEFAULT NULL COMMENT '曾經上傳過的分片號',                                             
  `url` varchar(255) DEFAULT NULL COMMENT 'bos中的url,或者是本機的url地址',                                                 
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP  COMMENT '本條記錄建立時間',     
  `update_time` timestamp NULL DEFAULT NULL  COMMENT '本條記錄更新時間',                                                  
  `total_chunks` int(11) DEFAULT NULL COMMENT '檔案的總分片數',                                                          
  PRIMARY KEY (`id`)                                                                                              
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8                                                             

12、關於什麼時候mergechunk

在本文中給出的demo中,merge是後端處理完成所有的chunk後,像前端返回 merge=1,這個表示來實現的。

前端拿著這個欄位去傳送/merge請求去合併所有的chunk。

值得注意的地方是:這個請求是在uploader.js認為所有的分片全部成功上傳後,在單個檔案成功上傳的回撥中執行的。我想了一下,感覺這麼搞其實不太友好,萬一merge的過程中失敗了,或者是某個chunk丟失了,chunk中的資料缺失,最終merge的產物的md5值其實並不等於原檔案。當這種情況發生的時候,其實上傳是失敗的。但是後端既然告訴uploader.js 可以合併了,說明後端的upload函式認為任務是成功的。vue-simple-uploader上傳完最後一個chunk得到的狀態碼是200,它也會覺得任務是成功的,於是在前端段展示綠色的上傳成功給使用者看~(然而上傳是失敗的), 這麼看來,整個過程其實控制的不太好~

我現在的實現:直接幹掉merge請求,前端1條執行緒傳送請求,將chunk依次傳送到後端。後端檢測到所有的chunk都上傳過來後主動merge,merge完成後馬上校驗檔案的md5值是否符合預期。這個處理過程在上傳最後一個chunk的請求中進行,因此可以實現的控制前端上傳成功還是失敗的樣式~

相關文章