快速傳超大檔案的解決方案
需求:
專案要支援大檔案上傳功能,經過討論,初步將檔案上傳大小控制在20G內,因此自己需要在專案中進行檔案上傳部分的調整和配置,自己將大小都以20G來進行限制。
PC端全平臺支援,要求支援Windows,Mac,Linux
支援所有瀏覽器。
支援檔案批量上傳
支援資料夾上傳,且要求在服務端保留層級結構。資料夾數量要求支援到10W。
支援大檔案斷點續傳,要求重新整理瀏覽器,重啟瀏覽器,重啟電腦後仍然能夠繼續上傳。檔案大小要求能夠支援到20個G。
支援自動載入本地檔案,要求能夠自動載入指定的本地檔案。
支援檔案批量下載,要求不要在伺服器打包。因為20G的檔案在伺服器打包時間比較長。
支援資料夾下載,要求不要在伺服器打包,下載到本地後要求保留層級結構
檔案列表面板支援路徑導航,新建資料夾
一. 大檔案上傳基礎描述:
各種WEB框架中,對於瀏覽器上傳檔案的請求,都有自己的處理物件負責對Http MultiPart協議內容進行解析,並供開發人員呼叫請求的表單內容。
比如:
Spring 框架中使用類似CommonsMultipartFile物件處理表二進位制檔案資訊。
而.NET 中使用HtmlInputFile/ HttpPostedFile物件處理二進位制檔案資訊。
優點:使用框架內建物件可以很方便的處理來自瀏覽器的MultiPart二進位制資訊請求,協議分析操作不用開發人員參與。
缺點:其接收資料包過程完全被封閉在框架內建物件中,直到本次請求資訊處理(接收)完畢後,才允許開發人員從介面調取表單及檔案內容。上傳過程中的進度資訊無法訪問,無法上傳大尺寸檔案(比如幾百兆以上的大檔案二進位制資訊)。
目標:我們要在JAVA WEB框架中,依靠Filter過濾器的能力,實現不依靠框架內建物件,從瀏覽器請求位元組流中解析MultiPart協議,取得本次使用者請求的所有資訊,包括多二進位制檔案資訊及其他表單項資訊。使用者上傳的檔案尺寸將不受限制。而且在傳輸過程中,我們可以實時獲得當前傳輸進度資訊。
注:.NET框架中可依靠IHttpModule介面物件達到JAVA框架中Filter的能力,本文不做描述。
之前仿造
但無論外掛再怎麼靈活,也難以應付所有的需求,比如,你要上傳一個2G的檔案。以現在我們的網速,恐怕再快也得傳半小時。要命的是,如果你在上傳到90%的時候不小心關掉了瀏覽器,或者是手一抖摁了F5,完了,一切還得從頭再來。這種使用者體驗簡直太糟糕了。所以,斷點續傳就十分有必要了。什麼是續傳我就不解釋了,用QQ傳檔案這麼多年,大家都見過了。
這裡要說的是斷點續傳都有哪些技術要點。使用傳統的表單提交檔案或是HTML5的FormData都是將檔案“整塊”提交,服務端取到該檔案後再進行轉移、重新命名等操作,因此,無法實時儲存檔案的已上傳部分。而且在http協議下,我們無法保持瀏覽器與服務端的長連線,不能以檔案流的形式來提交。所以要解決的問題具體來講有以下幾點:
對上傳的檔案進行分割,每次只上傳一小片。服務端接收到檔案後追加到原來部分,最後合併成完整的檔案。
每次上傳檔案片前先獲取已上傳的檔案大小,確定本次應切割的位置
每次上傳完成後更新已上傳檔案大小的記錄
標識客戶端和服務端的檔案,保證不會把A檔案的內容追加到B檔案上
在參考了張鑫旭大哥的
工作原理/技術要點
首先的首先,要明確,如果我們有一個10M的檔案,每次切割上傳1M,那麼是需要發10次請求來完成的。在http協議下,只能這麼搞。斷點上傳分三步來完成:
選擇一個檔案後,獲取該檔案在伺服器上的大小,通過本地儲存或自定義的函式來獲取。
根據已上傳大小切割檔案,發出n次請求不斷向伺服器提交檔案片,服務端不斷追加檔案內容
當已上傳檔案大小達到檔案總大小時,上傳結束
首先是檔案的分割,HTML5新增了Blob資料型別,並且提供了一個可以分割資料的方法:slice(),其用法和字串、陣列的slice()方法一樣,可以擷取一個二進位制檔案的一部分。
其次是檔案片的儲存與追加,我後臺用PHP寫的,先用file_get_contents獲取檔案的二進位制格式,再用file_put_contents每次將檔案追加,具體的寫法可以參照後面,或者是下載我打包好的檔案。
接下來我們還需要實時儲存已上傳檔案的大小,以便於下次上傳前進行正確切割。使用HTML5的localStorage是一種方法,將已上傳的大小儲存在本地,下次上傳前先從本地讀取。不過這種方式是很侷限的,拋開使用者可能通過各種管家清除掉本地資料不講,假如使用者在A頁面上傳了一個檔案的50%,然後在B頁面想把該檔案上傳到另外一個地方,結果從本地一讀檔案已上傳50%了,直接從51%的位置開始上傳了,顯然是個錯誤。問題就在於本地不能存太多的資訊,通過File API只能獲取到檔案的原始名稱,無法正確的與伺服器上的檔案正確匹配。所以真正在專案中用,還得依靠服務端來儲存這些資料。
關於如何將資料存在服務端,已經前端如何取資料,我在下面會講到。
技術要點就上面的那麼多了,其實也沒有多少技術含量哈~來看看我的外掛如何使用吧。
續傳功能的使用方法
檔案的引入就不講了,可參考上一篇關於外掛的介紹。關鍵點是新增的幾個配置,先來看一下:
breakPoints:false,//是否開啟斷點續傳
fileSplitSize:1024*1024,//斷點續傳的檔案塊大小,單位Byte,預設1M
getUploadedSize:null,//型別:function,自定義獲取已上傳檔案的大小函式,用於開啟斷點續傳模式,可傳入一個引數file,即當前上傳的檔案物件,需返回number型別
saveUploadedSize:null,//型別:function,自定義儲存已上傳檔案的大小函式,用於開啟斷點續傳模式,可傳入兩個引數:file:當前上傳的檔案物件,value:已上傳檔案的大小,單位Byte
saveInfoLocal:false,//用於開啟斷點續傳模式,是否使用localStorage儲存已上傳檔案大小
這是外掛中的預設配置值。一個續傳功能竟然要配置五個項,真要命!不要著急聽我慢慢道來,這五個並不是要同時出現的,是為了滿足可能出現的複雜業務而準備的。
breakPoints是開啟斷點續傳的開關,要使用的話設為true,預設是不開啟的。
fileSplitSize是每次切割的檔案片的大小,預設是1M,可根據實際情況來定。如果你的系統上傳的檔案普遍都在1G以上,可以配置的大一點。
getUploadedSize是用來自定義獲取已上傳的檔案大小的函式,還記得上面說過的localStorage的侷限吧,所以我這裡直接把獲取檔案大小的函式交給你來定義,你可以從session、cookie,從檔案、資料庫或者任何地方取,可以傳送一個ajax請求到你想要的地址,傳遞你需要的引數。注意你定義的函式將來會被外掛呼叫,所以一定要返回一個Number型別的結果。
saveUploadedSize與getUploadedSize對應,你自己定義如何儲存已上傳檔案的大小,只要你存的資料你自己能取到就OK。當然前提是你要注意到上面說過的localStorage的侷限,確保你的邏輯正確能夠操作到正確的檔案。
saveInfoLocal是當你使用localStorage儲存資料時需要開啟的一個開關。外掛預設提供使用localStorage方式的支援。只要開啟此選項就可以了。當然,這種情況下你的業務邏輯必須足夠簡單,比如只是做一個上傳的demo,或者這系統的使用者只有你一個人,你明白如何避開那些侷限的地方。
掌握了這五個配置的作用,你就可以實現一個足夠靈活的斷點上傳功能了!在我打包好的檔案裡,提供了使用localStorage方式的demo,抱歉我無法將資料庫表都發給你,所以只能用本地儲存來演示。
在服務端儲存資料
使用者在使用上傳的時候可能有各種你意想不到的操作,這裡我發揮想象描述一下使用者可能的行為:
同一臺機器使用不同帳號登入,上傳同一個檔案
檔案上傳了一部分,然後修改了檔案內容,再次上傳
檔案上傳完成100%,再次上傳該檔案
同一個頁面有多個上傳按鈕,上傳同一個檔案,或在不同頁面上傳同一個檔案
僅僅上面四條,是不是情況就夠複雜了?再加上你係統還有自己的業務邏輯,所以在服務端儲存已上傳檔案資料是非常有必要的。而且儲存資料和獲取資料的函式都交給你來定義,抱著外掛有足夠的靈活性。
因為涉及到了服務端的技術,無法演示,我將我專案中的真實使用場景在此講解一下,來展示一下如何自已定義方法來實現服務端儲存資料的可靠上傳。我定義的getUploadedSize函式如下:
getUploadedSize:function(file){
var data = {
data : {
fileName : file.name,
lastModifiedDate : file.lastModifiedDate.getTime()
}
};
var url = 'http://localhost/uploadfile/';
var uploadedSize = 0;
$.ajax({
url : url,
data : data,
async : false,
type : 'POST',
success : function(returnData){
returnData = JSON.parse(returnData);
uploadedSize = returnData.uploadedSize;
}
});
return uploadedSize;
}
我向後臺的某個地址傳送一個請求,傳遞檔名和檔案的最後修改時間為引數,後臺根據這兩個引數來找到與前臺所選擇的檔案對應的伺服器上的檔案,將伺服器返回的檔案大小return出去,來被外掛使用。為什麼要傳遞這兩個引數呢?我們在前臺無法知道伺服器上的這個檔案的名稱,所以使用原始檔名作為一個輔助標識。為了防止使用者在兩次上傳間隔修改了檔案,我們把檔案的最後修改時間也傳給服務端,讓服務端進行比較,若時間不對應則返回已上傳大小為0,重新上傳此檔案。
再來看後臺都要做哪些工作。資料庫中需要有一張表來記錄每個已檔案的情況,包含的欄位大致有:
欄位 |
描述 |
client_filename |
檔案在客戶端的原始名稱 |
server_filename |
檔案在伺服器上重新命名後的名稱 |
last_modified_date |
檔案的最後修改時間,時間戳 |
status |
檔案的狀態,已完成、未完成 |
uploaded_size |
已上傳檔案的大小 |
根據client_filename和last_modified_date,再加上系統中的其他關聯資訊,可以定位到本次上傳的檔案在服務端的大小,然後返回給客戶端。當然這是我自己的用法,你也可以根據自己的需求靈活設計。總之最終的目的就是要找到前臺選擇的檔案在伺服器上真正對應的檔案,並將已上傳大小正確返回。
另外需注意的一點,就是在續傳的第二步,不斷提交檔案片的過程中,也需要服務端準確定位到相應的檔案,不能把A的資料追加到B上。採用的方式也是提交fileName和lastModifyDate兩個引數(已寫在外掛內部,可服務端直接獲取),服務端找到對應的檔案進行追加。
另外再囉嗦一句,後臺獲取檔案的時候需要取成二進位制的,而我們提交是使用FormData來提交的,所以PHP程式碼需要這麼寫:
file_put_contents('uploads/'.$filename,file_get_contents($_FILES["file"]["tmp_name"]),FILE_APPEND);
如果上面的說明還是不夠清楚,就需要你自己來探索一下了,畢竟考慮到外掛可能應用在複雜的系統中,很多工作還是需要你來做的。或者你也可以給我留言,我很樂意為你解答疑惑。
該版本的其他改動
從1.0到2.0,Huploadify又新加了很多東西,不過只是新加,使用方式跟之前的沒有變化。例如上面的斷點續傳功能,你如果不想使用,只需設定breakPoints為false即可,外掛仍按照以前的方式工作。除了斷點續傳這個大頭,外掛還做了如下改動:
增加了onSelect回撥函式,在選擇了檔案之後觸發,用法與uploadify官網的一致
刪除掉正在上傳的檔案,中斷髮送請求
完善了input file元件的accept屬性支援,瀏覽時只顯示執行的檔案格式,就是這個東東:
4. 對外開放了方法呼叫介面,upload、stop、cancel、disable、ennable。我在demo中有演示。使用方法如下:
var up = $('#upload').Huploadify({
auto:false,
fileTypeExts:'*.jpg;*.png;*.exe;*.mp3;*.mp4;*.zip;*.doc;*.docx;*.ppt;*.pptx;*.xls;*.xlsx;*.pdf',
multi:true
});
up.upload(1);//開始上傳檔案,接收一個引數,表示上傳第幾個檔案,可傳入*上傳佇列中的所有檔案
up.stop();//暫停上傳佇列中的所有檔案,不接收引數。用於開啟了斷點需傳
up.cancel(1);//刪除佇列中的某個檔案,接收一個引數,表示刪除第幾個檔案,可傳入*刪除佇列中的所有檔案
up.disable();//使選擇檔案按鈕失效,不接收引數
up.ennable();//使選擇檔案按鈕生效,不接收引數 5. 修改其他已知bug
結束
我在demo中使用了本地儲存來做已上傳檔案大小的儲存,下載壓縮包後可看一下效果。上傳一個比較大的視訊檔案,上傳到中間關閉瀏覽器,再次開啟瀏覽器上傳同一個檔案,會看到從上次斷掉的地方繼續上傳。
詳細內容可以參考我寫的這篇文章:
討論群:374992201