add chown and chmod functionality and cleanup source

This commit is contained in:
2020-11-04 02:06:41 +01:00
parent d7adaf3362
commit e49a43cdf0

View File

@@ -40,6 +40,7 @@ class Task:
self._task_id = task_id self._task_id = task_id
self._job = task self._job = task
self._callback = callback self._callback = callback
self._task = None self._task = None
self._log = logging.getLogger((logname or self.__class__.__name__)) self._log = logging.getLogger((logname or self.__class__.__name__))
@@ -49,7 +50,9 @@ class Task:
if self._callback is not None: if self._callback is not None:
self._callback(self._event) self._callback(self._event)
self._task = None self._task = None
self._log.info(f"execute task {self._task_id}") self._log.info(f"execute task {self._task_id}")
await asyncio.shield(self._job(self._event, self._task_id)) await asyncio.shield(self._job(self._event, self._task_id))
self._log.info(f"task {self._task_id} finished") self._log.info(f"task {self._task_id} finished")
@@ -91,15 +94,19 @@ class TaskScheduler:
assert callable(task), \ assert callable(task), \
f"task: expected callable, got {type(task)}" f"task: expected callable, got {type(task)}"
self._task = task self._task = task
assert isinstance(delay, int), \ assert isinstance(delay, int), \
f"delay: expected {type(int)}, got {type(delay)}" f"delay: expected {type(int)}, got {type(delay)}"
self._delay = delay self._delay = delay
assert isinstance(files, bool), \ assert isinstance(files, bool), \
f"files: expected {type(bool)}, got {type(files)}" f"files: expected {type(bool)}, got {type(files)}"
self._files = files self._files = files
assert isinstance(dirs, bool), \ assert isinstance(dirs, bool), \
f"dirs: expected {type(bool)}, got {type(dirs)}" f"dirs: expected {type(bool)}, got {type(dirs)}"
self._dirs = dirs self._dirs = dirs
self._tasks = {} self._tasks = {}
self._logname = (logname or self.__class__.__name__) self._logname = (logname or self.__class__.__name__)
self._log = logging.getLogger(self._logname) self._log = logging.getLogger(self._logname)
@@ -111,12 +118,14 @@ class TaskScheduler:
def schedule(self, event): def schedule(self, event):
self._log.debug(f"received {event}") self._log.debug(f"received {event}")
if (not event.dir and not self._files) or \ if (not event.dir and not self._files) or \
(event.dir and not self._dirs): (event.dir and not self._dirs):
return return
path = event.pathname path = event.pathname
maskname = event.maskname.split("|", 1)[0] maskname = event.maskname.split("|", 1)[0]
if path in self._tasks: if path in self._tasks:
task = self._tasks[path] task = self._tasks[path]
self._log.info(f"received event {maskname} on '{path}', " self._log.info(f"received event {maskname} on '{path}', "
@@ -136,6 +145,7 @@ class TaskScheduler:
def cancel(self, event): def cancel(self, event):
self._log.debug(f"received {event}") self._log.debug(f"received {event}")
path = event.pathname path = event.pathname
maskname = event.maskname.split("|", 1)[0] maskname = event.maskname.split("|", 1)[0]
if path in self._tasks: if path in self._tasks:
@@ -151,21 +161,22 @@ class ShellScheduler(TaskScheduler):
assert isinstance(cmd, str), \ assert isinstance(cmd, str), \
f"cmd: expected {type('')}, got {type(cmd)}" f"cmd: expected {type('')}, got {type(cmd)}"
self._cmd = cmd self._cmd = cmd
logname = (logname or self.__class__.__name__) logname = (logname or self.__class__.__name__)
super().__init__(*args, task=self.task, logname=logname, **kwargs) super().__init__(*args, task=self.task, logname=logname, **kwargs)
async def task(self, event, task_id): async def task(self, event, task_id):
maskname = event.maskname.split("|", 1)[0] 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"): if hasattr(event, "src_pathname"):
src_pathname = event.src_pathname src_pathname = event.src_pathname
else: else:
src_pathname = "" src_pathname = ""
cmd = cmd.replace( cmd = self._cmd.replace("{maskname}", shell_quote(maskname)).replace(
"{pathname}", shell_quote(event.pathname)).replace(
"{src_pathname}", shell_quote(src_pathname)) "{src_pathname}", shell_quote(src_pathname))
self._log.info(f"{task_id}: execute shell command: {cmd}") self._log.info(f"{task_id}: execute shell command: {cmd}")
proc = await asyncio.create_subprocess_shell(cmd) proc = await asyncio.create_subprocess_shell(cmd)
await proc.communicate() await proc.communicate()
@@ -177,6 +188,7 @@ class EventMap:
def __init__(self, event_map=None, default_task=None): def __init__(self, event_map=None, default_task=None):
self._map = {} self._map = {}
if default_task is not None: if default_task is not None:
assert callable(default_task), \ assert callable(default_task), \
f"default_task: expected callable, got {type(default_task)}" f"default_task: expected callable, got {type(default_task)}"
@@ -195,10 +207,7 @@ class EventMap:
def set(self, flag, values): def set(self, flag, values):
assert flag in EventMap.flags, \ assert flag in EventMap.flags, \
f"event_map: invalid flag: {flag}" f"event_map: invalid flag: {flag}"
if values is None: if values is not None:
if flag in self._map:
del self._map[flag]
else:
if not isinstance(values, list): if not isinstance(values, list):
values = [values] values = [values]
@@ -207,6 +216,8 @@ class EventMap:
f"event_map: {flag}: expected callable, got {type(value)}" f"event_map: {flag}: expected callable, got {type(value)}"
self._map[flag] = values self._map[flag] = values
elif flag in self._map:
del self._map[flag]
class Watch: class Watch:
@@ -214,6 +225,7 @@ class Watch:
assert isinstance(path, str), \ assert isinstance(path, str), \
f"path: expected {type('')}, got {type(path)}" f"path: expected {type('')}, got {type(path)}"
self.path = path self.path = path
if isinstance(event_map, EventMap): if isinstance(event_map, EventMap):
self.event_map = event_map self.event_map = event_map
elif isinstance(event_map, dict): elif isinstance(event_map, dict):
@@ -226,6 +238,7 @@ class Watch:
assert isinstance(rec, bool), \ assert isinstance(rec, bool), \
f"rec: expected {type(bool)}, got {type(rec)}" f"rec: expected {type(bool)}, got {type(rec)}"
self.rec = rec self.rec = rec
assert isinstance(auto_add, bool), \ assert isinstance(auto_add, bool), \
f"auto_add: expected {type(bool)}, got {type(auto_add)}" f"auto_add: expected {type(bool)}, got {type(auto_add)}"
self.auto_add = 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, wm.add_watch(self.path, mask, rec=self.rec, auto_add=self.auto_add,
do_glob=True) do_glob=True)
return pyinotify.AsyncioNotifier(wm, loop, default_proc_fun=handler) return pyinotify.AsyncioNotifier(wm, loop, default_proc_fun=handler)
@@ -249,17 +263,42 @@ class Rule:
valid_actions = ["copy", "move", "delete"] valid_actions = ["copy", "move", "delete"]
def __init__(self, action, src_re, dst_re="", auto_create=False, def __init__(self, action, src_re, dst_re="", auto_create=False,
dirmode=None, filemode=None, user=None, group=None,
rec=False): rec=False):
assert action in self.valid_actions, \ assert action in self.valid_actions, \
f"action: expected [{Rule.valid_actions.join(', ')}], got{action}" f"action: expected [{Rule.valid_actions.join(', ')}], got{action}"
self.action = action self.action = action
self.src_re = re.compile(src_re) self.src_re = re.compile(src_re)
assert isinstance(dst_re, str), \ assert isinstance(dst_re, str), \
f"dst_re: expected {type('')}, got {type(dst_re)}" f"dst_re: expected {type('')}, got {type(dst_re)}"
self.dst_re = dst_re self.dst_re = dst_re
assert isinstance(auto_create, bool), \ assert isinstance(auto_create, bool), \
f"auto_create: expected {type(bool)}, got {type(auto_create)}" f"auto_create: expected {type(bool)}, got {type(auto_create)}"
self.auto_create = 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), \ assert isinstance(rec, bool), \
f"rec: expected {type(bool)}, got {type(rec)}" f"rec: expected {type(bool)}, got {type(rec)}"
self.rec = rec self.rec = rec
@@ -280,6 +319,13 @@ class FileManager:
def add_rule(self, *args, **kwargs): def add_rule(self, *args, **kwargs):
self._rules.append(Rule(*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): async def task(self, event, task_id):
path = event.pathname path = event.pathname
match = None match = None
@@ -288,33 +334,66 @@ class FileManager:
if match: if match:
break break
if match is not None: if not match:
self._log.debug(
f"{task_id}: path '{path}' matches no rule in ruleset")
return
try: try:
if rule.action in ["copy", "move"]: if rule.action in ["copy", "move"]:
dest = rule.src_re.sub(rule.dst_re, path) dst = rule.src_re.sub(rule.dst_re, path)
if not dest: if not dst:
raise RuntimeError( raise RuntimeError(
f"unable to {rule.action} '{path}', " f"unable to {rule.action} '{path}', "
f"resulting destination path is empty") f"resulting destination path is empty")
dest_dir = os.path.dirname(dest)
if not os.path.isdir(dest_dir) and rule.auto_create: dst_dir = os.path.dirname(dst)
if not os.path.isdir(dst_dir) and rule.auto_create:
self._log.info( self._log.info(
f"{task_id}: create directory '{dest_dir}'") f"{task_id}: create directory '{dst_dir}'")
os.makedirs(dest_dir) os.makedirs(dst_dir)
elif os.path.exists(dest): elif os.path.exists(dst):
raise RuntimeError( raise RuntimeError(
f"unable to move file from '{path} to '{dest}', " f"unable to move file from '{path} to '{dst}', "
f"destination path exists already") f"dstination path exists already")
self._log.info( self._log.info(
f"{task_id}: {rule.action} '{path}' to '{dest}'") f"{task_id}: {rule.action} '{path}' to '{dst}'")
if rule.action == "copy": if rule.action == "copy":
if os.path.isdir(path): if os.path.isdir(path):
shutil.copytree(path, dest) shutil.copytree(path, dst)
else: else:
shutil.copy2(path, dest) shutil.copy2(path, dst)
else: else:
os.rename(path, dest) os.rename(path, dst)
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": elif rule.action == "delete":
self._log.info( self._log.info(
@@ -324,17 +403,16 @@ class FileManager:
shutil.rmtree(path) shutil.rmtree(path)
else: else:
shutil.rmdir(path) shutil.rmdir(path)
else: else:
os.remove(path) os.remove(path)
except RuntimeError as e: except RuntimeError as e:
self._log.error(f"{task_id}: {e}") self._log.error(f"{task_id}: {e}")
except Exception as e: except Exception as e:
self._log.exception(f"{task_id}: {e}") self._log.exception(f"{task_id}: {e}")
else:
self._log.warning(f"{task_id}: no rule matches path '{path}'")
class PyinotifydConfig: class PyinotifydConfig:
def __init__(self, watches=[], loglevel=logging.INFO, shutdown_timeout=30): def __init__(self, watches=[], loglevel=logging.INFO, shutdown_timeout=30):
@@ -390,6 +468,7 @@ async def shutdown(signame, notifiers, logname, timeout=30):
def main(): def main():
myname = "pyinotifyd" myname = "pyinotifyd"
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description=myname, description=myname,
formatter_class=lambda prog: argparse.HelpFormatter( formatter_class=lambda prog: argparse.HelpFormatter(
@@ -460,10 +539,9 @@ def main():
notifiers.append(watch.event_notifier(wm, loop)) notifiers.append(watch.event_notifier(wm, loop))
for signame in ["SIGINT", "SIGTERM"]: for signame in ["SIGINT", "SIGTERM"]:
loop.add_signal_handler(getattr(signal, signame), loop.add_signal_handler(
lambda: asyncio.ensure_future( getattr(signal, signame), lambda: asyncio.ensure_future(shutdown(
shutdown(signame, notifiers, myname, signame, notifiers, myname, timeout=cfg.shutdown_timeout)))
timeout=cfg.shutdown_timeout)))
loop.run_forever() loop.run_forever()
loop.close() loop.close()