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.
412 lines
13 KiB
412 lines
13 KiB
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 : IToJson
|
|
{
|
|
|
|
#region
|
|
|
|
private int _expires = 0;
|
|
|
|
internal ApiRequest _request;
|
|
internal ApiResponse _response;
|
|
internal ApiOptions _options;
|
|
internal ApiProvider _provider;
|
|
|
|
static int SafeExpires(int seconds)
|
|
{
|
|
var s = seconds;
|
|
if (s < 0) s = 0;
|
|
if (s > 2592000) s = 2592000;
|
|
return s;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region 内部属性和方法。
|
|
|
|
/// <summary>处理当前模型的 API 请求。</summary>
|
|
protected ApiRequest Request { get => _request; }
|
|
|
|
/// <summary>处理当前模型的 API 响应。</summary>
|
|
protected ApiResponse Response { get => Response; }
|
|
|
|
/// <summary>处理当前模型的 API 选项。</summary>
|
|
protected ApiOptions Options { get => _options; }
|
|
|
|
/// <summary>处理当前模型的服务程序实例。</summary>
|
|
protected ApiProvider Provider { get => _provider; }
|
|
|
|
/// <summary>在 Response 头中添加用于设置文件名的属性。</summary>
|
|
protected void SetAttachment()
|
|
{
|
|
if (_provider == null) return;
|
|
var name = Attachment;
|
|
if (string.IsNullOrEmpty(name)) return;
|
|
var encoded = TextUtility.EncodeUrl(name);
|
|
_provider.SetHeader("Content-Disposition", $"attachment; filename={encoded}");
|
|
}
|
|
|
|
private List<string> WriteHeader()
|
|
{
|
|
if (_provider == null) return null;
|
|
|
|
var status = Status > 0 ? Status : 200;
|
|
if (status != 200) _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;
|
|
_provider.SetHeader(header.Key, header.Value);
|
|
added.Add(header.Key.Lower());
|
|
}
|
|
}
|
|
|
|
SetAttachment();
|
|
_provider.SetCache(Expires);
|
|
if (!added.Contains("content-type")) _provider.SetContentType(ContentType);
|
|
return added;
|
|
}
|
|
|
|
/// <summary>以指定参数输出。</summary>
|
|
protected void Output(byte[] bytes)
|
|
{
|
|
if (_provider == null) return;
|
|
if (_provider.PreWrite().NotEmpty()) return;
|
|
var added = WriteHeader();
|
|
|
|
var length = bytes == null ? 0 : bytes.Length;
|
|
if (!added.Contains("content-length")) _provider.SetContentLength(length);
|
|
if (length > 0) _provider.ResponseBody().Write(bytes);
|
|
_provider.Sent();
|
|
}
|
|
|
|
/// <summary>以指定参数输出。</summary>
|
|
protected void Output(Stream stream, bool dispose)
|
|
{
|
|
if (_provider == null) return;
|
|
if (_provider.PreWrite().NotEmpty()) return;
|
|
var added = WriteHeader();
|
|
|
|
if (!added.Contains("content-length"))
|
|
{
|
|
var length = stream.Length - stream.Position;
|
|
_provider.SetContentLength(length);
|
|
}
|
|
_provider.ResponseBody().Write(stream);
|
|
_provider.Sent();
|
|
if (dispose) RuntimeUtility.Dispose(stream);
|
|
}
|
|
|
|
#endregion
|
|
|
|
/// <summary>状态。</summary>
|
|
/// <remarks>默认值:200。</remarks>
|
|
public virtual int Status { get; set; }
|
|
|
|
/// <summary>内容类型。当 Headers 中包含 Content-Type 时此属性将被忽略。</summary>
|
|
public virtual string ContentType { get; set; }
|
|
|
|
/// <summary>响应缓存的过期时间,以秒为单位。</summary>
|
|
public virtual int Expires { get => _expires; set => _expires = SafeExpires(value); }
|
|
|
|
/// <summary>设置文件名,告知客户端此附件处理此响应。</summary>
|
|
public virtual string Attachment { get; set; }
|
|
|
|
/// <summary>设置 Response 头。</summary>
|
|
public virtual StringPairs Headers { get; set; }
|
|
|
|
/// <summary>执行输出。</summary>
|
|
/// <remarks>此方法由 API 调用器发起调用,用户程序不应主动调用。</remarks>
|
|
/// <exception cref="InvalidOperationException"></exception>
|
|
public abstract void Output();
|
|
|
|
/// <summary>创建对象实例,并设置默认属性。</summary>
|
|
public ApiModel()
|
|
{
|
|
Status = 200;
|
|
ContentType = "application/octet-stream";
|
|
Expires = 0;
|
|
Attachment = null;
|
|
Headers = new StringPairs();
|
|
}
|
|
|
|
/// <summary></summary>
|
|
public Json ToJson()
|
|
{
|
|
var json = new Json();
|
|
json.SetProperty("status", Status);
|
|
json.SetProperty("content-type", ContentType);
|
|
return json;
|
|
}
|
|
|
|
}
|
|
|
|
/// <summary>输出二进制的 Response 模型。</summary>
|
|
public class ApiBytesModel : ApiModel
|
|
{
|
|
|
|
/// <summary>向 Body 写入的字节数组。</summary>
|
|
public byte[] Bytes { get; set; }
|
|
|
|
/// <summary>输出字节数组。</summary>
|
|
public override void Output() => Output(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() => Output(Stream, AutoDispose);
|
|
|
|
/// <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()
|
|
{
|
|
try
|
|
{
|
|
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(stream, false);
|
|
}
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
/// <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() => Output(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()
|
|
{
|
|
var json = (Json != null && Json.Available) ? Json : Json.NewObject();
|
|
if (Camel) Json.Camel(json);
|
|
Output(TextUtility.Bytes(json.ToString(Indented)));
|
|
}
|
|
|
|
/// <summary>创建对象实例,并设置默认属性。</summary>
|
|
public ApiJsonModel(Json json = null, bool camel = false, bool indented = true)
|
|
{
|
|
ContentType = "application/json";
|
|
Camel = camel;
|
|
Indented = indented;
|
|
Json = json;
|
|
}
|
|
|
|
}
|
|
|
|
/// <summary>输出重定向的 Response 模型。</summary>
|
|
public class ApiRedirectModel : ApiModel
|
|
{
|
|
|
|
/// <summary>将要重定向的位置。</summary>
|
|
public string Location { get; set; }
|
|
|
|
/// <summary>执行重定向。</summary>
|
|
public override void Output()
|
|
{
|
|
var location = Location;
|
|
if (string.IsNullOrEmpty(location)) return;
|
|
if (Provider == null) return;
|
|
Provider.SetRedirect(Location);
|
|
}
|
|
|
|
/// <summary>重定向到指定的 URL。</summary>
|
|
public ApiRedirectModel(string location = null)
|
|
{
|
|
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()
|
|
{
|
|
Status = 500;
|
|
ContentType = "text/plain";
|
|
Output(ToString().Bytes());
|
|
}
|
|
|
|
/// <summary></summary>
|
|
public ApiExceptionModel(Exception exception = null)
|
|
{
|
|
Exception = exception;
|
|
}
|
|
|
|
/// <summary></summary>
|
|
public override string ToString()
|
|
{
|
|
var ex = Exception;
|
|
var sb = new StringBuilder();
|
|
if (ex == null)
|
|
{
|
|
sb.Append("Invalid Exception");
|
|
}
|
|
else
|
|
{
|
|
try
|
|
{
|
|
sb.Append(Exception.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() => Output(Bytes);
|
|
|
|
/// <summary></summary>
|
|
public ApiStatusModel(int status = 200) => Status = status;
|
|
|
|
/// <summary></summary>
|
|
public ApiStatusModel(HttpStatusCode status) => Status = (int)status;
|
|
|
|
}
|
|
|
|
}
|
|
|