1. 程式人生 > >前端上傳元件WebUploader大檔案上傳與Python結合的實現

前端上傳元件WebUploader大檔案上傳與Python結合的實現

Python實現大檔案分片上傳

引言

想借著這篇文章簡要談談WebUploader大檔案上傳與Python結合的實現。

WebUploader是百度團隊對大檔案上傳的前端實現,而後端需要根據不同的語言自己實現。這裡我採用Python語言的Flask框架搭建後端,配合使用Bootstrap前端框架渲染上傳進度條,效果圖在文章底部。

WebUploader官網:點這裡;WebUploader API:點這裡


實施

http協議並不是非常適合上傳大檔案,所以要考慮分片,即把大檔案分割後再上傳,而WebUploader所做的事,正是將一個大檔案分片,一部分一部分的上傳到伺服器。在上傳每個分片的http請求中,需要同時攜帶:

1)該檔案的唯一標識:task_id;

2)該檔案的分片總數:chunks;

3)該分片在該檔案所有分片中的位置:chunk;

其中後兩個WebUploader已經替我們自動上傳了,而第一個task_id僅需要我們呼叫對應函式即可產生,然後再將其寫入form-data。

WebUploader是一個前端框架,所以接收檔案的部分需要我們自己實現,而我選用了Python和其的Flask框架。
後端要做的是接收這一大堆分片,然後將它們重新合併成一個檔案,那麼有如下三個問題:

1)如何判定某個分片上傳後,是不是整個檔案也上傳結束了?

WebUploader已經為我們解決了,詳見下面程式碼。

  1. <script type="text/javascript">

  2. $(document).ready(function() {

  3. var task_id = WebUploader.Base.guid(); //產生task_id,唯一標識該檔案

  4. var uploader = WebUploader.create({

  5. server: '/upload/accept', //伺服器接收並處理分片的url地址

  6. formData: {

  7. task_id: task_id, //上傳分片的http請求攜帶的資料

  8. },

  9. });

  10. uploader.on('uploadSuccess', function(file) { //當該檔案所有分片均上傳成功時呼叫該函式

  11. //上傳的資訊(檔案唯一識別符號,檔案字尾名)

  12. var data = { 'task_id': task_id, 'ext': file.source['ext'], 'type': file.source['type'] };

  13. $.get('/upload/complete', data); //ajax攜帶data向該url發請求

  14. });

  15. });

  16. </script>

2)如何處理接收分片和將分片內容寫入檔案的關係? 方案一:開一個字串,一邊接收分片,一邊將分片裡的內容讀取出來後新增到字串末尾;全部分片接收完畢後,再將字串寫入新檔案中。 方案二:建立一個檔案,一邊接收分片,一邊將分片裡的內容讀取出來寫入檔案末尾。

方案三:為每個分片建立一個新的臨時檔案來儲存其內容;待全部分片上傳完畢後,再按順序讀取所有臨時檔案的內容,將資料寫入新檔案中。

前兩個方案看似不錯,但其實有些問題。方案一因等待所有分片的時間過長容易造成記憶體溢位;由於分片不一定是按序上傳,所以方案二也不行;故只能選擇方案三了。

3)後端如何區分不同使用者的檔案?如何區分同個檔案不同分片的先後順序? 通過http請求攜帶的task_id可以區分不同的檔案。同個檔案分片的先後順序,可以通過http請求攜帶的chunk來區分。因此,task_id+chunk的組合可以在眾多不同使用者不同檔案的分片中唯一標記某個分片,即某個檔案的某個分片名稱是task_id+chunk。

關鍵程式碼


前端程式碼

  1. <!DOCTYPE html>

  2. <html>

  3. <head>

  4. <meta http-equiv="content-type" content="text/html; charset=utf-8" />

  5. <script src="./static/jquery-1.11.1.min.js"></script>

  6. <script src="./static/bootstrap/js/bootstrap.min.js"></script>

  7. <script src="./static/webuploader/webuploader.min.js"></script>

  8. <link rel="stylesheet" type="text/css" href="./static/webuploader/webuploader.css">

  9. <link rel="stylesheet" type="text/css" href="./static/bootstrap/css/bootstrap.min.css">

  10. </head>

  11. <body>

  12. <div>

  13. <div id="picker">請選擇</div> <!-- 上傳按鈕,必須指定id選擇器的值 -->

  14. <div class="progress"> <!-- 進度條 -->

  15. <div class="progress-bar progress-bar-striped active" role="progressbar" style="width:0%;"></div>

  16. </div>

  17. </div>

  18. <script type="text/javascript">

  19. $(document).ready(function() {

  20. var task_id = WebUploader.Base.guid(); //產生task_id

  21. var uploader = WebUploader.create({ //建立上傳控制元件

  22. swf: './static/webuploader/Uploader.swf', //swf位置,這個可能與flash有關

  23. server: '/upload/accept', //接收每一個分片的伺服器地址

  24. pick: '#picker', //填上傳按鈕的id選擇器值

  25. auto: true, //選擇檔案後,是否自動上傳

  26. chunked: true, //是否分片

  27. chunkSize: 20 * 1024 * 1024, //每個分片的大小,這裡為20M

  28. chunkRetry: 3, //某分片若上傳失敗,重試次數

  29. threads: 1, //執行緒數量,考慮到伺服器,這裡就選了1

  30. duplicate: true, //分片是否自動去重

  31. formData: { //每次上傳分片,一起攜帶的資料

  32. task_id: task_id,

  33. },

  34. });

  35. uploader.on('startUpload', function() { //開始上傳時,呼叫該方法

  36. $('.progress-bar').css('width', '0%');

  37. $('.progress-bar').text('0%');

  38. });

  39. uploader.on('uploadProgress', function(file, percentage) { //一個分片上傳成功後,呼叫該方法

  40. $('.progress-bar').css('width', percentage * 100 - 1 + '%');

  41. $('.progress-bar').text(Math.floor(percentage * 100 - 1) + '%');

  42. });

  43. uploader.on('uploadSuccess', function(file) { //整個檔案的所有分片都上傳成功,呼叫該方法

  44. //上傳的資訊(檔案唯一識別符號,檔名)

  45. var data = {'task_id': task_id, 'filename': file.source['name'] };

  46. $.get('/upload/complete', data); //ajax攜帶data向該url發請求

  47. $('.progress-bar').css('width', '100%');

  48. $('.progress-bar').text('上傳完成');

  49. });

  50. uploader.on('uploadError', function(file) { //上傳過程中發生異常,呼叫該方法

  51. $('.progress-bar').css('width', '100%');

  52. $('.progress-bar').text('上傳失敗');

  53. });

  54. uploader.on('uploadComplete', function(file) {//上傳結束,無論檔案最終是否上傳成功,該方法都會被呼叫

  55. $('.progress-bar').removeClass('active progress-bar-striped');

  56. });

  57. });

  58. </script>

  59. </body>

  60. </html>

後端程式碼

  1. @app.route('/', methods=['GET', 'POST'])

  2. def index(): # 一個分片上傳後被呼叫

  3. if request.method == 'POST':

  4. upload_file = request.files['file']

  5. task = request.form.get('task_id') # 獲取檔案唯一識別符號

  6. chunk = request.form.get('chunk', 0) # 獲取該分片在所有分片中的序號

  7. filename = '%s%s' % (task, chunk) # 構成該分片唯一識別符號

  8. upload_file.save('./upload/%s' % filename) # 儲存分片到本地

  9. return rt('./index.html')

  10. @app.route('/success', methods=['GET'])

  11. def upload_success(): # 所有分片均上傳完後被呼叫

  12. target_filename = request.args.get('filename')  # 獲取上傳檔案的檔名

  13. task = request.args.get('task_id')  # 獲取檔案的唯一識別符號

  14. chunk = 0  # 分片序號

  15. with open('./upload/%s' % target_filename, 'wb') as target_file: # 建立新檔案

  16. while True:

  17. try:

  18. filename = './upload/%s%d' % (task, chunk)

  19. source_file = open(filename, 'rb') # 按序開啟每個分片

  20. target_file.write(source_file.read()) # 讀取分片內容寫入新檔案

  21. source_file.close()

  22. except IOError:

  23. break

  24. chunk += 1

  25. os.remove(filename) # 刪除該分片,節約空間

  26. return rt('./index.html')

結果

效果圖

上傳成功

上傳失敗

測試

三臺計算機,一臺做伺服器,分別在另兩臺上同時各上傳一本電影,大小為2.6G與3.8G;上傳完畢後,兩本電影均可在伺服器上正常播放。


備註

如果有想要原始碼的朋友,可以移步這裡。對該專案還會不斷改進,如果你感興趣,不妨star一下,謝謝。