一、背景
使用docker部署的nginx+fancyindex部署文件服务器,缺少上传模块nginx-upload-module,没法直接添加模块重新编译。
二、准备文件上传服务
# -*- coding: utf-8 -*-
# author: zplin
# datetime: 2023/12/28
# Description:文件上传服务
from flask import Flask, request, jsonify
from flask_cors import CORS
import os
from Log import Log
log = Log()
app = Flask(__name__)
CORS(app)
#日志记录,添加请求后的响应状态码response.status_code
@app.after_request
def after_request(response):
log.info(
f"{request.remote_addr} - - {request.method} {request.path} {request.environ['SERVER_PROTOCOL']} {response.status_code}")
return response
# 上传文件接口
@app.route('/upload_file/<path:subpath>', methods=['POST'])
def upload_file(subpath):
dirpath = r'E:\nginx\download\\'
file_chunk = request.stream.read() # 读取文件流的数据
file_path = os.path.join(dirpath, subpath) # 设置文件存储的路径
os.makedirs(os.path.dirname(file_path), exist_ok=True) # 确保目录存在
chunk_number = request.args.get('chunk_number') # 获取分片计数器参数
if chunk_number.isdigit():
chunk_number = int(chunk_number) # 获取分片计数器参数
if chunk_number == 0:
if os.path.exists(file_path):
# print(file_path)
os.remove(file_path) # 如果文件存在,则删除原文件
with open(file_path, 'ab') as f:
f.write(file_chunk) # 将数据写入文件,使用追加模式
# 检查是否是最后一个分片,如果是,进行文件合并
if 'chunk_number' in request.args and request.args.get('chunk_number') == 'end':
merge_file(dirpath, subpath) # 调用合并函数
return jsonify({'message': f'{os.path.basename(file_path)} chunk {str(chunk_number)} uploaded successfully'}), 200
# 合并文件
def merge_file(dirpath, subpath):
file_list = [f for f in os.listdir(dirpath) if f.startswith(subpath) and os.path.isfile(os.path.join(dirpath, f))]
file_list.sort(key=lambda x: int(x.split("_")[1])) # 根据文件名中的分片序号进行排序
file_path = os.path.join(dirpath, subpath) # 设置文件合并后的路径
with open(file_path, 'ab') as f: # 将分片文件内容逐一合并
for chunk_file in file_list:
with open(os.path.join(dirpath, chunk_file), 'rb') as chunk:
f.write(chunk.read())
os.remove(os.path.join(dirpath, chunk_file)) # 删除已合并的分片文件
log.info(f"File {file_path} merged successfully")
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
三、添加上传文件入口
修改Fancyindex容器下/theme/Nginx-Fancyindex-Theme/header.html,这里设置为点击3次Fancyindex打开Windows文件选择弹窗。
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/721876146be448af9c0440ce2b77747a.png)
给Fancyindex添加文件输入框
<div class="mdl-layout__drawer">
<ul class="mdl-list">
<li class="mdl-layout-title mdl-list__item mdl-navigation__link">
<span class="mdl-color-text--primary mdl-list__item-primary-content" id="FancyIndex" onclick="handleFancyIndexClick()">
<i class="mdl-color-text--primary material-icons mdl-list__item-icon">filter_drama</i>
FancyIndex
</span>
</li>
</ul>
<input type="file" id="fileInput" style="display: none;" onchange="handleFileSelect(event)" />
定义handleFileSelect方法
let clickCount = 0;
function handleFancyIndexClick() {
clickCount++;
if (clickCount === 3) {
clickCount = 0; // 重置点击次数
document.getElementById('fileInput').click(); // 触发文件选择框点击
}
}
document.getElementById('FancyIndex').addEventListener('click', handleFancyIndexClick);
四、选择文件后上传文件到服务器
function handleFileSelect(event) {
let file = event.target.files[0]; // 从事件对象中获取用户选择的文件
if (file) {
//获取当前路径,将文件存到当前目录
const subpath = document.getElementById('currentPathTitle').textContent.replace(/^\//, '');
let chunkSize = 10 * 1024 * 1024;
let totalChunks = Math.ceil(file.size / chunkSize); // 计算分片总数
let chunkNumber = 0; // 初始化分片计数器
let start = 0;
let end = 0;
function readChunk() {
let blob = file.slice(start, end);
if (blob.size > 0) {
let reader = new FileReader();
reader.onload = function(e) {
let data = e.target.result;
let serverUrl = 'http://127.0.0.1:5000/upload_file/'; // 服务器接收文件的URL
let url = serverUrl + subpath + file.name + '?chunk_number=' + (chunkNumber === totalChunks - 1 ? 'end' : chunkNumber.toString());
fetch(url, {
method: 'POST',
body: data, // 直接传入 ArrayBuffer 数据
}).then(response => response.text()).then(data => {
console.log(data); // 打印服务器的原始响应内容
if (data.includes("uploaded successfully")) {
chunkNumber++;
start = end;
end = start + chunkSize;
readChunk(); // 继续下一个分片的读取和上传
}else {
console.log(url+"失败");
}
});
}
reader.readAsArrayBuffer(blob);
}
}
start = 0;
end = chunkSize;
readChunk();
}
}
document.getElementById('FancyIndex').addEventListener('click', handleFancyIndexClick);
document.getElementById('fileInput').addEventListener('change', handleFileSelect);
五、header.html中引用上传文件JS(/fancyindex/uploadFile.js)
<script src="/fancyindex/mdl/material.min.js"></script>
<link rel="stylesheet" href="/fancyindex/mdl/material.min.css">
<link rel="stylesheet" href="/fancyindex/fancyindex.css">
<script src="/fancyindex/uploadFile.js"></script>
</head>
六、记录
1、使用flask.after_request钩子函数,在函数中获取响应状态码并写入日志。
@app.after_request
def after_request(response):
log.info(
f"{request.remote_addr} - - {request.method} {request.path} {request.environ['SERVER_PROTOCOL']} {response.status_code}")
return response
2、大文件传输处理
- 使用流式传输: 避免将整个文件加载到内存中再进行上传。在Flask中用request.stream来处理流式传输。
- 分片上传: 考虑将大文件进行分片处理,分成若干小块分别上传,然后在服务器端进行合并。
file_chunk = request.stream.read() # 读取文件流的数据
with open(file_path, 'ab') as f:
f.write(file_chunk) # 将数据写入文件,使用追加模式
3、同名文件删除
分片上传使用追加模式写入,在上传时添加分片计数器chunk_number,当chunk_number为0而文件又存在时删除文件。
chunk_number = request.args.get('chunk_number') # 获取分片计数器参数
if chunk_number.isdigit():
chunk_number = int(chunk_number) # 获取分片计数器参数
if chunk_number == 0:
if os.path.exists(file_path):
# print(file_path)
os.remove(file_path) # 如果文件存在,则删除原文件
4、文件流按顺序整合
背景:文件上传后有问题,如:上传的zip解压出错
上传成功后再执行下一个分片的请求
if (data.includes("uploaded successfully")) {
chunkNumber++;
start = end;
end = start + chunkSize;
readChunk(); // 继续下一个分片的读取和上传
}