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.utils import timezone 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.resolve() now = timezone.now() jobs = files = 0 for job in FormulaExport.objects.filter(status="succeeded", expires_at__lte=now).iterator(): path = Path(job.path) if job.path else None resolved = path.resolve() if path and path.is_absolute() and not path.is_symlink() else None in_root = bool(resolved and resolved.is_relative_to(root)) try: with transaction.atomic(): if in_root and resolved.is_file(): resolved.unlink() files += 1 job.status, job.path = "expired", "" job.save(update_fields=["status", "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