- F01: _safe_unlink_export 不跟随符号链接、逐层校验父目录,导出失败路径与 清理命令统一使用,只清理受控目录内本次创建的文件 - F02: 清理失败记录告警日志;清理命令回收 failed 任务残留文件 - F05: 定义保存事务内锁数据集行,配额原子检查;revision 冲突返回 409; 保留最近 20 版历史;支持个人/团队共享;审计与变更同事务 - F06: 工作簿参数页毛利率改为只读快照;可编辑单元格解锁并加数值校验 - F07: 对账按指标分容差(人数 0、比率 0.0001、金额 0.01);自定义列使用 服务端 Decimal 表达式求值参考 - F08: 导出任务执行/下载阶段重校验账号有效性与角色 - 对话草稿会话恢复;载入定义后追问携带原定义(F04);下载改浏览器直传 - 迁移 0006-0008;PostgreSQL 咨询锁语义修正(import_worker)
76 lines
3.0 KiB
Python
76 lines
3.0 KiB
Python
import time
|
|
from datetime import timedelta
|
|
from pathlib import Path
|
|
|
|
from django.conf import settings
|
|
from django.core.management.base import BaseCommand
|
|
from django.db import transaction
|
|
from django.db.models import Q
|
|
from django.utils import timezone
|
|
|
|
from finance.exporting import _safe_unlink_export
|
|
from finance.models import FormulaExport
|
|
|
|
|
|
class Command(BaseCommand):
|
|
help = "Remove expired formula workbooks and stale temporary files"
|
|
|
|
def add_arguments(self, parser):
|
|
parser.add_argument("--loop", action="store_true")
|
|
parser.add_argument("--interval", type=int, default=3600)
|
|
|
|
def handle(self, *args, **options):
|
|
while True:
|
|
jobs, files = self.clean_expired()
|
|
self.stdout.write(f"Expired {jobs} jobs; removed {files} files")
|
|
if not options["loop"]:
|
|
return
|
|
time.sleep(max(60, options["interval"]))
|
|
|
|
def clean_expired(self):
|
|
export_root = Path(settings.MEDIA_ROOT) / "formula-exports"
|
|
if export_root.is_symlink():
|
|
raise ValueError("公式导出目录不能是符号链接")
|
|
root = export_root.absolute()
|
|
now = timezone.now()
|
|
jobs = files = 0
|
|
|
|
jobs_qs = FormulaExport.objects.filter(
|
|
Q(status="succeeded", expires_at__lte=now) | Q(status="failed", path__gt="")
|
|
).iterator()
|
|
for job in jobs_qs:
|
|
path = Path(job.path) if job.path else None
|
|
try:
|
|
with transaction.atomic():
|
|
deleted = bool(path and _safe_unlink_export(path, root))
|
|
if job.status == "succeeded":
|
|
if deleted or not path or not path.is_absolute() or not path.is_relative_to(root):
|
|
if deleted:
|
|
files += 1
|
|
job.status, job.path = "expired", ""
|
|
job.save(update_fields=["status", "path"])
|
|
jobs += 1
|
|
elif deleted:
|
|
files += 1
|
|
job.path = ""
|
|
job.save(update_fields=["path"])
|
|
jobs += 1
|
|
except OSError as exc:
|
|
self.stderr.write(f"Could not remove expired export {job.pk}: {exc}")
|
|
|
|
cutoff = (now - timedelta(hours=24)).timestamp()
|
|
if root.is_dir() and not root.is_symlink():
|
|
for user_dir in root.iterdir():
|
|
if user_dir.is_symlink() or not user_dir.is_dir():
|
|
continue
|
|
for path in user_dir.glob("*.tmp"):
|
|
try:
|
|
if path.is_symlink() or not path.is_file() or path.stat().st_mtime > cutoff:
|
|
continue
|
|
path.unlink()
|
|
files += 1
|
|
except OSError as exc:
|
|
self.stderr.write(f"Could not remove stale temporary export {path.name}: {exc}")
|
|
|
|
return jobs, files
|