trash 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  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. DEFAULT_CONFIG = {
  13. 'trashes': [
  14. '$HOME/trash/',
  15. ]
  16. }
  17. def parse_args():
  18. parser = argparse.ArgumentParser(description=__doc__)
  19. parser.add_argument('names', nargs='*')
  20. parser.add_argument('-d', '--dry-run', action='store_true')
  21. parser.add_argument('-l', '--list', action='store_true')
  22. return parser.parse_args()
  23. def lowest_mount(path: Path) -> Path:
  24. """Find lowest level mount point of given path"""
  25. while not path.is_mount() and path.parent != path:
  26. return lowest_mount(path.parent)
  27. return path
  28. def getconfig() -> dict:
  29. config_file = Path('~/.config/trash.json').expanduser()
  30. config = DEFAULT_CONFIG
  31. if config_file.is_file():
  32. with open(config_file) as f:
  33. config.update(json.load(f))
  34. return config
  35. def get_trashes(config):
  36. trashes: list[Path] = []
  37. for trashdir in config['trashes']:
  38. trashdir = Path(os.path.expandvars(trashdir)).expanduser().resolve()
  39. trashes.append(trashdir)
  40. return trashes
  41. def trash(names, config, dry_run=False):
  42. for name in names:
  43. name = Path(os.path.expandvars(name))
  44. if not (name.is_file() or name.is_dir()):
  45. logging.warning(f'{name} is not a file nor directory')
  46. continue
  47. name = name.absolute()
  48. trashed = False
  49. for trashdir in get_trashes(config):
  50. try:
  51. if lowest_mount(trashdir) == lowest_mount(name):
  52. newname = (trashdir / datetime.now().isoformat(
  53. timespec='seconds').replace(':', '_') /
  54. '/'.join(name.parts[1:]))
  55. newname.parent.mkdir(parents=True, exist_ok=True)
  56. logging.debug(f'Will move {name} to {newname}')
  57. if not dry_run:
  58. name.rename(newname)
  59. trashed = True
  60. break
  61. except ValueError:
  62. pass
  63. if not trashed:
  64. logging.warning(f'Unable to trash {name}')
  65. def trashed_sort_key(trashed_path: Path) -> int:
  66. # length of the used ISO format is 19!
  67. date_string = trashed_path.parts[-1][-19:].replace('_', ':')
  68. logging.debug(f'Extracted {date_string} from {trashed_path}')
  69. return datetime.fromisoformat(date_string).timestamp()
  70. def list_trashed(config):
  71. """ Yield trashed entries for trashdirs of given config
  72. """
  73. for trashdir in get_trashes(config):
  74. for entry in sorted(trashdir.iterdir(), key=trashed_sort_key):
  75. yield entry
  76. def main():
  77. args = parse_args()
  78. logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
  79. config = getconfig()
  80. logging.debug(config)
  81. if args.list:
  82. print(*list(list_trashed(config)), sep='\n')
  83. sys.exit()
  84. trash(args.names, config, dry_run=args.dry_run)
  85. if args.dry_run:
  86. logging.info('Dry run. Nothin happened. I guess.')
  87. if __name__ == '__main__':
  88. main()