1. 程式人生 > >大檔案上傳、斷點續傳、秒傳、beego、vue

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

## 大檔案上傳 ### 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。如果我們將前端的併發數調大,就會出現下面的異常情況: ```go 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上傳的有問題,於是重新上傳~ ```js options: { target: 'http://localhost:8081/file/upload', maxChunkRetries: 3, permanentErrors:[502], // 永久性的上傳失敗~,會認為整個檔案都上傳失敗了 successStatuses:[200], // 當前chunk上傳成功後的狀態嗎 ... } ``` ### 7、超過重傳次數後,怎麼辦? 比如我們設定出錯後重傳的次數為3,那麼無論當前分片是第幾片,整個檔案的上傳狀態被標記為false,這就意味著會終止所有的上傳。 肯定不會出現這種情況:chunk1重傳3次後失敗了,chunk2還能再去上傳,這樣的話資料肯定不一致了。 ### 8、如何控制上傳多大的檔案? 目前瞭解到nginx端的限制上單次上傳不能超過1M。 前端會對大檔案進行切片突破nginx的限制。 ```js 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](https://img2020.cnblogs.com/blog/1496926/202006/1496926-20200622231557172-2130180178.png) 整個的fileList會將會被渲染成下面這樣。 ![image-20200621214718940](https://img2020.cnblogs.com/blog/1496926/202006/1496926-20200622231557931-318079909.png) --- 我們上傳的檔案被vue-simple-uploader的作者封裝成一個file.vue元件,這個物件中會有個配置引數, 比如它會長下面這樣。 ```js options: { target: 'http://localhost:8081/file/upload', statusText: { success: '上傳成功', error: '上傳出錯,請重試', typeError: '暫不支援上傳您新增的檔案格式', uploading: '上傳中', emptyError:'不能上傳空檔案', paused: '請確認檔案後點擊上傳', waiting: '等待中' } } }, ``` 我們將上面的配置新增給Uploader.js ```go const uploader = new Uploader(this.options) ``` 在file元件中有如下計算屬性的,分別是status和statusText ```js 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上 ```html ``` 對應的CSS樣式入下: ```css .uploader-file[status="error"] .uploader-file-progress { background: #ffe0e0; } ``` 綜上:有了上面程式碼的編寫,我們可以直接像下面這樣控制就好了 ```js file.typeError = true // 表示檔案的型別不符合我們的預期,不允許RD上傳 file.error = true // 表示檔案上傳失敗了 file.emptyError = true // 表示檔案為空,不允許上傳 ``` ### 11、後端資料庫表設計 ```bash 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的請求中進行,因此可以實現的控制前端上傳成功還是失敗的