Files
ylt_diy/lib/api/download_api.py
2026-07-21 15:21:29 +08:00

205 lines
7.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
文件下载 API
"""
import os
import zipfile
from datetime import datetime
from flask import request, jsonify, send_from_directory
from lib.db import execute_query
from lib.logger import log_info, log_error
from . import download_bp
def get_reports_dir():
"""获取报表文件目录"""
return os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), 'public', 'reports')
def get_temp_dir():
"""获取临时zip文件目录"""
temp_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), 'public', 'temp_zip')
os.makedirs(temp_dir, exist_ok=True)
return temp_dir
def cleanup_old_temp_zips():
"""清理1小时前的临时zip文件"""
try:
temp_dir = get_temp_dir()
now = datetime.now().timestamp()
count = 0
for filename in os.listdir(temp_dir):
if filename.endswith('.zip'):
filepath = os.path.join(temp_dir, filename)
if now - os.path.getmtime(filepath) > 3600: # 1小时前的
os.remove(filepath)
count += 1
if count > 0:
log_info(f'[下载API] 清理了 {count} 个过期临时zip文件', 'download')
except Exception as e:
log_error(f'[下载API] 清理临时文件失败: {e}', 'download')
@download_bp.route('/download', methods=['GET'])
def download_file():
"""下载文件
支持两种方式:
1. path: 直接传文件路径
2. id: 传历史记录ID从数据库查询文件路径
"""
try:
file_path = request.args.get('path')
history_id = request.args.get('id')
log_info(f'[下载API] 请求path={file_path}, id={history_id}', 'download')
# 如果传了id从历史记录表查询文件路径
if history_id:
result = execute_query(
'SELECT file_path, status FROM t_daily_report_history WHERE id = %s LIMIT 1',
(history_id,)
)
if not result:
log_error(f'[下载API] 历史记录不存在id={history_id}', 'download')
return jsonify({'success': False, 'message': '历史记录不存在'}), 404
history = result[0]
if history.get('status') != 1:
log_error(f'[下载API] 生成失败的记录无法下载id={history_id}', 'download')
return jsonify({'success': False, 'message': '该记录生成失败,无法下载'}), 400
file_path = history.get('file_path')
if not file_path:
log_error(f'[下载API] 文件路径为空id={history_id}', 'download')
return jsonify({'success': False, 'message': '文件路径不能为空'}), 400
log_info(f'[下载API] 从历史记录查询到文件路径: {file_path}', 'download')
if not file_path:
return jsonify({'success': False, 'message': '文件路径不能为空'}), 400
# 安全检查:确保路径在 public/reports 目录下
if not file_path.startswith('/reports/') and not file_path.startswith('/public/reports/'):
return jsonify({'success': False, 'message': '无效的文件路径'}), 400
# 提取文件名
if file_path.startswith('/public/reports/'):
filename = file_path.replace('/public/reports/', '')
else:
filename = file_path.replace('/reports/', '')
reports_dir = get_reports_dir()
full_path = os.path.join(reports_dir, filename)
if not os.path.exists(full_path):
log_error(f'[下载API] 文件不存在: {full_path}', 'download')
return jsonify({'success': False, 'message': '文件不存在'}), 404
log_info(f'[下载API] 下载文件: {filename}', 'download')
return send_from_directory(reports_dir, filename, as_attachment=True)
except Exception as e:
log_error(f'[下载API] 错误: {e}', 'download')
import traceback
traceback.print_exc()
return jsonify({'success': False, 'message': str(e)}), 500
@download_bp.route('/download/batch', methods=['POST'])
def batch_download():
"""批量打包下载
接收 ids 数组将对应的报表文件打包成zip后下载
"""
try:
data = request.get_json(silent=True) or {}
ids = data.get('ids', [])
log_info(f'[下载API] 批量下载,数量: {len(ids)}', 'download')
if not ids or len(ids) == 0:
return jsonify({'success': False, 'message': '请选择要下载的记录'}), 400
# 清理旧的临时文件
cleanup_old_temp_zips()
# 查询所有成功的历史记录
placeholders = ', '.join(['%s'] * len(ids))
result = execute_query(
f'SELECT id, file_path, config_name, report_date, status FROM t_daily_report_history WHERE id IN ({placeholders})',
tuple(ids)
)
if not result:
return jsonify({'success': False, 'message': '未找到对应的历史记录'}), 404
reports_dir = get_reports_dir()
temp_dir = get_temp_dir()
# 收集要打包的文件
files_to_zip = []
failed_names = []
for item in result:
if item.get('status') != 1:
failed_names.append(f"{item.get('config_name', '未知')} (生成失败)")
continue
file_path = item.get('file_path')
if not file_path:
failed_names.append(f"{item.get('config_name', '未知')} (无文件)")
continue
# 提取文件名
if file_path.startswith('/public/reports/'):
filename = file_path.replace('/public/reports/', '')
elif file_path.startswith('/reports/'):
filename = file_path.replace('/reports/', '')
else:
filename = os.path.basename(file_path)
full_path = os.path.join(reports_dir, filename)
if not os.path.exists(full_path):
failed_names.append(f"{item.get('config_name', '未知')} (文件不存在)")
continue
# 使用配置名作为zip内的文件名避免重名
config_name = item.get('config_name', '未知')
report_date = item.get('report_date', '')
ext = os.path.splitext(filename)[1]
if report_date:
zip_inner_name = f"{config_name}_{report_date}{ext}"
else:
zip_inner_name = f"{config_name}{ext}"
files_to_zip.append({
'full_path': full_path,
'inner_name': zip_inner_name
})
if len(files_to_zip) == 0:
return jsonify({'success': False, 'message': '没有可下载的文件'}), 400
# 生成zip文件名
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
zip_filename = f'批量下载_{timestamp}.zip'
zip_path = os.path.join(temp_dir, zip_filename)
# 打包
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf:
for file_info in files_to_zip:
zf.write(file_info['full_path'], file_info['inner_name'])
log_info(f'[下载API] 批量打包完成,共 {len(files_to_zip)} 个文件,失败 {len(failed_names)}', 'download')
# 返回zip文件
from flask import send_file
return send_file(
zip_path,
as_attachment=True,
download_name=zip_filename,
mimetype='application/zip'
)
except Exception as e:
log_error(f'[下载API] 批量下载错误: {e}', 'download')
import traceback
traceback.print_exc()
return jsonify({'success': False, 'message': str(e)}), 500