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.

405 lines
16 KiB

  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Text;
  5. namespace Apewer.Web
  6. {
  7. /// <summary>静态站点控制器。</summary>
  8. public class StaticController : ApiController
  9. {
  10. /// <summary></summary>
  11. public StaticController()
  12. {
  13. AllowFunction = false;
  14. AfterInitialized = Execute;
  15. }
  16. /// <summary>获取此静态站点的目录。</summary>
  17. public virtual string GetSiteDirectory()
  18. {
  19. var app = WebUtility.AppDirectory;
  20. var www = StorageUtility.CombinePath(app, "www");
  21. if (!Directory.Exists(www)) return null;
  22. return www;
  23. }
  24. /// <summary>解析 URL 的路径,获取本地路径。</summary>
  25. protected string MapPath(string urlPath)
  26. {
  27. var path = GetSiteDirectory();
  28. if (!string.IsNullOrEmpty(urlPath))
  29. {
  30. foreach (var split in urlPath.Split('/'))
  31. {
  32. var seg = split.SafeTrim();
  33. if (string.IsNullOrEmpty(seg)) continue;
  34. if (seg == "." || seg == "..") continue;
  35. path = StorageUtility.CombinePath(path, seg);
  36. }
  37. }
  38. return path;
  39. }
  40. /// <summary>处理请求。</summary>
  41. public virtual void Execute()
  42. {
  43. if (Request == null || Request.Url == null) return;
  44. var path = MapPath(Request.Url.AbsolutePath);
  45. if (StorageUtility.DirectoryExists(path))
  46. {
  47. ExecuteDirectory(path);
  48. return;
  49. }
  50. if (StorageUtility.FileExists(path))
  51. {
  52. ExecuteFile(path);
  53. return;
  54. }
  55. Respond404(path);
  56. }
  57. void ExecuteFile(string path)
  58. {
  59. if (!File.Exists(path))
  60. {
  61. Respond404(path);
  62. return;
  63. }
  64. // 获取文件扩展名。
  65. var ext = Path.GetExtension(path).SafeLower();
  66. if (ext.Length > 1 && ext.StartsWith(".")) ext = ext.Substring(1);
  67. // 按扩展名获取缓存过期时间。
  68. var expires = GetExpires(ext);
  69. if (expires > 0) Response.Expires = expires;
  70. // 按扩展名获取 Content-Type。
  71. var type = GetContentType(ext);
  72. // var name = Path.GetFileName(path);
  73. // Server Side Includes
  74. if (ext == "html" || ext == "htm" || ext == "shtml")
  75. {
  76. var html = ReadWithSSI(path);
  77. var bytes = html.ToBinary();
  78. Response.Binary(bytes, type);
  79. return;
  80. }
  81. var stream = StorageUtility.OpenFile(path, true);
  82. Response.Binary(stream, type);
  83. }
  84. void ExecuteDirectory(string path)
  85. {
  86. if (!Directory.Exists(path))
  87. {
  88. Respond404(path);
  89. return;
  90. }
  91. var @default = GetDefaultFile(path);
  92. if (string.IsNullOrEmpty(@default)) Respond403(path);
  93. else ExecuteFile(@default);
  94. }
  95. // 获取目录中的默认文件,返回文件的完整路径。
  96. string GetDefaultFile(string directory)
  97. {
  98. var subs = StorageUtility.GetSubFiles(directory);
  99. if (subs.Count < 0) return null;
  100. subs.Sort();
  101. var names = new Dictionary<string, string>();
  102. foreach (var sub in subs)
  103. {
  104. var name = Path.GetFileName(sub).ToLower();
  105. if (names.ContainsKey(name)) continue;
  106. names.Add(name, sub);
  107. }
  108. if (names.ContainsKey("index.html")) return names["index.html"];
  109. if (names.ContainsKey("index.htm")) return names["index.htm"];
  110. if (names.ContainsKey("default.html")) return names["default.html"];
  111. if (names.ContainsKey("default.htm")) return names["default.htm"];
  112. return null;
  113. }
  114. /// <summary>列出子项。可指定筛选器,传入完整路径。</summary>
  115. public void ListChildren(string directory)
  116. {
  117. if (!Directory.Exists(directory)) return;
  118. var json = Json.NewObject();
  119. json.SetProperty("directories", ListDirectories(directory));
  120. json.SetProperty("files", ListFiles(directory));
  121. Response.Data = json;
  122. }
  123. Json ListDirectories(string directory)
  124. {
  125. var array = Json.NewArray();
  126. var subs = StorageUtility.GetSubDirectories(directory);
  127. subs.Sort();
  128. foreach (var sub in subs)
  129. {
  130. var name = Path.GetDirectoryName(sub);
  131. var lower = name.SafeLower();
  132. if (lower == ".") continue;
  133. if (lower == "..") continue;
  134. if (lower == "@eadir") continue; // Synology
  135. if (lower == "#recycle") continue; // Synology
  136. if (lower == "$recycle.bin") continue; // Windows
  137. if (lower == "recycler") continue; // Windows
  138. if (lower == "system volume information") continue; // Windows
  139. var json = Json.NewObject();
  140. json.SetProperty("name", name);
  141. try
  142. {
  143. var info = new DirectoryInfo(sub);
  144. json.SetProperty("modified", info.LastWriteTimeUtc.ToStamp());
  145. }
  146. catch { }
  147. array.AddItem(json);
  148. }
  149. return array;
  150. }
  151. Json ListFiles(string directory)
  152. {
  153. var array = Json.NewArray();
  154. var subs = StorageUtility.GetSubFiles(directory);
  155. subs.Sort();
  156. foreach (var sub in subs)
  157. {
  158. var name = Path.GetFileName(sub);
  159. var lower = name.SafeLower();
  160. if (lower == ".ds_store") continue; // macOS
  161. if (lower == ".localized") continue; // macOS
  162. if (lower == "desktop.ini") continue; // Windiows
  163. if (lower == "thumbs.db") continue; // Windiows
  164. var json = Json.NewObject();
  165. json.SetProperty("name", name);
  166. try
  167. {
  168. var info = new FileInfo(sub);
  169. json.SetProperty("size", info.Length);
  170. json.SetProperty("modified", info.LastWriteTimeUtc.ToStamp());
  171. }
  172. catch { }
  173. array.AddItem(json);
  174. }
  175. return array;
  176. }
  177. /// <summary>响应 403 状态。</summary>
  178. public virtual void Respond403(string path)
  179. {
  180. Response.Error("403");
  181. }
  182. /// <summary>响应 404 状态。</summary>
  183. public virtual void Respond404(string path)
  184. {
  185. Response.Error("404");
  186. }
  187. /// <summary>按扩展名获取 Content-Type。</summary>
  188. public virtual string GetContentType(string extension)
  189. {
  190. if (!string.IsNullOrEmpty(extension))
  191. {
  192. var lower = extension.ToLower();
  193. switch (lower)
  194. {
  195. case "3gp": return "video/3gpp";
  196. case "3gpp": return "video/3gpp";
  197. case "7z": return "application/x-7z-compressed";
  198. case "ai": return "application/postscript";
  199. case "asf": return "video/x-ms-asf";
  200. case "asx": return "video/x-ms-asf";
  201. case "atom": return "application/atom+xml";
  202. case "avi": return "video/x-msvideo";
  203. case "bin": return "application/octet-stream";
  204. case "bmp": return "image/x-ms-bmp";
  205. case "cco": return "application/x-cocoa";
  206. case "crt": return "application/x-x509-ca-cert";
  207. case "css": return "text/css";
  208. case "deb": return "application/octet-stream";
  209. case "der": return "application/x-x509-ca-cert";
  210. case "dll": return "application/octet-stream";
  211. case "dmg": return "application/octet-stream";
  212. case "doc": return "application/msword";
  213. case "docx": return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
  214. case "ear": return "application/java-archive";
  215. case "eot": return "application/vnd.ms-fontobject";
  216. case "eps": return "application/postscript";
  217. case "exe": return "application/octet-stream";
  218. case "flv": return "video/x-flv";
  219. case "gif": return "image/gif";
  220. case "hqx": return "application/mac-binhex40";
  221. case "htc": return "text/x-component";
  222. case "htm": return "text/html";
  223. case "html": return "text/html";
  224. case "ico": return "image/x-icon";
  225. case "img": return "application/octet-stream";
  226. case "iso": return "application/octet-stream";
  227. case "jad": return "text/vnd.sun.j2me.app-descriptor";
  228. case "jar": return "application/java-archive";
  229. case "jardiff": return "application/x-java-archive-diff";
  230. case "jng": return "image/x-jng";
  231. case "jnlp": return "application/x-java-jnlp-file";
  232. case "jpeg": return "image/jpeg";
  233. case "jpg": return "image/jpeg";
  234. case "js": return "application/javascript";
  235. // case "json": return "application/json";
  236. case "json": return "text/json";
  237. case "kar": return "audio/midi";
  238. case "kml": return "application/vnd.google-earth.kml+xml";
  239. case "kmz": return "application/vnd.google-earth.kmz";
  240. // case "m3u8": return "application/vnd.apple.mpegurl";
  241. case "m3u8": return "text/vnd.apple.mpegurl";
  242. case "m4a": return "audio/x-m4a";
  243. case "m4v": return "video/x-m4v";
  244. case "mid": return "audio/midi";
  245. case "midi": return "audio/midi";
  246. case "mml": return "text/mathml";
  247. case "mng": return "video/x-mng";
  248. case "mov": return "video/quicktime";
  249. case "mp3": return "audio/mpeg";
  250. case "mp4": return "video/mp4";
  251. case "mpeg": return "video/mpeg";
  252. case "mpg": return "video/mpeg";
  253. case "msi": return "application/octet-stream";
  254. case "msm": return "application/octet-stream";
  255. case "msp": return "application/octet-stream";
  256. case "odg": return "application/vnd.oasis.opendocument.graphics";
  257. case "odp": return "application/vnd.oasis.opendocument.presentation";
  258. case "ods": return "application/vnd.oasis.opendocument.spreadsheet";
  259. case "odt": return "application/vnd.oasis.opendocument.text";
  260. case "ogg": return "audio/ogg";
  261. case "pdb": return "application/x-pilot";
  262. case "pdf": return "application/pdf";
  263. case "pem": return "application/x-x509-ca-cert";
  264. case "pl": return "application/x-perl";
  265. case "pm": return "application/x-perl";
  266. case "png": return "image/png";
  267. case "ppt": return "application/vnd.ms-powerpoint";
  268. case "pptx": return "application/vnd.openxmlformats-officedocument.presentationml.presentation";
  269. case "prc": return "application/x-pilot";
  270. case "ps": return "application/postscript";
  271. case "ra": return "audio/x-realaudio";
  272. case "rar": return "application/x-rar-compressed";
  273. case "rpm": return "application/x-redhat-package-manager";
  274. case "rss": return "application/rss+xml";
  275. case "rtf": return "application/rtf";
  276. case "run": return "application/x-makeself";
  277. case "sea": return "application/x-sea";
  278. case "shtml": return "text/html";
  279. case "sit": return "application/x-stuffit";
  280. case "svg": return "image/svg+xml";
  281. case "svgz": return "image/svg+xml";
  282. case "swf": return "application/x-shockwave-flash";
  283. case "tcl": return "application/x-tcl";
  284. case "tif": return "image/tiff";
  285. case "tiff": return "image/tiff";
  286. case "tk": return "application/x-tcl";
  287. case "ts": return "video/mp2t";
  288. case "txt": return "text/plain";
  289. case "war": return "application/java-archive";
  290. case "wbmp": return "image/vnd.wap.wbmp";
  291. case "webm": return "video/webm";
  292. case "webp": return "image/webp";
  293. case "wml": return "text/vnd.wap.wml";
  294. case "wmlc": return "application/vnd.wap.wmlc";
  295. case "wmv": return "video/x-ms-wmv";
  296. case "woff": return "font/woff";
  297. case "woff2": return "font/woff2";
  298. case "xhtml": return "application/xhtml+xml";
  299. case "xls": return "application/vnd.ms-excel";
  300. case "xlsx": return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
  301. case "xml": return "text/xml";
  302. case "xpi": return "application/x-xpinstall";
  303. case "xspf": return "application/xspf+xml";
  304. case "zip": return "application/zip";
  305. }
  306. }
  307. return "application/octet-stream";
  308. }
  309. /// <summary>按扩展名获取过期时间,单位为秒,默认值为 0(不缓存)。</summary>
  310. public virtual int GetExpires(string extension) => 0;
  311. // Server Side Includes
  312. string ReadWithSSI(string path, int recursive = 0)
  313. {
  314. if (recursive > 10) return "";
  315. var input = StorageUtility.ReadFile(path, true);
  316. if (input == null || input.LongLength < 1) return "";
  317. // 尝试以 UTF-8 解码。
  318. var html = TextUtility.FromBinary(input);
  319. if (string.IsNullOrEmpty(html)) return "";
  320. // 按首尾截取。
  321. const string left = "<!--";
  322. const string right = "-->";
  323. const string head = "#include virtual=";
  324. var sb = new StringBuilder();
  325. var text = html;
  326. while (true)
  327. {
  328. var offset = text.IndexOf(left);
  329. if (offset < 0)
  330. {
  331. sb.Append(text);
  332. break;
  333. }
  334. if (offset > 0)
  335. {
  336. sb.Append(text.Substring(0, offset));
  337. text = text.Substring(offset + left.Length);
  338. }
  339. else text = text.Substring(left.Length);
  340. var length = text.IndexOf(right);
  341. if (length < 1)
  342. {
  343. sb.Append(left);
  344. sb.Append(text);
  345. break;
  346. }
  347. var inner = text.Substring(0, length);
  348. var temp = inner.SafeTrim();
  349. if (temp.StartsWith(head))
  350. {
  351. temp = temp.Substring(head.Length);
  352. temp = temp.Replace("\"", "");
  353. var subPath = MapPath(temp);
  354. var subText = ReadWithSSI(subPath, recursive + 1);
  355. if (subText != null && subText.Length > 0) sb.Append(subText);
  356. }
  357. else
  358. {
  359. sb.Append(left);
  360. sb.Append(inner);
  361. sb.Append(right);
  362. }
  363. text = text.Substring(length + right.Length);
  364. }
  365. var output = sb.ToString();
  366. return output;
  367. }
  368. }
  369. }