python(35) : 文件服务系统

Python3.8

Python 是一种高级、解释型、通用的编程语言,以其简洁易读的语法而闻名,适用于广泛的应用,包括Web开发、数据分析、人工智能和自动化脚本

1.前言

1.1.效果图

1.2.发布说明

世界树文件服务系统V1.1

更新功能说明 : 

  • 新增复制洗澡地址
  • 新增文件夹打包下载

世界树文件服务系统V1.0

发布时间 : 2025-07-01

无需依赖python环境程序包(浏览器图标icon.png放在同一个目录即可):

一句话功能说明 : 文件共享, 上传下载删除, 批量下载及删除, 创建文件夹

详细功能说明 : 

  • 地址栏路径就是文件夹路径, 便于分享及保存书签
  • 创建文件夹支持创建多级, 如 1/2/3
  • 批量下载及批量删除文件
  • 文件删除
  • 批量上传文件到当前目录
  • 文件关键字搜索
  • 文件名称/创建时间/修改时间/大小 排序

2.安装依赖

# python版本
3.6+
# 安装依赖
pip3 install pyinstaller flask flask_cors -i https://mirrors.aliyun.com/pypi/simple/ requests
# 打包
pyinstaller --onefile file_service_system.py

3.代码

3.1.V1.0

3.1.1.file_service_system.py

# -*- coding: utf-8 -*-
"""
# python版本
3.6+
# 安装依赖
pip3 install pyinstaller flask flask_cors -i https://mirrors.aliyun.com/pypi/simple/ requests
# 打包
pyinstaller -F --clean --noconfirm  file_service_system.py
"""
import argparse
import base64
import datetime
import fnmatch
import json
import mimetypes
import os
import shutil
import sys
 
from flask import Flask, request, send_from_directory, jsonify, redirect, url_for, send_file
from flask_cors import CORS
 
# *********************************** 变量 ***********************************
app = Flask(__name__)
CORS(app)  # 启用 CORS 支持跨域请求
ROOT_FOLDER = os.getcwd()
# 允许较大的上传(例如 10GB)
app.config['MAX_CONTENT_LENGTH'] = 10 * 1024 * 1024 * 1024
os_is_win = False
if sys.platform.startswith('win'):
    os_is_win = True
is_delete = False
is_create_folder = False
 
# *********************************** 公共函数 ***********************************
 
# 获取真实路径
def get_real_path(path):
    if path == '' or path == '/':
        return ROOT_FOLDER
    if not path.startswith('/'):
        path = '/' + path
    file_path = convert2_os_path(path)
    full_path = ROOT_FOLDER + file_path
    return full_path
 
 
# 获取MIME类型
def get_mime_type(file_path):
    mime = mimetypes.guess_type(file_path)[0]
    return mime or 'application/octet-stream'
 
 
# 获取文件信息
def get_file_info(path):
    name = os.path.basename(path)
    is_dir = os.path.isdir(path)
    size = 0 if is_dir else os.path.getsize(path)
    ctime = datetime.fromtimestamp(os.path.getctime(path)).strftime('%Y-%m-%d %H:%M:%S')
    mtime = datetime.fromtimestamp(os.path.getmtime(path)).strftime('%Y-%m-%d %H:%M:%S')
    this_path = path.replace(ROOT_FOLDER, '')
    this_path = convert_win_path(this_path)
    return {
        'name': name,
        'is_dir': is_dir,
        'size': size,
        'ctime': ctime,
        'mtime': mtime,
        'path': this_path
    }
 
 
def batch_get_file_info(dir_path, filter_name):
    """非递归批量获取目录下所有文件和文件夹的信息"""
    result = []
    try:
        with os.scandir(dir_path) as entries:
            for entry in entries:
                try:
                    if filter_name is not None and filter_name != '':
                        if filter_name not in entry.name:
                            continue
                    info = entry.stat(follow_symlinks=False)
                    this_path = (dir_path + os.sep + entry.name).replace(ROOT_FOLDER, '')
                    this_path = convert_win_path(this_path)
                    result.append({
                        'name': entry.name,
                        'path': this_path,
                        'is_dir': entry.is_dir(),
                        'size': info.st_size,
                        'ctime': datetime.datetime.fromtimestamp(info.st_ctime).strftime('%Y-%m-%d %H:%M:%S'),
                        'mtime': datetime.datetime.fromtimestamp(info.st_mtime).strftime('%Y-%m-%d %H:%M:%S'),
                    })
                except (PermissionError, OSError):
                    # 忽略权限不足或无法访问的文件
                    continue
    except (PermissionError, OSError) as e:
        print(f"Error accessing directory {dir_path}: {e}")
 
    return result
 
 
# 转换各个系统路径
def convert2_os_path(path):
    if os_is_win:
        return path.replace('/', os.sep)
    return path
 
 
# 转换windows系统路径为/
def convert_win_path(path):
    if os_is_win:
        return path.replace(os.sep, '/')
    return path
 
 
# *********************************** 接口 ***********************************
# 获取排序后的文件列表
@app.route('/___bk__api___/files', methods=['GET'])
def list_files():
    dir_path = request.args.get('path', '')
    filter_name = request.args.get('filterName', '')
    dir_path = convert_win_path(dir_path)
    full_path = get_real_path(request.args.get('path', ''))
 
    if not os.path.exists(full_path):
        return jsonify({'error': 'Directory not found'}), 404
    files = []
    dirs = []
 
    all_files = batch_get_file_info(full_path, filter_name)
    for f in all_files:
        if f['is_dir']:
            dirs.append(f)
        else:
            files.append(f)
 
    # 排序参数
    sort_by = request.args.get('sort_by', 'name')
    order = request.args.get('order', 'asc')
 
    if sort_by in ['size', 'ctime', 'mtime']:
        dirs.sort(key=lambda x: x[sort_by], reverse=(order == 'desc'))
        files.sort(key=lambda x: x[sort_by], reverse=(order == 'desc'))
    else:
        dirs.sort(key=lambda x: x['name'].lower(), reverse=(order == 'desc'))
        files.sort(key=lambda x: x['name'].lower(), reverse=(order == 'desc'))
 
    return jsonify({'directories': dirs, 'files': files, 'current_path': dir_path})
 
 
# 下载文件
@app.route('/___bk__api___/download')
def download_file():
    full_path = get_real_path(request.args.get('file_path', ''))
    if not os.path.exists(full_path) or os.path.isdir(full_path):
        return jsonify({'error': 'File not found'}), 404
 
    directory = os.path.dirname(full_path)
    filename = os.path.basename(full_path)
    return send_from_directory(directory, filename, as_attachment=True)
 
 
# 预览文件
@app.route('/___bk__api___/preview/<path:file_path>')
def preview_file(file_path):
    full_path = get_real_path(file_path)
    if not os.path.exists(full_path) or os.path.isdir(full_path):
        return jsonify({'error': 'File not found'}), 404
 
    directory = os.path.dirname(full_path)
    filename = os.path.basename(full_path)
    mime_type = get_mime_type(full_path)
 
    # 如果是支持预览的类型,则直接发送文件,否则触发下载
    preview_types = ['text/', 'image/', 'audio/', 'video/']
    if any(mime_type.startswith(t) for t in preview_types):
        return send_from_directory(directory, filename)
    else:
        return redirect(url_for('download_file', file_path=file_path))
 
 
# 删除文件或目录
@app.route('/___bk__api___/create_folder', methods=['POST'])
def create_folder():
    data = json.loads(request.data)
    folder_name = data.get('folder_name', '')
    path = data.get('path', '')
    full_path = get_real_path(path)
    os.makedirs(full_path + os.sep + folder_name, exist_ok=True)
    return jsonify({'success': True})
 
 
# 删除文件或目录
@app.route('/___bk__api___/delete', methods=['POST'])
def delete_item():
    data = json.loads(request.data)
    path = data.get('path', '')
    full_path = get_real_path(path)
 
    if not os.path.exists(full_path):
        return jsonify({'error': 'Item not found'}), 404
 
    try:
        if os.path.isdir(full_path):
            shutil.rmtree(full_path)
        else:
            os.remove(full_path)
        return jsonify({'success': True})
    except Exception as e:
        return jsonify({'error': str(e)}), 500
 
 
# 上传文件
@app.route('/___bk__api___/upload', methods=['POST'])
def upload_file():
    full_path = get_real_path(request.form.get('current_path', ''))
    # 确保上传目录存在
    os.makedirs(full_path, exist_ok=True)
    files = request.files.getlist('file')
    success_count = 0
    fail_count = 0
    for file in files:
        try:
            if file.filename:
                file.save(full_path + os.sep + file.filename)
                success_count += 1
        except Exception as e:
            fail_count += 1
            print(f"文件上传失败, Error: {e}")
    if fail_count == 0:
        msg = f'【{success_count}】个文件全部上传成功'
    else:
        msg = f'部分文件上传成功, 成功:【{success_count}】, 失败:【{fail_count}】'
    return jsonify({'success': True, 'msg': msg})
 
 
# 添加新的路由处理搜索请求
@app.route('/___bk__api___/search', methods=['GET'])
def search_files():
    query = request.args.get('q', '').strip()
    current_path = request.args.get('path', '')
 
    if not query:
        return jsonify({'error': 'Search query is required'}), 400
 
    full_path = os.path.join(ROOT_FOLDER, current_path)
    if not os.path.exists(full_path):
        return jsonify({'error': 'Directory not found'}), 404
 
    results = []
 
    # 搜索当前目录及其子目录
    for root, dirs, files in os.walk(full_path):
        # 计算相对路径,用于返回给前端
        relative_root = root.replace(full_path, '').lstrip('/')
 
        # 检查目录名是否匹配
        for dir_name in dirs:
            if fnmatch.fnmatch(dir_name.lower(), f'*{query.lower()}*'):
                dir_path = os.path.join(root, dir_name)
                info = get_file_info(dir_path)
                info['relative_path'] = f"{relative_root}/{dir_name}" if relative_root else dir_name
                results.append(info)
 
        # 检查文件名是否匹配
        for file_name in files:
            if fnmatch.fnmatch(file_name.lower(), f'*{query.lower()}*'):
                file_path = os.path.join(root, file_name)
                info = get_file_info(file_path)
                info['relative_path'] = f"{relative_root}/{file_name}" if relative_root else file_name
                results.append(info)
 
    # 按照路径深度排序(目录优先,然后是文件)
    results.sort(key=lambda x: (not x['is_dir'], x['name'].lower()))
 
    return jsonify({
        'results': results,
        'query': query,
        'current_path': current_path,
        'total': len(results)
    })
 
 
# 上传文件
@app.route('/___bk__api___/config', methods=['GET'])
def config():
    config = {
        'is_delete': is_delete,
        'is_create_folder': is_create_folder
    }
    return jsonify({'success': True, 'data': config})
 
 
# *********************************** 页面 ***********************************
 
@app.route('/', defaults={'path': ''})
@app.route('/<path:path>', methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH'])
def index(path):
    file_path = convert2_os_path(path)
    full_path = os.path.join(ROOT_FOLDER, file_path)
    if not os.path.exists(full_path) or os.path.isdir(full_path):
        return send_file('index.html')
    else:
        return preview_file(path)
 
 
@app.route('/icon.png')
def icon():
    if os.path.exists('icon.png'):
        return send_file('icon.png')
    else:
        return jsonify({'success': False, 'msg': 'icon.png文件不存在'}), 404
 
 
# *********************************** 启动 ***********************************
 
if __name__ == '__main__':
    # 是否调试, 等于1时html代码使用当前目录的index.html
    debug = 0
    # 启动
    parser = argparse.ArgumentParser(description='世界树文件服务系统')
    parser.add_argument('--port', type=int, default=92, help='监听端口')
    parser.add_argument('--path', type=str, default=os.getcwd(), help='服务目录, 默认当前目录')
    parser.add_argument('--delete', type=int, default=1, help='是否开启删除(1:是/0:否)')
    parser.add_argument('--create_folder', type=int, default=1, help='是否开启删除(1:是/0:否)')
 
    args, _ = parser.parse_known_args()
    is_delete = args.delete == 1
    is_create_folder = args.create_folder == 1
    ROOT_FOLDER = args.path
 
    # 调试
    # is_delete = True
    # ROOT_FOLDER = r'F:\my\my-py\test'
    # is_create_folder = True
    # 调试
 
    # 以多线程方式运行并关闭调试热重载,减小连接被重置的可能性
    app.run(host='0.0.0.0', debug=False, threaded=True, port=args.port)

3.1.2.index.html

<!DOCTYPE html>
<html lang="zh-CN">

<head>
    <meta charset="UTF-8">
    <title>世界树文件服务系统</title>
    <link rel="icon" href="/icon.png" type="image/png">
    <style>
        :root {
            --primary-color: #aaaaff;
            --secondary-color: #f5f5f5;
            --text-color: #333;
            --hover-color: #e0e0e0;
        }

        * {
            box-sizing: border-box;
        }

        body {
            font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
            margin: 0;
            padding: 0;
            background-color: var(--secondary-color);
        }

        .container {
            max-width: 90%;
            margin: 0 auto;
            padding: 20px;
        }

        h1 {
            color: var(--primary-color);
            text-align: center;
            margin-bottom: 30px;
        }

        .breadcrumb {
            display: flex;
            align-items: center;
            margin-bottom: 20px;
            background: white;
            padding: 10px 15px;
            border-radius: 4px;
            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
        }

        .breadcrumb span {
            cursor: pointer;
            color: var(--primary-color);
            margin-right: 5px;
        }

        .breadcrumb span:last-child {
            color: var(--text-color);
            margin-left: 5px;
        }

        .toolbar {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 20px;
            background: white;
            padding: 15px;
            border-radius: 4px;
            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
        }

        .sort-options select {
            padding: 5px 10px;
            margin-right: 10px;
        }

        .upload-container {
            position: relative;
            display: inline-block;
            margin-right: 10px;
        }

        .upload-btn {
            padding: 6px 12px;
            background-color: var(--primary-color);
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
        }

        .upload-container input[type="file"] {
            position: absolute;
            left: 0;
            top: 0;
            opacity: 0;
            width: 100%;
            height: 100%;
            cursor: pointer;
        }

        table {
            width: 100%;
            border-collapse: collapse;
            background: white;
            border-radius: 4px;
            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
        }

        th,
        td {
            padding: 12px 15px;
            text-align: left;
            border-bottom: 1px solid #ddd;
        }

        th {
            background-color: #f8f8f8;
            cursor: pointer;
            user-select: none;
        }

        tr:hover {
            background-color: var(--hover-color);
        }

        .filename {
            display: flex;
            align-items: center;
            cursor: pointer;
        }

        .icon-folder {
            width: 24px;
            height: 24px;
            margin-right: 10px;
        }

        .actions button {
            margin-right: 5px;
            padding: 4px 8px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }

        .btn-download {
            color: #b57fcb;
            cursor: pointer;
        }

        .btn-delete {
            color: #e8b94b;
            cursor: pointer;
            margin-left: 5px;
        }

        .checkbox-column input {
            transform: scale(1.2);
        }

        .bulk-actions {
            margin-top: 10px;
        }

        .bulk-actions button {
            margin-right: 10px;
            padding: 6px 12px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }

        .bulk-actions .btn-delete {
            background-color: #f44336;
            color: white;
        }

        @media (max-width: 768px) {

            th,
            td {
                padding: 8px 10px;
            }

            .toolbar {
                flex-direction: column;
                align-items: flex-start;
            }

            .sort-options,
            .bulk-actions {
                margin-top: 10px;
            }
        }

        .search-container {
            display: flex;
            align-items: center;
            margin-bottom: 20px;
            background: white;
            padding: 10px 15px;
            border-radius: 4px;
            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
        }

        .search-container input[type="text"] {
            padding: 6px 12px;
            font-size: 14px;
            border: 1px solid #ddd;
            border-radius: 4px;
            width: 300px;
            margin-right: 10px;
        }

        .search-btn {
            padding: 6px 12px;
            background-color: var(--primary-color);
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
        }

        .search-results-info {
            margin-top: 10px;
            color: var(--primary-color);
            display: none;
        }

        /* 上传状态弹框 */
        #uploadModalOverlay {
            display: none;
            position: fixed;
            left: 0;
            top: 0;
            width: 100%;
            height: 100%;
            background: rgba(0, 0, 0, 0.35);
            z-index: 2000;
        }

        #uploadModal {
            position: absolute;
            right: 24px;
            top: 80px;
            width: 420px;
            max-height: 60vh;
            overflow: auto;
            background: #fff;
            border-radius: 6px;
            box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
            padding: 12px 12px 6px 12px;
        }

        .upload-modal-header {
            display: flex;
            align-items: center;
            justify-content: space-between;
            padding: 6px 4px 10px 4px;
            border-bottom: 1px solid #eee;
            margin-bottom: 10px;
        }

        .upload-list {
            display: flex;
            flex-direction: column;
            gap: 10px;
        }

        .upload-item-name {
            font-size: 13px;
            color: #333;
            word-break: break-all;
        }

        .progress-bar-wrap {
            width: 100%;
            height: 8px;
            background: #f0f0f0;
            border-radius: 10px;
            overflow: hidden;
        }

        .progress-bar {
            height: 100%;
            width: 0%;
            background: linear-gradient(90deg, #aa55ff, #ff88ff);
            transition: width 0.15s ease-out;
        }

        .upload-item-footer {
            display: flex;
            justify-content: space-between;
            font-size: 12px;
            color: #666;
        }

        .status-chip {
            padding: 2px 6px;
            border-radius: 10px;
            background: #f2f2f2;
            font-size: 12px;
        }

        .status-success {
            background: #e9f7ef;
            color: #2e7d32;
        }

        .status-error {
            background: #fdecea;
            color: #c62828;
        }

        /* 标题 */
        .title {
            -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
            --antd-wave-shadow-color: #aa55ff;
            font-family: -apple-system, BlinkMacSystemFont, Segoe UI, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Helvetica Neue, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;
            font-variant: tabular-nums;
            font-feature-settings: "tnum";
            text-rendering: optimizeLegibility;
            -webkit-font-smoothing: antialiased;
            cursor: pointer;
            box-sizing: border-box;
            white-space: pre-wrap;
            display: inline-block;
            vertical-align: middle;
            font-size: 24px;
            font-weight: 500;
            color: #aa55ff;
            background: -webkit-linear-gradient(309deg, #ff00ff, #aa55ff);
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
            opacity: 1;
        }

        /* 提示框 */
        .toast {
            min-width: 160px;
            color: #fff;
            text-align: center;
            border-radius: 4px;
            padding: 12px 20px;
            position: fixed;
            z-index: 9999;
            left: 50%;
            top: 10%;
            transform: translateX(-50%);
            opacity: 1;
            transition: opacity 0.5s, transform 0.5s;
            font-size: 14px;
        }

        .toast-success {
            background-color: #aa55ff;
        }

        .toast-warning {
            background-color: #ff9800;
        }

        .toast-error {
            background-color: #f44336;
        }

        /* 批量操作 */
        #menu {
            display: none;
            position: fixed;
            background-color: white;
            border: 1px solid #ccc;
            border-radius: 4px;
            padding: 8px 0;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
            z-index: 1000;
        }

        .menu-item {
            padding: 6px 20px;
            cursor: pointer;
            white-space: nowrap;
        }

        .menu-item:hover {
            background-color: #f1f1f1;
        }

        .menu-divider {
            border-bottom: 1px solid #eee;
            margin: 5px 0;
        }

        /* 隐藏创建文件夹 */
        .hide_create_folder {
            display: none;
        }

        /* 隐藏删除 */
        .hide_delete {
            display: none;
        }
    </style>
</head>

<body>
    <div class="container">
        <div>
            <span class="search-container">
                <span class="title">世界树文件服务系统</span>
                <div style="margin-left: auto;">
                    <span class="hide_create_folder">
                        <input type="text" id="folderName" placeholder="文件夹名称"
                            style="margin-right: -10px;width: 100px;">
                        <button class="search-btn" onclick="createFolder()"
                            style="margin-left: 0px;margin-right: 10px;">创建文件夹</button>
                    </span>
                    <button class="search-btn" id="batchOp" style="margin-right: 10px;">批量操作⌵</button>
                    <div class="upload-container">
                        <input type="file" id="fileInput" multiple>
                        <button class="search-btn" for="fileInput" style="margin-right: 0px;">上传文件</button>
                    </div>
                    <button class="search-btn" id="uploadStatusBtn" style="margin-right: 10px;">上传进度</button>
                    <input type="text" id="searchInput" placeholder="输入关键词搜索文件..."
                        style="margin-right: -10px;width: 200px;" oninput="loadFiles()">
                    <button class="search-btn" onclick="resetSearch()" style="margin-left: 0px;">重置</button>
                </div>
            </span>
        </div>
        <div>
            <span class="breadcrumb" id="breadcrumb"></span>
        </div>
        <table id="fileTable">
            <thead>
                <tr>
                    <th class="checkbox-column"><input type="checkbox" id="selectAll"></th>
                    <th id="sort-name">名称<span id="sort_name"></span></th>
                    <th id="sort-ctime">创建时间<span id="sort_ctime"></span></th>
                    <th id="sort-mtime">修改时间<span id="sort_mtime"></span></th>
                    <th id="sort-size">大小<span id="sort_size"></span></th>
                    <th>操作</th>
                </tr>
            </thead>
            <tbody id="fileList">
                <!-- 文件列表将在这里动态加载 -->
            </tbody>
        </table>
    </div>
    <div id="menu">
        <div class="menu-item" data-action="download">批量下载</div>
        <div class="hide_delete">
            <div class="menu-divider"></div>
            <div class="menu-item" data-action="delete">批量删除</div>
        </div>
    </div>
    <!-- 上传状态弹框 -->
    <div id="uploadModalOverlay">
        <div id="uploadModal">
            <div class="upload-modal-header">
                <div style="font-weight: 600;color:#333;">上传进度</div>
                <div>
                    <button class="search-btn" id="closeUploadModalBtn">关闭</button>
                </div>
            </div>
            <div id="uploadSummary" style="font-size:12px;color:#666;margin: 0 4px 8px 4px;"></div>
            <div class="upload-list" id="uploadList"></div>
        </div>
    </div>
    <script>
        let currentPath = '';
        let sortBy = 'name';
        let sortOrder = 'asc';
        let currentFiles = [];
        var host = ''
        var indexPathMap = new Map()
        const menu = document.getElementById('menu');
        const menuItems = document.querySelectorAll('.menu-item');
        const batchOp = document.getElementById('batchOp');
        const uploadStatusBtn = document.getElementById('uploadStatusBtn');
        const uploadModalOverlay = document.getElementById('uploadModalOverlay');
        const uploadModal = document.getElementById('uploadModal');
        const uploadListEl = document.getElementById('uploadList');
        const uploadSummaryEl = document.getElementById('uploadSummary');
        const closeUploadModalBtn = document.getElementById('closeUploadModalBtn');
        let uploadTasks = [];
        let uploadingInProgress = false;
        var config;
        // 初始化
        function init() {
            loadFiles();
            setupEventListeners();
        }
        // 设置事件监听器
        function setupEventListeners() {
            document.getElementById('selectAll').addEventListener('change', toggleSelectAll);
            document.getElementById('fileInput').addEventListener('change', handleFileUpload);
            uploadStatusBtn.addEventListener('click', () => {
                openUploadModal();
            });
            closeUploadModalBtn.addEventListener('click', () => {
                closeUploadModal();
            });
            // 点击遮罩层关闭弹窗(仅当点击在弹窗外部时)
            uploadModalOverlay.addEventListener('click', (e) => {
                if (e.target === uploadModalOverlay) {
                    closeUploadModal();
                }
            });
            // 为每个排序列添加点击事件
            document.querySelectorAll('[id^="sort-"]').forEach(column => {
                column.addEventListener('click', () => {
                    const field = column.id.replace('sort-', '');
                    if (field !== sortBy) {
                        sortBy = field;
                    }
                    sortOrder = sortOrder === 'asc' ? 'desc' : 'asc';
                    loadFiles();
                });
            });
        }
        // 重置搜索
        function resetSearch() {
            document.getElementById('searchInput').value = ''
            loadFiles()
        }
        // 加载文件列表
        function loadFiles() {
            let filterName = document.getElementById('searchInput').value
            fetch(host +
                `/___bk__api___/files?path=${encodeURIComponent(currentPath)}&sort_by=${sortBy}&order=${sortOrder}&filterName=${filterName}`
            )
                .then(response => response.json())
                .then(data => {
                    document.getElementById('selectAll').checked = false
                    currentFiles = [...data.directories, ...data.files];
                    updateBreadcrumb(data.current_path);
                    renderFileList(currentFiles);
                    updateSortIcons()
                })
                .catch(error => {
                    showToast('加载文件列表失败 ' + error, 'error')
                });
        }
        // 更新排序图标
        function updateSortIcons() {
            // 清空所有排序图标
            document.querySelectorAll('#sort_name, #sort_ctime, #sort_mtime, #sort_size').forEach(el => {
                el.innerHTML = '';
            });
            // 根据当前排序条件设置对应图标
            let sortIcon = '';
            if (sortOrder === 'asc') {
                sortIcon = '⇧'; // 向上箭头
            } else {
                sortIcon = '⇩'; // 向下箭头
            }
            const targetSpanId = `sort_${sortBy}`;
            if (targetSpanId) {
                document.getElementById(targetSpanId).innerHTML = sortIcon;
            }
        }
        // 渲染文件列表
        function renderFileList(files) {
            const fileList = document.getElementById('fileList');
            fileList.innerHTML = '';
            if (files.length === 0) {
                return;
            }
            files.forEach(file => {
                const row = document.createElement('tr');
                let del_html =
                    `<span class="btn-delete" onclick="deleteItem('${file.path}')">${file.is_dir ? '删除' : '删除'}`
                if (!config.is_delete) {
                    del_html = ''
                }
                row.innerHTML = `
                    <td class="checkbox-column"><input type="checkbox" class="file-checkbox" data-path="${file.path}" ${file.is_dir ? 'disabled' : ''}></td>
                    <td class="filename" data-path="${file.path}">
                        ${file.is_dir ?
                        `<svg class="icon-folder" viewBox="0 0 24 24"><path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg>` :
                        `<svg class="icon-folder" viewBox="0 0 24 24"><path d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm4 18H6V4h7v5h5v11z"/></svg>`}
                        ${file.name}
                    </td>
                    <td>${file.ctime}</td>
                    <td>${file.mtime}</td>
                    <td>${file.is_dir ? '-' : formatFileSize(file.size)}</td>
                    <td class="actions">
							${!file.is_dir ? `<span class="btn-download" onclick=\"downloadFile('${file.path}')\">下载</span>` : ''}
							${!file.is_dir ? `<span class="btn-download" style="margin-left: 8px;" onclick=\"copyDownloadLink('${file.name}')\">复制下载地址</span>` : ''}
                        ${del_html}</span>
                    </td>
                `;
                // 添加点击事件到文件名
                row.querySelector('.filename').addEventListener('click', () => {
                    if (file.is_dir) {
                        currentPath = file.path;
                        sortBy = 'name';
                        sortOrder = 'asc';
                        history.pushState({}, '', currentPath);
                        loadFiles();
                    } else {
                        let nurl = window.location.origin
                        console.log(currentPath)
                        if (!currentPath.endsWith('/')) {
                            nurl += currentPath + '/' + file.name
                        } else {
                            nurl += currentPath + file.name
                        }
                        window.open(nurl, '_blank')
                    }
                });
                fileList.appendChild(row);
            });
        }
        // 处理排序方式改变
        function handleSortChange(event) {
            sortBy = event.target.value;
            loadFiles();
        }
        function c(d) {
            console.log(d)
        }
        // 更新面包屑导航
        function updateBreadcrumb(path) {
            const breadcrumb = document.getElementById('breadcrumb');
            breadcrumb.innerHTML = '';
            const rootSpan = document.createElement('span');
            rootSpan.textContent = '根目录';
            rootSpan.addEventListener('click', () => {
                currentPath = '';
                loadFiles();
                history.pushState({}, '', '/');
            });
            breadcrumb.appendChild(rootSpan);
            if (!path) return;
            const parts = path.split('/');
            let currentPathPart = '';
            let clickPath = ''
            parts.forEach((part, index) => {
                if (!part) return;
                const separator = document.createElement('span');
                separator.textContent = ' / ';
                breadcrumb.appendChild(separator);
                currentPathPart += (index > 0 || parts[0] ? '/' : '') + part;
                const span = document.createElement('span');
                span.textContent = part;
                clickPath += '/' + part
                indexPathMap.set(index, clickPath)
                if (index < parts.length - 1) {
                    span.style.color = '#3f51b5';
                    span.addEventListener('click', () => {
                        currentPath = indexPathMap.get(index);
                        history.pushState({}, '', currentPath);
                        loadFiles();
                    });
                } else {
                    span.style.marginLeft = '5px';
                    span.style.color = 'var(--text-color)';
                }
                breadcrumb.appendChild(span);
            });
        }
        // 创建文件夹
        function createFolder() {
            const folderName = document.getElementById('folderName').value
            if (folderName === '') {
                showToast('请输入要创建的文件夹名称', 'warning');
                return;
            }
            fetch(host + '/___bk__api___/create_folder', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({
                    folder_name: folderName,
                    path: currentPath
                })
            })
                .then(response => response.json())
                .then(data => {
                    if (data.success) {
                        showToast('创建文件夹成功')
                        document.getElementById('folderName').value = ''
                        loadFiles();
                    } else {
                        showToast('创建文件夹失败' + data.msg, 'error')
                    }
                })
                .catch(error => {
                    showToast('删除失败 ' + error, 'error')
                });
        }
        // 下载文件
        function downloadFile(filePath) {
            window.open(host + `/___bk__api___/download?file_path=${encodeURIComponent(filePath)}`);
        }
        // 复制下载链接
        function copyDownloadLink(filename) {
            const link = window.location.href + '/' + filename;
            if (navigator.clipboard && navigator.clipboard.writeText) {
                navigator.clipboard.writeText(link).then(() => {
                    showToast('下载地址已复制');
                }).catch(() => {
                    fallbackCopyText(link);
                });
            } else {
                fallbackCopyText(link);
            }
        }
        function fallbackCopyText(text) {
            const textarea = document.createElement('textarea');
            textarea.value = text;
            textarea.style.position = 'fixed';
            textarea.style.top = '-1000px';
            document.body.appendChild(textarea);
            textarea.focus();
            textarea.select();
            try {
                document.execCommand('copy');
                showToast('下载地址已复制');
            } catch (e) {
                showToast('复制失败', 'error');
            }
            document.body.removeChild(textarea);
        }
        // 删除文件
        function deleteItem(filePath) {
            if (confirm('确定要删除这个文件/文件夹吗?')) {
                fetch(host + '/___bk__api___/delete', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json'
                    },
                    body: JSON.stringify({
                        path: filePath
                    })
                })
                    .then(response => response.json())
                    .then(data => {
                        if (data.success) {
                            loadFiles();
                        } else {
                            showToast('删除失败 ' + data.error, 'error')
                        }
                    })
                    .catch(error => {
                        showToast('删除失败 ' + error, 'error')
                    });
            }
        }
        // 下载选中项
        function downloadeSelected() {
            const selected = Array.from(document.querySelectorAll('.file-checkbox:checked'));
            if (selected.length === 0) {
                showToast('请先选择要下载的文件', 'warning');
                return;
            }
            // 逐个创建并提交表单
            selected.forEach((checkbox, index) => {
                setTimeout(() => {
                    const filePath = checkbox.dataset.path;
                    const form = document.createElement('form');
                    form.action = host + `/___bk__api___/download`;
                    form.method = 'GET';
                    form.target = '_blank';
                    form.style.display = 'none';
                    const input = document.createElement('input');
                    input.type = 'hidden';
                    input.name = 'file_path';
                    input.value = filePath;
                    form.appendChild(input);
                    document.body.appendChild(form);
                    form.submit();
                    document.body.removeChild(form);
                }, index * 300); // 每个请求间隔300毫秒
            });
        }
        // 删除选中项
        function deleteSelected() {
            const selected = Array.from(document.querySelectorAll('.file-checkbox:checked'));
            if (selected.length === 0) {
                showToast('请先选择要删除的文件', 'warning')
                return;
            }
            if (confirm(`确定要删除选中的 ${selected.length} 个文件吗?`)) {
                const promises = selected.map(checkbox => {
                    const filePath = checkbox.dataset.path;
                    return fetch(host + '/___bk__api___/delete', {
                        method: 'POST',
                        headers: {
                            'Content-Type': 'application/json'
                        },
                        body: JSON.stringify({
                            path: filePath
                        })
                    }).then(response => response.json());
                });
                Promise.all(promises).then(results => {
                    const failed = results.filter(r => !r.success).length;
                    const success = results.length - failed;
                    if (failed === 0) {
                        showToast(`成功删除 ${success} 个文件 `)
                        loadFiles();
                    } else {
                        showToast(`部分文件删除失败(成功: ${success}, 失败: ${failed})`, 'warning')
                    }
                }).catch(error => {
                    showToast('删除过程中发生错误 ' + error, 'error')
                });
            }
        }
        // 切换全选
        function toggleSelectAll(event) {
            const checkboxes = document.querySelectorAll('.file-checkbox');
            checkboxes.forEach(checkbox => {
                if (!checkbox.disabled) {
                    checkbox.checked = event.target.checked;
                }
            });
        }
        // 处理文件上传
        function handleFileUpload(event) {
            const files = Array.from(event.target.files || []);
            if (files.length === 0) return;
            // 初始化任务并打开弹框
            files.forEach(file => {
                uploadTasks.push({
                    name: file.name,
                    size: file.size,
                    uploaded: 0,
                    status: 'waiting', // waiting | uploading | success | error
                    error: '',
                    startTime: null,
                    endTime: null
                });
            });
            renderUploadModal();
            openUploadModal();
            // 开始顺序上传
            if (!uploadingInProgress) {
                uploadingInProgress = true;
                uploadSequential(files).then(() => {
                    uploadingInProgress = false;
                    // 全部完成后自动关闭(若没有错误)
                    const hasError = uploadTasks.some(t => t.status === 'error');
                    if (!hasError) {
                        setTimeout(() => closeUploadModal(), 600);
                    }
                    loadFiles();
                });
            }
            // 重置文件输入以允许再次选择相同文件
            event.target.value = null;
        }

        async function uploadSequential(files) {
            for (let i = 0; i < files.length; i++) {
                await uploadSingleFile(files[i]);
            }
        }

        function uploadSingleFile(file) {
            return new Promise(resolve => {
                const task = uploadTasks.find(t => t.name === file.name && t.status === 'waiting');
                if (!task) return resolve();
                task.status = 'uploading';
                task.startTime = Date.now();
                renderUploadModal();
                const formData = new FormData();
                formData.append('file', file);
                formData.append('current_path', currentPath);
                const xhr = new XMLHttpRequest();
                xhr.open('POST', host + '/___bk__api___/upload');
                xhr.upload.onprogress = function (e) {
                    if (e.lengthComputable) {
                        task.uploaded = e.loaded;
                        renderUploadModal();
                    }
                };
                xhr.onerror = function () {
                    task.status = 'error';
                    task.error = '网络错误';
                    renderUploadModal();
                    resolve();
                };
                xhr.onload = function () {
                    try {
                        const resp = JSON.parse(xhr.responseText || '{}');
                        if (resp && resp.success) {
                            task.uploaded = task.size;
                            task.status = 'success';
                            task.endTime = Date.now();
                        } else {
                            task.status = 'error';
                            task.error = (resp && (resp.error || resp.msg)) ? (resp.error || resp.msg) : '上传失败';
                        }
                    } catch (e) {
                        task.status = 'error';
                        task.error = '响应解析失败';
                    }
                    renderUploadModal();
                    resolve();
                };
                xhr.send(formData);
            });
        }

        function openUploadModal() {
            uploadModalOverlay.style.display = 'block';
        }
        function closeUploadModal() {
            uploadModalOverlay.style.display = 'none';
        }
        function percent(num, den) {
            if (!den || den === 0) return 0;
            return Math.min(100, Math.round((num / den) * 100));
        }
        function formatBytes(bytes) {
            if (!bytes || bytes === 0) return '0 B';
            const k = 1024;
            const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
            const i = Math.floor(Math.log(bytes) / Math.log(k));
            return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
        }
        function formatSpeed(bps) {
            if (!bps || bps <= 0) return '—';
            return formatBytes(bps) + '/s';
        }
        function formatETA(seconds) {
            if (!isFinite(seconds) || seconds <= 0) return '—';
            const s = Math.round(seconds);
            const h = Math.floor(s / 3600);
            const m = Math.floor((s % 3600) / 60);
            const sec = s % 60;
            if (h > 0) {
                return `${h}小时 ${m.toString().padStart(2, '0')}分 ${sec.toString().padStart(2, '0')}秒`;
            }
            return `${m.toString().padStart(2, '0')}:${sec.toString().padStart(2, '0')}`;
        }
        function renderUploadModal() {
            const total = uploadTasks.length;
            const done = uploadTasks.filter(t => t.status === 'success').length;
            const failed = uploadTasks.filter(t => t.status === 'error').length;
            uploadSummaryEl.textContent = `共 ${total} 个文件,已完成 ${done},失败 ${failed}`;
            uploadListEl.innerHTML = '';
            uploadTasks.forEach((t, idx) => {
                let now = Date.now();
                let elapsedMs = t.startTime ? ((t.endTime || now) - t.startTime) : 0;
                let bps = elapsedMs > 0 ? (t.uploaded / (elapsedMs / 1000)) : 0;
                let etaSec = bps > 0 ? ((t.size - t.uploaded) / bps) : Infinity;
                const item = document.createElement('div');
                item.innerHTML = `
						<div class="upload-item-name">${idx + 1}. ${t.name}</div>
						<div class="progress-bar-wrap"><div class="progress-bar" style="width: ${percent(t.uploaded, t.size)}%"></div></div>
						<div class="upload-item-footer">
							<span>${formatBytes(t.uploaded)} / ${formatBytes(t.size)}</span>
							<span class="status-chip ${t.status === 'success' ? 'status-success' : (t.status === 'error' ? 'status-error' : '')}">
								${t.status === 'waiting' ? '待上传' : t.status === 'uploading' ? '上传中' : t.status === 'success' ? '完成' : '失败'}
							</span>
						</div>
						<div class="upload-item-footer">
							<span>速度:${t.status === 'uploading' || t.status === 'success' ? formatSpeed(bps) : '—'}</span>
							<span>预计完成:${t.status === 'uploading' ? formatETA(etaSec) : '—'}</span>
						</div>
					`;
                uploadListEl.appendChild(item);
            });
        }
        // 格式化文件大小
        function formatFileSize(bytes) {
            if (bytes === 0) return '0 B';
            const k = 1024;
            const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
            const i = Math.floor(Math.log(bytes) / Math.log(k));
            return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
        }
        function showToast(message, type = 'info', duration = 2000) {
            const toast = document.createElement('div');
            toast.textContent = message;
            toast.classList.add('toast');
            // 根据类型添加新的类名
            switch (type.toLowerCase()) {
                case 'success':
                    toast.classList.add('toast-success');
                    break;
                case 'warning':
                    toast.classList.add('toast-warning');
                    break;
                case 'error':
                    toast.classList.add('toast-error');
                    break;
                default:
                    toast.classList.add('toast-success'); // 默认为 success
                    break;
            }
            // 添加到 body 中
            document.body.appendChild(toast);
            // 设置定时隐藏
            setTimeout(() => {
                if (document.body.contains(toast)) {
                    document.body.removeChild(toast);
                }
            }, duration);
        }
        // 批量操作
        // 右键点击显示菜单
        batchOp.addEventListener('click', function (e) {
            e.preventDefault();
            // 设置菜单位置
            menu.style.left = e.pageX + 'px';
            menu.style.top = e.pageY + 'px';
            // 显示菜单
            menu.style.display = 'block';
        });
        // 点击菜单项
        menuItems.forEach(item => {
            item.addEventListener('click', function () {
                const action = this.getAttribute('data-action');
                menu.style.display = 'none';
                switch (action) {
                    case 'delete':
                        deleteSelected()
                        break
                    case 'download':
                        downloadeSelected()
                        break
                }
            });
        });
        // 点击页面其他地方隐藏菜单
        document.addEventListener('click', function (e) {
            if (!menu.contains(e.target) && e.target !== batchOp) {
                menu.style.display = 'none';
            }
        });
        // 初始化后执行
        const curi = decodeURIComponent(window.location.pathname)
        if (curi !== '' && curi !== '/') {
            currentPath = curi
        }
        // 初始化
        fetch(host + '/___bk__api___/config')
            .then(response => response.json())
            .then(data => {
                if (data.success) {
                    config = data.data
                    if (config.is_create_folder) {
                        document.querySelectorAll('.hide_create_folder')[0].classList.remove('hide_create_folder');
                    }
                    if (config.is_delete) {
                        document.querySelectorAll('.hide_delete')[0].classList.remove('hide_delete');
                    }
                    // 加载文件列表
                    init();
                } else {
                    showToast('加载配置文件失败 ' + data.error, 'error')
                }
            })
            .catch(error => {
                showToast('加载配置文件失败 ' + error, 'error')
            });
    </script>
</body>

</html>

3.2.V1.1

3.2.1.file_service_system.py

# -*- coding: utf-8 -*-
"""
# python版本
3.6+
# 安装依赖
pip3 install pyinstaller flask flask_cors -i https://mirrors.aliyun.com/pypi/simple/ requests
# 打包
pyinstaller -F --clean --noconfirm  file_service_system.py
"""
import argparse
import base64
import datetime
import fnmatch
import json
import mimetypes
import os
import shutil
import sys
import zipfile
import tempfile
import io
 
from flask import Flask, request, send_from_directory, jsonify, redirect, url_for, send_file
from flask_cors import CORS
 
# *********************************** 变量 ***********************************
app = Flask(__name__)
CORS(app)  # 启用 CORS 支持跨域请求
ROOT_FOLDER = os.getcwd()
# 允许较大的上传(例如 10GB)
app.config['MAX_CONTENT_LENGTH'] = 10 * 1024 * 1024 * 1024
os_is_win = False
if sys.platform.startswith('win'):
    os_is_win = True
is_delete = False
is_create_folder = False
 
# *********************************** 公共函数 ***********************************
 
# 获取真实路径
def get_real_path(path):
    if path == '' or path == '/':
        return ROOT_FOLDER
    if not path.startswith('/'):
        path = '/' + path
    file_path = convert2_os_path(path)
    full_path = ROOT_FOLDER + file_path
    return full_path
 
 
# 获取MIME类型
def get_mime_type(file_path):
    mime = mimetypes.guess_type(file_path)[0]
    return mime or 'application/octet-stream'
 
 
# 获取文件信息
def get_file_info(path):
    name = os.path.basename(path)
    is_dir = os.path.isdir(path)
    size = 0 if is_dir else os.path.getsize(path)
    ctime = datetime.fromtimestamp(os.path.getctime(path)).strftime('%Y-%m-%d %H:%M:%S')
    mtime = datetime.fromtimestamp(os.path.getmtime(path)).strftime('%Y-%m-%d %H:%M:%S')
    this_path = path.replace(ROOT_FOLDER, '')
    this_path = convert_win_path(this_path)
    return {
        'name': name,
        'is_dir': is_dir,
        'size': size,
        'ctime': ctime,
        'mtime': mtime,
        'path': this_path
    }
 
 
def batch_get_file_info(dir_path, filter_name):
    """非递归批量获取目录下所有文件和文件夹的信息"""
    result = []
    try:
        with os.scandir(dir_path) as entries:
            for entry in entries:
                try:
                    if filter_name is not None and filter_name != '':
                        if filter_name not in entry.name:
                            continue
                    info = entry.stat(follow_symlinks=False)
                    this_path = (dir_path + os.sep + entry.name).replace(ROOT_FOLDER, '')
                    this_path = convert_win_path(this_path)
                    result.append({
                        'name': entry.name,
                        'path': this_path,
                        'is_dir': entry.is_dir(),
                        'size': info.st_size,
                        'ctime': datetime.datetime.fromtimestamp(info.st_ctime).strftime('%Y-%m-%d %H:%M:%S'),
                        'mtime': datetime.datetime.fromtimestamp(info.st_mtime).strftime('%Y-%m-%d %H:%M:%S'),
                    })
                except (PermissionError, OSError):
                    # 忽略权限不足或无法访问的文件
                    continue
    except (PermissionError, OSError) as e:
        print(f"Error accessing directory {dir_path}: {e}")
 
    return result
 
 
# 转换各个系统路径
def convert2_os_path(path):
    if os_is_win:
        return path.replace('/', os.sep)
    return path
 
 
# 转换windows系统路径为/
def convert_win_path(path):
    if os_is_win:
        return path.replace(os.sep, '/')
    return path


# 递归计算文件夹大小
def get_folder_size(folder_path):
    total_size = 0
    try:
        for dirpath, dirnames, filenames in os.walk(folder_path):
            for filename in filenames:
                filepath = os.path.join(dirpath, filename)
                try:
                    total_size += os.path.getsize(filepath)
                except (OSError, PermissionError):
                    continue
    except (OSError, PermissionError):
        pass
    return total_size
 
 
# *********************************** 接口 ***********************************
# 获取排序后的文件列表
@app.route('/___bk__api___/files', methods=['GET'])
def list_files():
    dir_path = request.args.get('path', '')
    filter_name = request.args.get('filterName', '')
    dir_path = convert_win_path(dir_path)
    full_path = get_real_path(request.args.get('path', ''))
 
    if not os.path.exists(full_path):
        return jsonify({'error': 'Directory not found'}), 404
    files = []
    dirs = []
 
    all_files = batch_get_file_info(full_path, filter_name)
    for f in all_files:
        if f['is_dir']:
            dirs.append(f)
        else:
            files.append(f)
 
    # 排序参数
    sort_by = request.args.get('sort_by', 'name')
    order = request.args.get('order', 'asc')
 
    if sort_by in ['size', 'ctime', 'mtime']:
        dirs.sort(key=lambda x: x[sort_by], reverse=(order == 'desc'))
        files.sort(key=lambda x: x[sort_by], reverse=(order == 'desc'))
    else:
        dirs.sort(key=lambda x: x['name'].lower(), reverse=(order == 'desc'))
        files.sort(key=lambda x: x['name'].lower(), reverse=(order == 'desc'))
 
    return jsonify({'directories': dirs, 'files': files, 'current_path': dir_path})
 
 
# 下载文件
@app.route('/___bk__api___/download')
def download_file():
    full_path = get_real_path(request.args.get('file_path', ''))
    if not os.path.exists(full_path) or os.path.isdir(full_path):
        return jsonify({'error': 'File not found'}), 404
 
    directory = os.path.dirname(full_path)
    filename = os.path.basename(full_path)
    return send_from_directory(directory, filename, as_attachment=True)
 
 
# 预览文件
@app.route('/___bk__api___/preview/<path:file_path>')
def preview_file(file_path):
    full_path = get_real_path(file_path)
    if not os.path.exists(full_path) or os.path.isdir(full_path):
        return jsonify({'error': 'File not found'}), 404
 
    directory = os.path.dirname(full_path)
    filename = os.path.basename(full_path)
    mime_type = get_mime_type(full_path)
 
    # 如果是支持预览的类型,则直接发送文件,否则触发下载
    preview_types = ['text/', 'image/', 'audio/', 'video/']
    if any(mime_type.startswith(t) for t in preview_types):
        return send_from_directory(directory, filename)
    else:
        return redirect(url_for('download_file', file_path=file_path))
 
 
# 删除文件或目录
@app.route('/___bk__api___/create_folder', methods=['POST'])
def create_folder():
    data = json.loads(request.data)
    folder_name = data.get('folder_name', '')
    path = data.get('path', '')
    full_path = get_real_path(path)
    os.makedirs(full_path + os.sep + folder_name, exist_ok=True)
    return jsonify({'success': True})
 
 
# 删除文件或目录
@app.route('/___bk__api___/delete', methods=['POST'])
def delete_item():
    data = json.loads(request.data)
    path = data.get('path', '')
    full_path = get_real_path(path)
 
    if not os.path.exists(full_path):
        return jsonify({'error': 'Item not found'}), 404
 
    try:
        if os.path.isdir(full_path):
            shutil.rmtree(full_path)
        else:
            os.remove(full_path)
        return jsonify({'success': True})
    except Exception as e:
        return jsonify({'error': str(e)}), 500
 
 
# 上传文件
@app.route('/___bk__api___/upload', methods=['POST'])
def upload_file():
    full_path = get_real_path(request.form.get('current_path', ''))
    # 确保上传目录存在
    os.makedirs(full_path, exist_ok=True)
    files = request.files.getlist('file')
    success_count = 0
    fail_count = 0
    for file in files:
        try:
            if file.filename:
                file.save(full_path + os.sep + file.filename)
                success_count += 1
        except Exception as e:
            fail_count += 1
            print(f"文件上传失败, Error: {e}")
    if fail_count == 0:
        msg = f'【{success_count}】个文件全部上传成功'
    else:
        msg = f'部分文件上传成功, 成功:【{success_count}】, 失败:【{fail_count}】'
    return jsonify({'success': True, 'msg': msg})
 
 
# 添加新的路由处理搜索请求
@app.route('/___bk__api___/search', methods=['GET'])
def search_files():
    query = request.args.get('q', '').strip()
    current_path = request.args.get('path', '')
 
    if not query:
        return jsonify({'error': 'Search query is required'}), 400
 
    full_path = os.path.join(ROOT_FOLDER, current_path)
    if not os.path.exists(full_path):
        return jsonify({'error': 'Directory not found'}), 404
 
    results = []
 
    # 搜索当前目录及其子目录
    for root, dirs, files in os.walk(full_path):
        # 计算相对路径,用于返回给前端
        relative_root = root.replace(full_path, '').lstrip('/')
 
        # 检查目录名是否匹配
        for dir_name in dirs:
            if fnmatch.fnmatch(dir_name.lower(), f'*{query.lower()}*'):
                dir_path = os.path.join(root, dir_name)
                info = get_file_info(dir_path)
                info['relative_path'] = f"{relative_root}/{dir_name}" if relative_root else dir_name
                results.append(info)
 
        # 检查文件名是否匹配
        for file_name in files:
            if fnmatch.fnmatch(file_name.lower(), f'*{query.lower()}*'):
                file_path = os.path.join(root, file_name)
                info = get_file_info(file_path)
                info['relative_path'] = f"{relative_root}/{file_name}" if relative_root else file_name
                results.append(info)
 
    # 按照路径深度排序(目录优先,然后是文件)
    results.sort(key=lambda x: (not x['is_dir'], x['name'].lower()))
 
    return jsonify({
        'results': results,
        'query': query,
        'current_path': current_path,
        'total': len(results)
    })
 
 
# 获取文件夹大小
@app.route('/___bk__api___/folder_size', methods=['GET'])
def get_folder_size_api():
    folder_path = request.args.get('folder_path', '')
    full_path = get_real_path(folder_path)
    
    if not os.path.exists(full_path):
        return jsonify({'success': False, 'error': 'Folder not found'}), 404
    
    if not os.path.isdir(full_path):
        return jsonify({'success': False, 'error': 'Path is not a directory'}), 400
    
    try:
        size = get_folder_size(full_path)
        return jsonify({'success': True, 'size': size})
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)}), 500


# 打包下载文件夹
@app.route('/___bk__api___/download_folder', methods=['GET'])
def download_folder():
    folder_path = request.args.get('folder_path', '')
    full_path = get_real_path(folder_path)
    
    if not os.path.exists(full_path):
        return jsonify({'error': 'Folder not found'}), 404
    
    if not os.path.isdir(full_path):
        return jsonify({'error': 'Path is not a directory'}), 400
    
    try:
        folder_name = os.path.basename(full_path) or 'folder'
        zip_filename = f'{folder_name}.zip'
        
        # 创建临时zip文件
        zip_buffer = io.BytesIO()
        with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
            for root, dirs, files in os.walk(full_path):
                for file in files:
                    file_path = os.path.join(root, file)
                    try:
                        # 计算相对路径
                        arcname = os.path.relpath(file_path, full_path)
                        zip_file.write(file_path, arcname)
                    except (OSError, PermissionError):
                        continue
        
        zip_buffer.seek(0)
        # 确保文件名正确编码
        from urllib.parse import quote
        encoded_filename = quote(zip_filename.encode('utf-8'))
        response = send_file(
            zip_buffer,
            mimetype='application/zip',
            as_attachment=True,
            download_name=zip_filename
        )
        # 设置Content-Disposition头,支持中文文件名
        response.headers['Content-Disposition'] = f'attachment; filename="{zip_filename}"; filename*=UTF-8\'\'{encoded_filename}'
        return response
    except Exception as e:
        return jsonify({'error': str(e)}), 500


# 上传文件
@app.route('/___bk__api___/config', methods=['GET'])
def config():
    config = {
        'is_delete': is_delete,
        'is_create_folder': is_create_folder
    }
    return jsonify({'success': True, 'data': config})
 
 
# *********************************** 页面 ***********************************
 
@app.route('/', defaults={'path': ''})
@app.route('/<path:path>', methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH'])
def index(path):
    file_path = convert2_os_path(path)
    full_path = os.path.join(ROOT_FOLDER, file_path)
    if not os.path.exists(full_path) or os.path.isdir(full_path):
        return send_file('index.html')
    else:
        return preview_file(path)
 
 
@app.route('/icon.png')
def icon():
    if os.path.exists('icon.png'):
        return send_file('icon.png')
    else:
        return jsonify({'success': False, 'msg': 'icon.png文件不存在'}), 404
 
 
# *********************************** 启动 ***********************************
 
if __name__ == '__main__':
    # 是否调试, 等于1时html代码使用当前目录的index.html
    debug = 0
    # 启动
    parser = argparse.ArgumentParser(description='世界树文件服务系统')
    parser.add_argument('--port', type=int, default=92, help='监听端口')
    parser.add_argument('--path', type=str, default=os.getcwd(), help='服务目录, 默认当前目录')
    parser.add_argument('--delete', type=int, default=1, help='是否开启删除(1:是/0:否)')
    parser.add_argument('--create_folder', type=int, default=1, help='是否开启删除(1:是/0:否)')
 
    args, _ = parser.parse_known_args()
    is_delete = args.delete == 1
    is_create_folder = args.create_folder == 1
    ROOT_FOLDER = args.path
 
    # 调试
    # is_delete = True
    # ROOT_FOLDER = r'F:\my\my-py\test'
    # is_create_folder = True
    # 调试
 
    # 以多线程方式运行并关闭调试热重载,减小连接被重置的可能性
    app.run(host='0.0.0.0', debug=False, threaded=True, port=args.port)

3.2.2.index.html

<!DOCTYPE html>
<html lang="zh-CN">

<head>
    <meta charset="UTF-8">
    <title>世界树文件服务系统</title>
    <link rel="icon" href="/icon.png" type="image/png">
    <style>
        :root {
            --primary-color: #aaaaff;
            --secondary-color: #f5f5f5;
            --text-color: #333;
            --hover-color: #e0e0e0;
        }

        * {
            box-sizing: border-box;
        }

        body {
            font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
            margin: 0;
            padding: 0;
            background-color: var(--secondary-color);
        }

        .container {
            max-width: 90%;
            margin: 0 auto;
            padding: 20px;
        }

        h1 {
            color: var(--primary-color);
            text-align: center;
            margin-bottom: 30px;
        }

        .breadcrumb {
            display: flex;
            align-items: center;
            margin-bottom: 20px;
            background: white;
            padding: 10px 15px;
            border-radius: 4px;
            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
        }

        .breadcrumb span {
            cursor: pointer;
            color: var(--primary-color);
            margin-right: 5px;
        }

        .breadcrumb span:last-child {
            color: var(--text-color);
            margin-left: 5px;
        }

        .toolbar {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 20px;
            background: white;
            padding: 15px;
            border-radius: 4px;
            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
        }

        .sort-options select {
            padding: 5px 10px;
            margin-right: 10px;
        }

        .upload-container {
            position: relative;
            display: inline-block;
            margin-right: 10px;
        }

        .upload-btn {
            padding: 6px 12px;
            background-color: var(--primary-color);
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
        }

        .upload-container input[type="file"] {
            position: absolute;
            left: 0;
            top: 0;
            opacity: 0;
            width: 100%;
            height: 100%;
            cursor: pointer;
        }

        table {
            width: 100%;
            border-collapse: collapse;
            background: white;
            border-radius: 4px;
            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
        }

        th,
        td {
            padding: 12px 15px;
            text-align: left;
            border-bottom: 1px solid #ddd;
        }

        th {
            background-color: #f8f8f8;
            cursor: pointer;
            user-select: none;
        }

        tr:hover {
            background-color: var(--hover-color);
        }

        .filename {
            display: flex;
            align-items: center;
            cursor: pointer;
        }

        .icon-folder {
            width: 24px;
            height: 24px;
            margin-right: 10px;
        }

        .actions button {
            margin-right: 5px;
            padding: 4px 8px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }

        .btn-download {
            color: #b57fcb;
            cursor: pointer;
        }

        .btn-delete {
            color: #e8b94b;
            cursor: pointer;
            margin-left: 5px;
        }

        .checkbox-column input {
            transform: scale(1.2);
        }

        .bulk-actions {
            margin-top: 10px;
        }

        .bulk-actions button {
            margin-right: 10px;
            padding: 6px 12px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }

        .bulk-actions .btn-delete {
            background-color: #f44336;
            color: white;
        }

        @media (max-width: 768px) {

            th,
            td {
                padding: 8px 10px;
            }

            .toolbar {
                flex-direction: column;
                align-items: flex-start;
            }

            .sort-options,
            .bulk-actions {
                margin-top: 10px;
            }
        }

        .search-container {
            display: flex;
            align-items: center;
            margin-bottom: 20px;
            background: white;
            padding: 10px 15px;
            border-radius: 4px;
            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
        }

        .search-container input[type="text"] {
            padding: 6px 12px;
            font-size: 14px;
            border: 1px solid #ddd;
            border-radius: 4px;
            width: 300px;
            margin-right: 10px;
        }

        .search-btn {
            padding: 6px 12px;
            background-color: var(--primary-color);
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
        }

        .search-results-info {
            margin-top: 10px;
            color: var(--primary-color);
            display: none;
        }

        /* 上传状态弹框 */
        #uploadModalOverlay {
            display: none;
            position: fixed;
            left: 0;
            top: 0;
            width: 100%;
            height: 100%;
            background: rgba(0, 0, 0, 0.35);
            z-index: 2000;
        }

        #uploadModal {
            position: absolute;
            right: 24px;
            top: 80px;
            width: 420px;
            max-height: 60vh;
            overflow: auto;
            background: #fff;
            border-radius: 6px;
            box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
            padding: 12px 12px 6px 12px;
        }

        .upload-modal-header {
            display: flex;
            align-items: center;
            justify-content: space-between;
            padding: 6px 4px 10px 4px;
            border-bottom: 1px solid #eee;
            margin-bottom: 10px;
        }

        .upload-list {
            display: flex;
            flex-direction: column;
            gap: 10px;
        }

        .upload-item-name {
            font-size: 13px;
            color: #333;
            word-break: break-all;
        }

        .progress-bar-wrap {
            width: 100%;
            height: 8px;
            background: #f0f0f0;
            border-radius: 10px;
            overflow: hidden;
        }

        .progress-bar {
            height: 100%;
            width: 0%;
            background: linear-gradient(90deg, #aa55ff, #ff88ff);
            transition: width 0.15s ease-out;
        }

        .upload-item-footer {
            display: flex;
            justify-content: space-between;
            font-size: 12px;
            color: #666;
        }

        .status-chip {
            padding: 2px 6px;
            border-radius: 10px;
            background: #f2f2f2;
            font-size: 12px;
        }

        .status-success {
            background: #e9f7ef;
            color: #2e7d32;
        }

        .status-error {
            background: #fdecea;
            color: #c62828;
        }

        /* 标题 */
        .title {
            -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
            --antd-wave-shadow-color: #aa55ff;
            font-family: -apple-system, BlinkMacSystemFont, Segoe UI, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Helvetica Neue, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;
            font-variant: tabular-nums;
            font-feature-settings: "tnum";
            text-rendering: optimizeLegibility;
            -webkit-font-smoothing: antialiased;
            cursor: pointer;
            box-sizing: border-box;
            white-space: pre-wrap;
            display: inline-block;
            vertical-align: middle;
            font-size: 24px;
            font-weight: 500;
            color: #aa55ff;
            background: -webkit-linear-gradient(309deg, #ff00ff, #aa55ff);
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
            opacity: 1;
        }

        /* 提示框 */
        .toast {
            min-width: 160px;
            color: #fff;
            text-align: center;
            border-radius: 4px;
            padding: 12px 20px;
            position: fixed;
            z-index: 9999;
            left: 50%;
            top: 10%;
            transform: translateX(-50%);
            opacity: 1;
            transition: opacity 0.5s, transform 0.5s;
            font-size: 14px;
        }

        .toast-success {
            background-color: #aa55ff;
        }

        .toast-warning {
            background-color: #ff9800;
        }

        .toast-error {
            background-color: #f44336;
        }

        /* 批量操作 */
        #menu {
            display: none;
            position: fixed;
            background-color: white;
            border: 1px solid #ccc;
            border-radius: 4px;
            padding: 8px 0;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
            z-index: 1000;
        }

        .menu-item {
            padding: 6px 20px;
            cursor: pointer;
            white-space: nowrap;
        }

        .menu-item:hover {
            background-color: #f1f1f1;
        }

        .menu-divider {
            border-bottom: 1px solid #eee;
            margin: 5px 0;
        }

        /* 隐藏创建文件夹 */
        .hide_create_folder {
            display: none;
        }

        /* 隐藏删除 */
        .hide_delete {
            display: none;
        }
    </style>
</head>

<body>
    <div class="container">
        <div>
            <span class="search-container">
                <span class="title">世界树文件服务系统</span>
                <div style="margin-left: auto;">
                    <span class="hide_create_folder">
                        <input type="text" id="folderName" placeholder="文件夹名称"
                            style="margin-right: -10px;width: 100px;">
                        <button class="search-btn" onclick="createFolder()"
                            style="margin-left: 0px;margin-right: 10px;">创建文件夹</button>
                    </span>
                    <button class="search-btn" id="batchOp" style="margin-right: 10px;">批量操作⌵</button>
                    <div class="upload-container">
                        <input type="file" id="fileInput" multiple>
                        <button class="search-btn" for="fileInput" style="margin-right: 0px;">上传文件</button>
                    </div>
                    <button class="search-btn" id="uploadStatusBtn" style="margin-right: 10px;">上传进度</button>
                    <input type="text" id="searchInput" placeholder="输入关键词搜索文件..."
                        style="margin-right: -10px;width: 200px;" oninput="loadFiles()">
                    <button class="search-btn" onclick="resetSearch()" style="margin-left: 0px;">重置</button>
                </div>
            </span>
        </div>
        <div>
            <span class="breadcrumb" id="breadcrumb"></span>
        </div>
        <table id="fileTable">
            <thead>
                <tr>
                    <th class="checkbox-column"><input type="checkbox" id="selectAll"></th>
                    <th id="sort-name">名称<span id="sort_name"></span></th>
                    <th id="sort-ctime">创建时间<span id="sort_ctime"></span></th>
                    <th id="sort-mtime">修改时间<span id="sort_mtime"></span></th>
                    <th id="sort-size">大小<span id="sort_size"></span></th>
                    <th>操作</th>
                </tr>
            </thead>
            <tbody id="fileList">
                <!-- 文件列表将在这里动态加载 -->
            </tbody>
        </table>
    </div>
    <div id="menu">
        <div class="menu-item" data-action="download">批量下载</div>
        <div class="hide_delete">
            <div class="menu-divider"></div>
            <div class="menu-item" data-action="delete">批量删除</div>
        </div>
    </div>
    <!-- 上传状态弹框 -->
    <div id="uploadModalOverlay">
        <div id="uploadModal">
            <div class="upload-modal-header">
                <div style="font-weight: 600;color:#333;">上传进度</div>
                <div>
                    <button class="search-btn" id="closeUploadModalBtn">关闭</button>
                </div>
            </div>
            <div id="uploadSummary" style="font-size:12px;color:#666;margin: 0 4px 8px 4px;"></div>
            <div class="upload-list" id="uploadList"></div>
        </div>
    </div>
    <script>
        let currentPath = '';
        let sortBy = 'name';
        let sortOrder = 'asc';
        let currentFiles = [];
        var host = ''
        var indexPathMap = new Map()
        const menu = document.getElementById('menu');
        const menuItems = document.querySelectorAll('.menu-item');
        const batchOp = document.getElementById('batchOp');
        const uploadStatusBtn = document.getElementById('uploadStatusBtn');
        const uploadModalOverlay = document.getElementById('uploadModalOverlay');
        const uploadModal = document.getElementById('uploadModal');
        const uploadListEl = document.getElementById('uploadList');
        const uploadSummaryEl = document.getElementById('uploadSummary');
        const closeUploadModalBtn = document.getElementById('closeUploadModalBtn');
        let uploadTasks = [];
        let uploadingInProgress = false;
        var config;
        let packingInProgress = new Set(); // 用于跟踪正在打包的文件夹
        // 初始化
        function init() {
            loadFiles();
            setupEventListeners();
        }
        // 设置事件监听器
        function setupEventListeners() {
            document.getElementById('selectAll').addEventListener('change', toggleSelectAll);
            document.getElementById('fileInput').addEventListener('change', handleFileUpload);
            uploadStatusBtn.addEventListener('click', () => {
                openUploadModal();
            });
            closeUploadModalBtn.addEventListener('click', () => {
                closeUploadModal();
            });
            // 点击遮罩层关闭弹窗(仅当点击在弹窗外部时)
            uploadModalOverlay.addEventListener('click', (e) => {
                if (e.target === uploadModalOverlay) {
                    closeUploadModal();
                }
            });
            // 为每个排序列添加点击事件
            document.querySelectorAll('[id^="sort-"]').forEach(column => {
                column.addEventListener('click', () => {
                    const field = column.id.replace('sort-', '');
                    if (field !== sortBy) {
                        sortBy = field;
                    }
                    sortOrder = sortOrder === 'asc' ? 'desc' : 'asc';
                    loadFiles();
                });
            });
        }
        // 重置搜索
        function resetSearch() {
            document.getElementById('searchInput').value = ''
            loadFiles()
        }
        // 加载文件列表
        function loadFiles() {
            let filterName = document.getElementById('searchInput').value
            fetch(host +
                `/___bk__api___/files?path=${encodeURIComponent(currentPath)}&sort_by=${sortBy}&order=${sortOrder}&filterName=${filterName}`
            )
                .then(response => response.json())
                .then(data => {
                    document.getElementById('selectAll').checked = false
                    currentFiles = [...data.directories, ...data.files];
                    updateBreadcrumb(data.current_path);
                    renderFileList(currentFiles);
                    updateSortIcons()
                })
                .catch(error => {
                    showToast('加载文件列表失败 ' + error, 'error')
                });
        }
        // 更新排序图标
        function updateSortIcons() {
            // 清空所有排序图标
            document.querySelectorAll('#sort_name, #sort_ctime, #sort_mtime, #sort_size').forEach(el => {
                el.innerHTML = '';
            });
            // 根据当前排序条件设置对应图标
            let sortIcon = '';
            if (sortOrder === 'asc') {
                sortIcon = '⇧'; // 向上箭头
            } else {
                sortIcon = '⇩'; // 向下箭头
            }
            const targetSpanId = `sort_${sortBy}`;
            if (targetSpanId) {
                document.getElementById(targetSpanId).innerHTML = sortIcon;
            }
        }
        // 渲染文件列表
        function renderFileList(files) {
            const fileList = document.getElementById('fileList');
            fileList.innerHTML = '';
            if (files.length === 0) {
                return;
            }
            files.forEach(file => {
                const row = document.createElement('tr');
                let del_html =
                    `<span class="btn-delete" onclick="deleteItem('${file.path}')">${file.is_dir ? '删除' : '删除'}`
                if (!config.is_delete) {
                    del_html = ''
                }
                // 文件夹操作按钮
                let folderActions = '';
                if (file.is_dir) {
                    const isPacking = packingInProgress.has(file.path);
                    const packingStyle = isPacking ? 'style="opacity: 0.5; cursor: not-allowed;"' : '';
                    // 转义路径中的单引号
                    const escapedPath = file.path.replace(/'/g, "\\'");
                    const packingOnclick = isPacking ? 'onclick="showToast(\'正在打包中,请稍候...\', \'warning\')"' : `onclick="downloadFolder('${escapedPath}')"`;
                    folderActions = `
                        <span class="btn-download" ${packingOnclick} ${packingStyle}>${isPacking ? '打包中...' : '打包下载'}</span>
                        <span class="btn-download" style="margin-left: 8px;" onclick="copyFolderDownloadLink('${escapedPath}')">复制打包下载地址</span>
                    `;
                }
                row.innerHTML = `
                    <td class="checkbox-column"><input type="checkbox" class="file-checkbox" data-path="${file.path}" ${file.is_dir ? 'disabled' : ''}></td>
                    <td class="filename" data-path="${file.path}">
                        ${file.is_dir ?
                        `<svg class="icon-folder" viewBox="0 0 24 24"><path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg>` :
                        `<svg class="icon-folder" viewBox="0 0 24 24"><path d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm4 18H6V4h7v5h5v11z"/></svg>`}
                        ${file.name}
                    </td>
                    <td>${file.ctime}</td>
                    <td>${file.mtime}</td>
                    <td>${file.is_dir ? '-' : formatFileSize(file.size)}</td>
                    <td class="actions">
							${!file.is_dir ? `<span class="btn-download" onclick=\"downloadFile('${file.path}')\">下载</span>` : ''}
							${!file.is_dir ? `<span class="btn-download" style="margin-left: 8px;" onclick=\"copyDownloadLink('${file.name}')\">复制下载地址</span>` : ''}
                        ${folderActions}
                        ${del_html}</span>
                    </td>
                `;
                // 添加点击事件到文件名
                row.querySelector('.filename').addEventListener('click', () => {
                    if (file.is_dir) {
                        currentPath = file.path;
                        sortBy = 'name';
                        sortOrder = 'asc';
                        history.pushState({}, '', currentPath);
                        loadFiles();
                    } else {
                        let nurl = window.location.origin
                        console.log(currentPath)
                        if (!currentPath.endsWith('/')) {
                            nurl += currentPath + '/' + file.name
                        } else {
                            nurl += currentPath + file.name
                        }
                        window.open(nurl, '_blank')
                    }
                });
                fileList.appendChild(row);
            });
        }
        // 处理排序方式改变
        function handleSortChange(event) {
            sortBy = event.target.value;
            loadFiles();
        }
        function c(d) {
            console.log(d)
        }
        // 更新面包屑导航
        function updateBreadcrumb(path) {
            const breadcrumb = document.getElementById('breadcrumb');
            breadcrumb.innerHTML = '';
            const rootSpan = document.createElement('span');
            rootSpan.textContent = '根目录';
            rootSpan.addEventListener('click', () => {
                currentPath = '';
                loadFiles();
                history.pushState({}, '', '/');
            });
            breadcrumb.appendChild(rootSpan);
            if (!path) return;
            const parts = path.split('/');
            let currentPathPart = '';
            let clickPath = ''
            parts.forEach((part, index) => {
                if (!part) return;
                const separator = document.createElement('span');
                separator.textContent = ' / ';
                breadcrumb.appendChild(separator);
                currentPathPart += (index > 0 || parts[0] ? '/' : '') + part;
                const span = document.createElement('span');
                span.textContent = part;
                clickPath += '/' + part
                indexPathMap.set(index, clickPath)
                if (index < parts.length - 1) {
                    span.style.color = '#3f51b5';
                    span.addEventListener('click', () => {
                        currentPath = indexPathMap.get(index);
                        history.pushState({}, '', currentPath);
                        loadFiles();
                    });
                } else {
                    span.style.marginLeft = '5px';
                    span.style.color = 'var(--text-color)';
                }
                breadcrumb.appendChild(span);
            });
        }
        // 创建文件夹
        function createFolder() {
            const folderName = document.getElementById('folderName').value
            if (folderName === '') {
                showToast('请输入要创建的文件夹名称', 'warning');
                return;
            }
            fetch(host + '/___bk__api___/create_folder', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({
                    folder_name: folderName,
                    path: currentPath
                })
            })
                .then(response => response.json())
                .then(data => {
                    if (data.success) {
                        showToast('创建文件夹成功')
                        document.getElementById('folderName').value = ''
                        loadFiles();
                    } else {
                        showToast('创建文件夹失败' + data.msg, 'error')
                    }
                })
                .catch(error => {
                    showToast('删除失败 ' + error, 'error')
                });
        }
        // 下载文件
        function downloadFile(filePath) {
            window.open(host + `/___bk__api___/download?file_path=${encodeURIComponent(filePath)}`);
        }
        // 复制下载链接
        function copyDownloadLink(filename) {
            const link = window.location.href + '/' + filename;
            if (navigator.clipboard && navigator.clipboard.writeText) {
                navigator.clipboard.writeText(link).then(() => {
                    showToast('下载地址已复制');
                }).catch(() => {
                    fallbackCopyText(link);
                });
            } else {
                fallbackCopyText(link);
            }
        }
        // 打包下载文件夹
        function downloadFolder(folderPath) {
            // 检查是否正在打包
            if (packingInProgress.has(folderPath)) {
                showToast('正在打包中,请稍候...', 'warning');
                return;
            }
            
            // 获取文件夹大小
            fetch(host + `/___bk__api___/folder_size?folder_path=${encodeURIComponent(folderPath)}`)
                .then(response => response.json())
                .then(data => {
                    if (!data.success) {
                        showToast('获取文件夹大小失败: ' + (data.error || '未知错误'), 'error');
                        return;
                    }
                    
                    const sizeInGB = data.size / (1024 * 1024 * 1024);
                    
                    // 如果大于1GB,显示确认提示
                    if (sizeInGB > 1) {
                        const confirmMsg = `文件夹大小约为 ${sizeInGB.toFixed(2)} GB,打包下载可能需要较长时间,确定要继续吗?`;
                        if (!confirm(confirmMsg)) {
                            return;
                        }
                    }
                    
                    // 标记为正在打包
                    packingInProgress.add(folderPath);
                    // 重新渲染文件列表以更新按钮状态
                    renderFileList(currentFiles);
                    
                    // 使用fetch下载,可以检测下载是否真正开始
                    const downloadUrl = host + `/___bk__api___/download_folder?folder_path=${encodeURIComponent(folderPath)}`;
                    let downloadFilename = 'folder.zip';
                    
                    fetch(downloadUrl)
                        .then(response => {
                            if (!response.ok) {
                                throw new Error('下载失败: ' + response.statusText);
                            }
                            // 获取文件名
                            const contentDisposition = response.headers.get('content-disposition');
                            if (contentDisposition) {
                                const filenameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
                                if (filenameMatch && filenameMatch[1]) {
                                    downloadFilename = filenameMatch[1].replace(/['"]/g, '');
                                }
                            }
                            // 返回blob
                            return response.blob();
                        })
                        .then(blob => {
                            // 创建下载链接
                            const url = window.URL.createObjectURL(blob);
                            const link = document.createElement('a');
                            link.href = url;
                            link.download = downloadFilename;
                            link.style.display = 'none';
                            document.body.appendChild(link);
                            link.click();
                            document.body.removeChild(link);
                            // 释放URL对象
                            window.URL.revokeObjectURL(url);
                            
                            // 下载已开始,恢复按钮状态
                            packingInProgress.delete(folderPath);
                            renderFileList(currentFiles);
                            showToast('下载已开始', 'success');
                        })
                        .catch(error => {
                            showToast('下载失败: ' + error.message, 'error');
                            packingInProgress.delete(folderPath);
                            renderFileList(currentFiles);
                        });
                })
                .catch(error => {
                    showToast('获取文件夹大小失败: ' + error, 'error');
                    packingInProgress.delete(folderPath);
                    renderFileList(currentFiles);
                });
        }
        // 复制文件夹打包下载地址
        function copyFolderDownloadLink(folderPath) {
            const link = window.location.origin + host + `/___bk__api___/download_folder?folder_path=${encodeURIComponent(folderPath)}`;
            if (navigator.clipboard && navigator.clipboard.writeText) {
                navigator.clipboard.writeText(link).then(() => {
                    showToast('打包下载地址已复制');
                }).catch(() => {
                    fallbackCopyText(link);
                });
            } else {
                fallbackCopyText(link);
            }
        }
        function fallbackCopyText(text) {
            const textarea = document.createElement('textarea');
            textarea.value = text;
            textarea.style.position = 'fixed';
            textarea.style.top = '-1000px';
            document.body.appendChild(textarea);
            textarea.focus();
            textarea.select();
            try {
                document.execCommand('copy');
                showToast('下载地址已复制');
            } catch (e) {
                showToast('复制失败', 'error');
            }
            document.body.removeChild(textarea);
        }
        // 删除文件
        function deleteItem(filePath) {
            if (confirm('确定要删除这个文件/文件夹吗?')) {
                fetch(host + '/___bk__api___/delete', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json'
                    },
                    body: JSON.stringify({
                        path: filePath
                    })
                })
                    .then(response => response.json())
                    .then(data => {
                        if (data.success) {
                            loadFiles();
                        } else {
                            showToast('删除失败 ' + data.error, 'error')
                        }
                    })
                    .catch(error => {
                        showToast('删除失败 ' + error, 'error')
                    });
            }
        }
        // 下载选中项
        function downloadeSelected() {
            const selected = Array.from(document.querySelectorAll('.file-checkbox:checked'));
            if (selected.length === 0) {
                showToast('请先选择要下载的文件', 'warning');
                return;
            }
            // 逐个创建并提交表单
            selected.forEach((checkbox, index) => {
                setTimeout(() => {
                    const filePath = checkbox.dataset.path;
                    const form = document.createElement('form');
                    form.action = host + `/___bk__api___/download`;
                    form.method = 'GET';
                    form.target = '_blank';
                    form.style.display = 'none';
                    const input = document.createElement('input');
                    input.type = 'hidden';
                    input.name = 'file_path';
                    input.value = filePath;
                    form.appendChild(input);
                    document.body.appendChild(form);
                    form.submit();
                    document.body.removeChild(form);
                }, index * 300); // 每个请求间隔300毫秒
            });
        }
        // 删除选中项
        function deleteSelected() {
            const selected = Array.from(document.querySelectorAll('.file-checkbox:checked'));
            if (selected.length === 0) {
                showToast('请先选择要删除的文件', 'warning')
                return;
            }
            if (confirm(`确定要删除选中的 ${selected.length} 个文件吗?`)) {
                const promises = selected.map(checkbox => {
                    const filePath = checkbox.dataset.path;
                    return fetch(host + '/___bk__api___/delete', {
                        method: 'POST',
                        headers: {
                            'Content-Type': 'application/json'
                        },
                        body: JSON.stringify({
                            path: filePath
                        })
                    }).then(response => response.json());
                });
                Promise.all(promises).then(results => {
                    const failed = results.filter(r => !r.success).length;
                    const success = results.length - failed;
                    if (failed === 0) {
                        showToast(`成功删除 ${success} 个文件 `)
                        loadFiles();
                    } else {
                        showToast(`部分文件删除失败(成功: ${success}, 失败: ${failed})`, 'warning')
                    }
                }).catch(error => {
                    showToast('删除过程中发生错误 ' + error, 'error')
                });
            }
        }
        // 切换全选
        function toggleSelectAll(event) {
            const checkboxes = document.querySelectorAll('.file-checkbox');
            checkboxes.forEach(checkbox => {
                if (!checkbox.disabled) {
                    checkbox.checked = event.target.checked;
                }
            });
        }
        // 处理文件上传
        function handleFileUpload(event) {
            const files = Array.from(event.target.files || []);
            if (files.length === 0) return;
            // 初始化任务并打开弹框
            files.forEach(file => {
                uploadTasks.push({
                    name: file.name,
                    size: file.size,
                    uploaded: 0,
                    status: 'waiting', // waiting | uploading | success | error
                    error: '',
                    startTime: null,
                    endTime: null
                });
            });
            renderUploadModal();
            openUploadModal();
            // 开始顺序上传
            if (!uploadingInProgress) {
                uploadingInProgress = true;
                uploadSequential(files).then(() => {
                    uploadingInProgress = false;
                    // 全部完成后自动关闭(若没有错误)
                    const hasError = uploadTasks.some(t => t.status === 'error');
                    if (!hasError) {
                        setTimeout(() => closeUploadModal(), 600);
                    }
                    loadFiles();
                });
            }
            // 重置文件输入以允许再次选择相同文件
            event.target.value = null;
        }

        async function uploadSequential(files) {
            for (let i = 0; i < files.length; i++) {
                await uploadSingleFile(files[i]);
            }
        }

        function uploadSingleFile(file) {
            return new Promise(resolve => {
                const task = uploadTasks.find(t => t.name === file.name && t.status === 'waiting');
                if (!task) return resolve();
                task.status = 'uploading';
                task.startTime = Date.now();
                renderUploadModal();
                const formData = new FormData();
                formData.append('file', file);
                formData.append('current_path', currentPath);
                const xhr = new XMLHttpRequest();
                xhr.open('POST', host + '/___bk__api___/upload');
                xhr.upload.onprogress = function (e) {
                    if (e.lengthComputable) {
                        task.uploaded = e.loaded;
                        renderUploadModal();
                    }
                };
                xhr.onerror = function () {
                    task.status = 'error';
                    task.error = '网络错误';
                    renderUploadModal();
                    resolve();
                };
                xhr.onload = function () {
                    try {
                        const resp = JSON.parse(xhr.responseText || '{}');
                        if (resp && resp.success) {
                            task.uploaded = task.size;
                            task.status = 'success';
                            task.endTime = Date.now();
                        } else {
                            task.status = 'error';
                            task.error = (resp && (resp.error || resp.msg)) ? (resp.error || resp.msg) : '上传失败';
                        }
                    } catch (e) {
                        task.status = 'error';
                        task.error = '响应解析失败';
                    }
                    renderUploadModal();
                    resolve();
                };
                xhr.send(formData);
            });
        }

        function openUploadModal() {
            uploadModalOverlay.style.display = 'block';
        }
        function closeUploadModal() {
            uploadModalOverlay.style.display = 'none';
        }
        function percent(num, den) {
            if (!den || den === 0) return 0;
            return Math.min(100, Math.round((num / den) * 100));
        }
        function formatBytes(bytes) {
            if (!bytes || bytes === 0) return '0 B';
            const k = 1024;
            const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
            const i = Math.floor(Math.log(bytes) / Math.log(k));
            return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
        }
        function formatSpeed(bps) {
            if (!bps || bps <= 0) return '—';
            return formatBytes(bps) + '/s';
        }
        function formatETA(seconds) {
            if (!isFinite(seconds) || seconds <= 0) return '—';
            const s = Math.round(seconds);
            const h = Math.floor(s / 3600);
            const m = Math.floor((s % 3600) / 60);
            const sec = s % 60;
            if (h > 0) {
                return `${h}小时 ${m.toString().padStart(2, '0')}分 ${sec.toString().padStart(2, '0')}秒`;
            }
            return `${m.toString().padStart(2, '0')}:${sec.toString().padStart(2, '0')}`;
        }
        function renderUploadModal() {
            const total = uploadTasks.length;
            const done = uploadTasks.filter(t => t.status === 'success').length;
            const failed = uploadTasks.filter(t => t.status === 'error').length;
            uploadSummaryEl.textContent = `共 ${total} 个文件,已完成 ${done},失败 ${failed}`;
            uploadListEl.innerHTML = '';
            uploadTasks.forEach((t, idx) => {
                let now = Date.now();
                let elapsedMs = t.startTime ? ((t.endTime || now) - t.startTime) : 0;
                let bps = elapsedMs > 0 ? (t.uploaded / (elapsedMs / 1000)) : 0;
                let etaSec = bps > 0 ? ((t.size - t.uploaded) / bps) : Infinity;
                const item = document.createElement('div');
                item.innerHTML = `
						<div class="upload-item-name">${idx + 1}. ${t.name}</div>
						<div class="progress-bar-wrap"><div class="progress-bar" style="width: ${percent(t.uploaded, t.size)}%"></div></div>
						<div class="upload-item-footer">
							<span>${formatBytes(t.uploaded)} / ${formatBytes(t.size)}</span>
							<span class="status-chip ${t.status === 'success' ? 'status-success' : (t.status === 'error' ? 'status-error' : '')}">
								${t.status === 'waiting' ? '待上传' : t.status === 'uploading' ? '上传中' : t.status === 'success' ? '完成' : '失败'}
							</span>
						</div>
						<div class="upload-item-footer">
							<span>速度:${t.status === 'uploading' || t.status === 'success' ? formatSpeed(bps) : '—'}</span>
							<span>预计完成:${t.status === 'uploading' ? formatETA(etaSec) : '—'}</span>
						</div>
					`;
                uploadListEl.appendChild(item);
            });
        }
        // 格式化文件大小
        function formatFileSize(bytes) {
            if (bytes === 0) return '0 B';
            const k = 1024;
            const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
            const i = Math.floor(Math.log(bytes) / Math.log(k));
            return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
        }
        function showToast(message, type = 'info', duration = 2000) {
            const toast = document.createElement('div');
            toast.textContent = message;
            toast.classList.add('toast');
            // 根据类型添加新的类名
            switch (type.toLowerCase()) {
                case 'success':
                    toast.classList.add('toast-success');
                    break;
                case 'warning':
                    toast.classList.add('toast-warning');
                    break;
                case 'error':
                    toast.classList.add('toast-error');
                    break;
                default:
                    toast.classList.add('toast-success'); // 默认为 success
                    break;
            }
            // 添加到 body 中
            document.body.appendChild(toast);
            // 设置定时隐藏
            setTimeout(() => {
                if (document.body.contains(toast)) {
                    document.body.removeChild(toast);
                }
            }, duration);
        }
        // 批量操作
        // 右键点击显示菜单
        batchOp.addEventListener('click', function (e) {
            e.preventDefault();
            // 设置菜单位置
            menu.style.left = e.pageX + 'px';
            menu.style.top = e.pageY + 'px';
            // 显示菜单
            menu.style.display = 'block';
        });
        // 点击菜单项
        menuItems.forEach(item => {
            item.addEventListener('click', function () {
                const action = this.getAttribute('data-action');
                menu.style.display = 'none';
                switch (action) {
                    case 'delete':
                        deleteSelected()
                        break
                    case 'download':
                        downloadeSelected()
                        break
                }
            });
        });
        // 点击页面其他地方隐藏菜单
        document.addEventListener('click', function (e) {
            if (!menu.contains(e.target) && e.target !== batchOp) {
                menu.style.display = 'none';
            }
        });
        // 初始化后执行
        const curi = decodeURIComponent(window.location.pathname)
        if (curi !== '' && curi !== '/') {
            currentPath = curi
        }
        // 初始化
        fetch(host + '/___bk__api___/config')
            .then(response => response.json())
            .then(data => {
                if (data.success) {
                    config = data.data
                    if (config.is_create_folder) {
                        document.querySelectorAll('.hide_create_folder')[0].classList.remove('hide_create_folder');
                    }
                    if (config.is_delete) {
                        document.querySelectorAll('.hide_delete')[0].classList.remove('hide_delete');
                    }
                    // 加载文件列表
                    init();
                } else {
                    showToast('加载配置文件失败 ' + data.error, 'error')
                }
            })
            .catch(error => {
                showToast('加载配置文件失败 ' + error, 'error')
            });
    </script>
</body>

</html>

4.运维

python(54) : centos7使用systemd托管python服务, 开启自启, 自动重启-CSDN博客

4.1.启动

cat > nfss_start.sh <<'EOF'
nohup python3 file_service_system.py &
EOF

配置说明

  • --port 端口
  • --path 文件管理目录, 默认当前目录
  • --delete 是否开启删除文件权限, 默认否
  • --create_folder 是否开启创建文件夹权限, 默认否 

4.2.停止

cat > nfss_stop.sh <<'EOF'
kill -9 $(ps aux|grep file_service_system.py | grep -v color| awk '{print $2}')
EOF

5.打包

注 : 打包为linux环境可用的需要在linux环境打包, windows亦然

pyinstaller -F --clean --noconfirm  file_service_system.py

您可能感兴趣的与本文相关的镜像

Python3.8

Python3.8

Conda
Python

Python 是一种高级、解释型、通用的编程语言,以其简洁易读的语法而闻名,适用于广泛的应用,包括Web开发、数据分析、人工智能和自动化脚本

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值