trash 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. #! /usr/bin/env python3
  2. """ Move files and directories to custom trash directories, taking the
  3. corresponding mount points into account.
  4. """
  5. from pathlib import Path
  6. import argparse
  7. import logging
  8. import sys
  9. import os
  10. import json
  11. from datetime import datetime
  12. from subprocess import check_output
  13. DEFAULT_CONFIG = {
  14. 'trashes': [
  15. '$HOME/trash/',
  16. ]
  17. }
  18. def parse_args():
  19. parser = argparse.ArgumentParser(description=__doc__)
  20. parser.add_argument('names', nargs='*')
  21. parser.add_argument('-d', '--dry-run', action='store_true')
  22. parser.add_argument('-l', '--list', action='store_true')
  23. return parser.parse_args()
  24. def lowest_mount(path: Path) -> Path:
  25. """Find lowest level mount point of given path"""
  26. while not path.is_mount() and path.parent != path:
  27. return lowest_mount(path.parent)
  28. return path
  29. def getconfig() -> dict:
  30. config_file = Path('~/.config/trash.json').expanduser()
  31. config = DEFAULT_CONFIG
  32. if config_file.is_file():
  33. with open(config_file) as f:
  34. config.update(json.load(f))
  35. return config
  36. def get_trashes(config):
  37. trashes: list[Path] = []
  38. for trashdir in config['trashes']:
  39. trashdir = Path(os.path.expandvars(trashdir)).expanduser().resolve()
  40. trashes.append(trashdir)
  41. return trashes
  42. def write_meta(original_dir, trash_timestamp_dir, calc_size=False):
  43. if calc_size:
  44. size = check_output(['du', '-sh', original_dir]).split()[0].decode('utf8')
  45. else:
  46. size = '?'
  47. with open(trash_timestamp_dir / '.trash_meta', 'w') as f:
  48. f.writelines([l + '\n' for l in [str(original_dir), size]])
  49. def read_meta(meta_file: Path) -> tuple:
  50. with open(meta_file) as f:
  51. try:
  52. trashed_dir, size = f.readlines()
  53. trashed_dir = trashed_dir.strip()
  54. size = size.strip()
  55. except ValueError:
  56. logging.info(f'Meta information at {meta_file} outdated or corrupt')
  57. trashed_dir = size = None
  58. return trashed_dir, size
  59. def trash(names, config, dry_run=False):
  60. for name in names:
  61. name = Path(os.path.expandvars(name))
  62. if not (name.is_file() or name.is_dir()):
  63. logging.warning(f'{name} is not a file nor directory')
  64. continue
  65. name = name.absolute()
  66. trashed = False
  67. for trashdir in get_trashes(config):
  68. try:
  69. if lowest_mount(trashdir) == lowest_mount(name):
  70. timestamp_prefix = trashdir / datetime.now().isoformat(
  71. timespec='seconds').replace(':', '_')
  72. newname = timestamp_prefix / '/'.join(name.parts[1:])
  73. newname.parent.mkdir(parents=True, exist_ok=True)
  74. logging.debug(f'Will move {name} to {newname}')
  75. if not dry_run:
  76. write_meta(name, timestamp_prefix)
  77. name.rename(newname)
  78. trashed = True
  79. break
  80. except ValueError:
  81. pass
  82. if not trashed:
  83. logging.warning(f'Unable to trash {name}')
  84. def extract_timestamp(trashed_path: Path) -> datetime:
  85. # length of the used ISO format is 19!
  86. date_string = trashed_path.parts[-1][-19:].replace('_', ':')
  87. logging.debug(f'Extracted {date_string} from {trashed_path}')
  88. return datetime.fromisoformat(date_string)
  89. def trashed_sort_key(trashed_path: Path) -> int:
  90. return extract_timestamp(trashed_path).timestamp()
  91. def list_trashed(config) -> str:
  92. """ Yield trashed entries for trashdirs of given config
  93. """
  94. for trashdir in get_trashes(config):
  95. for entry in sorted(trashdir.iterdir(), key=trashed_sort_key):
  96. meta_file = entry / '.trash_meta'
  97. if meta_file.is_file():
  98. trashed_dir, size = read_meta(meta_file)
  99. yield f'{trashed_dir or entry} ({extract_timestamp(entry)}, {size})'
  100. else:
  101. yield entry
  102. def main():
  103. args = parse_args()
  104. logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
  105. config = getconfig()
  106. logging.debug(config)
  107. if args.list:
  108. print(*list(list_trashed(config)), sep='\n')
  109. sys.exit()
  110. trash(args.names, config, dry_run=args.dry_run)
  111. if args.dry_run:
  112. logging.info('Dry run. Nothin happened. I guess.')
  113. if __name__ == '__main__':
  114. main()