add chown and chmod functionality and cleanup source
This commit is contained in:
186
pyinotifyd.py
186
pyinotifyd.py
@@ -33,13 +33,14 @@ __version__ = "0.0.1"
|
|||||||
|
|
||||||
class Task:
|
class Task:
|
||||||
def __init__(self, event, delay, task_id, task, callback=None,
|
def __init__(self, event, delay, task_id, task, callback=None,
|
||||||
logname=None):
|
logname=None):
|
||||||
self._event = event
|
self._event = event
|
||||||
self._path = event.pathname
|
self._path = event.pathname
|
||||||
self._delay = delay
|
self._delay = delay
|
||||||
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(
|
||||||
"{src_pathname}", shell_quote(src_pathname))
|
"{pathname}", shell_quote(event.pathname)).replace(
|
||||||
|
"{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,
|
||||||
rec=False):
|
dirmode=None, filemode=None, user=None, group=None,
|
||||||
|
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,52 +334,84 @@ class FileManager:
|
|||||||
if match:
|
if match:
|
||||||
break
|
break
|
||||||
|
|
||||||
if match is not None:
|
if not match:
|
||||||
try:
|
self._log.debug(
|
||||||
if rule.action in ["copy", "move"]:
|
f"{task_id}: path '{path}' matches no rule in ruleset")
|
||||||
dest = rule.src_re.sub(rule.dst_re, path)
|
return
|
||||||
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")
|
|
||||||
|
|
||||||
self._log.info(
|
try:
|
||||||
f"{task_id}: {rule.action} '{path}' to '{dest}'")
|
if rule.action in ["copy", "move"]:
|
||||||
if rule.action == "copy":
|
dst = rule.src_re.sub(rule.dst_re, path)
|
||||||
if os.path.isdir(path):
|
if not dst:
|
||||||
shutil.copytree(path, dest)
|
raise RuntimeError(
|
||||||
else:
|
f"unable to {rule.action} '{path}', "
|
||||||
shutil.copy2(path, dest)
|
f"resulting destination path is empty")
|
||||||
else:
|
|
||||||
os.rename(path, dest)
|
|
||||||
|
|
||||||
elif rule.action == "delete":
|
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}: {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 os.path.isdir(path):
|
||||||
if rule.rec:
|
shutil.copytree(path, dst)
|
||||||
shutil.rmtree(path)
|
|
||||||
else:
|
|
||||||
shutil.rmdir(path)
|
|
||||||
else:
|
else:
|
||||||
os.remove(path)
|
shutil.copy2(path, dst)
|
||||||
|
|
||||||
except RuntimeError as e:
|
else:
|
||||||
self._log.error(f"{task_id}: {e}")
|
os.rename(path, dst)
|
||||||
except Exception as e:
|
|
||||||
self._log.exception(f"{task_id}: {e}")
|
|
||||||
|
|
||||||
else:
|
if (rule.user is rule.group is None):
|
||||||
self._log.warning(f"{task_id}: no rule matches path '{path}'")
|
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:
|
class PyinotifydConfig:
|
||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user