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
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}";
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());
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>
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);
}
}
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