轉載請注明出處: http://blog.csdn.net/jinixin/article/details/77545140
引言
想借着這篇文章簡要談談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已經為我們解決了,詳見下面代碼。
<script type="text/javascript">
$(document).ready(function() {
var task_id = WebUploader.Base.guid(); //産生task_id,唯一辨別該檔案
var uploader = WebUploader.create({
server: '/upload/accept', //伺服器接收并處理分片的url位址
formData: {
task_id: task_id, //上傳分片的http請求攜帶的資料
},
});
uploader.on('uploadSuccess', function(file) { //當該檔案所有分片均上傳成功時調用該函數
//上傳的資訊(檔案唯一辨別符,檔案字尾名)
var data = { 'task_id': task_id, 'ext': file.source['ext'], 'type': file.source['type'] };
$.get('/upload/complete', data); //ajax攜帶data向該url發請求
});
});
</script>
2)如何處理接收分片和将分片内容寫入檔案的關系?
方案一:開一個字元串,一邊接收分片,一邊将分片裡的内容讀取出來後添加到字元串末尾;全部分片接收完畢後,再将字元串寫入新檔案中。
方案二:建立一個檔案,一邊接收分片,一邊将分片裡的内容讀取出來寫入檔案末尾。
方案三:為每個分片建立一個新的臨時檔案來儲存其内容;待全部分片上傳完畢後,再按順序讀取所有臨時檔案的内容,将資料寫入新檔案中。
前兩個方案看似不錯,但其實有些問題。方案一因等待所有分片的時間過長容易造成記憶體溢出;由于分片不一定是按序上傳,是以方案二也不行;故隻能選擇方案三了。
3)後端如何區分不同使用者的檔案?如何區分同個檔案不同分片的先後順序?
通過http請求攜帶的task_id可以區分不同的檔案。同個檔案分片的先後順序,可以通過http請求攜帶的chunk來區分。是以,task_id+chunk的組合可以在衆多不同使用者不同檔案的分片中唯一标記某個分片,即某個檔案的某個分片名稱是task_id+chunk。
關鍵代碼
前端代碼
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<script src="./static/jquery-1.11.1.min.js"></script>
<script src="./static/bootstrap/js/bootstrap.min.js"></script>
<script src="./static/webuploader/webuploader.min.js"></script>
<link rel="stylesheet" type="text/css" href="./static/webuploader/webuploader.css" target="_blank" rel="external nofollow" >
<link rel="stylesheet" type="text/css" href="./static/bootstrap/css/bootstrap.min.css" target="_blank" rel="external nofollow" >
</head>
<body>
<div>
<div id="picker">請選擇</div> <!-- 上傳按鈕,必須指定id選擇器的值 -->
<div class="progress"> <!-- 進度條 -->
<div class="progress-bar progress-bar-striped active" role="progressbar" style="width:0%;"></div>
</div>
</div>
<script type="text/javascript">
$(document).ready(function() {
var task_id = WebUploader.Base.guid(); //産生task_id
var uploader = WebUploader.create({ //建立上傳控件
swf: './static/webuploader/Uploader.swf', //swf位置,這個可能與flash有關
server: '/upload/accept', //接收每一個分片的伺服器位址
pick: '#picker', //填上傳按鈕的id選擇器值
auto: true, //選擇檔案後,是否自動上傳
chunked: true, //是否分片
chunkSize: 20 * 1024 * 1024, //每個分片的大小,這裡為20M
chunkRetry: 3, //某分片若上傳失敗,重試次數
threads: 1, //線程數量,考慮到伺服器,這裡就選了1
duplicate: true, //分片是否自動去重
formData: { //每次上傳分片,一起攜帶的資料
task_id: task_id,
},
});
uploader.on('startUpload', function() { //開始上傳時,調用該方法
$('.progress-bar').css('width', '0%');
$('.progress-bar').text('0%');
});
uploader.on('uploadProgress', function(file, percentage) { //一個分片上傳成功後,調用該方法
$('.progress-bar').css('width', percentage * 100 - 1 + '%');
$('.progress-bar').text(Math.floor(percentage * 100 - 1) + '%');
});
uploader.on('uploadSuccess', function(file) { //整個檔案的所有分片都上傳成功,調用該方法
//上傳的資訊(檔案唯一辨別符,檔案名)
var data = {'task_id': task_id, 'filename': file.source['name'] };
$.get('/upload/complete', data); //ajax攜帶data向該url發請求
$('.progress-bar').css('width', '100%');
$('.progress-bar').text('上傳完成');
});
uploader.on('uploadError', function(file) { //上傳過程中發生異常,調用該方法
$('.progress-bar').css('width', '100%');
$('.progress-bar').text('上傳失敗');
});
uploader.on('uploadComplete', function(file) {//上傳結束,無論檔案最終是否上傳成功,該方法都會被調用
$('.progress-bar').removeClass('active progress-bar-striped');
});
});
</script>
</body>
</html>
後端代碼
@app.route('/', methods=['GET', 'POST'])
def index(): # 一個分片上傳後被調用
if request.method == 'POST':
upload_file = request.files['file']
task = request.form.get('task_id') # 擷取檔案唯一辨別符
chunk = request.form.get('chunk', 0) # 擷取該分片在所有分片中的序号
filename = '%s%s' % (task, chunk) # 構成該分片唯一辨別符
upload_file.save('./upload/%s' % filename) # 儲存分片到本地
return rt('./index.html')
@app.route('/success', methods=['GET'])
def upload_success(): # 所有分片均上傳完後被調用
target_filename = request.args.get('filename') # 擷取上傳檔案的檔案名
task = request.args.get('task_id') # 擷取檔案的唯一辨別符
chunk = 0 # 分片序号
with open('./upload/%s' % target_filename, 'wb') as target_file: # 建立新檔案
while True:
try:
filename = './upload/%s%d' % (task, chunk)
source_file = open(filename, 'rb') # 按序打開每個分片
target_file.write(source_file.read()) # 讀取分片内容寫入新檔案
source_file.close()
except IOError:
break
chunk += 1
os.remove(filename) # 删除該分片,節約空間
return rt('./index.html')
結果
效果圖
測試
三台計算機,一台做伺服器,分别在另兩台上同時各上傳一本電影,大小為2.6G與3.8G;上傳完畢後,兩本電影均可在伺服器上正常播放。
備注
如果有想要源碼的朋友,可以移步這裡。對該項目還會不斷改進,如果你感興趣,不妨star一下,謝謝。