大檔案上傳、斷點續傳、秒傳、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。如果我們將前端的併發數調大,就會出現下面的異常情況:
```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的請求中進行,因此可以實現的控制前端上傳成功還是失敗的