文件托管生成器

文件托管&&index.html生成器

DS写的
开发服务器(node.js)

import express from 'express';
import fs from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import moment from 'moment';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const app = express();
const PORT = 3000;

// 生成目录索引HTML
async function generateDirectoryIndex(dirPath, requestPath) {
    const items = await fs.readdir(dirPath);
    const rows = [];

    // 添加返回上级目录的链接(如果不是根目录)
    if (requestPath !== '/') {
        rows.push(`
      <tr>
        <td><a href="../">../</a></td>
        <td>-</td>
        <td>-</td>
      </tr>
    `);
    }

    for (const item of items) {
        const fullPath = path.join(dirPath, item);
        const stat = await fs.stat(fullPath);
        const isDirectory = stat.isDirectory();
        const itemName = isDirectory ? `${item}/` : item;
        const itemPath = path.join(requestPath, item);

        const size = isDirectory ? '-' : formatFileSize(stat.size);
        const modifiedTime = moment(stat.mtime).format('YYYY-MM-DD HH:mm:ss');

        rows.push(`
      <tr>
        <td><a href="${itemPath}">${itemName}</a></td>
        <td>${modifiedTime}</td>
        <td>${size}</td>
      </tr>
    `);
    }

    return `
    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Index of ${requestPath}</title>
      <style>
        body {
          font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
          line-height: 1.6;
          max-width: 1200px;
          margin: 40px auto;
          padding: 0 20px;
          color: #333;
        }
        h1 { font-size: 1.8rem; border-bottom: 1px solid #eaecef; padding-bottom: 0.3em; }
        table { width: 100%; border-collapse: collapse; }
        th, td { text-align: left; padding: 8px 12px; border-bottom: 1px solid #eaecef; }
        th { background-color: #f6f8fa; font-weight: 600; }
        tr:hover { background-color: #f6f8fa; }
        a { color: #0366d6; text-decoration: none; }
        a:hover { text-decoration: underline; }
        .footer { margin-top: 30px; font-size: 0.8rem; color: #666; text-align: center; }
      </style>
    </head>
    <body>
      <h1>Index of ${requestPath}</h1>
      <table>
        <thead>
          <tr><th>Name</th><th>Last Modified</th><th>Size</th></tr>
        </thead>
        <tbody>
          ${rows.join('')}
        </tbody>
      </table>
      <div class="footer">Powered by Node.js</div>
    </body>
    </html>
  `;
}

// 辅助函数:格式化文件大小
function formatFileSize(bytes) {
    if (bytes === 0) return '0 Bytes';
    const k = 1024;
    const sizes = ['Bytes', 'KB', 'MB', 'GB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}

// 中间件:处理静态文件或目录索引
app.use(async (req, res) => {
    const filePath = path.join(__dirname, 'public', req.path);

    try {
        const stat = await fs.stat(filePath);

        if (stat.isDirectory()) {
            // 如果是目录,生成索引页面
            const html = await generateDirectoryIndex(filePath, req.path);
            res.send(html);
        } else {
            // 如果是文件,直接返回
            res.sendFile(filePath);
        }
    } catch (error) {
        // 文件不存在或其他错误
        res.status(404).send('File not found');
    }
});

app.listen(PORT, () => {
    console.log(`Server running at http://localhost:${PORT}/`);
    console.log('Serve files from ./public directory');
});

生成器

import fs from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import moment from 'moment';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const PUBLIC_DIR = path.join(__dirname, 'public');
const OUTPUT_DIR = path.join(__dirname, 'html');

// 辅助函数:格式化文件大小
function formatFileSize(bytes) {
    if (bytes === 0) return '0 Bytes';
    const k = 1024;
    const sizes = ['Bytes', 'KB', 'MB', 'GB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}

// 生成目录索引HTML(与服务器风格完全一致)
async function generateDirectoryIndex(dirPath, requestPath) {
    const items = await fs.readdir(dirPath);
    const rows = [];

    // 添加返回上级目录的链接(如果不是根目录)
    if (requestPath !== '/') {
        rows.push(`
      <tr>
        <td><a href="../">../</a></td>
        <td>-</td>
        <td>-</td>
      </tr>
    `);
    }

    for (const item of items) {
        const fullPath = path.join(dirPath, item);
        const stat = await fs.stat(fullPath);
        const isDirectory = stat.isDirectory();
        const itemName = isDirectory ? `${item}/` : item;
        const itemPath = path.join(requestPath, item);

        const size = isDirectory ? '-' : formatFileSize(stat.size);
        const modifiedTime = moment(stat.mtime).format('YYYY-MM-DD HH:mm:ss');

        rows.push(`
      <tr>
        <td><a href="${itemPath}">${escapeHtml(itemName)}</a></td>
        <td>${modifiedTime}</td>
        <td>${size}</td>
      </tr>
    `);
    }

    return `
    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Index of ${requestPath}</title>
      <style>
        body {
          font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
          line-height: 1.6;
          max-width: 1200px;
          margin: 40px auto;
          padding: 0 20px;
          color: #333;
        }
        h1 { font-size: 1.8rem; border-bottom: 1px solid #eaecef; padding-bottom: 0.3em; }
        table { width: 100%; border-collapse: collapse; }
        th, td { text-align: left; padding: 8px 12px; border-bottom: 1px solid #eaecef; }
        th { background-color: #f6f8fa; font-weight: 600; }
        tr:hover { background-color: #f6f8fa; }
        a { color: #0366d6; text-decoration: none; }
        a:hover { text-decoration: underline; }
        .footer { margin-top: 30px; font-size: 0.8rem; color: #666; text-align: center; }
      </style>
    </head>
    <body>
      <h1>Index of ${requestPath}</h1>
      <table>
        <thead>
          <tr><th>Name</th><th>Last Modified</th><th>Size</th></tr>
        </thead>
        <tbody>
          ${rows.join('')}
        </tbody>
      </table>
      <div class="footer">Powered by Node.js | Static Generated</div>
    </body>
    </html>
  `;
}

// HTML转义函数(防止XSS攻击)
function escapeHtml(text) {
    const map = {
        '&': '&amp;',
        '<': '&lt;',
        '>': '&gt;',
        '"': '&quot;',
        "'": '&#39;'
    };
    return text.replace(/[&<>"']/g, m => map[m]);
}

// 递归处理目录并生成HTML文件
async function processDirectory(dirPath, outputPath, webPath) {
    // 创建输出目录
    await fs.mkdir(outputPath, { recursive: true });
    
    // 生成当前目录的索引HTML
    const html = await generateDirectoryIndex(dirPath, webPath);
    const indexPath = path.join(outputPath, 'index.html');
    await fs.writeFile(indexPath, html, 'utf-8');
    console.log(`✓ 生成: ${webPath || '/'} -> ${indexPath}`);
    
    // 处理子目录和文件
    const items = await fs.readdir(dirPath);
    
    for (const item of items) {
        const itemPath = path.join(dirPath, item);
        const stat = await fs.stat(itemPath);
        const itemOutputPath = path.join(outputPath, item);
        const itemWebPath = webPath === '/' ? `/${item}` : `${webPath}/${item}`;
        
        if (stat.isDirectory()) {
            // 递归处理子目录
            await processDirectory(itemPath, itemOutputPath, itemWebPath);
        } else {
            // 复制文件到输出目录(保留原文件)
            const fileOutputPath = itemOutputPath;
            const fileDir = path.dirname(fileOutputPath);
            await fs.mkdir(fileDir, { recursive: true });
            await fs.copyFile(itemPath, fileOutputPath);
            console.log(`✓ 复制文件: ${itemWebPath}`);
        }
    }
}

// 清理输出目录
async function cleanOutputDir() {
    try {
        await fs.rm(OUTPUT_DIR, { recursive: true, force: true });
        console.log('✓ 清理输出目录');
    } catch (error) {
        // 目录不存在时忽略错误
    }
}

// 主函数
async function main() {
    console.log('🚀 开始生成静态网站...\n');
    
    // 检查public目录是否存在
    try {
        await fs.access(PUBLIC_DIR);
    } catch (error) {
        console.error('❌ public 目录不存在!请先创建 public 目录并放入文件。');
        process.exit(1);
    }
    
    // 清理并重建输出目录
    await cleanOutputDir();
    await fs.mkdir(OUTPUT_DIR, { recursive: true });
    
    // 开始处理
    await processDirectory(PUBLIC_DIR, OUTPUT_DIR, '/');
    
    console.log('\n✨ 静态网站生成完成!');
    console.log(`📁 输出目录: ${OUTPUT_DIR}`);
    console.log('\n🌐 可以使用以下命令预览:');
    console.log('   npx serve html');
    console.log('   python -m http.server --directory html 8080');
    console.log('   cd html && php -S localhost:8000');
}

// 运行
main().catch(console.error);

依赖如下
expressmoment


文件托管生成器
http://zec-cn.pages.dev/2026/05/17/files/
作者
zec
发布于
2026年5月17日
许可协议