|
@@ -1,149 +0,0 @@
|
|
|
-#! /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()
|
|
|