storage_monitor.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. """storage_monitor.py — 磁盘配额检查与后台定时任务。"""
  2. import asyncio
  3. import logging
  4. import shutil
  5. from pathlib import Path
  6. from app.config import settings
  7. logger = logging.getLogger(__name__)
  8. # ------------------------------------------------------------------ #
  9. # 磁盘统计工具
  10. # ------------------------------------------------------------------ #
  11. def _dir_size(path: Path) -> int:
  12. """递归计算目录占用字节数;目录不存在返回 0。"""
  13. if not path.exists():
  14. return 0
  15. return sum(f.stat().st_size for f in path.rglob("*") if f.is_file())
  16. def get_storage_info() -> dict:
  17. """
  18. 扫描 tmp/ 目录,返回存储统计信息:
  19. {
  20. disk_total_bytes, disk_used_bytes,
  21. tmp_total_bytes, quota_bytes, quota_exceeded,
  22. active_users, per_user_quota_bytes,
  23. users: [{ user_id, used_bytes, quota_exceeded }]
  24. }
  25. """
  26. tmp_path = Path(settings.temp_dir)
  27. disk = shutil.disk_usage(tmp_path if tmp_path.exists() else ".")
  28. disk_total = disk.total
  29. disk_used = disk.used
  30. quota_bytes = int(disk_total * settings.disk_quota_ratio)
  31. tmp_total = _dir_size(tmp_path)
  32. quota_exceeded = tmp_total > quota_bytes
  33. # 遍历用户子目录(tmp/{user_id}/)
  34. users: list[dict] = []
  35. if tmp_path.exists():
  36. for user_dir in sorted(tmp_path.iterdir()):
  37. if not user_dir.is_dir():
  38. continue
  39. used = _dir_size(user_dir)
  40. if used > 0:
  41. users.append({"user_id": user_dir.name, "used_bytes": used})
  42. active_users = len(users)
  43. per_user_quota = (quota_bytes // active_users) if active_users > 0 else quota_bytes
  44. for u in users:
  45. u["quota_exceeded"] = u["used_bytes"] > per_user_quota
  46. return {
  47. "disk_total_bytes": disk_total,
  48. "disk_used_bytes": disk_used,
  49. "tmp_total_bytes": tmp_total,
  50. "quota_bytes": quota_bytes,
  51. "quota_exceeded": quota_exceeded,
  52. "active_users": active_users,
  53. "per_user_quota_bytes": per_user_quota,
  54. "users": users,
  55. }
  56. # ------------------------------------------------------------------ #
  57. # 配额检查(导出后调用 / 定时任务共用)
  58. # ------------------------------------------------------------------ #
  59. def _log_quota_warnings(info: dict, prefix: str) -> None:
  60. """将超限情况写入日志,供 check_quota 和定时任务共用。"""
  61. if info["quota_exceeded"]:
  62. logger.warning(
  63. "[%s] tmp/ 总占用 %d bytes 超过配额 %d bytes",
  64. prefix,
  65. info["tmp_total_bytes"],
  66. info["quota_bytes"],
  67. )
  68. for u in info["users"]:
  69. if u["quota_exceeded"]:
  70. logger.warning(
  71. "[%s] 用户 %s 占用 %d bytes 超过均分配额 %d bytes",
  72. prefix,
  73. u["user_id"],
  74. u["used_bytes"],
  75. info["per_user_quota_bytes"],
  76. )
  77. def check_quota(user_id: str) -> str | None:
  78. """
  79. 检查存储配额,返回用户侧 warning 文本;无超限时返回 None。
  80. 同时将管理员级别超限情况写入日志。
  81. """
  82. info = get_storage_info()
  83. _log_quota_warnings(info, prefix="存储告警")
  84. user_entry = next((u for u in info["users"] if u["user_id"] == user_id), None)
  85. if user_entry and user_entry["quota_exceeded"]:
  86. return "您的存储空间已超出限额,请删除旧文件释放空间"
  87. return None
  88. # ------------------------------------------------------------------ #
  89. # 后台定时任务
  90. # ------------------------------------------------------------------ #
  91. async def _periodic_check(interval_seconds: int = 1800) -> None:
  92. """每 interval_seconds 秒(默认 30 分钟)执行一次全量磁盘检查。"""
  93. while True:
  94. await asyncio.sleep(interval_seconds)
  95. try:
  96. _log_quota_warnings(get_storage_info(), prefix="定时检查")
  97. except Exception:
  98. logger.exception("[定时检查] 磁盘检查异常")
  99. def start_background_monitor() -> asyncio.Task:
  100. """在当前事件循环中启动后台定时任务,返回 Task 对象。"""
  101. return asyncio.create_task(_periodic_check())