• 一种文件切片上传策略


    概要

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

    基本思路

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

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

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

    代码及实现

    客户端关键代码实现

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

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

        const fileUploader = ref(null);
        const state = reactive({
          uploadedSize : 0,
          fileSize: 0,
        });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    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;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    该函数用于创建表单,表单数据包括文件名,已上传文件大小,时间戳和文件片。

    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    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;
                }
               
            } 
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    为了处理大文件,所以增加DisableRequestSizeLimit的Annotation,避免上传大文件,服务器端直接报错。

    Asp.Net服务器端代码逻辑与Express的实现逻辑基本相同,不再赘述。

    注意:无论使用Express还是Asp.Net,在拼接路径的时候,尽量使用已有的方法,不要直接使用字符串操作。因为服务器端代码可能部署在Windows或Linux的服务器上。

    附录

    Express跨域代码:

    const cors = require("cors");
    app.use(cors());
    
    • 1
    • 2

    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
  • 相关阅读:
    Dubbo管理控制台dubbo-admin搭建
    vue3 组件v-model绑定props里的值,修改组件的值要触发回调
    【机器学习】近邻类模型:KNN算法在数据科学中的实践与探索
    qt5.15 升级 qt 6.5 部分问题 解决修复
    uniapp常用的生命周期
    【C 语言进阶(12)】动态内存管理笔试题
    Failed to connect to github.com port 443:connection timed out
    java EE初阶 — volatile关键字保证内存可见性
    CSS总结第六天
    twitter推文案例
  • 原文地址:https://blog.csdn.net/weixin_43263355/article/details/126482301