文件浏览功能
This commit is contained in:
78
admin/dashboard/blueprints/file_browser.py
Normal file
78
admin/dashboard/blueprints/file_browser.py
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
from flask import Blueprint, render_template, jsonify, send_file, request
|
||||||
|
import os
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
file_browser_bp = Blueprint('file_browser', __name__)
|
||||||
|
|
||||||
|
@file_browser_bp.route('/file_browser')
|
||||||
|
def file_browser():
|
||||||
|
"""文件浏览器页面"""
|
||||||
|
return render_template('file_browser.html')
|
||||||
|
|
||||||
|
@file_browser_bp.route('/api/list_files')
|
||||||
|
def list_files():
|
||||||
|
"""获取指定目录下的文件列表"""
|
||||||
|
try:
|
||||||
|
path = request.args.get('path', '')
|
||||||
|
# 获取项目根目录
|
||||||
|
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||||
|
# 构建完整路径
|
||||||
|
full_path = os.path.join(project_root, path)
|
||||||
|
|
||||||
|
# 安全检查:确保路径在项目根目录内
|
||||||
|
if not os.path.abspath(full_path).startswith(project_root):
|
||||||
|
return jsonify({"success": False, "message": "访问被拒绝:路径超出项目范围"})
|
||||||
|
|
||||||
|
if not os.path.exists(full_path):
|
||||||
|
return jsonify({"success": False, "message": "目录不存在"})
|
||||||
|
|
||||||
|
items = []
|
||||||
|
for item in os.listdir(full_path):
|
||||||
|
item_path = os.path.join(full_path, item)
|
||||||
|
is_dir = os.path.isdir(item_path)
|
||||||
|
items.append({
|
||||||
|
"name": item,
|
||||||
|
"is_dir": is_dir,
|
||||||
|
"size": os.path.getsize(item_path) if not is_dir else 0,
|
||||||
|
"modified": os.path.getmtime(item_path)
|
||||||
|
})
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"success": True,
|
||||||
|
"data": {
|
||||||
|
"items": items,
|
||||||
|
"current_path": path
|
||||||
|
}
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"列出文件失败: {e}")
|
||||||
|
return jsonify({"success": False, "message": str(e)})
|
||||||
|
|
||||||
|
@file_browser_bp.route('/api/download_file')
|
||||||
|
def download_file():
|
||||||
|
"""下载指定文件"""
|
||||||
|
try:
|
||||||
|
path = request.args.get('path', '')
|
||||||
|
# 获取项目根目录
|
||||||
|
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||||
|
# 构建完整路径
|
||||||
|
full_path = os.path.join(project_root, path)
|
||||||
|
|
||||||
|
# 安全检查:确保路径在项目根目录内
|
||||||
|
if not os.path.abspath(full_path).startswith(project_root):
|
||||||
|
return jsonify({"success": False, "message": "访问被拒绝:路径超出项目范围"})
|
||||||
|
|
||||||
|
if not os.path.exists(full_path):
|
||||||
|
return jsonify({"success": False, "message": "文件不存在"})
|
||||||
|
|
||||||
|
if os.path.isdir(full_path):
|
||||||
|
return jsonify({"success": False, "message": "不能下载目录"})
|
||||||
|
|
||||||
|
return send_file(
|
||||||
|
full_path,
|
||||||
|
as_attachment=True,
|
||||||
|
download_name=os.path.basename(full_path)
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"下载文件失败: {e}")
|
||||||
|
return jsonify({"success": False, "message": str(e)})
|
||||||
@@ -139,6 +139,7 @@ class DashboardServer:
|
|||||||
from admin.dashboard.blueprints.main import main_bp
|
from admin.dashboard.blueprints.main import main_bp
|
||||||
from admin.dashboard.blueprints.plugin_routes import plugin_routes
|
from admin.dashboard.blueprints.plugin_routes import plugin_routes
|
||||||
from admin.dashboard.blueprints.virtual_group import virtual_group_bp
|
from admin.dashboard.blueprints.virtual_group import virtual_group_bp
|
||||||
|
from admin.dashboard.blueprints.file_browser import file_browser_bp
|
||||||
|
|
||||||
# 在app.register_blueprint部分添加
|
# 在app.register_blueprint部分添加
|
||||||
app.register_blueprint(virtual_group_bp, url_prefix='/virtual_group')
|
app.register_blueprint(virtual_group_bp, url_prefix='/virtual_group')
|
||||||
@@ -150,6 +151,7 @@ class DashboardServer:
|
|||||||
app.register_blueprint(stats_bp)
|
app.register_blueprint(stats_bp)
|
||||||
app.register_blueprint(system_bp)
|
app.register_blueprint(system_bp)
|
||||||
app.register_blueprint(plugin_routes)
|
app.register_blueprint(plugin_routes)
|
||||||
|
app.register_blueprint(file_browser_bp)
|
||||||
|
|
||||||
self.LOG.info("所有蓝图已注册")
|
self.LOG.info("所有蓝图已注册")
|
||||||
|
|
||||||
|
|||||||
143
admin/dashboard/templates/file_browser.html
Normal file
143
admin/dashboard/templates/file_browser.html
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}文件浏览器{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">文件浏览器</h3>
|
||||||
|
<div class="card-tools">
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" id="current-path" class="form-control" readonly>
|
||||||
|
<div class="input-group-append">
|
||||||
|
<button class="btn btn-default" onclick="navigateUp()">
|
||||||
|
<i class="fas fa-arrow-up"></i> 上级目录
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>名称</th>
|
||||||
|
<th>类型</th>
|
||||||
|
<th>大小</th>
|
||||||
|
<th>修改时间</th>
|
||||||
|
<th>操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="file-list">
|
||||||
|
<!-- 文件列表将通过JavaScript动态加载 -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
let currentPath = '';
|
||||||
|
|
||||||
|
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 formatDate(timestamp) {
|
||||||
|
return new Date(timestamp * 1000).toLocaleString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadFiles(path = '') {
|
||||||
|
currentPath = path;
|
||||||
|
$('#current-path').val(path || '/');
|
||||||
|
|
||||||
|
$.get('/api/list_files', { path: path })
|
||||||
|
.done(function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
const fileList = $('#file-list');
|
||||||
|
fileList.empty();
|
||||||
|
|
||||||
|
response.data.items.forEach(function(item) {
|
||||||
|
const row = $('<tr>');
|
||||||
|
|
||||||
|
// 名称列
|
||||||
|
const nameCell = $('<td>');
|
||||||
|
const nameLink = $('<a>')
|
||||||
|
.text(item.name)
|
||||||
|
.attr('href', '#')
|
||||||
|
.css('color', item.is_dir ? '#007bff' : 'inherit');
|
||||||
|
|
||||||
|
if (item.is_dir) {
|
||||||
|
nameLink.click(function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
loadFiles(path ? path + '/' + item.name : item.name);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
nameCell.append(nameLink);
|
||||||
|
row.append(nameCell);
|
||||||
|
|
||||||
|
// 类型列
|
||||||
|
row.append($('<td>').text(item.is_dir ? '目录' : '文件'));
|
||||||
|
|
||||||
|
// 大小列
|
||||||
|
row.append($('<td>').text(item.is_dir ? '-' : formatFileSize(item.size)));
|
||||||
|
|
||||||
|
// 修改时间列
|
||||||
|
row.append($('<td>').text(formatDate(item.modified)));
|
||||||
|
|
||||||
|
// 操作列
|
||||||
|
const actionsCell = $('<td>');
|
||||||
|
if (!item.is_dir) {
|
||||||
|
const downloadBtn = $('<button>')
|
||||||
|
.addClass('btn btn-sm btn-primary')
|
||||||
|
.text('下载')
|
||||||
|
.click(function() {
|
||||||
|
window.location.href = '/api/download_file?path=' +
|
||||||
|
encodeURIComponent(path ? path + '/' + item.name : item.name);
|
||||||
|
});
|
||||||
|
actionsCell.append(downloadBtn);
|
||||||
|
}
|
||||||
|
row.append(actionsCell);
|
||||||
|
|
||||||
|
fileList.append(row);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
alert('加载文件列表失败:' + response.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.fail(function() {
|
||||||
|
alert('加载文件列表失败');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigateUp() {
|
||||||
|
if (!currentPath) return;
|
||||||
|
|
||||||
|
const lastSlashIndex = currentPath.lastIndexOf('/');
|
||||||
|
if (lastSlashIndex === -1) {
|
||||||
|
loadFiles('');
|
||||||
|
} else {
|
||||||
|
loadFiles(currentPath.substring(0, lastSlashIndex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载完成后加载根目录
|
||||||
|
$(document).ready(function() {
|
||||||
|
loadFiles('');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user