using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Text; namespace Apewer.Web { /// <summary>Response 模型。</summary> public abstract class ApiModel : IApiModel { #region Headers int _expires = 0; StringPairs _headers = new StringPairs(); int SafeExpires(int seconds) { var s = seconds; if (s < 0) s = 0; if (s > 2592000) s = 2592000; return s; } /// <summary>响应缓存的过期时间,以秒为单位。</summary> public virtual int Expires { get => _expires; set => _expires = SafeExpires(value); } /// <summary>状态。</summary> /// <remarks>默认值:200。</remarks> public virtual int Status { get; set; } /// <summary>设置 Response 头。</summary> public virtual StringPairs Headers { get => _headers; set => _headers = value ?? new StringPairs(); } /// <summary>内容类型。当 Headers 中包含 Content-Type 时此属性将被忽略。</summary> public virtual string ContentType { get; set; } /// <summary>设置文件名,告知客户端此附件处理此响应。</summary> public virtual string Attachment { get; set; } #endregion #region Output /// <summary>执行输出。</summary> /// <remarks>此方法由 API 调用器发起调用,用户程序不应主动调用。</remarks> /// <exception cref="InvalidOperationException"></exception> public abstract void Output(ApiContext context); /// <summary>向 HTTP 写入头。</summary> /// <returns>已写入的头。</returns> List<string> WriteHeaders(ApiContext context) { var status = Status > 0 ? Status : 200; if (status != 200) context.Provider.SetStatus(status); var headers = Headers; var added = new List<string>(32); if (headers != null) { foreach (var header in headers) { if (header.Key.IsEmpty()) continue; if (header.Value.IsEmpty()) continue; context.Provider.SetHeader(header.Key, header.Value); added.Add(header.Key.Lower()); } } SetAttachment(context); context.Provider.SetCache(Expires); if (!added.Contains("content-type")) context.Provider.SetContentType(ContentType); return added; } /// <summary>在 Response 头中添加用于设置文件名的属性。</summary> void SetAttachment(ApiContext context) { var name = Attachment; if (string.IsNullOrEmpty(name)) return; var encoded = TextUtility.EncodeUrl(name); context.Provider.SetHeader("Content-Disposition", $"attachment; filename={encoded}"); } /// <summary>输出头和响应体,响应体是字节数组。</summary> /// <exception cref="ArgumentNullException" /> protected void Output(ApiContext context, byte[] bytes) { if (context == null) throw new ArgumentNullException(nameof(context)); // 写入头 if (context.Provider == null) return; if (context.Provider.PreWrite().NotEmpty()) return; var added = WriteHeaders(context); // 写入头 var length = bytes == null ? 0 : bytes.Length; if (!added.Contains("content-length")) context.Provider.SetContentLength(length); // 写入主体 if (length > 0) context.Provider.ResponseBody().Write(bytes); // 发送 context.Provider.Sent(); } /// <summary>以指定参数输出。</summary> /// <exception cref="ArgumentNullException" /> protected void Output(ApiContext context, Stream stream) { if (context == null) throw new ArgumentNullException(nameof(context)); // 写入头 if (context.Provider == null) return; if (context.Provider.PreWrite().NotEmpty()) return; var added = WriteHeaders(context); if (stream == null) { context.Provider.SetContentLength(0); context.Provider.Sent(); } else { // 写入头 if (!added.Contains("content-length")) { var length = stream.Length - stream.Position; context.Provider.SetContentLength(length); } // 写入主体 context.Provider.ResponseBody().Write(stream); // 发送 context.Provider.Sent(); } } #endregion /// <summary>创建对象实例,并设置默认属性。</summary> public ApiModel() { Status = 200; ContentType = "application/octet-stream"; Expires = 0; Attachment = null; Headers = new StringPairs(); } } /// <summary>输出二进制的 Response 模型。</summary> public class ApiBytesModel : ApiModel { /// <summary>向 Body 写入的字节数组。</summary> public byte[] Bytes { get; set; } /// <summary>输出字节数组。</summary> public override void Output(ApiContext context) => Output(context, Bytes); /// <summary>创建对象实例,并设置默认属性。</summary> public ApiBytesModel(byte[] bytes = null, string contentType = "application/octet-stream") { Bytes = bytes; ContentType = contentType; } } /// <summary>输出二进制的 Response 模型。</summary> public class ApiStreamModel : ApiModel, IDisposable { /// <summary>将要读取的流,用于向 Body 写入。</summary> public Stream Stream { get; set; } /// <summary>执行输出后释放流。</summary> /// <remarks>默认值:TRUE。</remarks> public bool AutoDispose { get; set; } /// <summary>输出流。</summary> public override void Output(ApiContext context) { if (AutoDispose) { using (var stream = Stream) Output(context, Stream); } else { Output(context, Stream); } } /// <summary>当指定 AutoDispose 属性时释放流。</summary> public void Dispose() { if (AutoDispose) RuntimeUtility.Dispose(Stream); } /// <summary>创建对象实例,并设置默认属性。</summary> public ApiStreamModel(Stream stream = null, bool autoDispose = true) { Stream = stream; AutoDispose = autoDispose; } } /// <summary>输出二进制的 Response 模型。</summary> public class ApiFileModel : ApiModel { string _path; void SetPath(string path) { if (string.IsNullOrEmpty(path)) throw new FileNotFoundException("没有指定文件路径。"); if (!File.Exists(path)) throw new FileNotFoundException($"文件 {path} 不存在。"); _path = path; } /// <summary>将要读取的文件所在路径,用于向 Body 写入。</summary> /// <exception cref="FileNotFoundException"></exception> public string Path { get => _path; set => SetPath(value); } /// <summary>输出指定路径的文件。</summary> public override void Output(ApiContext context) { if (!File.Exists(Path)) return; var info = new FileInfo(Path); if (string.IsNullOrEmpty(Attachment)) Attachment = info.Name; using (var stream = new FileStream(Path, FileMode.Open, FileAccess.Read, FileShare.Read)) { Output(context, stream); } } /// <summary></summary> /// <exception cref="FileNotFoundException"></exception> public ApiFileModel(string path) { SetPath(path); } } /// <summary>输出文本的 Response 模型。</summary> public class ApiTextModel : ApiModel { /// <summary>自定义文本。</summary> public string Text { get; set; } /// <summary>输出文本。</summary> public override void Output(ApiContext context) => Output(context, TextUtility.Bytes(Text)); /// <summary>创建对象实例,并设置默认属性。</summary> public ApiTextModel(string text = null, string contentType = "text/plain") { ContentType = contentType; Text = text; } } /// <summary>输出文本的 Response 模型。</summary> public class ApiJsonModel : ApiModel { /// <summary>Json 对象。</summary> public Json Json { get; set; } /// <summary>缩进排版。</summary> /// <remarks>默认值:TRUE</remarks> public bool Indented { get; set; } /// <summary>转为属性名为驼峰形式。</summary> /// <remarks>默认值:FALSE</remarks> public bool Camel { get; set; } /// <summary>输出文本。</summary> public override void Output(ApiContext context) { var json = (Json != null && Json.Available) ? Json : Json.NewObject(); if (Camel) Json.Camel(json); Output(context, TextUtility.Bytes(json.ToString(Indented))); } /// <summary>创建对象实例,并设置默认属性。</summary> public ApiJsonModel(Json json = null, bool indented = false, bool camel = true) { ContentType = "application/json"; Camel = camel; Indented = indented; Json = json; } } /// <summary>输出重定向的 Response 模型。</summary> public class ApiRedirectModel : ApiModel { /// <summary>将要重定向的位置。</summary> public string Location { get; private set; } /// <summary>执行重定向。</summary> public override void Output(ApiContext context) { var location = Location; if (string.IsNullOrEmpty(location)) return; context.Provider.SetRedirect(Location); } /// <summary>重定向到指定的 URL。</summary> /// <exception cref="ArgumentNullException" /> public ApiRedirectModel(string location) { if (location.IsEmpty()) throw new ArgumentNullException(nameof(location)); Location = location; } } /// <summary>输出说明 Exception 的 Response 模型。</summary> public class ApiExceptionModel : ApiModel { /// <summary>要输出的 Exception。</summary> public Exception Exception { get; set; } /// <summary>解析 Exception 的内容并输出。</summary> public override void Output(ApiContext context) { Status = 500; ContentType = "text/plain"; Output(context, Format(Exception).Bytes()); } /// <summary></summary> /// <exception cref="ArgumentNullException" /> public ApiExceptionModel(Exception exception) { if (exception == null) throw new ArgumentNullException(nameof(exception)); Exception = exception; } /// <summary></summary> static string Format(Exception ex) { var sb = new StringBuilder(); if (ex == null) { sb.Append("Invalid Exception"); } else { try { sb.Append(ex.GetType().FullName); var props = ex.GetType().GetProperties(); foreach (var prop in props) { var getter = prop.GetGetMethod(); if (getter == null) continue; var value = getter.Invoke(ex, null); if (value == null) continue; sb.Append("\r\n\r\n"); sb.Append(prop.Name); sb.Append(" : "); sb.Append(prop.PropertyType.FullName); sb.Append("\r\n"); if (value is Json) sb.Append(((Json)value).ToString(true)); else sb.Append(value.ToString() ?? ""); } // sb.Append("\r\n\r\nToString\r\n"); // sb.Append("\r\n"); // sb.Append(ex.ToString()); } catch { } } sb.Append("\r\n"); var text = sb.ToString(); return text; } } /// <summary>输出带有指定 Status 的 Response 模型。</summary> public class ApiStatusModel : ApiModel { /// <summary>向 Body 写入的字节数组。</summary> public byte[] Bytes { get; set; } /// <summary>执行重定向。</summary> public override void Output(ApiContext context) => Output(context, Bytes); /// <summary></summary> public ApiStatusModel(int status = 200) => Status = status; /// <summary></summary> public ApiStatusModel(HttpStatusCode status) => Status = (int)status; } }