using System; using System.Collections.Generic; using System.IO; using System.Text; namespace Apewer.Web { /// 静态站点控制器。 public class StaticController : ApiController { /// public StaticController() { AllowFunction = false; AfterInitialized = Execute; } /// 获取此静态站点的目录。 public virtual string GetSiteDirectory() { var app = WebUtility.AppDirectory; var www = StorageUtility.CombinePath(app, "www"); if (!Directory.Exists(www)) return null; return www; } /// 解析 URL 的路径,获取本地路径。 protected string MapPath(string urlPath) { var path = GetSiteDirectory(); if (!string.IsNullOrEmpty(urlPath)) { foreach (var split in urlPath.Split('/')) { var seg = split.SafeTrim(); if (string.IsNullOrEmpty(seg)) continue; if (seg == "." || seg == "..") continue; path = StorageUtility.CombinePath(path, seg); } } return path; } /// 处理请求。 public virtual void Execute() { if (Request == null || Request.Url == null) return; var path = MapPath(Request.Url.AbsolutePath); if (StorageUtility.DirectoryExists(path)) { ExecuteDirectory(path); return; } if (StorageUtility.FileExists(path)) { ExecuteFile(path); return; } Respond404(path); } void ExecuteFile(string path) { if (!File.Exists(path)) { Respond404(path); return; } // 获取文件扩展名。 var ext = Path.GetExtension(path).SafeLower(); if (ext.Length > 1 && ext.StartsWith(".")) ext = ext.Substring(1); // 按扩展名获取缓存过期时间。 var expires = GetExpires(ext); if (expires > 0) Response.Expires = expires; // 按扩展名获取 Content-Type。 var type = GetContentType(ext); // var name = Path.GetFileName(path); // Server Side Includes if (ext == "html" || ext == "htm" || ext == "shtml") { var html = ReadWithSSI(path); var bytes = html.ToBinary(); Response.Binary(bytes, type); return; } var stream = StorageUtility.OpenFile(path, true); Response.Binary(stream, type); } void ExecuteDirectory(string path) { if (!Directory.Exists(path)) { Respond404(path); return; } var @default = GetDefaultFile(path); if (string.IsNullOrEmpty(@default)) Respond403(path); else ExecuteFile(@default); } // 获取目录中的默认文件,返回文件的完整路径。 string GetDefaultFile(string directory) { var subs = StorageUtility.GetSubFiles(directory); if (subs.Count < 0) return null; subs.Sort(); var names = new Dictionary(); foreach (var sub in subs) { var name = Path.GetFileName(sub).ToLower(); if (names.ContainsKey(name)) continue; names.Add(name, sub); } if (names.ContainsKey("index.html")) return names["index.html"]; if (names.ContainsKey("index.htm")) return names["index.htm"]; if (names.ContainsKey("default.html")) return names["default.html"]; if (names.ContainsKey("default.htm")) return names["default.htm"]; return null; } /// 列出子项。可指定筛选器,传入完整路径。 public void ListChildren(string directory) { if (!Directory.Exists(directory)) return; var json = Json.NewObject(); json.SetProperty("directories", ListDirectories(directory)); json.SetProperty("files", ListFiles(directory)); Response.Data = json; } Json ListDirectories(string directory) { var array = Json.NewArray(); var subs = StorageUtility.GetSubDirectories(directory); subs.Sort(); foreach (var sub in subs) { var name = Path.GetDirectoryName(sub); var lower = name.SafeLower(); if (lower == ".") continue; if (lower == "..") continue; if (lower == "@eadir") continue; // Synology if (lower == "#recycle") continue; // Synology if (lower == "$recycle.bin") continue; // Windows if (lower == "recycler") continue; // Windows if (lower == "system volume information") continue; // Windows var json = Json.NewObject(); json.SetProperty("name", name); try { var info = new DirectoryInfo(sub); json.SetProperty("modified", info.LastWriteTimeUtc.ToStamp()); } catch { } array.AddItem(json); } return array; } Json ListFiles(string directory) { var array = Json.NewArray(); var subs = StorageUtility.GetSubFiles(directory); subs.Sort(); foreach (var sub in subs) { var name = Path.GetFileName(sub); var lower = name.SafeLower(); if (lower == ".ds_store") continue; // macOS if (lower == ".localized") continue; // macOS if (lower == "desktop.ini") continue; // Windiows if (lower == "thumbs.db") continue; // Windiows var json = Json.NewObject(); json.SetProperty("name", name); try { var info = new FileInfo(sub); json.SetProperty("size", info.Length); json.SetProperty("modified", info.LastWriteTimeUtc.ToStamp()); } catch { } array.AddItem(json); } return array; } /// 响应 403 状态。 public virtual void Respond403(string path) { Response.Error("403"); } /// 响应 404 状态。 public virtual void Respond404(string path) { Response.Error("404"); } /// 按扩展名获取 Content-Type。 public virtual string GetContentType(string extension) { if (!string.IsNullOrEmpty(extension)) { var lower = extension.ToLower(); switch (lower) { case "3gp": return "video/3gpp"; case "3gpp": return "video/3gpp"; case "7z": return "application/x-7z-compressed"; case "ai": return "application/postscript"; case "asf": return "video/x-ms-asf"; case "asx": return "video/x-ms-asf"; case "atom": return "application/atom+xml"; case "avi": return "video/x-msvideo"; case "bin": return "application/octet-stream"; case "bmp": return "image/x-ms-bmp"; case "cco": return "application/x-cocoa"; case "crt": return "application/x-x509-ca-cert"; case "css": return "text/css"; case "deb": return "application/octet-stream"; case "der": return "application/x-x509-ca-cert"; case "dll": return "application/octet-stream"; case "dmg": return "application/octet-stream"; case "doc": return "application/msword"; case "docx": return "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; case "ear": return "application/java-archive"; case "eot": return "application/vnd.ms-fontobject"; case "eps": return "application/postscript"; case "exe": return "application/octet-stream"; case "flv": return "video/x-flv"; case "gif": return "image/gif"; case "hqx": return "application/mac-binhex40"; case "htc": return "text/x-component"; case "htm": return "text/html"; case "html": return "text/html"; case "ico": return "image/x-icon"; case "img": return "application/octet-stream"; case "iso": return "application/octet-stream"; case "jad": return "text/vnd.sun.j2me.app-descriptor"; case "jar": return "application/java-archive"; case "jardiff": return "application/x-java-archive-diff"; case "jng": return "image/x-jng"; case "jnlp": return "application/x-java-jnlp-file"; case "jpeg": return "image/jpeg"; case "jpg": return "image/jpeg"; case "js": return "application/javascript"; // case "json": return "application/json"; case "json": return "text/json"; case "kar": return "audio/midi"; case "kml": return "application/vnd.google-earth.kml+xml"; case "kmz": return "application/vnd.google-earth.kmz"; // case "m3u8": return "application/vnd.apple.mpegurl"; case "m3u8": return "text/vnd.apple.mpegurl"; case "m4a": return "audio/x-m4a"; case "m4v": return "video/x-m4v"; case "mid": return "audio/midi"; case "midi": return "audio/midi"; case "mml": return "text/mathml"; case "mng": return "video/x-mng"; case "mov": return "video/quicktime"; case "mp3": return "audio/mpeg"; case "mp4": return "video/mp4"; case "mpeg": return "video/mpeg"; case "mpg": return "video/mpeg"; case "msi": return "application/octet-stream"; case "msm": return "application/octet-stream"; case "msp": return "application/octet-stream"; case "odg": return "application/vnd.oasis.opendocument.graphics"; case "odp": return "application/vnd.oasis.opendocument.presentation"; case "ods": return "application/vnd.oasis.opendocument.spreadsheet"; case "odt": return "application/vnd.oasis.opendocument.text"; case "ogg": return "audio/ogg"; case "pdb": return "application/x-pilot"; case "pdf": return "application/pdf"; case "pem": return "application/x-x509-ca-cert"; case "pl": return "application/x-perl"; case "pm": return "application/x-perl"; case "png": return "image/png"; case "ppt": return "application/vnd.ms-powerpoint"; case "pptx": return "application/vnd.openxmlformats-officedocument.presentationml.presentation"; case "prc": return "application/x-pilot"; case "ps": return "application/postscript"; case "ra": return "audio/x-realaudio"; case "rar": return "application/x-rar-compressed"; case "rpm": return "application/x-redhat-package-manager"; case "rss": return "application/rss+xml"; case "rtf": return "application/rtf"; case "run": return "application/x-makeself"; case "sea": return "application/x-sea"; case "shtml": return "text/html"; case "sit": return "application/x-stuffit"; case "svg": return "image/svg+xml"; case "svgz": return "image/svg+xml"; case "swf": return "application/x-shockwave-flash"; case "tcl": return "application/x-tcl"; case "tif": return "image/tiff"; case "tiff": return "image/tiff"; case "tk": return "application/x-tcl"; case "ts": return "video/mp2t"; case "txt": return "text/plain"; case "war": return "application/java-archive"; case "wbmp": return "image/vnd.wap.wbmp"; case "webm": return "video/webm"; case "webp": return "image/webp"; case "wml": return "text/vnd.wap.wml"; case "wmlc": return "application/vnd.wap.wmlc"; case "wmv": return "video/x-ms-wmv"; case "woff": return "font/woff"; case "woff2": return "font/woff2"; case "xhtml": return "application/xhtml+xml"; case "xls": return "application/vnd.ms-excel"; case "xlsx": return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; case "xml": return "text/xml"; case "xpi": return "application/x-xpinstall"; case "xspf": return "application/xspf+xml"; case "zip": return "application/zip"; } } return "application/octet-stream"; } /// 按扩展名获取过期时间,单位为秒,默认值为 0(不缓存)。 public virtual int GetExpires(string extension) => 0; // Server Side Includes string ReadWithSSI(string path, int recursive = 0) { if (recursive > 10) return ""; var input = StorageUtility.ReadFile(path, true); if (input == null || input.LongLength < 1) return ""; // 尝试以 UTF-8 解码。 var html = TextUtility.FromBinary(input); if (string.IsNullOrEmpty(html)) return ""; // 按首尾截取。 const string left = ""; const string head = "#include virtual="; var sb = new StringBuilder(); var text = html; while (true) { var offset = text.IndexOf(left); if (offset < 0) { sb.Append(text); break; } if (offset > 0) { sb.Append(text.Substring(0, offset)); text = text.Substring(offset + left.Length); } else text = text.Substring(left.Length); var length = text.IndexOf(right); if (length < 1) { sb.Append(left); sb.Append(text); break; } var inner = text.Substring(0, length); var temp = inner.SafeTrim(); if (temp.StartsWith(head)) { temp = temp.Substring(head.Length); temp = temp.Replace("\"", ""); var subPath = MapPath(temp); var subText = ReadWithSSI(subPath, recursive + 1); if (subText != null && subText.Length > 0) sb.Append(subText); } else { sb.Append(left); sb.Append(inner); sb.Append(right); } text = text.Substring(length + right.Length); } var output = sb.ToString(); return output; } } }