From e49a43cdf055d2fa1215fcf75c9453987eb3fac2 Mon Sep 17 00:00:00 2001 From: Thomas Oettli Date: Wed, 4 Nov 2020 02:06:41 +0100 Subject: [PATCH] add chown and chmod functionality and cleanup source --- pyinotifyd.py | 186 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 132 insertions(+), 54 deletions(-) diff --git a/pyinotifyd.py b/pyinotifyd.py index 3cee040..ca90c6d 100755 --- a/pyinotifyd.py +++ b/pyinotifyd.py @@ -33,13 +33,14 @@ __version__ = "0.0.1" class Task: def __init__(self, event, delay, task_id, task, callback=None, - logname=None): + logname=None): self._event = event self._path = event.pathname self._delay = delay self._task_id = task_id self._job = task self._callback = callback + self._task = None self._log = logging.getLogger((logname or self.__class__.__name__)) @@ -49,7 +50,9 @@ class Task: if self._callback is not None: self._callback(self._event) + self._task = None + self._log.info(f"execute task {self._task_id}") await asyncio.shield(self._job(self._event, self._task_id)) self._log.info(f"task {self._task_id} finished") @@ -91,15 +94,19 @@ class TaskScheduler: assert callable(task), \ f"task: expected callable, got {type(task)}" self._task = task + assert isinstance(delay, int), \ f"delay: expected {type(int)}, got {type(delay)}" self._delay = delay + assert isinstance(files, bool), \ f"files: expected {type(bool)}, got {type(files)}" self._files = files + assert isinstance(dirs, bool), \ f"dirs: expected {type(bool)}, got {type(dirs)}" self._dirs = dirs + self._tasks = {} self._logname = (logname or self.__class__.__name__) self._log = logging.getLogger(self._logname) @@ -111,12 +118,14 @@ class TaskScheduler: def schedule(self, event): self._log.debug(f"received {event}") + if (not event.dir and not self._files) or \ (event.dir and not self._dirs): return path = event.pathname maskname = event.maskname.split("|", 1)[0] + if path in self._tasks: task = self._tasks[path] self._log.info(f"received event {maskname} on '{path}', " @@ -136,6 +145,7 @@ class TaskScheduler: def cancel(self, event): self._log.debug(f"received {event}") + path = event.pathname maskname = event.maskname.split("|", 1)[0] if path in self._tasks: @@ -151,21 +161,22 @@ class ShellScheduler(TaskScheduler): assert isinstance(cmd, str), \ f"cmd: expected {type('')}, got {type(cmd)}" self._cmd = cmd + logname = (logname or self.__class__.__name__) super().__init__(*args, task=self.task, logname=logname, **kwargs) async def task(self, event, task_id): maskname = event.maskname.split("|", 1)[0] - cmd = self._cmd - cmd = cmd.replace("{maskname}", shell_quote(maskname)) - cmd = cmd.replace("{pathname}", shell_quote(event.pathname)) + if hasattr(event, "src_pathname"): src_pathname = event.src_pathname else: src_pathname = "" - cmd = cmd.replace( - "{src_pathname}", shell_quote(src_pathname)) + cmd = self._cmd.replace("{maskname}", shell_quote(maskname)).replace( + "{pathname}", shell_quote(event.pathname)).replace( + "{src_pathname}", shell_quote(src_pathname)) + self._log.info(f"{task_id}: execute shell command: {cmd}") proc = await asyncio.create_subprocess_shell(cmd) await proc.communicate() @@ -177,6 +188,7 @@ class EventMap: def __init__(self, event_map=None, default_task=None): self._map = {} + if default_task is not None: assert callable(default_task), \ f"default_task: expected callable, got {type(default_task)}" @@ -195,10 +207,7 @@ class EventMap: def set(self, flag, values): assert flag in EventMap.flags, \ f"event_map: invalid flag: {flag}" - if values is None: - if flag in self._map: - del self._map[flag] - else: + if values is not None: if not isinstance(values, list): values = [values] @@ -207,6 +216,8 @@ class EventMap: f"event_map: {flag}: expected callable, got {type(value)}" self._map[flag] = values + elif flag in self._map: + del self._map[flag] class Watch: @@ -214,6 +225,7 @@ class Watch: assert isinstance(path, str), \ f"path: expected {type('')}, got {type(path)}" self.path = path + if isinstance(event_map, EventMap): self.event_map = event_map elif isinstance(event_map, dict): @@ -226,6 +238,7 @@ class Watch: assert isinstance(rec, bool), \ f"rec: expected {type(bool)}, got {type(rec)}" self.rec = rec + assert isinstance(auto_add, bool), \ f"auto_add: expected {type(bool)}, got {type(auto_add)}" self.auto_add = auto_add @@ -242,6 +255,7 @@ class Watch: wm.add_watch(self.path, mask, rec=self.rec, auto_add=self.auto_add, do_glob=True) + return pyinotify.AsyncioNotifier(wm, loop, default_proc_fun=handler) @@ -249,17 +263,42 @@ class Rule: valid_actions = ["copy", "move", "delete"] def __init__(self, action, src_re, dst_re="", auto_create=False, - rec=False): + dirmode=None, filemode=None, user=None, group=None, + rec=False): assert action in self.valid_actions, \ f"action: expected [{Rule.valid_actions.join(', ')}], got{action}" self.action = action + self.src_re = re.compile(src_re) + assert isinstance(dst_re, str), \ f"dst_re: expected {type('')}, got {type(dst_re)}" self.dst_re = dst_re + assert isinstance(auto_create, bool), \ f"auto_create: expected {type(bool)}, got {type(auto_create)}" self.auto_create = auto_create + + if dirmode is not None: + assert isinstance(dirmode, int), \ + f"dirmode: expected {type(int)}, got {type(dirmode)}" + self.dirmode = dirmode + + if filemode is not None: + assert isinstance(filemode, int), \ + f"filemode: expected {type(int)}, got {type(filemode)}" + self.filemode = filemode + + if user is not None: + assert isinstance(user, str), \ + f"user: expected {type('')}, got {type(user)}" + self.user = user + + if group is not None: + assert isinstance(group, str), \ + f"group: expected {type('')}, got {type(group)}" + self.group = group + assert isinstance(rec, bool), \ f"rec: expected {type(bool)}, got {type(rec)}" self.rec = rec @@ -280,6 +319,13 @@ class FileManager: def add_rule(self, *args, **kwargs): self._rules.append(Rule(*args, **kwargs)) + async def set_mode_and_chown(self, path, mode, chown): + if mode is not None: + os.chmod(path, mode) + + if chown is not None: + shutil.chown(path, *chown) + async def task(self, event, task_id): path = event.pathname match = None @@ -288,52 +334,84 @@ class FileManager: if match: break - if match is not None: - try: - if rule.action in ["copy", "move"]: - dest = rule.src_re.sub(rule.dst_re, path) - if not dest: - raise RuntimeError( - f"unable to {rule.action} '{path}', " - f"resulting destination path is empty") - dest_dir = os.path.dirname(dest) - if not os.path.isdir(dest_dir) and rule.auto_create: - self._log.info( - f"{task_id}: create directory '{dest_dir}'") - os.makedirs(dest_dir) - elif os.path.exists(dest): - raise RuntimeError( - f"unable to move file from '{path} to '{dest}', " - f"destination path exists already") + if not match: + self._log.debug( + f"{task_id}: path '{path}' matches no rule in ruleset") + return - self._log.info( - f"{task_id}: {rule.action} '{path}' to '{dest}'") - if rule.action == "copy": - if os.path.isdir(path): - shutil.copytree(path, dest) - else: - shutil.copy2(path, dest) - else: - os.rename(path, dest) + try: + if rule.action in ["copy", "move"]: + dst = rule.src_re.sub(rule.dst_re, path) + if not dst: + raise RuntimeError( + f"unable to {rule.action} '{path}', " + f"resulting destination path is empty") - elif rule.action == "delete": + dst_dir = os.path.dirname(dst) + if not os.path.isdir(dst_dir) and rule.auto_create: self._log.info( - f"{task_id}: {rule.action} '{path}'") + f"{task_id}: create directory '{dst_dir}'") + os.makedirs(dst_dir) + elif os.path.exists(dst): + raise RuntimeError( + f"unable to move file from '{path} to '{dst}', " + f"dstination path exists already") + + self._log.info( + f"{task_id}: {rule.action} '{path}' to '{dst}'") + if rule.action == "copy": if os.path.isdir(path): - if rule.rec: - shutil.rmtree(path) - else: - shutil.rmdir(path) + shutil.copytree(path, dst) else: - os.remove(path) + shutil.copy2(path, dst) - except RuntimeError as e: - self._log.error(f"{task_id}: {e}") - except Exception as e: - self._log.exception(f"{task_id}: {e}") + else: + os.rename(path, dst) - else: - self._log.warning(f"{task_id}: no rule matches path '{path}'") + if (rule.user is rule.group is None): + chown = None + else: + chown = (rule.user, rule.group) + + work_on_dirs = not (rule.dirmode is chown is None) + work_on_files = not (rule.filemode is chown is None) + + if work_on_dirs or work_on_files: + if os.path.isfile(dst): + generator = [(os.path.dirname(dst), + [], + [os.path.basename(dst)])] + else: + generator = os.walk(path) + + for root, dirs, files in generator: + if work_on_dirs: + for path in [os.path.join(root, d) for d in dirs]: + await self.set_mode_and_chown( + path, rule.dirmode, chown) + + if work_on_files: + for path in [os.path.join(root, f) for f in files]: + await self.set_mode_and_chown( + path, rule.filemode, chown) + + elif rule.action == "delete": + self._log.info( + f"{task_id}: {rule.action} '{path}'") + if os.path.isdir(path): + if rule.rec: + shutil.rmtree(path) + else: + shutil.rmdir(path) + + else: + os.remove(path) + + except RuntimeError as e: + self._log.error(f"{task_id}: {e}") + + except Exception as e: + self._log.exception(f"{task_id}: {e}") class PyinotifydConfig: @@ -390,6 +468,7 @@ async def shutdown(signame, notifiers, logname, timeout=30): def main(): myname = "pyinotifyd" + parser = argparse.ArgumentParser( description=myname, formatter_class=lambda prog: argparse.HelpFormatter( @@ -460,10 +539,9 @@ def main(): notifiers.append(watch.event_notifier(wm, loop)) for signame in ["SIGINT", "SIGTERM"]: - loop.add_signal_handler(getattr(signal, signame), - lambda: asyncio.ensure_future( - shutdown(signame, notifiers, myname, - timeout=cfg.shutdown_timeout))) + loop.add_signal_handler( + getattr(signal, signame), lambda: asyncio.ensure_future(shutdown( + signame, notifiers, myname, timeout=cfg.shutdown_timeout))) loop.run_forever() loop.close()