@ghini/kit
Version:
js practical tools to assist efficient development
827 lines (742 loc) • 23 kB
JavaScript
export { fn_static };
import kit from "../../main.js";
import { extname } from "path";
import { createReadStream } from "fs";
// Configuration
const MAX_PREVIEW_SIZE = 2 * 1024 * 1024; // 2MB max for text preview (reduced from 10MB)
const CHUNK_SIZE = 1024 * 1024; // 1MB chunks for streaming large files
// Define media file types that should be handled in a specific way
const MEDIA_EXTENSIONS = {
// Images
".jpg": true,
".jpeg": true,
".png": true,
".gif": true,
".webp": true,
".svg": true,
".ico": true,
".bmp": true,
".tiff": true,
".tif": true,
".heic": true,
".avif": true,
// Videos
".mp4": true,
".webm": true,
".avi": true,
".mov": true,
".wmv": true,
".flv": true,
".mkv": true,
".mpeg": true,
".mpg": true,
".m4v": true,
".3gp": true,
".ts": true,
".asf": true,
".mts": true,
".m2ts": true,
// Audio
".mp3": true,
".wav": true,
".ogg": true,
".m4a": true,
".flac": true,
".aac": true,
".wma": true,
".mid": true,
".midi": true,
".opus": true,
".aiff": true,
".alac": true,
".amr": true,
".ape": true,
};
/**
* 静态页面,将定义的url往后的都当作静态资源解析
* @param {*} url
* @param {*} path
* @param {*} view 是否有视图界面 {html,auth}|true|false
*/
function fn_static(url, path = ".", view = false) {
let reg;
if (url === "/") reg = new RegExp(`^/(.*)?$`);
else reg = new RegExp(`^${url}(\/.*)?$`);
this.addr(reg, "get", async (g) => {
let filePath = kit.xpath(g.path.slice(url.length).replace(/^\//, ""), path);
try {
if (await kit.aisdir(filePath)) {
if (view) {
console.log(333,g.param)
if(view.auth&&g.param?.auth!=view.auth)return g.raw("auth error");
if (view.html) return g.html(view.html);
else await handleDirectory(g, filePath, url);
} else g.raw("not found");
} else if (await kit.aisfile(filePath)) {
await handleFile(g, filePath);
} else {
g.server._404(g);
}
} catch (error) {
console.error("Error handling request:", error);
g.err(500, "Server error processing request");
}
});
}
async function handleFile(g, filePath) {
// Check if this is a download request
const isDownload = g.query && g.query.download === "1";
const ext = extname(filePath).toLowerCase();
const contentType = getContentType(ext);
try {
// Get file stats to determine size
const stats = await kit.astat(filePath);
const fileSize = stats.size;
// Determine how to handle the file based on type and size
const isMediaFile = MEDIA_EXTENSIONS[ext];
// Headers with appropriate content-type
const headers = {
":status": 200,
"content-type": contentType,
"content-length": fileSize,
};
// Disable caching for all files
headers["cache-control"] = "no-store, no-cache, must-revalidate, max-age=0";
headers["pragma"] = "no-cache";
headers["expires"] = "0";
// Force download in these cases:
// 1. Explicit download request
// 2. Non-media files that are too large for text preview
// 3. Binary files that aren't media or text
// Check if this is a code/script file that should be treated as text
const isCodeFile = [
".php",
".py",
".java",
".js",
".ts",
".jsx",
".tsx",
".html",
".css",
".c",
".cpp",
".cs",
".go",
".rb",
".rs",
".swift",
".sh",
".bash",
".pl",
".lua",
".kt",
".xml",
".json",
".yaml",
".yml",
".vue",
".md",
".sql",
".ini",
".conf",
".toml",
].includes(ext);
const forceDownload =
isDownload ||
(!isMediaFile && !isCodeFile && fileSize > MAX_PREVIEW_SIZE) ||
(!isMediaFile &&
!isCodeFile &&
!contentType.startsWith("text/") &&
contentType !== "application/json");
if (forceDownload) {
const fileName = filePath.split("/").pop();
headers[
"content-disposition"
] = `attachment; filename="${encodeURIComponent(fileName)}"`;
}
g.respond(headers);
// Use streaming for files over a certain size
if (fileSize > CHUNK_SIZE) {
// Stream the file in chunks
const stream = createReadStream(filePath, { highWaterMark: CHUNK_SIZE });
stream.on("data", (chunk) => {
g.write(chunk);
});
stream.on("end", () => {
g.end();
});
stream.on("error", (error) => {
console.error("Stream error:", error);
g.end(); // End the response on error
});
} else {
// For smaller files, read the entire file at once
const content = await kit.arf(filePath, null);
g.end(content);
}
} catch (error) {
console.error("Error serving file:", error);
g.err(500, "Error serving file");
}
}
function formatFileSize(size) {
if (size < 1024) return size + " B";
if (size < 1024 * 1024) return (size / 1024).toFixed(1) + " KB";
if (size < 1024 * 1024 * 1024)
return (size / (1024 * 1024)).toFixed(1) + " MB";
return (size / (1024 * 1024 * 1024)).toFixed(1) + " GB";
}
function getContentType(ext) {
const mimeTypes = {
// Text and document formats
".html": "text/html; charset=utf-8",
".css": "text/css; charset=utf-8",
".js": "text/javascript; charset=utf-8",
".pdf": "application/pdf",
".json": "application/json; charset=utf-8",
".jsonc": "application/json; charset=utf-8",
".txt": "text/plain; charset=utf-8",
".doc": "application/msword",
".docx":
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
".xls": "application/vnd.ms-excel",
".xlsx":
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
".ppt": "application/vnd.ms-powerpoint",
".pptx":
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
// Image formats
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".gif": "image/gif",
".webp": "image/webp",
".svg": "image/svg+xml",
".ico": "image/x-icon",
".bmp": "image/bmp",
".tiff": "image/tiff",
".tif": "image/tiff",
".heic": "image/heic",
".avif": "image/avif",
// Audio formats
".mp3": "audio/mpeg",
".wav": "audio/wav",
".ogg": "audio/ogg",
".m4a": "audio/mp4",
".flac": "audio/flac",
".aac": "audio/aac",
".wma": "audio/x-ms-wma",
".mid": "audio/midi",
".midi": "audio/midi",
".opus": "audio/opus",
".aiff": "audio/aiff",
".alac": "audio/alac",
".amr": "audio/amr",
".ape": "audio/ape",
// Video formats
".mp4": "video/mp4",
".webm": "video/webm",
".avi": "video/x-msvideo",
".mov": "video/quicktime",
".wmv": "video/x-ms-wmv",
".flv": "video/x-flv",
".mkv": "video/x-matroska",
".mpeg": "video/mpeg",
".mpg": "video/mpeg",
".m4v": "video/mp4",
".3gp": "video/3gpp",
".ts": "video/mp2t",
".asf": "video/x-ms-asf",
".mts": "video/mp2t",
".m2ts": "video/mp2t",
// Archive formats
".zip": "application/zip",
// Source code and configuration files
".yaml": "text/plain; charset=utf-8",
".yml": "text/plain; charset=utf-8",
".php": "text/plain; charset=utf-8",
".java": "text/plain; charset=utf-8",
".py": "text/plain; charset=utf-8",
".c": "text/plain; charset=utf-8",
".cpp": "text/plain; charset=utf-8",
".h": "text/plain; charset=utf-8",
".hpp": "text/plain; charset=utf-8",
".cs": "text/plain; charset=utf-8",
".go": "text/plain; charset=utf-8",
".rs": "text/plain; charset=utf-8",
".rb": "text/plain; charset=utf-8",
".swift": "text/plain; charset=utf-8",
".kt": "text/plain; charset=utf-8",
".ts": "text/plain; charset=utf-8",
".jsx": "text/plain; charset=utf-8",
".tsx": "text/plain; charset=utf-8",
".vue": "text/plain; charset=utf-8",
".md": "text/plain; charset=utf-8",
".markdown": "text/plain; charset=utf-8",
".xml": "text/plain; charset=utf-8",
".sh": "text/plain; charset=utf-8",
".bash": "text/plain; charset=utf-8",
".csv": "text/plain; charset=utf-8",
".sql": "text/plain; charset=utf-8",
".log": "text/plain; charset=utf-8",
".ini": "text/plain; charset=utf-8",
".toml": "text/plain; charset=utf-8",
".conf": "text/plain; charset=utf-8",
".config": "text/plain; charset=utf-8",
};
return mimeTypes[ext] || "application/octet-stream";
}
// 修改 handleDirectory 函数获取和显示最后修改时间
async function handleDirectory(g, filePath, url) {
let files = await kit.adir(filePath);
let html = fileSystem;
if (url != g.path) {
let parentPath = g.path.split("/").slice(0, -1).join("/") || "/";
html += `<a href="${parentPath+g.search}" class="parent-link"><i class="fas fa-arrow-left"></i> 返回上级目录 (Parent Directory)</a>`;
}
html += `<ul class="file-list">`;
// 排序文件:目录优先,然后是常规文件
let directories = [];
let regularFiles = [];
for (let file of files) {
let fullPath = kit.xpath(file, filePath);
let isDir = await kit.aisdir(fullPath);
if (isDir) {
directories.push(file);
} else {
regularFiles.push(file);
}
}
directories.sort((a, b) => a.localeCompare(b));
regularFiles.sort((a, b) => a.localeCompare(b));
const sortedFiles = [...directories, ...regularFiles];
// 处理每个文件
for (let file of sortedFiles) {
let fullPath = kit.xpath(file, filePath);
let isDir = await kit.aisdir(fullPath);
let link = g.path === "/" ? "/" + file : g.path + "/" + file;
let icon = isDir ? "fa-folder" : "fa-file";
let fileName = file;
let fileSize = "";
let modifiedTime = "";
// 获取文件信息(大小和修改时间)
try {
const stats = await kit.astat(fullPath);
fileSize = formatFileSize(stats.size);
modifiedTime = formatModifiedTime(stats.mtime);
// 根据文件类型设置适当的图标
const ext = extname(fileName).toLowerCase();
if (ext) {
if (MEDIA_EXTENSIONS[ext]) {
if (ext === ".mp4" || ext === ".webm") {
icon = "fa-file-video";
} else if (ext === ".mp3" || ext === ".wav") {
icon = "fa-file-audio";
} else {
icon = "fa-file-image";
}
} else if (ext === ".pdf") {
icon = "fa-file-pdf";
} else if ([".doc", ".docx"].includes(ext)) {
icon = "fa-file-word";
} else if ([".xls", ".xlsx"].includes(ext)) {
icon = "fa-file-excel";
} else if ([".ppt", ".pptx"].includes(ext)) {
icon = "fa-file-powerpoint";
} else if ([".zip"].includes(ext)) {
icon = "fa-file-archive";
} else if (
[".html", ".css", ".js", ".jsx", ".ts", ".tsx", ".json"].includes(
ext
)
) {
icon = "fa-file-code";
} else if ([".txt", ".md", ".markdown"].includes(ext)) {
icon = "fa-file-alt";
}
}
} catch (error) {
fileSize = "Unknown size";
modifiedTime = "Unknown date";
}
let displayName;
if (isDir) {
displayName = `<span class="file-name">
<span class="file-name-main">${fileName}</span>
<span class="file-name-ext">/</span>
</span>`;
} else {
// 拆分文件名和扩展名
let lastDotIndex = fileName.lastIndexOf(".");
let nameMain =
lastDotIndex > 0 ? fileName.slice(0, lastDotIndex) : fileName;
let nameExt = lastDotIndex > 0 ? fileName.slice(lastDotIndex) : "";
displayName = `<span class="file-name">
<span class="file-name-main">${nameMain}</span>
<span class="file-name-ext">${nameExt}</span>
</span>`;
}
html += `
<li>
<div class="file-info">
<a href="${link+g.search}">
<i class="fas ${icon}"></i>
${displayName}
</a>`;
// 如果不是目录,显示文件大小和下载按钮
if (!isDir) {
html += `
<span class="file-size">${fileSize}</span>
<button onclick="window.location.href='${link}?download=1'"
class="download-btn"
title="下载文件"
type="button">
<i class="fas fa-download"></i>
</button>`;
}
html += `
</div>
<span class="file-modified-time">${modifiedTime}</span>`;
html += `</li>`;
}
html += `</ul></div></body></html>`;
g.respond({
":status": 200,
"content-type": "text/html; charset=utf-8",
});
g.end(html);
}
// 添加格式化修改时间的函数
function formatModifiedTime(date) {
if (!date) return "-";
const d = new Date(date);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
const hours = String(d.getHours()).padStart(2, '0');
const minutes = String(d.getMinutes()).padStart(2, '0');
return `${year.toString().slice(2)}${month}${day} ${hours}:${minutes}`;
}
// 修改 CSS 部分(文件系统模板的更新)
const fileSystem = `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>文件系统 | File System</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
transition: background-color 0.3s, color 0.3s;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
line-height: 1.4;
padding: 1.5rem;
}
body.dark-mode {
background:
color:
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1rem;
}
.header-container {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 2px solid
}
.dark-mode .header-container {
border-bottom-color:
}
h1 {
font-size: 1.8rem;
color:
margin: 0;
}
.dark-mode h1 {
color:
}
.theme-toggle {
background: none;
border: none;
cursor: pointer;
font-size: 1.5rem;
color:
padding: 0.5rem;
}
.dark-mode .theme-toggle {
color:
}
.file-list {
list-style: none;
}
.file-list li {
padding: 0.3rem 0.5rem;
transition: background-color 0.2s;
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 0.5rem;
height: 32px;
min-width: 0;
}
.file-info {
display: flex;
align-items: center;
min-width: 0;
width: 100%;
}
.file-list li:hover {
background-color:
}
.dark-mode .file-list li:hover {
background-color:
}
.file-list a {
display: flex;
align-items: center;
text-decoration: none;
color:
height: 100%;
min-width: 0;
flex: 1;
margin-right: 0.5rem;
}
.file-name {
display: flex;
white-space: nowrap;
min-width: 0;
max-width: 100%;
}
.file-name-main {
overflow: hidden;
text-overflow: ellipsis;
min-width: 10px;
}
.file-name-ext {
flex-shrink: 0;
}
.dark-mode .file-list a {
color:
}
.file-list i {
margin-right: 1rem;
font-size: 1.2rem;
line-height: 1;
}
.file-list .fa-folder {
color:
}
.file-list .fa-file {
color:
}
.file-list .fa-file-image {
color:
}
.file-list .fa-file-video {
color:
}
.file-list .fa-file-audio {
color:
}
.file-list .fa-file-pdf {
color:
}
.file-list .fa-file-word {
color:
}
.file-list .fa-file-excel {
color:
}
.file-list .fa-file-powerpoint {
color:
}
.file-list .fa-file-archive {
color:
}
.file-list .fa-file-code {
color:
}
.file-list .fa-file-alt {
color:
}
.dark-mode .file-list .fa-file {
color:
}
.file-size, .file-modified-time {
color:
font-size: 0.85rem;
white-space: nowrap;
}
.file-size {
margin-right: 0.5rem;
}
.dark-mode .file-size, .dark-mode .file-modified-time {
color:
}
.download-btn {
color:
cursor: pointer;
border: none;
background: none;
transition: color 0.2s;
display: flex;
align-items: center;
justify-content: center;
width: 1.5rem;
height: 100%;
padding: 0;
flex-shrink: 0;
}
.dark-mode .download-btn {
color:
}
.download-btn:hover {
color:
}
.dark-mode .download-btn:hover {
color:
}
.parent-link {
margin-bottom: 1rem;
display: inline-block;
padding: 0.5rem 1rem;
background:
border-radius: 4px;
text-decoration: none;
color:
}
.dark-mode .parent-link {
background:
color:
}
.parent-link:hover {
background:
}
.dark-mode .parent-link:hover {
background:
}
.file-too-large {
text-align: center;
padding: 2rem;
background:
border-radius: 8px;
margin: 2rem auto;
max-width: 600px;
}
.dark-mode .file-too-large {
background:
}
.file-too-large h2 {
margin-bottom: 1rem;
color:
}
.dark-mode .file-too-large h2 {
color:
}
.file-too-large p {
margin-bottom: 1.5rem;
color:
}
.dark-mode .file-too-large p {
color:
}
.file-too-large .btn {
display: inline-block;
padding: 0.5rem 1rem;
background:
color: white;
border-radius: 4px;
text-decoration: none;
transition: background-color 0.2s;
}
.file-too-large .btn:hover {
background:
}
@media (max-width: 768px) {
body {
padding: 1rem;
}
.container {
padding: 0.5rem;
}
h1 {
font-size: 1.2rem;
}
.file-list li {
padding: 0.3rem;
gap: 0.5rem;
}
.file-list i {
margin-right: 0.5rem;
font-size: 1rem;
}
.file-size {
font-size: 0.7rem;
}
.download-btn {
width: 1.2rem;
}
}
/* 增加一个中等尺寸屏幕的媒体查询,适配不同设备 */
@media (max-width: 992px) and (min-width: 769px) {
.file-list li {
gap: 0.3rem;
}
.file-modified-time {
font-size: 0.75rem;
}
.file-size {
font-size: 0.75rem;
}
}
</style>
</head>
<body class="dark-mode">
<div class="container">
<div class="header-container">
<h1>文件目录 Directory</h1>
<button class="theme-toggle" aria-label="Toggle dark mode">
<i class="fas fa-sun"></i>
</button>
</div>
<script>
// Theme toggle functionality
const body = document.body;
const themeToggle = document.querySelector('.theme-toggle');
const themeIcon = themeToggle.querySelector('i');
// Get theme settings from localStorage
const isDarkMode = localStorage.getItem('darkMode') !== 'false'; // Default to dark theme
body.classList.toggle('dark-mode', isDarkMode);
updateThemeIcon(isDarkMode);
themeToggle.addEventListener('click', () => {
const isDark = body.classList.toggle('dark-mode');
localStorage.setItem('darkMode', isDark);
updateThemeIcon(isDark);
});
function updateThemeIcon(isDark) {
themeIcon.className = isDark ? 'fas fa-sun' : 'fas fa-moon';
}
// Add file size display info
document.addEventListener('DOMContentLoaded', function() {
// Auto-download very large files
const urlParams = new URLSearchParams(window.location.search);
const autoDownload = urlParams.get('auto_download');
if (autoDownload === '1') {
const downloadUrl = window.location.pathname + '?download=1';
window.location.href = downloadUrl;
}
});
</script>
`;