1
0

__init__.py 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. """
  2. Delete file with the oldest modification date
  3. until a minimum of --free-bytes are available on the respective disk.
  4. """
  5. import argparse
  6. import datetime
  7. import logging
  8. import os
  9. import re
  10. import shutil
  11. # https://en.wikipedia.org/wiki/Template:Quantities_of_bytes
  12. _DATA_SIZE_UNIT_BYTE_CONVERSION_FACTOR = {
  13. "B": 1,
  14. "kB": 10**3,
  15. "KB": 10**3,
  16. "MB": 10**6,
  17. "GB": 10**9,
  18. "TB": 10**12,
  19. "KiB": 2**10,
  20. "MiB": 2**20,
  21. "GiB": 2**30,
  22. "TiB": 2**40,
  23. }
  24. def _data_size_to_bytes(size_with_unit: str) -> int:
  25. match = re.match(r"^([\d\.]+)\s*([A-Za-z]+)?$", size_with_unit)
  26. if not match:
  27. raise ValueError(f"Unable to parse data size {size_with_unit!r}")
  28. if unit_symbol := match.group(2):
  29. try:
  30. byte_conversion_factor = _DATA_SIZE_UNIT_BYTE_CONVERSION_FACTOR[unit_symbol]
  31. except KeyError as exc:
  32. raise ValueError(f"Unknown data size unit symbol {unit_symbol!r}") from exc
  33. else:
  34. byte_conversion_factor = 1
  35. byte_size = float(match.group(1)) * byte_conversion_factor
  36. return int(round(byte_size, 0))
  37. def _main() -> None:
  38. argparser = argparse.ArgumentParser(description=__doc__)
  39. argparser.add_argument("-d", "--debug", action="store_true")
  40. argparser.add_argument(
  41. "--delete-path-regex",
  42. metavar="REGULAR_EXPRESSION",
  43. type=re.compile,
  44. help="Only delete files with path matching regular expression (at any position)."
  45. " Paths will not be resolved or made absolute before check."
  46. r" Examples: \.mp4$ or ^/tmp/\d or ^rel/ative/ (default: no filter)",
  47. default="",
  48. )
  49. argparser.add_argument(
  50. "--free-bytes",
  51. type=_data_size_to_bytes,
  52. required=True,
  53. help="examples: 1024, 1024B, 4KiB, 4KB, 2TB",
  54. )
  55. argparser.add_argument("root_dir_path", metavar="ROOT_DIR")
  56. args = argparser.parse_args()
  57. logging.basicConfig(
  58. level=logging.DEBUG if args.debug else logging.INFO,
  59. format="%(asctime)s:%(levelname)s:%(message)s",
  60. datefmt="%Y-%m-%dT%H:%M:%S%z",
  61. )
  62. logging.debug("Required free bytes: %d", args.free_bytes)
  63. disk_usage = shutil.disk_usage(args.root_dir_path)
  64. logging.debug(disk_usage)
  65. if disk_usage.free >= args.free_bytes:
  66. logging.debug("Requirement already fulfilled")
  67. return
  68. file_paths = [
  69. os.path.join(dirpath, filename)
  70. for dirpath, _, filenames in os.walk(args.root_dir_path)
  71. for filename in filenames
  72. ]
  73. file_mtime_paths = [
  74. (os.stat(p).st_mtime, p) for p in file_paths if args.delete_path_regex.search(p)
  75. ]
  76. file_mtime_paths.sort()
  77. removed_files_counter = 0
  78. last_mtime = None
  79. for file_mtime, file_path in file_mtime_paths:
  80. if shutil.disk_usage(args.root_dir_path).free >= args.free_bytes:
  81. break
  82. os.remove(file_path)
  83. logging.debug("Removed file %s", file_path)
  84. removed_files_counter += 1
  85. last_mtime = file_mtime
  86. if removed_files_counter == 0:
  87. logging.warning("No files to remove")
  88. else:
  89. assert last_mtime is not None # for mypy
  90. logging.info(
  91. "Removed %d file(s) with modification date <= %sZ",
  92. removed_files_counter,
  93. datetime.datetime.utcfromtimestamp(last_mtime).isoformat("T"),
  94. )