NewLife/X

HttpServer支持多文件上传
大石头 authored at 2021-08-27 15:45:32
53973c8
Tree
1 Parent(s) ea27ce2
Summary: 6 changed files with 164 additions and 1 deletions.
Added +34 -0
Modified +68 -0
Modified +10 -0
Modified +3 -1
Modified +8 -0
Modified +41 -0
Added +34 -0
diff --git a/NewLife.Core/Http/FormFile.cs b/NewLife.Core/Http/FormFile.cs
new file mode 100644
index 0000000..4fdeb27
--- /dev/null
+++ b/NewLife.Core/Http/FormFile.cs
@@ -0,0 +1,34 @@
+using System;
+using System.IO;
+using NewLife.Data;
+
+namespace NewLife.Http
+{
+    /// <summary>表单部分</summary>
+    public class FormFile
+    {
+        #region 属性
+        /// <summary>名称</summary>
+        public String Name { get; set; }
+
+        /// <summary>内容部署</summary>
+        public String ContentDisposition { get; set; }
+
+        /// <summary>内容类型</summary>
+        public String ContentType { get; set; }
+
+        /// <summary>文件名</summary>
+        public String FileName { get; set; }
+
+        /// <summary>数据</summary>
+        public Packet Data { get; set; }
+
+        /// <summary>长度</summary>
+        public Int64 Length => Data?.Total ?? 0;
+        #endregion
+
+        /// <summary>打开数据读取流</summary>
+        /// <returns></returns>
+        public Stream OpenReadStream() => Data?.GetStream();
+    }
+}
\ No newline at end of file
Modified +68 -0
diff --git a/NewLife.Core/Http/HttpRequest.cs b/NewLife.Core/Http/HttpRequest.cs
index 13733ec..62dc145 100644
--- a/NewLife.Core/Http/HttpRequest.cs
+++ b/NewLife.Core/Http/HttpRequest.cs
@@ -1,4 +1,5 @@
 using System;
+using System.Collections.Generic;
 using NewLife.Collections;
 using NewLife.Data;
 
@@ -19,6 +20,9 @@ namespace NewLife.Http
 
         /// <summary>保持连接</summary>
         public Boolean KeepAlive { get; set; }
+
+        /// <summary>文件集合</summary>
+        public FormFile[] Files { get; set; }
         #endregion
 
         /// <summary>分析第一行</summary>
@@ -45,6 +49,7 @@ namespace NewLife.Http
         }
 
         private static readonly Byte[] NewLine = new[] { (Byte)'\r', (Byte)'\n' };
+        private static readonly Byte[] NewLine2 = new[] { (Byte)'\r', (Byte)'\n', (Byte)'\r', (Byte)'\n' };
         /// <summary>快速分析请求头,只分析第一行</summary>
         /// <param name="pk"></param>
         /// <returns></returns>
@@ -118,6 +123,69 @@ namespace NewLife.Http
             return sb.Put(true);
         }
 
+        /// <summary>分析表单数据</summary>
+        public virtual IDictionary<String, Object> ParseFormData()
+        {
+            var boundary = ContentType.Substring("boundary=", null);
+            if (boundary.IsNullOrEmpty()) return null;
+
+            var dic = new Dictionary<String, Object>();
+            var body = Body;
+            if (body == null || body.Total == 0) return dic;
+
+            /*
+             * ------WebKitFormBoundary3ZXeqQWNjAzojVR7
+             * Content-Disposition: form-data; name="name"
+             * 
+             * 大石头
+             * ------WebKitFormBoundary3ZXeqQWNjAzojVR7
+             * Content-Disposition: form-data; name="password"
+             * 
+             * 565656
+             * ------WebKitFormBoundary3ZXeqQWNjAzojVR7
+             * Content-Disposition: form-data; name="avatar"; filename="logo.png"
+             * Content-Type: image/jpeg
+             * 
+             */
+
+            var bd = boundary.GetBytes();
+            var p = 0;
+            do
+            {
+                p = body.IndexOf(bd, p);
+                if (p < 0) break;
+                p += bd.Length + 2;
+
+                var pPart = body.IndexOf(bd, p);
+                var part = body.Slice(p, pPart > 0 ? pPart - p : -1);
+
+                var pHeader = part.IndexOf(NewLine2);
+                var header = part.Slice(0, pHeader);
+
+                var lines = header.ToStr().SplitAsDictionary(":", Environment.NewLine);
+                if (lines.TryGetValue("Content-Disposition", out var str))
+                {
+                    var ss = str.SplitAsDictionary("=", ";", true);
+                    var file = new FormFile
+                    {
+                        Name = ss["name"],
+                        FileName = ss["filename"],
+                        ContentDisposition = ss["[0]"],
+                    };
+
+                    if (lines.TryGetValue("Content-Type", out str))
+                        file.ContentType = str;
+
+                    file.Data = part.Slice(pHeader + NewLine2.Length);
+
+                    if (!file.Name.IsNullOrEmpty()) dic[file.Name] = file.FileName.IsNullOrEmpty() ? file.Data?.ToStr() : file;
+                }
+
+            } while (p < body.Total);
+
+            return dic;
+        }
+
         /// <summary>已重载。</summary>
         /// <returns></returns>
         public override String ToString() => $"{Method} {Url}";
Modified +10 -0
diff --git a/NewLife.Core/Http/HttpSession.cs b/NewLife.Core/Http/HttpSession.cs
index ed61fb6..41e6f15 100644
--- a/NewLife.Core/Http/HttpSession.cs
+++ b/NewLife.Core/Http/HttpSession.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Net;
 using System.Web;
+using NewLife.Data;
 using NewLife.Net;
 using NewLife.Serialization;
 
@@ -84,6 +85,8 @@ namespace NewLife.Http
         /// <returns></returns>
         protected virtual HttpResponse ProcessRequest(HttpRequest request, ReceivedEventArgs e)
         {
+            if (request?.Url == null) return new HttpResponse { StatusCode = HttpStatusCode.NotFound };
+
             // 匹配路由处理器
             var server = (this as INetSession).Host as HttpServer;
             var path = request.Url.OriginalString;
@@ -152,6 +155,13 @@ namespace NewLife.Http
                         .ToDictionary(e => HttpUtility.UrlDecode(e.Key), e => HttpUtility.UrlDecode(e.Value));
                     ps.Merge(qs);
                 }
+                else if (req.ContentType.StartsWithIgnoreCase("multipart/form-data;"))
+                {
+                    var dic = req.ParseFormData();
+                    var fs = dic.Values.Where(e => e is FormFile).Cast<FormFile>().ToArray();
+                    if (fs.Length > 0) req.Files = fs;
+                    ps.Merge(dic);
+                }
                 else if (body[0] == (Byte)'{' && body[body.Total - 1] == (Byte)'}')
                 {
                     var js = JsonParser.Decode(body.ToStr());
Modified +3 -1
diff --git a/Test/form.html b/Test/form.html
index 3619a1b..7b86efc 100644
--- a/Test/form.html
+++ b/Test/form.html
@@ -8,13 +8,15 @@
 <body>
     <h2>用户信息</h2>
     <div>
-        <form action="/my" method="post">
+        <form action="/my" method="post" enctype="multipart/form-data">
             <label for="name">用户名:</label>
             <input type="text" name="name" placeholder="输入用户名" />
             <label for="password">密码:</label>
             <input type="password" name="password" />
             <label for="avatar">头像:</label>
             <input type="file" name="avatar" placeholder="上传头像" />
+            <label for="avatar2">头像:</label>
+            <input type="file" name="avatar2" placeholder="上传头像2" />
 
             <input type="submit" value="保存" />
         </form>
Modified +8 -0
diff --git a/Test/Program.cs b/Test/Program.cs
index 2e3149b..1316246 100644
--- a/Test/Program.cs
+++ b/Test/Program.cs
@@ -385,6 +385,14 @@ namespace Test
             {
                 var name = context.Parameters["name"];
                 var html = $"<h2>你好,<span color=\"red\">{name}</span></h2>";
+                var files = context.Request.Files;
+                if (files != null && files.Length >0)
+                {
+                    foreach (var file in files)
+                    {
+                        html += $"<br />文件:{file.FileName} 大小:{file.Length} 类型:{file.ContentType}";
+                    }
+                }
                 context.Response.SetResult(html);
             }
         }
Modified +41 -0
diff --git a/XUnitTest.Core/Http/HttpServerTests.cs b/XUnitTest.Core/Http/HttpServerTests.cs
index e0c374d..18307cb 100644
--- a/XUnitTest.Core/Http/HttpServerTests.cs
+++ b/XUnitTest.Core/Http/HttpServerTests.cs
@@ -161,5 +161,46 @@ namespace XUnitTest.Http
             Assert.Equal(WebSocketCloseStatus.NormalClosure, client.CloseStatus);
             Assert.Equal("Finished", client.CloseStatusDescription);
         }
+
+        [Fact]
+        public void ParseFormData()
+        {
+            var data = @"------WebKitFormBoundary3ZXeqQWNjAzojVR7
+Content-Disposition: form-data; name=""name""
+
+大石头
+------WebKitFormBoundary3ZXeqQWNjAzojVR7
+Content-Disposition: form-data; name=""password""
+
+565656
+------WebKitFormBoundary3ZXeqQWNjAzojVR7
+Content-Disposition: form-data; name=""avatar""; filename=""logo.png""
+Content-Type: image/jpeg";
+
+            var req = new HttpRequest
+            {
+                ContentType = "multipart/form-data;boundary=------WebKitFormBoundary3ZXeqQWNjAzojVR7",
+                Body = data.GetBytes()
+            };
+
+            var dic = req.ParseFormData();
+            Assert.NotNull(dic);
+
+            var rs = dic.TryGetValue("name", out var name);
+            Assert.True(rs);
+            Assert.NotEmpty((String)name);
+
+            rs = dic.TryGetValue("password", out var password);
+            Assert.True(rs);
+            Assert.NotEmpty((String)password);
+
+            rs = dic.TryGetValue("avatar", out var avatar);
+            Assert.True(rs);
+
+            var av = avatar as FormFile;
+            Assert.NotNull(av);
+            Assert.Equal("logo.png", av.FileName);
+            Assert.Equal("image/jpeg", av.ContentType);
+        }
     }
 }
\ No newline at end of file