NetCore开发的分布式文件上传系统

简介: 一个基于.Net Core构建的简单、跨平台分布式文件上传系统,支持分块上传、多个项目同时上传、接口权限控制采用JWT机制。

日常如果是上传一些小文件,在程序实现中,我们都是直接上传,一般都没什么问题。如果针对大文件上传的业务中,就会面临着:

1、网速问题,导致文件上传超时,而导致失败。

2、效率问题,上传大文件等待时间过长,如果是需要上传多个,就会更慢。

3、体验问题,用户无法预知上传还需花费的时间,系统没有及时反馈,用户无法判断文件是否还在上传,还是断开。

这时候就需要采用分布式文件上传系统。

项目简介

这是一个基于.Net Core构建的简单、跨平台分布式文件上传系统,支持分块上传、多个项目同时上传、接口权限控制采用JWT机制。

技术架构

1、跨平台:这是基于.Net Core开发的系统,可以部署在Docker, Windows, Linux, Mac。

2、.Net 2.1 + Jwt + simple-uploader

项目结构

图片

项目分为分块上传与一般上传Demo,Web、控制台上传Demo。ufs项目是分布式文件上传的统一接口,ufs会根据配置把上传的文件发到ufs.node节点,ufs.node会把上传成功路径返回给ufs并存储,用户访问的时候,ufs会访问对应节点返回资源。

UploadServer为一般文件上传接口,UploadServer.FrontEndDemo为Web上传文件Demo。

**使用

**

1、配置

配置允许上传域名、服务接口地址、允许的文件格式、文件大小、存储路径等信息。

{  "AllowedHosts": "*",  "urls": "http://localhost:6001",  "uploadServer": {    "rootUrl": "http://localhost:6001",    "entryPoint1": "/upload",    "entryPoint2": "/chunkUpload",    "virtualPath": "",    "physicalPath": "/Users/loogn/Desktop/uploader",    "appendMimes": ".htm3:text/html;",    "responseCache": 604800,    "jwtSecret": "1234561234",    "limitSize": "20mb",    "allowExts": ".txt;.jpg;.jpeg;.png;.doc;.docx;.xls;.xlsx;.ppt;.pptx;.pdf",    "apps": {      "default": {        "allowOrigins": "",        "enableThumbnail": true,        "limitExts": ".exe;",        "thumbnailExts": ".jpg;.jpeg;.png;"      },      "app1": {        "allowOrigins": "*"      }    }  }}

2、前端

一般上传代码

$("#file1").change(function () {
        $.ajaxFileUpload({
            fileElementId: 'file1',
            url: 'http://localhost:6001/upload',
            dataType: 'text',
            //
            success: function (data) {
                console.log("上传成功:", data);
            },
            data: {
                "jwt": jwt
            }
        });
    });

分块上传

var uploader = new Uploader({
        target: 'http://localhost:6001/chunkupload',
        headers: {jwt: jwt}
    });

    uploader.assignBrowse(document.getElementById('browseButton'));


    //uploader.assignBrowse(document.getElementById('folderButton'), true);

    //
    // 文件添加 单个文件
    uploader.on('fileAdded', function (file, event) {
        console.log("fileAdded:", file, event)
    });
    // 单个文件上传成功
    uploader.on('fileSuccess', function (rootFile, file, message) {

        console.log("fileSuccess:", rootFile, file, message)
    });
    // 根下的单个文件(文件夹)上传完成
    uploader.on('fileComplete', function (rootFile) {

        console.log("fileComplete:", rootFile)
    });
    // 某个文件上传失败了
    uploader.on('fileError', function (rootFile, file, message) {
        console.log("fileError:", rootFile, file, message)
    });

3、后端

一般上传

 public async Task InvokeAsync(HttpContext context, RequestDelegate next)
        {
            context.Response.Headers.Add("Access-Control-Allow-Origin", "*");
            context.Response.Headers.Add("Access-Control-Allow-Headers", "content-type,jwt,origin");
            if (context.Request.Method.Equals(HttpMethods.Options, StringComparison.OrdinalIgnoreCase))
            {
                context.Response.StatusCode = (int) HttpStatusCode.OK;
            }
            else if (context.Request.Method.Equals(HttpMethods.Post, StringComparison.OrdinalIgnoreCase))
            {
                //验证jwt
                string token = null;
                if (context.Request.Headers.TryGetValue("jwt", out StringValues jwt))
                {
                    token = jwt.ToString();
                }
                else if (context.Request.Form.TryGetValue("jwt", out jwt))
                {
                    token = jwt.ToString();
                }
                else
                {
                    await context.Response.WriteAsync(new UploadResult()
                    {
                        msg = "No JWT in the header and form"
                    }.toJson());
                    return;
                }

                try
                {
                    var payload = new JwtBuilder().WithSecret(_config.JWTSecret).MustVerifySignature()
                        .Decode<JwtPayload>(token);
                    var msg = payload.validate();
                    if (msg != null)
                    {
                        await context.Response.WriteAsync(new UploadResult()
                        {
                            msg = msg
                        }.toJson());
                        return;
                    }

                    //特定的配置
                    var appConfig = _config.GetAppConfig(payload.app);

                    //跨域
                    context.Request.Headers.TryGetValue("Origin", out var origins);
                    var origin = origins.ToString();
                    if (!string.IsNullOrEmpty(origin) && appConfig.IsAllowOrigin(origin))
                    {
                        context.Response.Headers.Add("Access-Control-Allow-Origin", origin);
                    }

                    //获取上传的文件
                    var file = context.Request.Form.Files.FirstOrDefault();
                    if (file == null || file.Length == 0)
                    {
                        await context.Response.WriteAsync(new UploadResult()
                        {
                            msg = "There is no file data"
                        }.toJson());
                        return;
                    }

                    //大小验证
                    if (file.Length > (payload.GetByteSize() ?? _config.GetByteSize()))
                    {
                        await context.Response.WriteAsync(new UploadResult()
                        {
                            msg = "The file is too big"
                        }.toJson());
                        return;
                    }

                    //后缀验证
                    var ext = Path.GetExtension(file.FileName);
                    if (!(payload.exts + _config.AllowExts).Contains(ext, StringComparison.OrdinalIgnoreCase)
                        || appConfig.LimitExts.Contains(ext, StringComparison.OrdinalIgnoreCase))
                    {
                        await context.Response.WriteAsync(new UploadResult()
                        {
                            msg = "File extension is not allowed"
                        }.toJson());

                        return;
                    }

                    //上传逻辑
                    var now = DateTime.Now;
                    var yy = now.ToString("yyyy");
                    var mm = now.ToString("MM");
                    var dd = now.ToString("dd");

                    var fileName = Guid.NewGuid().ToString("n") + ext;

                    var folder = Path.Combine(_config.PhysicalPath, payload.app, yy, mm, dd);
                    if (!Directory.Exists(folder))
                    {
                        Directory.CreateDirectory(folder);
                    }

                    var filePath = Path.Combine(folder, fileName);

                    using (var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write))
                    {
                        file.CopyTo(fileStream);
                        fileStream.Flush(true);
                    }

                    var fileUrl = _config.RootUrl + "/" + payload.app + "/" + yy + "/" + mm +
                                  "/" +
                                  dd +
                                  "/" + fileName;

                    await context.Response.WriteAsync(new UploadResult()
                    {
                        ok = true,
                        url = fileUrl
                    }.toJson());
                }
                catch (TokenExpiredException)
                {
                    await context.Response.WriteAsync(new UploadResult()
                    {
                        msg = "Token has expired"
                    }.toJson());
                }
                catch (SignatureVerificationException)
                {
                    await context.Response.WriteAsync(new UploadResult()
                    {
                        msg = "Token has invalid signature"
                    }.toJson());
                }
            }
            else
            {
                await context.Response.WriteAsync(new UploadResult()
                {
                    msg = $"Request method '{context.Request.Method}' is not supported"
                }.toJson());
            }
        }

分块上传

public async Task InvokeAsync(HttpContext context, RequestDelegate next)
        {
            context.Response.Headers.Add("Access-Control-Allow-Origin", "*");
            context.Response.Headers.Add("Access-Control-Allow-Headers", "content-type,jwt,origin");
            if (context.Request.Method.Equals(HttpMethods.Options, StringComparison.OrdinalIgnoreCase))
            {
                context.Response.StatusCode = (int)HttpStatusCode.OK;
            }
            else if (context.Request.Method.Equals(HttpMethods.Get, StringComparison.OrdinalIgnoreCase))
            {
                //简单实现
                context.Request.Query.TryGetValue("chunkNumber", out var chunkNumbers);
                int.TryParse(chunkNumbers.ToString(), out var chunkNumber);
                context.Request.Query.TryGetValue("identifier", out var identifiers);
                if (chunkNumber == 0 || string.IsNullOrEmpty(identifiers))
                {
                    context.Response.StatusCode = 204;
                }
                else
                {
                    var chunkFilename = getChunkFilename(_config.PhysicalPath, chunkNumber, identifiers);
                    if (File.Exists(chunkFilename))
                    {
                        await context.Response.WriteAsync("found");
                    }
                    else
                    {
                        context.Response.StatusCode = 204;
                    }
                }
            }
            else if (context.Request.Method.Equals(HttpMethods.Post, StringComparison.OrdinalIgnoreCase))
            {
                //验证jwt
                string token = null;
                if (context.Request.Headers.TryGetValue("jwt", out StringValues jwt))
                {
                    token = jwt.ToString();
                }
                else if (context.Request.Form.TryGetValue("jwt", out jwt))
                {
                    token = jwt.ToString();
                }
                else
                {
                    await context.Response.WriteAsync(new UploadResult()
                    {
                        msg = "No JWT in the header and form"
                    }.toJson());
                    return;
                }

                try
                {
                    var payload = new JwtBuilder().WithSecret(_config.JWTSecret).MustVerifySignature()
                        .Decode<JwtPayload>(token);
                    var msg = payload.validate();
                    if (msg != null)
                    {
                        await context.Response.WriteAsync(new UploadResult()
                        {
                            msg = msg
                        }.toJson());
                        return;
                    }

                    //特定的配置
                    var appConfig = _config.GetAppConfig(payload.app);

                    //跨域
                    context.Request.Headers.TryGetValue("Origin", out var origins);
                    var origin = origins.ToString();
                    if (!string.IsNullOrEmpty(origin) && appConfig.IsAllowOrigin(origin))
                    {
                        context.Response.Headers.Add("Access-Control-Allow-Origin", origin);
                    }

                    //获取上传的文件分片
                    var file = context.Request.Form.Files.FirstOrDefault();
                    if (file == null || file.Length == 0)
                    {
                        await context.Response.WriteAsync(new UploadResult()
                        {
                            msg = "There is no file data"
                        }.toJson());
                        return;
                    }

                    //后缀验证
                    var ext = Path.GetExtension(file.FileName);
                    if (!(payload.exts + _config.AllowExts).Contains(ext, StringComparison.OrdinalIgnoreCase)
                        || appConfig.LimitExts.Contains(ext, StringComparison.OrdinalIgnoreCase))
                    {
                        await context.Response.WriteAsync(new UploadResult()
                        {
                            msg = "File extension is not allowed"
                        }.toJson());
                        return;
                    }

                    //获取参数                    
                    getParams(context, out var chunkNumber, out var chunkSize, out var totalSize, out string identifier,
                        out string filename, out int totalChunks);

                    //验证参数
                    var validMsg = validateRequest(chunkNumber, chunkSize, totalSize, identifier, filename, file.Length, totalChunks, payload.GetByteSize() ?? _config.GetByteSize());
                    if (validMsg != null)
                    {
                        await context.Response.WriteAsync(new UploadResult()
                        {
                            msg = validMsg
                        }.toJson());
                        return;
                    }
                    else
                    {
                        var chunkFilename = getChunkFilename(_config.PhysicalPath, chunkNumber, identifier);
                        try
                        {
                            using (var fileStream = File.OpenWrite(chunkFilename))
                            {
                                var stream = file.OpenReadStream();
                                stream.CopyTo(fileStream);
                                fileStream.Flush(true);
                                countDict.AddOrUpdate(identifier, 1, (key, oldValue) => oldValue + 1);
                            }

                            if (chunkNumber == totalChunks)
                            {
                                //验证块的完整性
                                while (true)
                                {
                                    if (countDict.GetValueOrDefault(identifier) < totalChunks)
                                    {
                                        await Task.Delay(TimeSpan.FromMilliseconds(500));
                                    }
                                    else
                                    {
                                        countDict.Remove(identifier, out _);
                                        break;
                                    }
                                }

                                //merge file;
                                string[] chunkFiles = Directory.GetFiles(
                                    Path.Combine(_config.PhysicalPath, temporaryFolder),
                                    "uploader-" + identifier + ".*",
                                    SearchOption.TopDirectoryOnly);
                                var fileUrl = await MergeChunkFiles(payload, ext, chunkFiles);
                                await context.Response.WriteAsync(new UploadResult()
                                {
                                    ok = true,
                                    url = fileUrl
                                }.toJson());
                            }
                            else
                            {
                                await context.Response.WriteAsync("partly_done");
                                return;
                            }
                        }
                        catch (Exception exp)
                        {
                            await context.Response.WriteAsync(new UploadResult()
                            {
                                msg = exp.Message
                            }.toJson());
                            return;
                        }
                    }
                }
                catch (TokenExpiredException)
                {
                    await context.Response.WriteAsync(new UploadResult()
                    {
                        msg = "Token has expired"
                    }.toJson());
                }
                catch (SignatureVerificationException)
                {
                    await context.Response.WriteAsync(new UploadResult()
                    {
                        msg = "Token has invalid signature"
                    }.toJson());
                }
            }
            else
            {
                context.Response.StatusCode = (int)HttpStatusCode.MethodNotAllowed;
                await context.Response.WriteAsync($"Request method '{context.Request.Method}' is not supported");
            }
        }

4、上传结果

上传成功

{"ok":true,"msg":null,"url":"http://localhost:6001/test/2019/06/17/abcd.jpg"}

上传失败

{"ok":false,"msg":"The file is too big","url":null}

图片

Gitee: https://gitee.com/loogn/UploadServer

- End -

专注分享编程知识、热门有用有趣的开源项目

相关文章
|
20天前
|
存储 运维 负载均衡
构建高可用性GraphRAG系统:分布式部署与容错机制
【10月更文挑战第28天】作为一名数据科学家和系统架构师,我在构建和维护大规模分布式系统方面有着丰富的经验。最近,我负责了一个基于GraphRAG(Graph Retrieval-Augmented Generation)模型的项目,该模型用于构建一个高可用性的问答系统。在这个过程中,我深刻体会到分布式部署和容错机制的重要性。本文将详细介绍如何在生产环境中构建一个高可用性的GraphRAG系统,包括分布式部署方案、负载均衡、故障检测与恢复机制等方面的内容。
73 4
构建高可用性GraphRAG系统:分布式部署与容错机制
|
1月前
|
NoSQL Java Redis
开发实战:使用Redisson实现分布式延时消息,订单30分钟关闭的另外一种实现!
本文详细介绍了 Redisson 延迟队列(DelayedQueue)的实现原理,包括基本使用、内部数据结构、基本流程、发送和获取延时消息以及初始化延时队列等内容。文章通过代码示例和流程图,逐步解析了延迟消息的发送、接收及处理机制,帮助读者深入了解 Redisson 延迟队列的工作原理。
|
1月前
|
消息中间件 中间件 数据库
NServiceBus:打造企业级服务总线的利器——深度解析这一面向消息中间件如何革新分布式应用开发与提升系统可靠性
【10月更文挑战第9天】NServiceBus 是一个面向消息的中间件,专为构建分布式应用程序设计,特别适用于企业级服务总线(ESB)。它通过消息队列实现服务间的解耦,提高系统的可扩展性和容错性。在 .NET 生态中,NServiceBus 提供了强大的功能,支持多种传输方式如 RabbitMQ 和 Azure Service Bus。通过异步消息传递模式,各组件可以独立运作,即使某部分出现故障也不会影响整体系统。 示例代码展示了如何使用 NServiceBus 发送和接收消息,简化了系统的设计和维护。
48 3
|
1月前
|
消息中间件 存储 监控
消息队列系统中的确认机制在分布式系统中如何实现
消息队列系统中的确认机制在分布式系统中如何实现
|
1月前
|
消息中间件 存储 监控
【10月更文挑战第2天】消息队列系统中的确认机制在分布式系统中如何实现
【10月更文挑战第2天】消息队列系统中的确认机制在分布式系统中如何实现
|
1月前
|
存储 开发框架 .NET
C#语言如何搭建分布式文件存储系统
C#语言如何搭建分布式文件存储系统
70 2
|
1月前
|
消息中间件 存储 监控
消息队列系统中的确认机制在分布式系统中如何实现?
消息队列系统中的确认机制在分布式系统中如何实现?
|
2月前
|
存储 块存储
ceph分布式存储系统常见术语篇
关于Ceph分布式存储系统的常见术语解释和概述。
113 1
ceph分布式存储系统常见术语篇
|
1月前
|
存储 分布式计算 监控
C# 创建一个分布式文件存储系统需要怎么设计??
C# 创建一个分布式文件存储系统需要怎么设计??
35 0
|
3月前
|
开发者 云计算 数据库
从桌面跃升至云端的华丽转身:深入解析如何运用WinForms与Azure的强大组合,解锁传统应用向现代化分布式系统演变的秘密,实现性能与安全性的双重飞跃——你不可不知的开发新模式
【8月更文挑战第31天】在数字化转型浪潮中,传统桌面应用面临新挑战。本文探讨如何融合Windows Forms(WinForms)与Microsoft Azure,助力应用向云端转型。通过Azure的虚拟机、容器及无服务器计算,可轻松解决性能瓶颈,满足全球用户需求。文中还提供了连接Azure数据库的示例代码,并介绍了集成Azure Storage和Functions的方法。尽管存在安全性、网络延迟及成本等问题,但合理设计架构可有效应对,帮助开发者构建高效可靠的现代应用。
32 0

热门文章

最新文章