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


5020

被折叠的 条评论
为什么被折叠?



