上传文件
什么是分片上传
分片上传功能是将一个文件切割为一系列特定大小的小数据片,分别将这些
小数据片分别上传到服务端,全部上传完后再由服务端将这些小数据片合并成为一个完整的资源。
为什么要分片上传
在上传普通大小文件时,分片上传和普通上传的效果体验相差不大。但是在上传大型文件时,普通传统文件上传速度缓慢分片上传 可以大大提高上传的速度 并且可以实现断点续传操作,也就是当某一片上传失败时可以记录下来,进行重传或者其他处理,分片的附带好处还能很方便的实现进度条
这里直接代码 展示我的实现过程
前端
上传文件样式
前端分片操作
beforeUpload(){
return false //这里要返回 否则会上传失败
},
handleChange:function(file){
console.log(file.file)
//获取文件大小 1k = 1024字节
var size = file.file.size
console.log(size)
// 定义分片大小
var shardSize = 1024 * 200
// 总片数 向上去整
this.shardCount = Math.ceil(size / shardSize)
if(this.shardCount == 1){ // 如果只分了一片 就直接上传即可
let data1 = new FormData(); // 将数据文件与数据表单中
data1.append('file', file.file);
data1.append('filename',file.file.name);
// 参数与分片上传相比 少了分片个数 用于后端识别是否是分片上传
axios.post(this.baseURL + 'upload/',data1).then(resp=>{
console.log('上传文件',resp)
if(resp.data.errcode==0){
this.websrc = this.upload_dir + '/' + file.file.name
this.src = file.file.name
}
})
/* A */
}else{ // 否则进行切片操作
// 切片操作
for(var i=0;i<this.shardCount;i++){ // 循环切片次数
//开始位置 每一次循环的开始位置
var start = i * shardSize
// 结束位置 取最小值 如 378,0+200取200 378,200+200取378
var end = Math.min(size,start+shardSize)
//切片 slice 使用slice方法对文件进行分割
var shardfile = file.file.slice(start,end)
let data = new FormData(); // 将数据文件与数据表单中
data.append('file', shardfile);
data.append('count',i);
data.append('filename',file.file.name);
// 进行上传
axios.post(this.baseURL + 'upload/',data).then(resp=>{
console.log('分片上传',resp)
if(resp.data.errcode==0){ // 如果这一次分片上传成功了
this.finish +=1 // 记录一下上传次数
if(this.finish == this.shardCount){ // 如果上传次数 等于 分片个数 代表所有分片都上传了 进行合并即可
//合并分片
this.myaxios(this.baseURL + 'upload/','put',{filename:file.file.name,"size":size,"count":(this.shardCount)}).then(resp=>{
// this.myaxios('http://localhost:8888/scheduler/','get',{job_id:this.job_id,filename:file.file.name,"size":size,"count":this.shardCount}).then(resp=>{
if(resp.errcode==0){
this.websrc = this.upload_dir + '/' + file.file.name // 合并成功 赋值给一个变量 用于展示文件
this.src = file.file.name
}
// })
// if(resp.errcode==0){
// this.websrc = this.upload_dir + '/' + file.file.name
// this.src = file.file.name
// }
})
this.finish = 0 // 合并后对上传次数清零 用于下次上传分片
}
}
})
}
}
},
后端接口 上传文件接口
async def post(self):
# 获取文件实体
file = self.request.files['file'][0]
#获取分片次数下标 用来标记分片文件顺序
count = self.get_argument('count',None)
# 获取文件名
filename = self.get_argument('filename',None)
if not count: # 如果没有传分片次数 代表文件比较小 不需要分片 普通上传即可
content = file['body'] # 获取文件流
async with aiofiles.open(f'./static/upload/{filename}', 'wb')as f:
await f.write(content)
res = {'errcode': 0, 'msg': '文件已上传'}
# 获取文件内容
else:
content = file['body'] # 获取文件流
async with aiofiles.open(f'./static/upload/{count}_{filename}','wb')as f: # 写入文件 用count做标记 用来记录分片顺序
await f.write(content)
res = {'errcode': 0, 'msg': '分片已上传'}
return self.finish(res)
后端 合并分片接口
async def put(self):
filename = self.get_argument('filename', None) # 文件名
size = self.get_argument('size', None) # 文件总大小
try: # 查询合并后的文件是否已存在 并获取他的大小
f_size = os.path.getsize(f'./static/upload/{filename}')
except Exception as e: # 如果报错 说明没有此文件
f_size = 0
if int(size) != f_size: # 如果大小不相等 代表要么没有 要么之前合并错误
# 打开文件句柄
f_count = 0 # 循环次数
async with aiofiles.open(f'./static/upload/{filename}','ab')as f: # 创建并打开文件 开始合并
while True:
try:
# 读取分片
shard_file = open(f'./static/upload/{f_count}_{filename}','rb') # 读取每个分片文件实体
await f.write(shard_file.read()) # 写入
shard_file.close() # 关闭文件
except Exception as e: # 如果报错了 代表已经没有分片文件可以合并了
break
f_count+=1
return self.finish({'errcode': 1, 'msg': '合并完成'})
这里还写了一个接口 用于触发合并后对文件合并后的处理 使用了apscheduler定时任务
import os
import re
import redis
from tornado.ioloop import IOLoop,PeriodicCallback
from tornado.web import RequestHandler,Application,url
from apscheduler.schedulers.tornado import TornadoScheduler
scheduler = None
r = redis.Redis(decode_responses=True)
# 初始化
def init_scheduler(): # 初始化scheduler 对象
global scheduler
scheduler = TornadoScheduler()
scheduler.start() # 定时任务启动
print('定时任务启动')
# 声明任务
# 任务id
def task(job_id,filename,size,count):
filelist = os.listdir('./static/upload') # 获取文件夹下所有文件
re_list = []
for i in filelist:
res = re.match(r'^\d+_{}'.format(filename),i) # 利用正则匹配出此文件的所有分片文件名 放入列表中
if res:
re_list.append(i)
f_count = len(re_list)
if int(count) == f_count: # 如果分片文件个数等于分片次数
try: # 获取文件大小
f_size = os.path.getsize(f'./static/upload/{filename}')
except Exception as e: # 报错代表文件没有合并完成
f_size = 0
if int(size) == f_size: # 如果合并后文件大小等于上传的文件大小 代表合并完成
for i in range(0, f_count): # 循环次数下标
os.remove(f'./static/upload/{i}_{filename}') # 删除每个分片文件
r.lrem(filename,1,job_id) # 删除redis中的 任务id
scheduler.remove_job(job_id) # 删除这个定时任务
print('分片执行完毕,定时任务删除')
return True
print('合并未成功')
return False
print('分片传输未成功')
return False
# 声明服务控制器
class SchedulerHandler(RequestHandler):
async def get(self):
job_id = self.get_argument('job_id',None) # 获取任务id
filename = self.get_argument('filename', None)
size = self.get_argument('size', None) # 文件总大小
count = self.get_argument('count', None) # 分片个数
job_ids = r.lrange(filename,0,-1) # 获取redis中存的所有任务id
print(job_ids)
if job_id not in job_ids: # 不存在 执行这个定时任务 存在 代表这个任务还没完成
r.lpush(filename,job_id) # 任务id加入redis
# 任务函数名 间隔时间三秒 任务函数参数
scheduler.add_job(task,'interval',seconds=3,id=job_id,args=(job_id,filename,size,count)) # 开启定时任务
print('定时任务入队')
res = {'errcode':0,'msg':'ok'}
else:
res = {'errcode':1, 'msg': 'failed'}
return self.finish(res)
if __name__ == '__main__':
routes = [url(r'/scheduler/',SchedulerHandler)]
init_scheduler()
application = Application(routes,debug=True)
application.listen(8888)
IOLoop.current().start() # 开启事件循环
application = Application(routes,debug=True)
application.listen(8888)
IOLoop.current().start() # 开启事件循环