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

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace Apewer.Web
{
/// <summary>静态站点控制器。</summary>
public class StaticController : ApiController
{
/// <summary></summary>
public StaticController()
{
AllowFunction = false;
AfterInitialized = Execute;
}
/// <summary>获取此静态站点的目录。</summary>
public virtual string GetSiteDirectory()
{
var app = WebUtility.AppDirectory;
var www = StorageUtility.CombinePath(app, "www");
if (!Directory.Exists(www)) return null;
return www;
}
/// <summary>解析 URL 的路径,获取本地路径。</summary>
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;
}
/// <summary>处理请求。</summary>
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<string, string>();
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;
}
/// <summary>列出子项。可指定筛选器,传入完整路径。</summary>
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;
}
/// <summary>响应 403 状态。</summary>
public virtual void Respond403(string path)
{
Response.Error("403");
}
/// <summary>响应 404 状态。</summary>
public virtual void Respond404(string path)
{
Response.Error("404");
}
/// <summary>按扩展名获取 Content-Type。</summary>
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";
}
/// <summary>按扩展名获取过期时间,单位为秒,默认值为 0(不缓存)。</summary>
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 right = "-->";
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;
}
}
}