fix: guard ppt audit file integrity and disable preview on corrupted files
This commit is contained in:
@@ -1847,6 +1847,7 @@ def ppt_audit_file(filename: str):
|
||||
action = (request.args.get('action', 'view') or 'view').strip().lower()
|
||||
try:
|
||||
import os
|
||||
import zipfile
|
||||
from utils.security import safe_join
|
||||
|
||||
reports_dir = os.environ.get('REPORTS_DIR', '/app/data/reports')
|
||||
@@ -1858,6 +1859,17 @@ def ppt_audit_file(filename: str):
|
||||
if safe_path.suffix.lower() != '.pptx':
|
||||
return '不支援的檔案格式', 400
|
||||
|
||||
if action == 'view':
|
||||
try:
|
||||
with zipfile.ZipFile(safe_path, 'r') as zf:
|
||||
bad = zf.testzip()
|
||||
if bad is not None:
|
||||
return f'PPT 檔案損毀,無法預覽(損毀區段:{bad})', 409
|
||||
except zipfile.BadZipFile:
|
||||
return 'PPT 檔案損毀,無法預覽(非有效 zip)', 409
|
||||
except Exception as e:
|
||||
return f'預覽檢查失敗:{type(e).__name__}', 409
|
||||
|
||||
return send_file(
|
||||
str(safe_path),
|
||||
mimetype='application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
@@ -2171,6 +2183,7 @@ def budget_update(budget_id: int):
|
||||
def ppt_audit_history():
|
||||
"""掃 reports/ 目錄列指定月份 daily 報表 + 從 ppt_audit_results 讀審核歷史(Phase 38)"""
|
||||
import os
|
||||
import zipfile
|
||||
reports_dir = os.environ.get('REPORTS_DIR', '/app/data/reports')
|
||||
files = []
|
||||
audit_records = []
|
||||
@@ -2231,6 +2244,16 @@ def ppt_audit_history():
|
||||
next_month_label = f"{next_year:04d}-{next_month:02d}"
|
||||
show_next_month = (next_year < now.year) or (next_year == now.year and next_month <= now.month)
|
||||
|
||||
def _inspect_ppt_file(file_path: str):
|
||||
try:
|
||||
with zipfile.ZipFile(file_path, 'r') as zf:
|
||||
bad = zf.testzip()
|
||||
return (bad is None, None if bad is None else f'壓縮檔異常:{bad}')
|
||||
except zipfile.BadZipFile:
|
||||
return False, 'PPTX 檔案損毀(非有效 zip)'
|
||||
except Exception as e:
|
||||
return False, f'檢查失敗:{str(e)[:60]}'
|
||||
|
||||
try:
|
||||
if not os.path.isdir(reports_dir):
|
||||
error = f'{reports_dir} 目錄不存在'
|
||||
@@ -2248,6 +2271,7 @@ def ppt_audit_history():
|
||||
try:
|
||||
mtime = os.path.getmtime(full)
|
||||
if month_start_ts <= mtime < month_end_ts:
|
||||
is_valid, check_msg = _inspect_ppt_file(full)
|
||||
files_by_name[f] = {
|
||||
'source': 'filesystem',
|
||||
'name': f,
|
||||
@@ -2256,6 +2280,8 @@ def ppt_audit_history():
|
||||
'mtime_ts': mtime,
|
||||
'file_exists': True,
|
||||
'file_path': full,
|
||||
'is_valid_ppt': is_valid,
|
||||
'file_error': check_msg,
|
||||
}
|
||||
except OSError:
|
||||
continue
|
||||
@@ -2294,6 +2320,10 @@ def ppt_audit_history():
|
||||
continue
|
||||
candidate_path = os.path.join(reports_dir, name)
|
||||
exists = os.path.isfile(candidate_path)
|
||||
is_valid = False
|
||||
check_msg = '檔案未落盤'
|
||||
if exists:
|
||||
is_valid, check_msg = _inspect_ppt_file(candidate_path)
|
||||
files_by_name[name] = {
|
||||
'source': 'database',
|
||||
'name': name,
|
||||
@@ -2303,6 +2333,8 @@ def ppt_audit_history():
|
||||
'file_exists': exists,
|
||||
'file_path': candidate_path if exists else file_path,
|
||||
'report_type': rpt_type,
|
||||
'is_valid_ppt': is_valid,
|
||||
'file_error': None if exists else check_msg,
|
||||
}
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
@@ -286,19 +286,42 @@
|
||||
<td><small>{{ f.mtime }}</small></td>
|
||||
<td>
|
||||
{% if f.source == 'database' %}
|
||||
<span class="text-muted">{% if f.file_exists %}資料庫快取 + 檔案可存取{% else %}資料庫快取(檔案未落盤){% endif %}</span>
|
||||
{% if f.file_exists %}
|
||||
{% if f.is_valid_ppt %}
|
||||
<span class="text-success">資料庫快取 + 檔案可預覽</span>
|
||||
{% else %}
|
||||
<span class="status-bad">資料庫快取 + 檔案損毀,建議重跑</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="text-muted">資料庫快取(檔案未落盤)</span>
|
||||
{% endif %}
|
||||
{% elif f.source == 'both' %}
|
||||
{% if f.is_valid_ppt %}
|
||||
<span class="text-success">檔案 + 資料庫</span>
|
||||
{% else %}
|
||||
<span class="status-bad">檔案 + 資料庫,檔案損毀</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="text-muted">22:00 掃描落盤</span>
|
||||
{% if f.is_valid_ppt %}
|
||||
<span class="text-success">22:00 掃描落盤,可預覽</span>
|
||||
{% else %}
|
||||
<span class="status-bad">22:00 掃描落盤,檔案損毀</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if f.file_error %}
|
||||
<div><small class="text-muted">{{ f.file_error }}</small></div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div class="ppt-file-actions">
|
||||
{% if f.file_exists %}
|
||||
{% if f.is_valid_ppt %}
|
||||
<a class="btn btn-outline-primary btn-sm" href="{{ url_for('admin_observability.ppt_audit_file', filename=f.name) }}" target="_blank" rel="noopener">
|
||||
<i class="fas fa-file-powerpoint me-1"></i>開啟
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="small status-bad">檔案不可預覽</span>
|
||||
{% endif %}
|
||||
<a class="btn btn-outline-secondary btn-sm" href="{{ url_for('admin_observability.ppt_audit_file', filename=f.name, action='download') }}">
|
||||
<i class="fas fa-download me-1"></i>下載
|
||||
</a>
|
||||
|
||||
Reference in New Issue
Block a user