本文介绍一种文件切片上传策略,以应对项目开发中的可能遇到的大文件上传的需求。客户端采用Vue 3.0,服务器端提供Express和Asp.Net Core两种实现。
Web客户端将用户要上传的文件,按照指定的尺寸进行分片切割,每次只上传一片,并告知服务器端已经上传的文件大小。
服务器端根据客户端告知的已上传文件大小,决定每次是执行文件的创建操作还是继续进行写文件操作。
整个上传操作由客户端主导,当最后一个文件片上传成功后,上传操作完成。服务器端只进行写文件的操作,不进行任何状态管理。
客户单代码的只要任务是文件切割,并逐个将表单传给服务器端。
客户度采用Vue 3.0,采用表单上传的方式。
响应式数据如下:
const fileUploader = ref(null);
const state = reactive({
uploadedSize : 0,
fileSize: 0,
});
文件切片上传代码如下:
const createFormData = ({name, uploadedSize, stamp, file}) => {
const fd = new FormData();
fd.append("fileName", name);
fd.append("uploadedSize", uploadedSize);
fd.append("stamp", stamp);
fd.append("file", file);
return fd;
}
该函数用于创建表单,表单数据包括文件名,已上传文件大小,时间戳和文件片。
const uploadFile = async () => {
const {files:[file]} = fileUploader.value;
if (!file){
return;
}
const {name, size } = file;
const stamp = new Date().getTime();
while (state.uploadedSize < size){
let fileChunk = file.slice(state.uploadedSize, state.uploadedSize + CHUNK_SIZE);
let fd = createFormData({
name,
stamp,
uploadedSize: state.uploadedSize,
file: fileChunk
});
try{
await axios.post(API_UPLOAD_URL, fd, {timeout:3000});
state.uploadedSize += fileChunk.size;
}catch(err){
throw err;
}
}
};
服务器端代码的主要任务是写入文件,返回文件的URL。
Express作为服务器端的关键代码如下:
const { StatusCodes } = require('http-status-codes');
const { resolve } = require('path');
const { existsSync, promises } = require('fs');
const uploadFile = async (req,res) => {
const {uploadedSize, fileName ,stamp} = req.body;
const filePath = resolve(__dirname, `static_files/${stamp}/${fileName}`);
const { file } = req.files;
try {
if (Number(uploadedSize) !== 0){
const existed = existsSync(filePath);
if (!existed){
res.status(StatusCodes.NOT_FOUND).json({
msg:`The file ${fileName} does not exist`,
code:1,
});
return;
}
await promises.appendFile(filePath, file.data);
res.status(StatusCodes.OK).json({
msg:`The file has appended to ${fileName}.`,
code:0,
});
return;
}
await promises.mkdir(resolve(__dirname, `static_files/${stamp}`));
await promises.writeFile(filePath, file.data);
res.status(StatusCodes.CREATED).json({
msg:`The file is uploaded.`,
url: `http://localhost:3500/static_files/${stamp}/${fileName}`,
code:0,
});
return;
} catch (error) {
console.log(error);
}
}
module.exports = {
uploadFile
}
Asp.Net服务器端代码实现相同的功能,关键代码如下:
[EnableCors("upload")]
[HttpPost("upload"), DisableRequestSizeLimit]
public async Task<IActionResult> uploadFiles(
IFormFile file,
[FromForm] int uploadedSize,
[FromForm] string stamp,
[FromForm] string fileName
){
var pathToSave = Path.Combine(Directory.GetCurrentDirectory(), _uploadSettings.RootFolder);
pathToSave = Path.Combine(pathToSave, _uploadSettings.ImageFolder);
pathToSave = Path.Combine(pathToSave, stamp);
try
{
if (!Directory.Exists(pathToSave) && uploadedSize > 0){
return NotFound($"The file {fileName} does not existed.");
}
if (uploadedSize == 0)
{
Directory.CreateDirectory(pathToSave);
}
var fullPath = Path.Combine(pathToSave, fileName);
using (var fs = new FileStream(fullPath,
uploadedSize > 0 ? FileMode.Append : FileMode.Create)){
await file.CopyToAsync(fs);
}
var urlPath = $"{_uploadSettings.RootFolder}/{_uploadSettings.ImageFolder}/{stamp}/{fileName}";
var uri = new Uri(new Uri($"{Request.Scheme}://{Request.Host}"), urlPath);
if (uploadedSize == 0)
{
return Created(uri,null);
}
return Ok(uri.ToString());
}
catch (System.Exception)
{
throw;
}
}
为了处理大文件,所以增加DisableRequestSizeLimit的Annotation,避免上传大文件,服务器端直接报错。
Asp.Net服务器端代码逻辑与Express的实现逻辑基本相同,不再赘述。
注意:无论使用Express还是Asp.Net,在拼接路径的时候,尽量使用已有的方法,不要直接使用字符串操作。因为服务器端代码可能部署在Windows或Linux的服务器上。
Express跨域代码:
const cors = require("cors");
app.use(cors());
Asp.Net Core 跨域代码,本文使用的Asp.Net Core版本是3.1,如果在开发环境,并不需要https,请将https的中间件代码移除。如果在生产环境中使用https,请按照本文推荐的顺序调用中间件。
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(
options=>{
options.AddPolicy("upload", policy =>
{
policy.WithOrigins("http://localhost:8080/",
"http://192.168.31.206/")
.SetIsOriginAllowed((host)=>true)
.AllowAnyHeader()
.AllowAnyMethod();
});
}) ;
services
.Configure<UploadSettings>(Configuration.GetSection("UploadSettings"))
.AddControllersWithViews();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
var staticFolder = Configuration.GetSection("UploadSettings").Get<UploadSettings>().RootFolder;
app.UseStaticFiles( new StaticFileOptions{
FileProvider = new PhysicalFileProvider(
Path.Combine(env.ContentRootPath, staticFolder)
),
RequestPath = $"/{staticFolder }"
});
app.UseRouting();
app.UseCors("upload");
// app.UseHttpsRedirection();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}