Flask搭建HTML文档服务器-轻松共享和浏览文档

发布于:2025-08-05 ⋅ 阅读:(12) ⋅ 点赞:(0)

本文详细介绍如何用Flask搭建一个美观实用的HTML文档服务器,适合团队共享技术文档、产品手册等HTML内容。

一、什么是HTML文档服务器?

HTML文档服务器是一个专门用于托管和展示HTML文件的Web应用程序。想象一下你的团队有很多技术文档(比如用Markdown生成的手册),需要一个集中存放、方便访问的地方 - 这就是HTML文档服务器的作用。

为什么选择HTML而不是PDF?

  1. 无缝阅读体验:HTML文档没有PDF的分页问题,内容连续展示
  2. 响应式设计:自动适应手机、平板和电脑屏幕
  3. 更大预览区域:充分利用屏幕空间展示内容
  4. 轻量快速:加载速度比PDF更快
  5. 内部链接支持:文档内可以方便地添加跳转链接

二、设计思路

我们的服务器基于Python的Flask框架,提供以下核心功能:

  • 目录浏览:像文件管理器一样查看文档结构
  • 文档预览:直接查看HTML内容
  • 智能排序:自动按文档中的数字排序
  • 面包屑导航:清晰展示当前位置
  • 响应式设计:在手机、平板和电脑上都有良好体验

三、搭建步骤详解

步骤1:创建Flask应用

首先,我们需要编写服务器核心代码。这段代码创建了一个Flask应用,包含文档浏览和预览功能:

cat > /opt/html_doc_server.py  <<-'EOF'
import os
from flask import Flask, send_file, render_template_string, request, abort

app = Flask(__name__)

# 从环境变量获取HTML目录,默认为当前目录下的html_files
HTML_DIR = os.getenv('HTML_DIR', os.path.abspath('./html_files'))

def get_sorted_files(path):
    """获取指定路径下的文件和目录,并按数字排序"""
    items = []

    # 遍历目录内容
    for name in os.listdir(path):
        full_path = os.path.join(path, name)

        # 跳过隐藏文件和目录
        if name.startswith('.'):
            continue

        # 如果是目录,类型设为'dir'
        if os.path.isdir(full_path):
            items.append({
                'name': name,
                'type': 'dir',
                'num': float('inf')  # 目录排在文件后面
            })
        # 如果是HTML文件,提取数字并排序
        elif name.endswith('.html'):
            try:
                # 提取文件名中的数字部分
                num = int(''.join(filter(str.isdigit, name)))
            except ValueError:
                num = float('inf')  # 非数字文件名排在最后
            items.append({
                'name': name,
                'type': 'file',
                'num': num
            })

    # 排序:先数字文件(从小到大),然后非数字文件,最后目录
    sorted_items = sorted(items, key=lambda x: (x['type'] != 'dir', x['num']))
    return sorted_items

@app.route('/')
@app.route('/browse/')
@app.route('/browse/<path:subpath>')
def browse(subpath=''):
    """浏览目录内容(支持多级目录)"""
    try:
        # 构建完整路径
        base_path = os.path.abspath(HTML_DIR)
        full_path = os.path.join(base_path, subpath)

        # 安全性检查:确保路径在HTML_DIR内
        if not os.path.abspath(full_path).startswith(base_path):
            abort(403, "禁止访问该路径")

        # 检查路径是否存在
        if not os.path.exists(full_path):
            abort(404, "路径不存在")

        # 如果是文件,直接返回文件内容
        if os.path.isfile(full_path):
            if full_path.endswith('.html'):
                return send_file(full_path)
            else:
                abort(400, "仅支持HTML文件预览")

        # 获取排序后的目录内容
        items = get_sorted_files(full_path)

        # 生成面包屑导航
        breadcrumbs = []
        parts = subpath.split('/') if subpath else []
        current_path = ''

        # 添加根目录
        breadcrumbs.append({'name': '根目录', 'path': ''})

        # 添加中间路径
        for i, part in enumerate(parts):
            current_path = os.path.join(current_path, part) if current_path else part
            breadcrumbs.append({
                'name': part,
                'path': current_path if i < len(parts) - 1 else None
            })

        # 渲染目录浏览页面
        return render_template_string('''
            <!DOCTYPE html>
            <html>
            <head>
                <title>文件浏览器 - {{ subpath or '根目录' }}</title>
                <meta name="viewport" content="width=device-width, initial-scale=1">
                <style>
                    * { box-sizing: border-box; margin: 0; padding: 0; }
                    body {
                        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
                        line-height: 1.6;
                        color: #333;
                        background-color: #f8f9fa;
                        padding: 20px;
                        max-width: 1200px;
                        margin: 0 auto;
                    }

                    .header {
                        margin-bottom: 25px;
                        padding-bottom: 15px;
                        border-bottom: 1px solid #eaeaea;
                    }

                    h1 {
                        font-size: 28px;
                        color: #202124;
                        margin-bottom: 15px;
                    }

                    .breadcrumb {
                        display: flex;
                        flex-wrap: wrap;
                        align-items: center;
                        font-size: 15px;
                        margin-bottom: 20px;
                        background: #eef2f7;
                        padding: 10px 15px;
                        border-radius: 8px;
                    }

                    .breadcrumb a {
                        color: #1a73e8;
                        text-decoration: none;
                    }

                    .breadcrumb a:hover {
                        text-decoration: underline;
                    }

                    .breadcrumb span {
                        margin: 0 8px;
                        color: #5f6368;
                    }

                    .current-dir {
                        font-size: 16px;
                        color: #5f6368;
                        margin-bottom: 20px;
                        background: #f1f3f4;
                        padding: 12px 15px;
                        border-radius: 8px;
                    }

                    .grid-container {
                        display: grid;
                        grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
                        gap: 20px;
                    }

                    .item {
                        background: white;
                        border-radius: 8px;
                        overflow: hidden;
                        box-shadow: 0 2px 6px rgba(0,0,0,0.1);
                        transition: all 0.3s ease;
                    }

                    .item:hover {
                        transform: translateY(-5px);
                        box-shadow: 0 6px 12px rgba(0,0,0,0.15);
                    }

                    .item-icon {
                        display: flex;
                        align-items: center;
                        justify-content: center;
                        height: 120px;
                        background: #f1f8ff;
                    }

                    .item-icon.dir {
                        background: #e6f4ea;
                    }

                    .item-icon i {
                        font-size: 48px;
                        color: #1a73e8;
                    }

                    .item-icon.dir i {
                        color: #34a853;
                    }

                    .item-content {
                        padding: 15px;
                    }

                    .item-name {
                        font-weight: 500;
                        font-size: 16px;
                        margin-bottom: 5px;
                        word-break: break-word;
                    }

                    .item-type {
                        font-size: 13px;
                        color: #5f6368;
                    }

                    a.item-link {
                        text-decoration: none;
                        color: inherit;
                        display: block;
                        height: 100%;
                    }

                    .empty {
                        text-align: center;
                        padding: 40px;
                        grid-column: 1 / -1;
                    }

                    .empty i {
                        font-size: 60px;
                        color: #dadce0;
                        margin-bottom: 15px;
                    }

                    .footer {
                        margin-top: 30px;
                        text-align: center;
                        color: #5f6368;
                        font-size: 14px;
                        padding: 20px;
                    }

                    @media (max-width: 600px) {
                        .grid-container {
                            grid-template-columns: 1fr;
                        }
                    }
                </style>
                <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
            </head>
            <body>
                <div class="header">
                    <h1>HTML文件浏览器</h1>
                    <div class="current-dir">
                        <strong>当前目录:</strong> /{{ subpath or '根目录' }}
                    </div>
                </div>

                <div class="breadcrumb">
                    {% for bc in breadcrumbs %}
                        {% if bc.path is not none %}
                            <a href="/browse/{{ bc.path }}">{{ bc.name }}</a>
                        {% else %}
                            <span>{{ bc.name }}</span>
                        {% endif %}
                        {% if not loop.last %}
                            <span>/</span>
                        {% endif %}
                    {% endfor %}
                </div>

                <div class="grid-container">
                    {% if items %}
                        {% for item in items %}
                            <div class="item">
                                <a href="{% if item.type == 'dir' %}/browse/{{ subpath }}/{{ item.name }}{% else %}/view/{{ subpath }}/{{ item.name }}{% endif %}" class="item-link">
                                    <div class="item-icon {% if item.type == 'dir' %}dir{% endif %}">
                                        <i class="material-icons">{% if item.type == 'dir' %}folder{% else %}description{% endif %}</i>
                                    </div>
                                    <div class="item-content">
                                        <div class="item-name">{{ item.name }}</div>
                                        <div class="item-type">
                                            {% if item.type == 'dir' %}
                                                目录
                                            {% else %}
                                                HTML文件
                                            {% endif %}
                                        </div>
                                    </div>
                                </a>
                            </div>
                        {% endfor %}
                    {% else %}
                        <div class="empty">
                            <i class="material-icons">folder_open</i>
                            <h3>目录为空</h3>
                            <p>当前目录下没有HTML文件或子目录</p>
                        </div>
                    {% endif %}
                </div>

                <div class="footer">
                    服务器目录: {{ html_dir }} | 当前路径: /{{ subpath or '' }}
                </div>
            </body>
            </html>
        ''', items=items, subpath=subpath, breadcrumbs=breadcrumbs, html_dir=HTML_DIR)

    except Exception as e:
        abort(500, f"服务器错误: {str(e)}")

@app.route('/view/<path:filepath>')
def view_html(filepath):
    """预览HTML文件(支持多级目录)"""
    try:
        # 构建完整路径
        full_path = os.path.join(HTML_DIR, filepath)

        # 安全性检查
        if not os.path.abspath(full_path).startswith(os.path.abspath(HTML_DIR)):
            abort(403, "禁止访问该路径")

        # 检查文件是否存在
        if not os.path.exists(full_path):
            abort(404, "文件不存在")

        # 检查是否是HTML文件
        if not full_path.endswith('.html'):
            abort(400, "仅支持HTML文件预览")

        # 发送文件内容
        return send_file(full_path)

    except Exception as e:
        abort(500, f"服务器错误: {str(e)}")

@app.errorhandler(400)
@app.errorhandler(403)
@app.errorhandler(404)
@app.errorhandler(500)
def handle_error(e):
    code = e.code
    name = e.name
    description = e.description

    return render_template_string('''
        <!DOCTYPE html>
        <html>
        <head>
            <title>{{ code }} {{ name }}</title>
            <style>
                body {
                    font-family: Arial, sans-serif;
                    background-color: #f8f9fa;
                    display: flex;
                    justify-content: center;
                    align-items: center;
                    height: 100vh;
                    margin: 0;
                    padding: 20px;
                    text-align: center;
                }

                .error-container {
                    max-width: 600px;
                    padding: 30px;
                    background: white;
                    border-radius: 10px;
                    box-shadow: 0 4px 12px rgba(0,0,0,0.1);
                }

                .error-code {
                    font-size: 80px;
                    font-weight: bold;
                    color: #ea4335;
                    margin-bottom: 20px;
                }

                .error-name {
                    font-size: 24px;
                    color: #202124;
                    margin-bottom: 15px;
                }

                .error-description {
                    font-size: 18px;
                    color: #5f6368;
                    margin-bottom: 30px;
                    line-height: 1.6;
                }

                .btn {
                    display: inline-block;
                    padding: 12px 24px;
                    background: #1a73e8;
                    color: white;
                    text-decoration: none;
                    border-radius: 6px;
                    font-size: 16px;
                    transition: background 0.3s;
                }

                .btn:hover {
                    background: #0d62cb;
                }
            </style>
        </head>
        <body>
            <div class="error-container">
                <div class="error-code">{{ code }}</div>
                <div class="error-name">{{ name }}</div>
                <div class="error-description">{{ description }}</div>
                <a href="/" class="btn">返回首页</a>
            </div>
        </body>
        </html>
    ''', code=code, name=name, description=description), code

if __name__ == '__main__':
    # 确保HTML目录存在
    os.makedirs(HTML_DIR, exist_ok=True)
    print(f"服务目录: {HTML_DIR}")

    # 运行服务器
    app.run(host='0.0.0.0', port=80, debug=True)
EOF
关键功能解析
  1. 安全防护

    # 防止路径遍历攻击
    if not full_path.startswith(base_path):
        abort(403, "禁止访问该路径")
    

    这段代码确保用户无法访问服务器指定目录外的文件

  2. 智能排序

    # 提取文件名中的数字
    num = int(''.join(filter(str.isdigit, name)))
    

    自动识别类似"Chapter1.html"、"Chapter2.html"这样的文件名并按数字顺序排序

  3. 响应式界面
    使用CSS Grid布局确保在各种设备上都能良好显示:

    .grid-container {
        display: grid;
        grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
        gap: 20px;
    }
    

步骤2:配置系统服务(开机自启)

我们需要创建一个系统服务,这样服务器就能在系统启动时自动运行:

# 创建启动脚本 /opt/run_html_doc_server.sh
cat > /opt/run_html_doc_server.sh <<-'EOF'
#!/bin/bash
# 设置HTML文档存储位置
export HTML_DIR=/home/public/read_write/doc
python3 /opt/html_doc_server.py
EOF

# 创建服务
cat <<EOF | sudo tee /etc/systemd/system/html_doc_server.service
[Unit]
Description=HTML文档服务器

[Service]
Type=simple
ExecStart=/usr/bin/bash /opt/run_html_doc_server.sh
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
EOF
启用服务
# 设置可执行权限
chmod +x /opt/run_html_doc_server.sh

# 启用并启动服务
sudo systemctl enable html_doc_server
sudo systemctl start html_doc_server

# 检查状态
sudo systemctl status html_doc_server

四、使用指南

服务器部署完成后:

  1. 将HTML文档放入配置的目录(默认为/home/public/docs
  2. 通过浏览器访问服务器IP地址
  3. 浏览目录结构,点击HTML文件查看内容

五、实际应用场景

  1. 技术团队文档共享:存放API文档、开发手册
  2. 产品说明中心:产品使用指南、教程
  3. 知识库系统:公司内部知识积累和分享
  4. 电子书服务器:托管HTML格式的电子书

功能亮点

  1. 直观的界面

    • 文件夹和文件采用卡片式设计
    • 悬停动画提升交互体验
    • 清晰的图标标识文件类型
  2. 面包屑导航

    根目录 / 技术文档 / API参考
    

    随时了解当前位置,快速返回上级

  3. 移动端优化

    @media (max-width: 600px) {
        .grid-container {
            grid-template-columns: 1fr;
        }
    }
    

    在手机上自动切换为单列布局

  4. 错误处理

    • 友好的404页面
    • 清晰的权限错误提示
    • 服务器错误日志记录

六、总结

通过这个Flask文档服务器,你可以:

✅ 轻松托管HTML文档集合
✅ 实现团队内文档共享
✅ 享受比PDF更好的阅读体验
✅ 自动排序整理文档
✅ 随时随地通过浏览器访问

这个解决方案轻量高效,只需基本的Python环境即可运行,特别适合中小团队快速搭建内部文档平台。


小贴士:你可以进一步扩展此服务器,比如添加搜索功能、访问权限控制或文档评论功能,打造更完善的文档管理系统。


网站公告

今日签到

点亮在社区的每一天
去签到