一种文件切片上传策略

7 篇文章 0 订阅
3 篇文章 0 订阅

概要

本文介绍一种文件切片上传策略,以应对项目开发中的可能遇到的大文件上传的需求。客户端采用Vue 3.0,服务器端提供Express和Asp.Net Core两种实现。

基本思路

Web客户端将用户要上传的文件,按照指定的尺寸进行分片切割,每次只上传一片,并告知服务器端已经上传的文件大小。

服务器端根据客户端告知的已上传文件大小,决定每次是执行文件的创建操作还是继续进行写文件操作。

整个上传操作由客户端主导,当最后一个文件片上传成功后,上传操作完成。服务器端只进行写文件的操作,不进行任何状态管理。

代码及实现

客户端关键代码实现

客户单代码的只要任务是文件切割,并逐个将表单传给服务器端。

客户度采用Vue 3.0,采用表单上传的方式。
响应式数据如下:

    const fileUploader = ref(null);
    const state = reactive({
      uploadedSize : 0,
      fileSize: 0,
    });
  1. fileUploader 直接绑定到input:file上
  2. uploadedSize 表示已经上传的文件大小
  3. fileSize为整个文件的大小

文件切片上传代码如下:

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;
        }     
      } 
    };
  1. 获取input:file的引用值file
  2. 如果file为空,返回(UI操作已经略去)
  3. 从file中解构出文件名name和文件大小size
  4. 只要已经上传的文件大小小于文件实际大小,进行切片上传:
    1. 每次上传的切片文件大小是CHUNK_SIZE,本文是64*1024,即64K,该值可以根据实际网络情况进行调整;
    2. 调用createFormData方法生成表单,时间戳作为文件的唯一性标识;
    3. 采用Axios的post方法提交表单,注意服务器端的文件写操作可能要消耗的时间比较长,所以适当延长Axios的超时限制;
    4. 每次上传操作完成后,更新已上传的文件的大小。

服务器端代码实现

服务器端代码的主要任务是写入文件,返回文件的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
}
  1. 从Request对象的body中结构出已经上传文件大小,文件名和时间戳;
  2. 获取文件的绝对路径,时间戳在路径中,避免如果有多个人,同时上传不同的文件,文件内容互相覆盖;
  3. 如果已经上传的文件大小大于0,但是文件目录不存在,则返回404,表示已经上传的文件丢失;
  4. 如果已经上传的文件大小大于0,则执行文件的Append操作,将新的文件片内容写入已有的文件中,返回200;
  5. 如果已经上传的文件大小是0, 先创建目录,再创建文件,返回201。

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?}");
            });
        }
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值