1
0
mirror of https://github.com/deadc0de6/dotdrop.git synced 2026-02-06 10:12:59 +00:00
Files
dotdrop/dotdrop/uninstaller.py
2024-08-26 18:01:31 +02:00

151 lines
4.7 KiB
Python

"""
author: deadc0de6 (https://github.com/deadc0de6)
Copyright (c) 2023, deadc0de6
handle the un-installation of dotfiles
"""
import os
from dotdrop.logger import Logger
from dotdrop.utils import removepath, dir_empty
class Uninstaller:
"""dotfile uninstaller"""
def __init__(self, base='.', workdir='~/.config/dotdrop',
dry=False, safe=True, debug=False,
backup_suffix='.dotdropbak'):
"""
@base: directory path where to search for templates
@workdir: where to install template before symlinking
@dry: just simulate
@debug: enable debug
@backup_suffix: suffix for dotfile backup file
@safe: ask for any action
"""
base = os.path.expanduser(base)
base = os.path.normpath(base)
self.base = base
workdir = os.path.expanduser(workdir)
workdir = os.path.normpath(workdir)
self.workdir = workdir
self.dry = dry
self.safe = safe
self.debug = debug
self.backup_suffix = backup_suffix
self.log = Logger(debug=self.debug)
def uninstall(self, src, dst, linktype):
"""
uninstall dst
@src: dotfile source path in dotpath
@dst: dotfile destination path in the FS
@linktype: linktypes.LinkTypes
return
- True, None : success
- False, error_msg : error
"""
if not src or not dst:
self.log.dbg(f'cannot uninstall empty {src} or {dst}')
return True, None
# ensure exists
path = os.path.expanduser(dst)
path = os.path.normpath(path)
path = path.rstrip(os.sep)
if not os.path.isfile(path) and not os.path.isdir(path):
msg = f'cannot uninstall special file {path}'
return False, msg
if not os.path.exists(path):
self.log.dbg(f'cannot uninstall non existing {path}')
return True, None
msg = f'uninstalling \"{path}\" (link: {linktype})'
self.log.dbg(msg)
ret, msg = self._remove(path)
if ret:
if not self.dry:
self.log.sub(f'uninstall {dst}')
return ret, msg
def _descend(self, dirpath):
ret = True
self.log.dbg(f'recursively uninstall {dirpath}')
for sub in os.listdir(dirpath):
subpath = os.path.join(dirpath, sub)
if os.path.isdir(subpath):
self.log.dbg(f'under {dirpath} uninstall dir {subpath}')
self._descend(subpath)
else:
self.log.dbg(f'under {dirpath} uninstall file {subpath}')
subret, _ = self._remove(subpath)
if not subret:
ret = False
if dir_empty(dirpath):
# empty
self.log.dbg(f'remove empty dir {dirpath}')
if self.dry:
self.log.dry(f'would \"rm -r {dirpath}\"')
return True, ''
return self._remove_path(dirpath)
self.log.dbg(f'not removing non-empty dir {dirpath}')
return ret, ''
def _remove_path(self, path):
"""remove a file"""
try:
removepath(path)
except OSError as exc:
err = f'removing \"{path}\" failed: {exc}'
return False, err
return True, ''
def _remove(self, path):
"""remove path"""
self.log.dbg(f'handling uninstall of {path}')
if path.endswith(self.backup_suffix):
self.log.dbg(f'skip {path} ignored')
return True, ''
backup = f'{path}{self.backup_suffix}'
if os.path.exists(backup):
self.log.dbg(f'backup exists for {path}: {backup}')
return self._replace(path, backup)
self.log.dbg(f'no backup file for {path}')
if os.path.isdir(path):
self.log.dbg(f'{path} is a directory')
return self._descend(path)
if self.dry:
self.log.dry(f'would \"rm {path}\"')
return True, ''
msg = f'Remove {path}?'
if self.safe and not self.log.ask(msg):
return False, 'user refused'
self.log.dbg(f'removing {path}')
return self._remove_path(path)
def _replace(self, path, backup):
"""replace path by backup"""
if self.dry:
self.log.dry(f'would \"mv {backup} {path}\"')
return True, ''
msg = f'Restore {path} from {backup}?'
if self.safe and not self.log.ask(msg):
return False, 'user refused'
try:
self.log.dbg(f'mv {backup} {path}')
os.replace(backup, path)
except OSError as exc:
err = f'replacing \"{path}\" by \"{backup}\" failed: {exc}'
return False, err
return True, ''