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