You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

419 lines
13 KiB

4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
11 months ago
4 years ago
11 months ago
4 years ago
11 months ago
4 years ago
11 months ago
4 years ago
11 months ago
4 years ago
11 months ago
4 years ago
11 months ago
4 years ago
11 months ago
4 years ago
11 months ago
4 years ago
11 months ago
4 years ago
11 months ago
4 years ago
11 months ago
4 years ago
11 months ago
4 years ago
11 months ago
4 years ago
4 years ago
3 years ago
4 years ago
11 months ago
3 years ago
4 years ago
4 years ago
4 years ago
11 months ago
3 years ago
4 years ago
11 months ago
4 years ago
11 months ago
3 years ago
11 months ago
4 years ago
11 months ago
4 years ago
11 months ago
4 years ago
11 months ago
3 years ago
11 months ago
3 years ago
11 months ago
3 years ago
11 months ago
4 years ago
11 months ago
4 years ago
11 months ago
4 years ago
11 months ago
4 years ago
4 years ago
4 years ago
3 years ago
4 years ago
11 months ago
4 years ago
4 years ago
4 years ago
11 months ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
11 months ago
4 years ago
11 months ago
4 years ago
11 months ago
4 years ago
11 months ago
4 years ago
4 years ago
3 years ago
4 years ago
4 years ago
11 months ago
4 years ago
4 years ago
4 years ago
11 months ago
4 years ago
11 months ago
4 years ago
11 months ago
4 years ago
4 years ago
4 years ago
11 months ago
4 years ago
11 months ago
4 years ago
11 months ago
4 years ago
4 years ago
11 months ago
4 years ago
11 months ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
11 months ago
4 years ago
4 years ago
11 months ago
4 years ago
11 months ago
4 years ago
11 months ago
4 years ago
4 years ago
11 months ago
4 years ago
4 years ago
11 months ago
4 years ago
4 years ago
4 years ago
11 months ago
4 years ago
4 years ago
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Net;
  5. using System.Text;
  6. namespace Apewer.Web
  7. {
  8. /// <summary>Response 模型。</summary>
  9. public abstract class ApiModel : IApiModel
  10. {
  11. #region Headers
  12. int _expires = 0;
  13. StringPairs _headers = new StringPairs();
  14. int SafeExpires(int seconds)
  15. {
  16. var s = seconds;
  17. if (s < 0) s = 0;
  18. if (s > 2592000) s = 2592000;
  19. return s;
  20. }
  21. /// <summary>响应缓存的过期时间,以秒为单位。</summary>
  22. public virtual int Expires { get => _expires; set => _expires = SafeExpires(value); }
  23. /// <summary>状态。</summary>
  24. /// <remarks>默认值:200。</remarks>
  25. public virtual int Status { get; set; }
  26. /// <summary>设置 Response 头。</summary>
  27. public virtual StringPairs Headers { get => _headers; set => _headers = value ?? new StringPairs(); }
  28. /// <summary>内容类型。当 Headers 中包含 Content-Type 时此属性将被忽略。</summary>
  29. public virtual string ContentType { get; set; }
  30. /// <summary>设置文件名,告知客户端此附件处理此响应。</summary>
  31. public virtual string Attachment { get; set; }
  32. #endregion
  33. #region Output
  34. /// <summary>执行输出。</summary>
  35. /// <remarks>此方法由 API 调用器发起调用,用户程序不应主动调用。</remarks>
  36. /// <exception cref="InvalidOperationException"></exception>
  37. public abstract void Output(ApiContext context);
  38. /// <summary>向 HTTP 写入头。</summary>
  39. /// <returns>已写入的头。</returns>
  40. List<string> WriteHeaders(ApiContext context)
  41. {
  42. var status = Status > 0 ? Status : 200;
  43. if (status != 200) context.Provider.SetStatus(status);
  44. var headers = Headers;
  45. var added = new List<string>(32);
  46. if (headers != null)
  47. {
  48. foreach (var header in headers)
  49. {
  50. if (header.Key.IsEmpty()) continue;
  51. if (header.Value.IsEmpty()) continue;
  52. context.Provider.SetHeader(header.Key, header.Value);
  53. added.Add(header.Key.Lower());
  54. }
  55. }
  56. SetAttachment(context);
  57. context.Provider.SetCache(Expires);
  58. if (!added.Contains("content-type")) context.Provider.SetContentType(ContentType);
  59. return added;
  60. }
  61. /// <summary>在 Response 头中添加用于设置文件名的属性。</summary>
  62. void SetAttachment(ApiContext context)
  63. {
  64. var name = Attachment;
  65. if (string.IsNullOrEmpty(name)) return;
  66. var encoded = TextUtility.EncodeUrl(name);
  67. context.Provider.SetHeader("Content-Disposition", $"attachment; filename={encoded}");
  68. }
  69. /// <summary>输出头和响应体,响应体是字节数组。</summary>
  70. /// <exception cref="ArgumentNullException" />
  71. protected void Output(ApiContext context, byte[] bytes)
  72. {
  73. if (context == null) throw new ArgumentNullException(nameof(context));
  74. // 写入头
  75. if (context.Provider == null) return;
  76. if (context.Provider.PreWrite().NotEmpty()) return;
  77. var added = WriteHeaders(context);
  78. // 写入头
  79. var length = bytes == null ? 0 : bytes.Length;
  80. if (!added.Contains("content-length")) context.Provider.SetContentLength(length);
  81. // 写入主体
  82. if (length > 0) context.Provider.ResponseBody().Write(bytes);
  83. // 发送
  84. context.Provider.Sent();
  85. }
  86. /// <summary>以指定参数输出。</summary>
  87. /// <exception cref="ArgumentNullException" />
  88. protected void Output(ApiContext context, Stream stream)
  89. {
  90. if (context == null) throw new ArgumentNullException(nameof(context));
  91. // 写入头
  92. if (context.Provider == null) return;
  93. if (context.Provider.PreWrite().NotEmpty()) return;
  94. var added = WriteHeaders(context);
  95. if (stream == null)
  96. {
  97. context.Provider.SetContentLength(0);
  98. context.Provider.Sent();
  99. }
  100. else
  101. {
  102. // 写入头
  103. if (!added.Contains("content-length"))
  104. {
  105. var length = stream.Length - stream.Position;
  106. context.Provider.SetContentLength(length);
  107. }
  108. // 写入主体
  109. context.Provider.ResponseBody().Write(stream);
  110. // 发送
  111. context.Provider.Sent();
  112. }
  113. }
  114. #endregion
  115. /// <summary>创建对象实例,并设置默认属性。</summary>
  116. public ApiModel()
  117. {
  118. Status = 200;
  119. ContentType = "application/octet-stream";
  120. Expires = 0;
  121. Attachment = null;
  122. Headers = new StringPairs();
  123. }
  124. }
  125. /// <summary>输出二进制的 Response 模型。</summary>
  126. public class ApiBytesModel : ApiModel
  127. {
  128. /// <summary>向 Body 写入的字节数组。</summary>
  129. public byte[] Bytes { get; set; }
  130. /// <summary>输出字节数组。</summary>
  131. public override void Output(ApiContext context) => Output(context, Bytes);
  132. /// <summary>创建对象实例,并设置默认属性。</summary>
  133. public ApiBytesModel(byte[] bytes = null, string contentType = "application/octet-stream")
  134. {
  135. Bytes = bytes;
  136. ContentType = contentType;
  137. }
  138. }
  139. /// <summary>输出二进制的 Response 模型。</summary>
  140. public class ApiStreamModel : ApiModel, IDisposable
  141. {
  142. /// <summary>将要读取的流,用于向 Body 写入。</summary>
  143. public Stream Stream { get; set; }
  144. /// <summary>执行输出后释放流。</summary>
  145. /// <remarks>默认值:TRUE。</remarks>
  146. public bool AutoDispose { get; set; }
  147. /// <summary>输出流。</summary>
  148. public override void Output(ApiContext context)
  149. {
  150. if (AutoDispose)
  151. {
  152. using (var stream = Stream) Output(context, Stream);
  153. }
  154. else
  155. {
  156. Output(context, Stream);
  157. }
  158. }
  159. /// <summary>当指定 AutoDispose 属性时释放流。</summary>
  160. public void Dispose()
  161. {
  162. if (AutoDispose) RuntimeUtility.Dispose(Stream);
  163. }
  164. /// <summary>创建对象实例,并设置默认属性。</summary>
  165. public ApiStreamModel(Stream stream = null, bool autoDispose = true)
  166. {
  167. Stream = stream;
  168. AutoDispose = autoDispose;
  169. }
  170. }
  171. /// <summary>输出二进制的 Response 模型。</summary>
  172. public class ApiFileModel : ApiModel
  173. {
  174. string _path;
  175. void SetPath(string path)
  176. {
  177. if (string.IsNullOrEmpty(path)) throw new FileNotFoundException("没有指定文件路径。");
  178. if (!File.Exists(path)) throw new FileNotFoundException($"文件 {path} 不存在。");
  179. _path = path;
  180. }
  181. /// <summary>将要读取的文件所在路径,用于向 Body 写入。</summary>
  182. /// <exception cref="FileNotFoundException"></exception>
  183. public string Path { get => _path; set => SetPath(value); }
  184. /// <summary>输出指定路径的文件。</summary>
  185. public override void Output(ApiContext context)
  186. {
  187. if (!File.Exists(Path)) return;
  188. var info = new FileInfo(Path);
  189. if (string.IsNullOrEmpty(Attachment)) Attachment = info.Name;
  190. using (var stream = new FileStream(Path, FileMode.Open, FileAccess.Read, FileShare.Read))
  191. {
  192. Output(context, stream);
  193. }
  194. }
  195. /// <summary></summary>
  196. /// <exception cref="FileNotFoundException"></exception>
  197. public ApiFileModel(string path)
  198. {
  199. SetPath(path);
  200. }
  201. }
  202. /// <summary>输出文本的 Response 模型。</summary>
  203. public class ApiTextModel : ApiModel
  204. {
  205. /// <summary>自定义文本。</summary>
  206. public string Text { get; set; }
  207. /// <summary>输出文本。</summary>
  208. public override void Output(ApiContext context) => Output(context, TextUtility.Bytes(Text));
  209. /// <summary>创建对象实例,并设置默认属性。</summary>
  210. public ApiTextModel(string text = null, string contentType = "text/plain")
  211. {
  212. ContentType = contentType;
  213. Text = text;
  214. }
  215. }
  216. /// <summary>输出文本的 Response 模型。</summary>
  217. public class ApiJsonModel : ApiModel
  218. {
  219. /// <summary>Json 对象。</summary>
  220. public Json Json { get; set; }
  221. /// <summary>缩进排版。</summary>
  222. /// <remarks>默认值:TRUE</remarks>
  223. public bool Indented { get; set; }
  224. /// <summary>转为属性名为驼峰形式。</summary>
  225. /// <remarks>默认值:FALSE</remarks>
  226. public bool Camel { get; set; }
  227. /// <summary>输出文本。</summary>
  228. public override void Output(ApiContext context)
  229. {
  230. var json = (Json != null && Json.Available) ? Json : Json.NewObject();
  231. if (Camel) Json.Camel(json);
  232. Output(context, TextUtility.Bytes(json.ToString(Indented)));
  233. }
  234. /// <summary>创建对象实例,并设置默认属性。</summary>
  235. public ApiJsonModel(Json json = null, bool indented = false, bool camel = true)
  236. {
  237. ContentType = "application/json";
  238. Camel = camel;
  239. Indented = indented;
  240. Json = json;
  241. }
  242. }
  243. /// <summary>输出重定向的 Response 模型。</summary>
  244. public class ApiRedirectModel : ApiModel
  245. {
  246. /// <summary>将要重定向的位置。</summary>
  247. public string Location { get; private set; }
  248. /// <summary>执行重定向。</summary>
  249. public override void Output(ApiContext context)
  250. {
  251. var location = Location;
  252. if (string.IsNullOrEmpty(location)) return;
  253. context.Provider.SetRedirect(Location);
  254. }
  255. /// <summary>重定向到指定的 URL。</summary>
  256. /// <exception cref="ArgumentNullException" />
  257. public ApiRedirectModel(string location)
  258. {
  259. if (location.IsEmpty()) throw new ArgumentNullException(nameof(location));
  260. Location = location;
  261. }
  262. }
  263. /// <summary>输出说明 Exception 的 Response 模型。</summary>
  264. public class ApiExceptionModel : ApiModel
  265. {
  266. /// <summary>要输出的 Exception。</summary>
  267. public Exception Exception { get; set; }
  268. /// <summary>解析 Exception 的内容并输出。</summary>
  269. public override void Output(ApiContext context)
  270. {
  271. Status = 500;
  272. ContentType = "text/plain";
  273. Output(context, Format(Exception).Bytes());
  274. }
  275. /// <summary></summary>
  276. /// <exception cref="ArgumentNullException" />
  277. public ApiExceptionModel(Exception exception)
  278. {
  279. if (exception == null) throw new ArgumentNullException(nameof(exception));
  280. Exception = exception;
  281. }
  282. /// <summary></summary>
  283. static string Format(Exception ex)
  284. {
  285. var sb = new StringBuilder();
  286. if (ex == null)
  287. {
  288. sb.Append("Invalid Exception");
  289. }
  290. else
  291. {
  292. try
  293. {
  294. sb.Append(ex.GetType().FullName);
  295. var props = ex.GetType().GetProperties();
  296. foreach (var prop in props)
  297. {
  298. var getter = prop.GetGetMethod();
  299. if (getter == null) continue;
  300. var value = getter.Invoke(ex, null);
  301. if (value == null) continue;
  302. sb.Append("\r\n\r\n");
  303. sb.Append(prop.Name);
  304. sb.Append(" : ");
  305. sb.Append(prop.PropertyType.FullName);
  306. sb.Append("\r\n");
  307. if (value is Json) sb.Append(((Json)value).ToString(true));
  308. else sb.Append(value.ToString() ?? "");
  309. }
  310. // sb.Append("\r\n\r\nToString\r\n");
  311. // sb.Append("\r\n");
  312. // sb.Append(ex.ToString());
  313. }
  314. catch { }
  315. }
  316. sb.Append("\r\n");
  317. var text = sb.ToString();
  318. return text;
  319. }
  320. }
  321. /// <summary>输出带有指定 Status 的 Response 模型。</summary>
  322. public class ApiStatusModel : ApiModel
  323. {
  324. /// <summary>向 Body 写入的字节数组。</summary>
  325. public byte[] Bytes { get; set; }
  326. /// <summary>执行重定向。</summary>
  327. public override void Output(ApiContext context) => Output(context, Bytes);
  328. /// <summary></summary>
  329. public ApiStatusModel(int status = 200) => Status = status;
  330. /// <summary></summary>
  331. public ApiStatusModel(HttpStatusCode status) => Status = (int)status;
  332. }
  333. }