后端:
import os
from flask import Flask, request, send_file, jsonify, abort, render_template, flash, redirect, url_for
app = Flask(__name__)
app.secret_key = 'supersecretkey' # 设置一个安全的密钥
# 配置上传文件夹和允许的扩展名
UPLOAD_FOLDER = 'uploads'
ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif', 'zip'}
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB大小限制
# 确保上传文件夹存在
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
def allowed_file(filename):
"""检查文件扩展名是否合法"""
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
def get_file_list():
"""获取文件列表信息"""
files = []
for filename in os.listdir(app.config['UPLOAD_FOLDER']):
path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
if os.path.isfile(path):
files.append({
'name': filename,
'size': os.path.getsize(path),
'url': f'/download/{filename}'
})
return files
@app.route('/')
def index():
"""首页,显示文件上传界面和文件列表"""
files = get_file_list()
return render_template('index.html', files=files)
# 修复1: 确保表单提交到/upload而不是根路径
@app.route('/upload', methods=['POST'])
def upload_file():
"""文件上传接口"""
# 检查请求中是否有文件部分
if 'file' not in request.files:
flash('没有文件部分', 'error')
return redirect(url_for('index'))
file = request.files['file']
# 如果用户没有选择文件
if file.filename == '':
flash('未选择文件', 'error')
return redirect(url_for('index'))
# 验证文件名和扩展名
if not allowed_file(file.filename):
flash('文件类型不允许', 'error')
return redirect(url_for('index'))
try:
# 安全保存文件
filename = os.path.join(app.config['UPLOAD_FOLDER'], file.filename)
file.save(filename)
flash(f'文件上传成功: {file.filename}', 'success')
return redirect(url_for('index'))
except Exception as e:
flash(f'上传失败: {str(e)}', 'error')
return redirect(url_for('index'))
@app.route('/download/<path:filename>', methods=['GET'])
def download_file(filename):
"""文件下载接口"""
# 防止目录遍历攻击
if '..' in filename or filename.startswith('/'):
abort(400)
file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
# 检查文件是否存在
if not os.path.isfile(file_path):
abort(404)
return send_file(file_path, as_attachment=True)
@app.route('/delete/<path:filename>', methods=['DELETE'])
def delete_file(filename):
"""文件删除接口"""
# 防止目录遍历攻击
if '..' in filename or filename.startswith('/'):
return jsonify({'error': '非法文件名'}), 400
file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
# 检查文件是否存在
if not os.path.isfile(file_path):
return jsonify({'error': '文件不存在'}), 404
try:
os.remove(file_path)
return jsonify({'message': f'文件已删除: {filename}'}), 200
except Exception as e:
return jsonify({'error': str(e)}), 500
# 修复2: 添加文件列表API
@app.route('/file-list', methods=['GET'])
def file_list_api():
"""返回文件列表的JSON数据"""
try:
files = get_file_list()
return jsonify({'files': files}), 200
except Exception as e:
return jsonify({'error': str(e)}), 500
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)
在 templates/index.html 文件中,修复表单的 action 属性:
HTML前端:
<!DOCTYPE html>
<html>
<head>
<title>文件上传服务</title>
<style>
body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; background-color: #f5f5f5; }
.container { background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
h1 { color: #333; text-align: center; margin-bottom: 30px; }
.upload-box {
border: 2px dashed #3498db;
padding: 25px;
text-align: center;
margin-bottom: 30px;
border-radius: 8px;
background-color: #f8f9fa;
transition: all 0.3s ease;
}
.upload-box:hover { background-color: #e3f2fd; border-color: #2980b9; }
.file-list { list-style: none; padding: 0; margin-top: 20px; }
.file-item {
padding: 12px 15px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
transition: background-color 0.2s;
}
.file-item:hover { background-color: #f9f9f9; }
.file-info { display: flex; flex-direction: column; }
.file-name { font-weight: bold; color: #2c3e50; }
.file-size { color: #7f8c8d; font-size: 0.9em; }
.file-actions { display: flex; gap: 15px; }
.btn {
background: #3498db;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1em;
transition: background 0.3s;
}
.btn:hover { background: #2980b9; }
.btn-download {
background: #27ae60;
}
.btn-download:hover {
background: #219653;
}
.btn-delete {
background: #e74c3c;
}
.btn-delete:hover {
background: #c0392b;
}
#fileInput { display: none; }
.file-selector { margin-bottom: 15px; }
.file-name-display { margin: 15px 0; color: #34495e; font-weight: bold; }
.message {
padding: 10px;
margin: 15px 0;
border-radius: 4px;
text-align: center;
}
.success { background-color: #d4edda; color: #155724; }
.error { background-color: #f8d7da; color: #721c24; }
.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
.refresh-btn { background: #9b59b6; }
.refresh-btn:hover { background: #8e44ad; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>文件上传服务</h1>
<button class="btn refresh-btn" onclick="refreshFileList()">刷新列表</button>
</div>
<div class="upload-box">
<h2>上传文件</h2>
<!-- 修复3: 设置表单action到/upload -->
<form action="/upload" method="post" enctype="multipart/form-data" id="uploadForm">
<div class="file-selector">
<button type="button" class="btn" onclick="document.getElementById('fileInput').click()">
选择文件
</button>
</div>
<div class="file-name-display" id="fileName">未选择文件</div>
<input type="file" name="file" id="fileInput">
<button type="submit" class="btn">开始上传</button>
</form>
<div id="message">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="message {{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
</div>
</div>
<h2>文件列表</h2>
<ul class="file-list" id="fileList">
{% if files %}
{% for file in files %}
<li class="file-item">
<div class="file-info">
<span class="file-name">{{ file.name }}</span>
<span class="file-size">{{ (file.size/1024)|round(2) }} KB</span>
</div>
<div class="file-actions">
<a href="/download/{{ file.name }}" class="btn btn-download">下载</a>
<button class="btn btn-delete" onclick="deleteFile('{{ file.name }}')">删除</button>
</div>
</li>
{% endfor %}
{% else %}
<li class="file-item">暂无文件</li>
{% endif %}
</ul>
</div>
<script>
// 显示选择的文件名
document.getElementById('fileInput').addEventListener('change', function(e) {
const fileNameDisplay = document.getElementById('fileName');
if (this.files[0]) {
fileNameDisplay.textContent = this.files[0].name;
fileNameDisplay.style.color = '#27ae60';
} else {
fileNameDisplay.textContent = '未选择文件';
fileNameDisplay.style.color = '#34495e';
}
});
// 删除文件
async function deleteFile(filename) {
if (!confirm(`确定删除 "${filename}" 吗?此操作不可撤销。`)) return;
try {
const response = await fetch(`/delete/${encodeURIComponent(filename)}`, {
method: 'DELETE'
});
if (response.ok) {
// 刷新文件列表
refreshFileList();
} else {
const error = await response.json();
alert('删除失败: ' + error.error);
}
} catch (error) {
alert('网络错误: ' + error.message);
}
}
// 刷新文件列表
async function refreshFileList() {
try {
const response = await fetch('/file-list');
const data = await response.json();
const fileList = document.getElementById('fileList');
fileList.innerHTML = '';
if (data.files && data.files.length > 0) {
data.files.forEach(file => {
const listItem = document.createElement('li');
listItem.className = 'file-item';
const sizeKB = (file.size / 1024).toFixed(2);
listItem.innerHTML = `
<div class="file-info">
<span class="file-name">${file.name}</span>
<span class="file-size">${sizeKB} KB</span>
</div>
<div class="file-actions">
<a href="/download/${encodeURIComponent(file.name)}" class="btn btn-download">下载</a>
<button class="btn btn-delete" onclick="deleteFile('${file.name}')">删除</button>
</div>
`;
fileList.appendChild(listItem);
});
} else {
const listItem = document.createElement('li');
listItem.className = 'file-item';
listItem.textContent = '暂无文件';
fileList.appendChild(listItem);
}
// 显示刷新成功消息
showMessage('文件列表已刷新', 'success');
} catch (error) {
showMessage('刷新失败: ' + error.message, 'error');
}
}
// 显示消息
function showMessage(text, type) {
const messageDiv = document.getElementById('message');
// 清除旧消息
while (messageDiv.firstChild) {
messageDiv.removeChild(messageDiv.firstChild);
}
const msgElement = document.createElement('div');
msgElement.className = `message ${type}`;
msgElement.textContent = text;
messageDiv.appendChild(msgElement);
// 3秒后移除消息
setTimeout(() => {
msgElement.remove();
}, 3000);
}
</script>
</body>
</html>
使用说明:
- 将 Python 代码保存为
app.py - 创建
templates文件夹,将 HTML 代码保存为templates/index.html - 安装依赖:
pip install flask - 运行应用:
python app.py - 访问
http://localhost:5000
创建项目结构:
project/
├── app.py
├── templates/
│ └── index.html
└── uploads/