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.

436 lines
18 KiB

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