| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149 |
- #! /usr/bin/env python3
- """ Move files and directories to custom trash directories, taking the
- corresponding mount points into account.
- """
- from pathlib import Path
- import argparse
- import logging
- import sys
- import os
- import json
- from datetime import datetime
- from subprocess import check_output
- DEFAULT_CONFIG = {
- 'trashes': [
- '$HOME/trash/',
- ]
- }
- def parse_args():
- parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument('names', nargs='*')
- parser.add_argument('-d', '--dry-run', action='store_true')
- parser.add_argument('-l', '--list', action='store_true')
- return parser.parse_args()
- def lowest_mount(path: Path) -> Path:
- """Find lowest level mount point of given path"""
- while not path.is_mount() and path.parent != path:
- return lowest_mount(path.parent)
- return path
- def getconfig() -> dict:
- config_file = Path('~/.config/trash.json').expanduser()
- config = DEFAULT_CONFIG
- if config_file.is_file():
- with open(config_file) as f:
- config.update(json.load(f))
- return config
- def get_trashes(config):
- trashes: list[Path] = []
- for trashdir in config['trashes']:
- trashdir = Path(os.path.expandvars(trashdir)).expanduser().resolve()
- trashes.append(trashdir)
- return trashes
- def write_meta(original_dir, trash_timestamp_dir, calc_size=False):
- if calc_size:
- size = check_output(['du', '-sh', original_dir]).split()[0].decode('utf8')
- else:
- size = '?'
- with open(trash_timestamp_dir / '.trash_meta', 'w') as f:
- f.writelines([l + '\n' for l in [str(original_dir), size]])
- def read_meta(meta_file: Path) -> tuple:
- with open(meta_file) as f:
- try:
- trashed_dir, size = f.readlines()
- trashed_dir = trashed_dir.strip()
- size = size.strip()
- except ValueError:
- logging.info(f'Meta information at {meta_file} outdated or corrupt')
- trashed_dir = size = None
- return trashed_dir, size
- def trash(names, config, dry_run=False):
- for name in names:
- name = Path(os.path.expandvars(name))
- if not (name.is_file() or name.is_dir()):
- logging.warning(f'{name} is not a file nor directory')
- continue
- name = name.absolute()
- trashed = False
- for trashdir in get_trashes(config):
- try:
- if lowest_mount(trashdir) == lowest_mount(name):
- timestamp_prefix = trashdir / datetime.now().isoformat(
- timespec='seconds').replace(':', '_')
- newname = timestamp_prefix / '/'.join(name.parts[1:])
- newname.parent.mkdir(parents=True, exist_ok=True)
- logging.debug(f'Will move {name} to {newname}')
- if not dry_run:
- write_meta(name, timestamp_prefix)
- name.rename(newname)
- trashed = True
- break
- except ValueError:
- pass
- if not trashed:
- logging.warning(f'Unable to trash {name}')
- def extract_timestamp(trashed_path: Path) -> datetime:
- # length of the used ISO format is 19!
- date_string = trashed_path.parts[-1][-19:].replace('_', ':')
- logging.debug(f'Extracted {date_string} from {trashed_path}')
- return datetime.fromisoformat(date_string)
- def trashed_sort_key(trashed_path: Path) -> int:
- return extract_timestamp(trashed_path).timestamp()
- def list_trashed(config) -> str:
- """ Yield trashed entries for trashdirs of given config
- """
- for trashdir in get_trashes(config):
- for entry in sorted(trashdir.iterdir(), key=trashed_sort_key):
- meta_file = entry / '.trash_meta'
- if meta_file.is_file():
- trashed_dir, size = read_meta(meta_file)
- yield f'{trashed_dir or entry} ({extract_timestamp(entry)}, {size})'
- else:
- yield entry
- def main():
- args = parse_args()
- logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
- config = getconfig()
- logging.debug(config)
- if args.list:
- print(*list(list_trashed(config)), sep='\n')
- sys.exit()
- trash(args.names, config, dry_run=args.dry_run)
- if args.dry_run:
- logging.info('Dry run. Nothin happened. I guess.')
- if __name__ == '__main__':
- main()
|