Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
961c64e422
|
||
|
|
c22bd73759
|
||
|
|
0ac52b23d4
|
||
|
|
88ce35930c
|
||
|
|
01ccd1817d
|
||
|
|
b80d71e95e
|
||
|
|
d07316990e
|
||
|
|
327bd919b9
|
||
|
|
a32725504d
|
||
|
|
f0b578bdb8
|
||
|
|
641d467069
|
||
|
|
f71af57288
|
||
|
|
085f8f1134
|
||
|
|
50d0dbef79
|
||
|
|
914ec8cfb9
|
||
|
|
769687bca6
|
||
|
|
f9ee0c92cb
|
||
|
|
1a9d695374
|
||
|
|
9794b89b5a
|
||
|
|
8445ca7cb4
|
||
|
|
75241f60f4
|
||
|
|
99724905be
|
||
|
|
34ac9cd596
|
||
|
|
5b589cc999
|
||
|
|
d1b1fc9a4e
|
||
|
|
13dbfeb8ee
|
||
|
|
d6a91d6a5f
|
||
|
|
fe3ecc0fa6
|
||
|
|
b1dffffb5c
|
||
|
|
6504662cb8
|
||
|
|
e8fe7915c1
|
||
|
|
4726c17366
|
||
|
|
1a8e7ccb92
|
||
|
|
dbe7ba83b6
|
||
|
|
56a8586168
|
||
|
|
977f06436d
|
||
|
|
2441ba2048
|
||
|
|
b830ddd0a5
|
||
|
|
9145ecec8b
|
||
|
|
464bc5f583
|
||
|
|
6e28f4ffe9
|
||
|
|
f15ca33a57
|
||
|
|
c2322277a7
|
||
|
|
53523fb705
|
||
|
|
4815480361
|
||
|
|
c26f5c92eb
|
||
|
|
5ca90a661b
|
||
|
|
1403fc0927
|
||
|
|
e2d3a16125
|
||
|
|
532b2f80ff
|
||
|
|
0c18e6097e
|
||
|
|
2a989cbfcc
|
||
|
|
78ba78b070
|
||
|
|
22b69cbb00
|
||
|
|
1427901ed1
|
||
|
|
50c59dd5e3
|
||
|
|
ccf6faef5b
|
||
|
|
175d52b3de
|
||
|
|
8e36dbc4a5
|
||
|
|
380045d6bf
|
||
|
|
fdaf2cee53
|
||
|
|
5d07e08618
|
||
|
|
6eefa17f5a
|
||
|
|
6bc23bccda
|
||
|
|
5bc521da33
|
||
|
|
b2e3d73dea
|
||
|
|
bccd9e2744
|
||
|
|
0104cdd966
|
||
|
|
e970dce5d0
|
||
|
|
82c6d4a96d
|
||
|
|
b2f0758931
|
||
|
|
7ac750220d
|
||
|
|
0641d78984
|
||
|
|
e1ebd29887
|
+2
-2
@@ -115,5 +115,5 @@ dmypy.json
|
|||||||
# Pyre type checker
|
# Pyre type checker
|
||||||
.pyre/
|
.pyre/
|
||||||
|
|
||||||
# config file
|
# Temporary Vim files
|
||||||
/config.py
|
.*.swp
|
||||||
|
|||||||
@@ -1,3 +1,2 @@
|
|||||||
include LICENSE README.md
|
include LICENSE README.md
|
||||||
recursive-include pyinotifyd/docs *
|
|
||||||
recursive-include pyinotifyd/misc *
|
recursive-include pyinotifyd/misc *
|
||||||
|
|||||||
@@ -1,25 +1,29 @@
|
|||||||
# pyinotifyd
|
# pyinotifyd
|
||||||
A daemon to monitore filesystems events with inotify on Linux and execute tasks (Python methods or Shell commands) with an optional delay. It is also possible to cancel delayed tasks.
|
A daemon for monitoring filesystem events with inotify on Linux and run tasks like filesystem operations (copy, move or delete), a shell command or custom async python methods.
|
||||||
|
|
||||||
## Requirements
|
It is possible to schedule tasks with a delay, which can then be canceled again in case a canceling event occurs. A useful example for this is to run tasks only if a file has not changed within a certain amount of time.
|
||||||
|
|
||||||
|
pyinotifyd offers great flexibility through its dev-op configuration approach, which enables you to do almost anything you want.
|
||||||
|
|
||||||
|
# Requirements
|
||||||
* [pyinotify](https://github.com/seb-m/pyinotify)
|
* [pyinotify](https://github.com/seb-m/pyinotify)
|
||||||
|
|
||||||
## Installation
|
# Installation
|
||||||
```sh
|
```sh
|
||||||
# install pyinotifyd with pip
|
# install pyinotifyd with pip
|
||||||
pip install pyinotifyd
|
pip install pyinotifyd
|
||||||
|
|
||||||
# install systemd service and create config directory
|
# install service files and config
|
||||||
pyinotifyd --install
|
pyinotifyd --install
|
||||||
|
|
||||||
# uninstall systemd service
|
# uninstall service files and unmodified config
|
||||||
pyinotifyd --uninstall
|
pyinotifyd --uninstall
|
||||||
```
|
```
|
||||||
|
|
||||||
### Autostart
|
## Autostart
|
||||||
The following init systems are supported.
|
The following init systems are supported.
|
||||||
|
|
||||||
#### systemd
|
### systemd
|
||||||
```sh
|
```sh
|
||||||
# start the daemon at boot time
|
# start the daemon at boot time
|
||||||
systemctl enable pyinotifyd.service
|
systemctl enable pyinotifyd.service
|
||||||
@@ -28,7 +32,7 @@ systemctl enable pyinotifyd.service
|
|||||||
systemctl start pyinotifyd.service
|
systemctl start pyinotifyd.service
|
||||||
```
|
```
|
||||||
|
|
||||||
#### OpenRC (Gentoo)
|
### OpenRC (Gentoo)
|
||||||
```sh
|
```sh
|
||||||
# start the daemon at boot time
|
# start the daemon at boot time
|
||||||
rc-update add pyinotifyd default
|
rc-update add pyinotifyd default
|
||||||
@@ -37,29 +41,59 @@ rc-update add pyinotifyd default
|
|||||||
rc-service pyinotifyd start
|
rc-service pyinotifyd start
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration
|
# Configuration
|
||||||
The config file **/etc/pyinotifyd/config.py** is written in Python syntax. pyinotifyd reads and executes its content, that means you can add your custom Python code to the config file.
|
The config file **/etc/pyinotifyd/config.py** is written in python syntax. pyinotifyd reads and executes its content, that means you can write your custom async python methods directly into the config file.
|
||||||
|
The basic idea is to instantiate one or multiple schedulers and map specific inotify events to schedulers with the help of event maps. Then, watch the given paths for events and run tasks as defined in the event maps.
|
||||||
|
|
||||||
### Tasks
|
## Schedulers
|
||||||
Tasks are Python methods that are called in case an event occurs. They can be bound directly to an event type in an event map. Although this is the easiest and quickest way, it is usually better to add a task to a scheduler and bind the scheduler to event types.
|
pyinotifyd has different schedulers to schedule tasks with an optional delay. The advantages of using a scheduler are consistent logging and the possibility to cancel delayed tasks. Furthermore, schedulers have the ability to differentiate between files and directories.
|
||||||
|
|
||||||
#### Simple
|
### TaskScheduler
|
||||||
This is a very basic example task that just logs each event and task_id:
|
Schedule a custom python method *job* with an optional *delay* in seconds. Skip scheduling of tasks for files and/or directories according to *files* and *dirs* arguments. If there already is a scheduled task, re-schedule it with *delay*. Use *logname* in log messages. All additional modules, functions and variables that are defined in the config file and are needed within the *job*, need to be passed as dictionary to the TaskManager through *global_vars*. If you want to limit the scheduler to run only one job at a time, set *singlejob* to True.
|
||||||
|
All arguments except for *job* are optional.
|
||||||
```python
|
```python
|
||||||
async def task(event, task_id):
|
# Please note that pyinotifyd uses pythons asyncio for asynchronous task execution.
|
||||||
|
# Do not run anything inside the custom python method that blocks the daemon.
|
||||||
|
#
|
||||||
|
# Bad: time.sleep(10)
|
||||||
|
# Good: await asyncio.sleep(10)
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
|
||||||
|
async def custom_job(event, task_id):
|
||||||
|
await asyncio.sleep(10)
|
||||||
logging.info(f"{task_id}: execute example task: {event}")
|
logging.info(f"{task_id}: execute example task: {event}")
|
||||||
|
|
||||||
|
task_sched = TaskScheduler(
|
||||||
|
job=custom_job,
|
||||||
|
files=True,
|
||||||
|
dirs=False,
|
||||||
|
delay=0,
|
||||||
|
logname="sched",
|
||||||
|
global_vars=globals(),
|
||||||
|
singlejob=False)
|
||||||
```
|
```
|
||||||
|
|
||||||
#### FileManager
|
### ShellScheduler
|
||||||
FileManager moves, copy or deletes files and/or directories following a list of *rules*.
|
Schedule a shell command *cmd*. Replace **{maskname}**, **{pathname}** and **{src_pathname}** in *cmd* with the actual values of occuring events. This scheduler is based on TaskScheduler and has the same optional arguments.
|
||||||
|
|
||||||
A rule holds an *action* (move, copy or delete) and a regular expression *src_re*. The FileManager task will be executed if *src_re* matches the path of an event.
|
|
||||||
If the action is copy or move, the destination path *dst_re* is mandatory and if *action* is delete and *rec* is set to True, non-empty directories will be deleted recursively.
|
|
||||||
With *auto_create* set to True, possibly missing subdirectories in *dst_re* are created automatically. Regex subgroups or named-subgroups may be used in *src_re* and *dst_re*.
|
|
||||||
Set the mode of moved/copied files/directories with *filemode* and *dirmode*. Ownership of moved/copied files/directories is set with *user* and *group*. Mode and ownership is also set to automatically created subdirectories.
|
|
||||||
Log messages with *logname*.
|
|
||||||
```python
|
```python
|
||||||
rule = Rule(
|
# Please note that **{src_pathname}** is only present for IN_MOVED_TO events and only
|
||||||
|
# in the case where the IN_MOVED_FROM events are watched too.
|
||||||
|
# If it is not present, the command line argument will be an empty string.
|
||||||
|
shell_sched = ShellScheduler(
|
||||||
|
cmd="/usr/local/bin/task.sh {maskname} {pathname} {src_pathname}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### FileManagerScheduler
|
||||||
|
Move, copy or delete files and/or directories following the list of *rules*, the first matching rule is executed.
|
||||||
|
This scheduler is based on TaskScheduler and has the same optional arguments.
|
||||||
|
|
||||||
|
A rule holds an *action* (move, copy or delete) and a regular expression *src_re*. The *action* will be executed if *src_re* matches the path of an event. In case where *action* is copy or move, use *dst_re* as destination path. Subgroups and/or named-subgroups may be used in *src_re* and *dst_re*.
|
||||||
|
Automatically create possibly missing sub-directories if *auto_create* is set to True. Set the mode and ownership of moved or copied files/directories and newly created sub-directories to *filemode* and *dirmode*. Override destination files if *override* is set to True.
|
||||||
|
If *action* is delete, delete non-empty directories if *rec* is set to True.
|
||||||
|
```python
|
||||||
|
move_rule = FileManagerRule(
|
||||||
action="move",
|
action="move",
|
||||||
src_re="^/src_path/(?P<path>.*).to_move$",
|
src_re="^/src_path/(?P<path>.*).to_move$",
|
||||||
dst_re="/dst_path/\g<path>",
|
dst_re="/dst_path/\g<path>",
|
||||||
@@ -68,49 +102,43 @@ rule = Rule(
|
|||||||
filemode=None,
|
filemode=None,
|
||||||
dirmode=None,
|
dirmode=None,
|
||||||
user=None,
|
user=None,
|
||||||
group=None)
|
group=None,
|
||||||
|
override=False)
|
||||||
|
|
||||||
fm = FileManager(
|
delete_rule = FileManagerRule(
|
||||||
rules=[rule],
|
action="delete",
|
||||||
logname="filemgr")
|
src_re="^/src_path/(?P<path>.*).to_delete$",
|
||||||
|
rec=False)
|
||||||
|
|
||||||
|
file_sched = FileManagerScheduler(
|
||||||
|
rules=[move_rule, delete_rule])
|
||||||
```
|
```
|
||||||
FileManager provides a task **fm.task**.
|
|
||||||
|
|
||||||
### Schedulers
|
## Event maps
|
||||||
pyinotifyd has different schedulers to schedule tasks with an optional delay. The advantages of using a scheduler are consistent logging and the possibility to cancel delayed tasks. Furthermore, schedulers have the ability to differentiate between files and directories.
|
Map specific events to one or multiple schedulers. Ignore the event if the scheduler is set to None. Use **Cancel** to cancel a scheduled task within a scheduler.
|
||||||
|
This is an example which schedules tasks for newly created files if they are not modified, moved or deleted within the delay time of the scheduler.
|
||||||
#### TaskScheduler
|
|
||||||
TaskScheduler schedules *task* with an optional *delay* in seconds. Use the *files* and *dirs* arguments to schedule tasks only for files and/or directories.
|
|
||||||
Log messages with *logname*. All arguments except for *task* are optional.
|
|
||||||
```python
|
```python
|
||||||
s = TaskScheduler(
|
event_map = {
|
||||||
task=task,
|
"IN_ACCESS": None,
|
||||||
files=True,
|
"IN_ATTRIB": None,
|
||||||
dirs=False,
|
"IN_CLOSE_NOWRITE": None,
|
||||||
delay=0,
|
"IN_CLOSE_WRITE": task_sched,
|
||||||
logname="sched")
|
"IN_CREATE": task_sched,
|
||||||
```
|
"IN_DELETE": Cancel(task_sched),
|
||||||
TaskScheduler provides two tasks which can be bound to an event in an event map.
|
"IN_DELETE_SELF": Cancel(task_sched),
|
||||||
* **s.schedule**
|
"IN_IGNORED": None,
|
||||||
Schedule a task. If there is already a scheduled task, it will be canceled first.
|
"IN_MODIFY": Cancel(task_sched),
|
||||||
* **s.cancel**
|
"IN_MOVE_SELF": None,
|
||||||
Cancel a scheduled task.
|
"IN_MOVED_FROM": Cancel(task_sched),
|
||||||
|
"IN_MOVED_TO": task_sched,
|
||||||
|
"IN_OPEN": None,
|
||||||
|
"IN_Q_OVERFLOW": None,
|
||||||
|
"IN_UNMOUNT": Cancel(task_sched)}
|
||||||
|
|
||||||
#### ShellScheduler
|
# It is possible to instantiate an event map with a default scheduler set for every event
|
||||||
ShellScheduler schedules Shell command *cmd*. The placeholders **{maskname}**, **{pathname}** and **{src_pathname}** are replaced with the actual values of the event. ShellScheduler has the same optional arguments as TaskScheduler and provides the same tasks.
|
event_map = EventMap(default_sched=task_sched)
|
||||||
```python
|
|
||||||
s1 = ShellScheduler(
|
|
||||||
cmd="/usr/local/bin/task.sh {maskname} {pathname} {src_pathname}")
|
|
||||||
```
|
```
|
||||||
### Event maps
|
The following events are available:
|
||||||
EventMap maps event types to tasks. It is possible to set a list of tasks to run multiple tasks on a single event. If the task of an event type is set to None, it is ignored.
|
|
||||||
This is an example:
|
|
||||||
```python
|
|
||||||
event_map = EventMap({
|
|
||||||
"IN_CLOSE_NOWRITE": [s.schedule, s1.schedule],
|
|
||||||
"IN_CLOSE_WRITE": s.schedule})
|
|
||||||
```
|
|
||||||
The following event types are available:
|
|
||||||
* **IN_ACCESS**: a file was accessed
|
* **IN_ACCESS**: a file was accessed
|
||||||
* **IN_ATTRIB**: a metadata changed
|
* **IN_ATTRIB**: a metadata changed
|
||||||
* **IN_CLOSE_NOWRITE**: an unwritable file was closed
|
* **IN_CLOSE_NOWRITE**: an unwritable file was closed
|
||||||
@@ -127,109 +155,108 @@ The following event types are available:
|
|||||||
* **IN_Q_OVERFLOW**: the event queue overflown. This event is not associated with any watch descriptor
|
* **IN_Q_OVERFLOW**: the event queue overflown. This event is not associated with any watch descriptor
|
||||||
* **IN_UNMOUNT**: when backing filesystem was unmounted. Notified to each watch of this filesystem
|
* **IN_UNMOUNT**: when backing filesystem was unmounted. Notified to each watch of this filesystem
|
||||||
|
|
||||||
### Watches
|
## Pyinotifyd
|
||||||
Watch watches *path* for event types in *event_map* and execute the corresponding task(s). If *rec* is True, a watch will be added on each subdirectory in *path*. If *auto_add* is True, a watch will be added automatically on newly created subdirectories in *path*.
|
pyinotifyd requires you to define a variable called **pyinotifyd** within the config file, which contains an instance of the Pyinotifyd class. Set the optional list of *watches* and the *shutdown_timeout*. Pyinotifyd will wait *shutdown_timeout* seconds for pending tasks to complete before shutdown. Use *logname* in log messages.
|
||||||
```python
|
|
||||||
watch = Watch(
|
|
||||||
path="/tmp",
|
|
||||||
event_map=event_map,
|
|
||||||
rec=False,
|
|
||||||
auto_add=False)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Pyinotifyd
|
|
||||||
pyinotifyd expects an instance of Pyinotifyd named **pyinotifyd** defined in the config file. The options are a list of *watches* and the *shutdown_timeout*. pyinotifyd will wait *shutdown_timeout* seconds for pending tasks to complete during shutdown. Log messages with *logname*.
|
|
||||||
```python
|
```python
|
||||||
pyinotifyd = Pyinotifyd(
|
pyinotifyd = Pyinotifyd(
|
||||||
watches=[watch],
|
watches=[],
|
||||||
shutdown_timeout=30,
|
shutdown_timeout=30,
|
||||||
logname="daemon")
|
logname="daemon")
|
||||||
```
|
```
|
||||||
|
|
||||||
### Logging
|
### Watches
|
||||||
|
A watch connects the *path* to an *event_map*. Automatically add a watch on each sub-directories in *path* if *rec* is set to True. If *auto_add* is True, a watch will be added automatically on newly created sub-directories in *path*.
|
||||||
|
```python
|
||||||
|
# Add a watch directly to Pyinotifyd.
|
||||||
|
pyinotifyd.add_watch(
|
||||||
|
path="/src_path",
|
||||||
|
event_map=event_map,
|
||||||
|
rec=False,
|
||||||
|
auto_add=False)
|
||||||
|
|
||||||
|
# Or instantiate and add it
|
||||||
|
w = Watch(
|
||||||
|
path="/src_path",
|
||||||
|
event_map=event_map,
|
||||||
|
rec=False,
|
||||||
|
auto_add=False)
|
||||||
|
|
||||||
|
pyinotifyd.add_watch(watch=w)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Logging
|
||||||
Pythons [logging](https://docs.python.org/3/howto/logging.html) framework is used to log messages (see https://docs.python.org/3/howto/logging.html).
|
Pythons [logging](https://docs.python.org/3/howto/logging.html) framework is used to log messages (see https://docs.python.org/3/howto/logging.html).
|
||||||
|
The following loglevels are available:
|
||||||
|
* DEBUG
|
||||||
|
* INFO
|
||||||
|
* WARNING
|
||||||
|
* ERROR
|
||||||
|
* CRITICAL
|
||||||
|
```python
|
||||||
|
# Configure global loglevel
|
||||||
|
setLoglevel(INFO)
|
||||||
|
|
||||||
Configure the global loglevel. This is the default:
|
# Configure loglevel per logname.
|
||||||
```python
|
setLoglevel(INFO, logname="daemon")
|
||||||
logging.getLogger().setLevel(logging.WARNING)
|
|
||||||
```
|
|
||||||
It is possible to configure the loglevel per log name. This is an example for logname **TaskScheduler**:
|
|
||||||
```python
|
|
||||||
logging.getLogger("TaskScheduler").setLevel(logging.INFO)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Syslog
|
### Syslog
|
||||||
Add this to your config file to send log messages to a local syslog server.
|
Send log messages to the local syslog server.
|
||||||
```python
|
```python
|
||||||
# send log messages to the Unix socket of the syslog server.
|
# Enable logging to local syslog server (/dev/log).
|
||||||
syslog = logging.handlers.SysLogHandler(
|
# Use *address* to specify a different target.
|
||||||
address="/dev/log")
|
enableSyslog(loglevel=INFO, address="/dev/log")
|
||||||
|
|
||||||
# set the log format of syslog messages
|
# Enable syslog per logname
|
||||||
log_format = "pyinotifyd/%(name)s: %(message)s"
|
enableSyslog(lglevel=INFO, name="daemon")
|
||||||
syslog.setFormatter(
|
|
||||||
logging.Formatter(formatter)
|
|
||||||
|
|
||||||
# set the log level for syslog messages
|
|
||||||
syslog.setLevel(logging.INFO)
|
|
||||||
|
|
||||||
# enable syslog
|
|
||||||
logging.getLogger().addHandler(syslog)
|
|
||||||
|
|
||||||
# or enable syslog just for TaskScheduler
|
|
||||||
logging.getLogger("TaskManager").addHandler(syslog)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Examples
|
# Examples
|
||||||
|
|
||||||
### Schedule Python task for all events
|
## Schedule python method for all events on files and directories
|
||||||
```python
|
```python
|
||||||
async def task(event, task_id):
|
import logging
|
||||||
|
|
||||||
|
async def custom_job(event, task_id):
|
||||||
logging.info(f"{task_id}: execute example task: {event}")
|
logging.info(f"{task_id}: execute example task: {event}")
|
||||||
|
|
||||||
s = TaskScheduler(
|
task_sched = TaskScheduler(
|
||||||
task=task,
|
job=custom_job,
|
||||||
files=True,
|
files=True,
|
||||||
dirs=True)
|
dirs=True)
|
||||||
|
|
||||||
event_map = EventMap(
|
event_map = EventMap(
|
||||||
default_task=s.schedule)
|
default_sched=task_sched)
|
||||||
|
|
||||||
watch = Watch(
|
pyinotifyd = Pyinotifyd()
|
||||||
path="/tmp",
|
pyinotifyd.add_watch(
|
||||||
event_map=event_map,
|
path="/src_path",
|
||||||
rec=True,
|
event_map=event_map,
|
||||||
auto_add=True)
|
rec=True,
|
||||||
|
auto_add=True)
|
||||||
pyinotifyd_config = PyinotifydConfig(
|
|
||||||
watches=[watch],
|
|
||||||
shutdown_timeout=5)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Schedule Shell commands for specific events on files
|
## Schedule shell commands for specific events on files
|
||||||
```python
|
```python
|
||||||
s = ShellScheduler(
|
shell_sched = ShellScheduler(
|
||||||
cmd="/usr/local/sbin/task.sh {pathname}",
|
cmd="/usr/local/sbin/task.sh {pathname}",
|
||||||
files=True,
|
files=True,
|
||||||
dirs=False)
|
dirs=False)
|
||||||
|
|
||||||
event_map = EventMap(
|
event_map = {
|
||||||
{"IN_WRITE_CLOSE": s.schedule})
|
"IN_WRITE_CLOSE": shell_sched}
|
||||||
|
|
||||||
watch = Watch(
|
pyinotifyd = Pyinotifyd()
|
||||||
path="/tmp",
|
pyinotifyd.add_watch(
|
||||||
event_map=event_map,
|
path="/src_path",
|
||||||
rec=True,
|
event_map=event_map,
|
||||||
auto_add=True)
|
rec=True,
|
||||||
|
auto_add=True)
|
||||||
pyinotifyd_config = PyinotifydConfig(
|
|
||||||
watches=[watch],
|
|
||||||
shutdown_timeout=5)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Move, copy or delete newly created files after a delay
|
## Move, copy or delete newly created files after a delay
|
||||||
```python
|
```python
|
||||||
move_rule = Rule(
|
move_rule = FileManagerRule(
|
||||||
action="move",
|
action="move",
|
||||||
src_re="^/src_path/(?P<path>.*)\.to_move$",
|
src_re="^/src_path/(?P<path>.*)\.to_move$",
|
||||||
dst_re="/dst_path/\g<path>",
|
dst_re="/dst_path/\g<path>",
|
||||||
@@ -237,7 +264,7 @@ move_rule = Rule(
|
|||||||
filemode=0o644,
|
filemode=0o644,
|
||||||
dirmode=0o755)
|
dirmode=0o755)
|
||||||
|
|
||||||
copy_rule = Rule(
|
copy_rule = FileManagerRule(
|
||||||
action="copy",
|
action="copy",
|
||||||
src_re="^/src_path/(?P<path>.*)\.to_copy$",
|
src_re="^/src_path/(?P<path>.*)\.to_copy$",
|
||||||
dst_re="/dst_path/\g<path>",
|
dst_re="/dst_path/\g<path>",
|
||||||
@@ -245,37 +272,33 @@ copy_rule = Rule(
|
|||||||
filemode=0o644,
|
filemode=0o644,
|
||||||
dirmode=0o755)
|
dirmode=0o755)
|
||||||
|
|
||||||
delete_rule = Rule(
|
delete_rule = FileManagerRule(
|
||||||
action="delete",
|
action="delete",
|
||||||
src_re="^/src_path/(?P<path>.*)\.to_delete$",
|
src_re="^/src_path/(?P<path>.*)\.to_delete$",
|
||||||
rec=False)
|
rec=False)
|
||||||
|
|
||||||
fm = FileManager(
|
file_sched = FileManagerScheduler(
|
||||||
rules=[move_rule, copy_rule, delete_rule])
|
rules=[move_rule, copy_rule, delete_rule],
|
||||||
|
delay=60,
|
||||||
s = TaskScheduler(
|
|
||||||
task=fm.task,
|
|
||||||
delay=30,
|
|
||||||
files=True,
|
files=True,
|
||||||
dirs=False)
|
dirs=False)
|
||||||
|
|
||||||
event_map = EventMap({
|
event_map = {
|
||||||
"IN_CLOSE_WRITE": s.schedule,
|
"IN_CLOSE_WRITE": file_sched,
|
||||||
"IN_DELETE": s.cancel,
|
"IN_CREATE": file_sched,
|
||||||
"IN_DELETE_SELF": s.cancel,
|
"IN_DELETE": Cancel(file_sched),
|
||||||
"IN_MODIFY": s.cancel,
|
"IN_DELETE_SELF": Cancel(file_sched),
|
||||||
"IN_MOVED_TO": s.schedule,
|
"IN_MODIFY": Cancel(file_sched),
|
||||||
"IN_UNMOUNT": s.cancel})
|
"IN_MOVED_FROM": Cancel(file_sched),
|
||||||
|
"IN_MOVED_TO": file_sched,
|
||||||
|
"IN_UNMOUNT": Cancel(file_sched)}
|
||||||
|
|
||||||
watch = Watch(
|
# Please note that the shutdown timeout should be greater than the greatest scheduler delay,
|
||||||
path="/src_path",
|
|
||||||
event_map=event_map,
|
|
||||||
rec=True,
|
|
||||||
auto_add=True)
|
|
||||||
|
|
||||||
# note that shutdown_timeout should be greater than the greatest scheduler delay,
|
|
||||||
# otherwise pending tasks may get cancelled during shutdown.
|
# otherwise pending tasks may get cancelled during shutdown.
|
||||||
pyinotifyd_config = PyinotifydConfig(
|
pyinotifyd = Pyinotifyd(shutdown_timeout=35)
|
||||||
watches=[watch],
|
pyinotifyd.add_watch(
|
||||||
shutdown_timeout=35)
|
path="/src_path",
|
||||||
|
event_map=event_map,
|
||||||
|
rec=True,
|
||||||
|
auto_add=True)
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# Copyright 2020 Gentoo Authors
|
||||||
|
# Distributed under the terms of the GNU General Public License v2
|
||||||
|
|
||||||
|
EAPI=7
|
||||||
|
PYTHON_COMPAT=( python3_{8..10} )
|
||||||
|
DISTUTILS_USE_SETUPTOOLS=rdepend
|
||||||
|
|
||||||
|
SCM=""
|
||||||
|
if [ "${PV#9999}" != "${PV}" ] ; then
|
||||||
|
SCM="git-r3"
|
||||||
|
EGIT_REPO_URI="https://github.com/spacefreak86/${PN}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
inherit ${SCM} distutils-r1 systemd
|
||||||
|
|
||||||
|
DESCRIPTION="Monitore filesystems events and execute Python methods or Shell commands."
|
||||||
|
HOMEPAGE="https://github.com/spacefreak86/pymodmilter"
|
||||||
|
if [ "${PV#9999}" != "${PV}" ] ; then
|
||||||
|
SRC_URI=""
|
||||||
|
KEYWORDS=""
|
||||||
|
# Needed for tests
|
||||||
|
S="${WORKDIR}/${PN}"
|
||||||
|
EGIT_CHECKOUT_DIR="${S}"
|
||||||
|
else
|
||||||
|
SRC_URI="https://github.com/spacefreak86/${PN}/archive/${PV}.tar.gz -> ${P}.tar.gz"
|
||||||
|
KEYWORDS="amd64 x86"
|
||||||
|
fi
|
||||||
|
|
||||||
|
LICENSE="GPL-3"
|
||||||
|
SLOT="0"
|
||||||
|
|
||||||
|
IUSE="systemd"
|
||||||
|
|
||||||
|
RDEPEND="dev-python/pyinotify[${PYTHON_USEDEP}]"
|
||||||
|
|
||||||
|
python_install_all() {
|
||||||
|
distutils-r1_python_install_all
|
||||||
|
|
||||||
|
dodir /etc/${PN}
|
||||||
|
insinto /etc/${PN}
|
||||||
|
newins ${PN}/misc/config.py.default config.py
|
||||||
|
|
||||||
|
use systemd && systemd_dounit ${PN}/misc/${PN}.service
|
||||||
|
|
||||||
|
newinitd ${PN}/misc/openrc/${PN}.initd ${PN}
|
||||||
|
newconfd ${PN}/misc/openrc/${PN}.confd ${PN}
|
||||||
|
}
|
||||||
Executable
+11
@@ -0,0 +1,11 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
set -x
|
||||||
|
PYTHON=$(which python)
|
||||||
|
|
||||||
|
script_dir=$(dirname "$(readlink -f -- "$BASH_SOURCE")")
|
||||||
|
pkg_dir=$(realpath "${script_dir}"/../..)
|
||||||
|
|
||||||
|
cd "${pkg_dir}"
|
||||||
|
${PYTHON} setup.py clean
|
||||||
|
${PYTHON} setup.py sdist bdist_wheel
|
||||||
Executable
+17
@@ -0,0 +1,17 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
TWINE=$(which twine)
|
||||||
|
|
||||||
|
script_dir=$(dirname "$(readlink -f -- "$BASH_SOURCE")")
|
||||||
|
pkg_dir=$(realpath "${script_dir}/../..")
|
||||||
|
|
||||||
|
cd "${pkg_dir}/dist"
|
||||||
|
ls -la
|
||||||
|
msg="Select version to distribute (cancel with CTRL+C):"
|
||||||
|
echo "${msg}"
|
||||||
|
select version in $(find . -maxdepth 1 -type f -name "pyinotifyd-*.*.*.tar.gz" -printf "%f\n" | sed "s#\.tar\.gz##g"); do
|
||||||
|
[ -n "${version}" ] && break
|
||||||
|
echo -e "\ninvalid choice\n\n${msg}"
|
||||||
|
done
|
||||||
|
${TWINE} upload "${version}"{.tar.gz,-*.whl}
|
||||||
+306
-64
@@ -14,6 +14,15 @@
|
|||||||
# along with pyinotifyd. If not, see <http://www.gnu.org/licenses/>.
|
# along with pyinotifyd. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"setLoglevel",
|
||||||
|
"enableSyslog",
|
||||||
|
"EventMap",
|
||||||
|
"Watch",
|
||||||
|
"Pyinotifyd",
|
||||||
|
"DaemonInstance",
|
||||||
|
"scheduler"]
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
@@ -22,21 +31,189 @@ import pyinotify
|
|||||||
import signal
|
import signal
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from pyinotifyd.watch import Watch, EventMap
|
from pyinotify import ProcessEvent
|
||||||
from pyinotifyd._install import install, uninstall
|
|
||||||
|
|
||||||
__version__ = "0.0.1"
|
from pyinotifyd._install import install, uninstall
|
||||||
|
from pyinotifyd.scheduler import TaskScheduler, Cancel
|
||||||
|
|
||||||
|
__version__ = "0.0.7"
|
||||||
|
|
||||||
|
|
||||||
|
def setLoglevel(loglevel, logname=None):
|
||||||
|
logger = logging.getLogger(logname)
|
||||||
|
logger.setLevel(loglevel)
|
||||||
|
|
||||||
|
|
||||||
|
def enableSyslog(loglevel=None, address="/dev/log", logname=None):
|
||||||
|
logger = logging.getLogger(logname)
|
||||||
|
syslog = logging.handlers.SysLogHandler(address=address)
|
||||||
|
syslog.setFormatter(
|
||||||
|
logging.Formatter(f"{Pyinotifyd.name}/%(name)s: %(message)s"))
|
||||||
|
if loglevel:
|
||||||
|
syslog.setLevel(loglevel)
|
||||||
|
|
||||||
|
logger.addHandler(syslog)
|
||||||
|
|
||||||
|
|
||||||
|
class _SchedulerList:
|
||||||
|
def __init__(self, schedulers=[], loop=None):
|
||||||
|
if not isinstance(schedulers, list):
|
||||||
|
schedulers = [schedulers]
|
||||||
|
|
||||||
|
self._schedulers = schedulers
|
||||||
|
self._loop = (loop or asyncio.get_event_loop())
|
||||||
|
|
||||||
|
def process_event(self, event):
|
||||||
|
for scheduler in self._schedulers:
|
||||||
|
self._loop.create_task(scheduler.process_event(event))
|
||||||
|
|
||||||
|
def schedulers(self):
|
||||||
|
return self._schedulers
|
||||||
|
|
||||||
|
|
||||||
|
class EventMap(ProcessEvent):
|
||||||
|
flags = {
|
||||||
|
**pyinotify.EventsCodes.OP_FLAGS,
|
||||||
|
**pyinotify.EventsCodes.EVENT_FLAGS}
|
||||||
|
|
||||||
|
def my_init(self, event_map=None, default_sched=None, loop=None,
|
||||||
|
logname="eventmap"):
|
||||||
|
self._map = {}
|
||||||
|
self._loop = (loop or asyncio.get_event_loop())
|
||||||
|
|
||||||
|
if default_sched is not None:
|
||||||
|
for flag in EventMap.flags:
|
||||||
|
self.set(flag, default_sched)
|
||||||
|
|
||||||
|
if event_map is not None:
|
||||||
|
assert isinstance(event_map, dict), \
|
||||||
|
f"event_map: expected {type(dict)}, got {type(event_map)}"
|
||||||
|
for flag, schedulers in event_map.items():
|
||||||
|
self.set_scheduler(flag, schedulers)
|
||||||
|
|
||||||
|
self._log = logging.getLogger((logname or __name__))
|
||||||
|
|
||||||
|
def set_scheduler(self, flag, schedulers):
|
||||||
|
assert flag in EventMap.flags, \
|
||||||
|
f"event_map: invalid flag: {flag}"
|
||||||
|
if schedulers is not None:
|
||||||
|
if not isinstance(schedulers, list):
|
||||||
|
schedulers = [schedulers]
|
||||||
|
|
||||||
|
instances = []
|
||||||
|
for scheduler in schedulers:
|
||||||
|
if issubclass(type(scheduler), TaskScheduler) or \
|
||||||
|
isinstance(scheduler, Cancel):
|
||||||
|
instances.append(scheduler)
|
||||||
|
else:
|
||||||
|
instances.append(
|
||||||
|
TaskScheduler(scheduler, loop=self._loop))
|
||||||
|
|
||||||
|
self._map[flag] = _SchedulerList(instances, loop=self._loop)
|
||||||
|
|
||||||
|
elif flag in self._map:
|
||||||
|
del self._map[flag]
|
||||||
|
|
||||||
|
def process_default(self, event):
|
||||||
|
msg = "received event"
|
||||||
|
for attr in [
|
||||||
|
"dir", "mask", "maskname", "pathname", "src_pathname", "wd"]:
|
||||||
|
value = getattr(event, attr, None)
|
||||||
|
if attr == "mask":
|
||||||
|
value = hex(value)
|
||||||
|
if value:
|
||||||
|
msg += f", {attr}={value}"
|
||||||
|
|
||||||
|
self._log.debug(msg)
|
||||||
|
maskname = event.maskname.split("|")[0]
|
||||||
|
if maskname in self._map:
|
||||||
|
self._map[maskname].process_event(event)
|
||||||
|
|
||||||
|
def schedulers(self):
|
||||||
|
schedulers = []
|
||||||
|
for scheduler_list in self._map.values():
|
||||||
|
schedulers.extend(
|
||||||
|
scheduler_list.schedulers())
|
||||||
|
|
||||||
|
return list(set(schedulers))
|
||||||
|
|
||||||
|
|
||||||
|
class Watch:
|
||||||
|
def __init__(self, path, event_map=None, default_sched=None, rec=False,
|
||||||
|
auto_add=False, logname="watch", loop=None):
|
||||||
|
assert isinstance(path, str), \
|
||||||
|
f"path: expected {type('')}, got {type(path)}"
|
||||||
|
|
||||||
|
if isinstance(event_map, EventMap):
|
||||||
|
self._event_map = event_map
|
||||||
|
else:
|
||||||
|
self._event_map = EventMap(
|
||||||
|
event_map=event_map, default_sched=default_sched)
|
||||||
|
|
||||||
|
assert isinstance(rec, bool), \
|
||||||
|
f"rec: expected {type(bool)}, got {type(rec)}"
|
||||||
|
assert isinstance(auto_add, bool), \
|
||||||
|
f"auto_add: expected {type(bool)}, got {type(auto_add)}"
|
||||||
|
logname = (logname or __name__)
|
||||||
|
self._loop = loop
|
||||||
|
|
||||||
|
self._path = path
|
||||||
|
self._rec = rec
|
||||||
|
self._auto_add = auto_add
|
||||||
|
|
||||||
|
self._watch_manager = pyinotify.WatchManager()
|
||||||
|
self._notifier = None
|
||||||
|
self._log = logging.getLogger(logname)
|
||||||
|
|
||||||
|
def path(self):
|
||||||
|
return self._path
|
||||||
|
|
||||||
|
def event_map(self):
|
||||||
|
return self._event_map
|
||||||
|
|
||||||
|
def start(self, loop=None):
|
||||||
|
loop = (loop or self._loop)
|
||||||
|
self._watch_manager.add_watch(self._path, pyinotify.ALL_EVENTS,
|
||||||
|
rec=self._rec, auto_add=self._auto_add,
|
||||||
|
do_glob=True)
|
||||||
|
|
||||||
|
self._notifier = pyinotify.AsyncioNotifier(
|
||||||
|
self._watch_manager, loop, default_proc_fun=self._event_map)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self._notifier.stop()
|
||||||
|
|
||||||
|
self._notifier = None
|
||||||
|
|
||||||
|
|
||||||
class Pyinotifyd:
|
class Pyinotifyd:
|
||||||
def __init__(self, watches=[], shutdown_timeout=30, logname="daemon"):
|
name = "pyinotifyd"
|
||||||
|
|
||||||
|
def __init__(self, watches=[], shutdown_timeout=30, logname="daemon",
|
||||||
|
loop=None):
|
||||||
self.set_watches(watches)
|
self.set_watches(watches)
|
||||||
self.set_shutdown_timeout(shutdown_timeout)
|
self.set_shutdown_timeout(shutdown_timeout)
|
||||||
logname = (logname or __name__)
|
logname = (logname or __name__)
|
||||||
|
self._loop = (loop or asyncio.get_event_loop())
|
||||||
|
|
||||||
self._log = logging.getLogger(logname)
|
self._log = logging.getLogger(logname)
|
||||||
self._loop = asyncio.get_event_loop()
|
|
||||||
self._notifiers = []
|
@staticmethod
|
||||||
self._wm = pyinotify.WatchManager()
|
def from_cfg_file(config_file):
|
||||||
|
config = {}
|
||||||
|
name = Pyinotifyd.name
|
||||||
|
exec("from logging import DEBUG, INFO, WARNING, ERROR, CRITICAL",
|
||||||
|
config)
|
||||||
|
exec(f"from {name} import Pyinotifyd, Watch", config)
|
||||||
|
exec(f"from {name} import setLoglevel, enableSyslog", config)
|
||||||
|
exec(f"from {name}.scheduler import *", config)
|
||||||
|
with open(config_file, "r") as fh:
|
||||||
|
exec(fh.read(), config)
|
||||||
|
instance = config[f"{name}"]
|
||||||
|
assert isinstance(instance, Pyinotifyd), \
|
||||||
|
f"{name}: expected {type(Pyinotifyd)}, " \
|
||||||
|
f"got {type(instance)}"
|
||||||
|
return instance
|
||||||
|
|
||||||
def set_watches(self, watches):
|
def set_watches(self, watches):
|
||||||
if not isinstance(watches, list):
|
if not isinstance(watches, list):
|
||||||
@@ -46,10 +223,16 @@ class Pyinotifyd:
|
|||||||
assert isinstance(watch, Watch), \
|
assert isinstance(watch, Watch), \
|
||||||
f"watches: expected {type(Watch)}, got {type(watch)}"
|
f"watches: expected {type(Watch)}, got {type(watch)}"
|
||||||
|
|
||||||
self._watches = watches
|
self._watches = []
|
||||||
|
self._watches.extend(watches)
|
||||||
|
|
||||||
def add_watch(self, *args, **kwargs):
|
def add_watch(self, *args, watch=None, **kwargs):
|
||||||
self._watches.append(Watch(*args, **kwargs))
|
if watch:
|
||||||
|
assert isinstance(watch, Watch), \
|
||||||
|
f"watch: expected {type(Watch)}, got {type(watch)}"
|
||||||
|
self._watches.append(watch)
|
||||||
|
else:
|
||||||
|
self._watches.append(Watch(*args, **kwargs))
|
||||||
|
|
||||||
def set_shutdown_timeout(self, timeout):
|
def set_shutdown_timeout(self, timeout):
|
||||||
assert isinstance(timeout, int), \
|
assert isinstance(timeout, int), \
|
||||||
@@ -57,58 +240,116 @@ class Pyinotifyd:
|
|||||||
f"got {type(timeout)}"
|
f"got {type(timeout)}"
|
||||||
self._shutdown_timeout = timeout
|
self._shutdown_timeout = timeout
|
||||||
|
|
||||||
|
def schedulers(self):
|
||||||
|
schedulers = []
|
||||||
|
for w in self._watches:
|
||||||
|
schedulers.extend(w.event_map().schedulers())
|
||||||
|
return list(set(schedulers))
|
||||||
|
|
||||||
def start(self, loop=None):
|
def start(self, loop=None):
|
||||||
if not loop:
|
loop = (loop or self._loop)
|
||||||
loop = self._loop
|
|
||||||
|
|
||||||
self._log.info("starting")
|
|
||||||
if len(self._watches) == 0:
|
if len(self._watches) == 0:
|
||||||
self._log.warning("no watches configured, the daemon will not do anything")
|
self._log.warning(
|
||||||
|
"no watches configured, the daemon will not do anything")
|
||||||
|
|
||||||
for watch in self._watches:
|
for watch in self._watches:
|
||||||
self._log.info(f"start watching '{watch.path}' for inotify events")
|
self._log.info(
|
||||||
self._notifiers.append(watch.event_notifier(self._wm, loop))
|
f"start listening for inotify events on '{watch.path()}'")
|
||||||
|
watch.start(loop)
|
||||||
|
|
||||||
def stop(self):
|
def pause(self):
|
||||||
self._log.info("stop watching for inotify events")
|
for scheduler in self.schedulers():
|
||||||
for notifier in self._notifiers:
|
scheduler.pause()
|
||||||
notifier.stop()
|
|
||||||
|
|
||||||
self._notifiers = []
|
async def shutdown(self):
|
||||||
return self._shutdown_timeout
|
schedulers = self.schedulers()
|
||||||
|
|
||||||
|
tasks = [s.shutdown(self._shutdown_timeout) for s in set(schedulers)]
|
||||||
|
if tasks:
|
||||||
|
await asyncio.gather(*tasks)
|
||||||
|
|
||||||
|
for watch in self._watches:
|
||||||
|
self._log.debug(
|
||||||
|
f"stop listening for inotify events on '{watch.path()}'")
|
||||||
|
watch.stop()
|
||||||
|
|
||||||
|
|
||||||
async def _shutdown(signame, daemon, log):
|
class DaemonInstance:
|
||||||
log.info(f"got signal {signame}, graceful shutdown")
|
def __init__(self, instance, logname="daemon"):
|
||||||
timeout = daemon.stop()
|
self._instance = instance
|
||||||
pending = [t for t in asyncio.all_tasks()
|
self._shutdown = False
|
||||||
if t is not asyncio.current_task()]
|
self._log = logging.getLogger(logname)
|
||||||
if len(pending) > 0:
|
|
||||||
log.info(
|
def start(self):
|
||||||
f"waiting {timeout}s for remaining tasks to complete")
|
self._instance.start()
|
||||||
|
|
||||||
|
async def shutdown(self, signame):
|
||||||
|
if self._shutdown:
|
||||||
|
self._log.warning(
|
||||||
|
f"got signal {signame}, but shutdown already in progress")
|
||||||
|
return
|
||||||
|
|
||||||
|
self._log.info(f"got signal {signame}, shutdown")
|
||||||
|
self._shutdown = True
|
||||||
|
|
||||||
try:
|
try:
|
||||||
future = asyncio.gather(*pending)
|
await self._instance.shutdown()
|
||||||
await asyncio.wait_for(future, timeout)
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
log.warning("forcefully terminate remaining tasks")
|
|
||||||
future.cancel()
|
|
||||||
future.exception()
|
|
||||||
|
|
||||||
log.info("shutdown complete")
|
pending = [t for t in asyncio.all_tasks()
|
||||||
asyncio.get_event_loop().stop()
|
if t is not asyncio.current_task()]
|
||||||
|
|
||||||
|
for task in pending:
|
||||||
|
task.cancel()
|
||||||
|
|
||||||
|
try:
|
||||||
|
await asyncio.gather(*pending)
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
self._log.exception(f"error during shutdown: {e}")
|
||||||
|
|
||||||
|
asyncio.get_event_loop().stop()
|
||||||
|
self._shutdown = False
|
||||||
|
self._log.info("shutdown complete")
|
||||||
|
|
||||||
|
async def reload(self, signame, config_file, debug=False):
|
||||||
|
if self._shutdown:
|
||||||
|
self._log.info(
|
||||||
|
f"got signal {signame}, but shutdown already in progress")
|
||||||
|
return
|
||||||
|
|
||||||
|
self._log.info(f"got signal {signame}, reload config file")
|
||||||
|
try:
|
||||||
|
instance = Pyinotifyd.from_cfg_file(config_file)
|
||||||
|
except Exception as e:
|
||||||
|
logging.exception(
|
||||||
|
f"unable to reload config file '{config_file}': {e}")
|
||||||
|
else:
|
||||||
|
if debug:
|
||||||
|
logging.getLogger().setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
old_instance = self._instance
|
||||||
|
|
||||||
|
old_instance.pause()
|
||||||
|
instance.start()
|
||||||
|
asyncio.create_task(old_instance.shutdown())
|
||||||
|
|
||||||
|
self._instance = instance
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
myname = "pyinotifyd"
|
name = Pyinotifyd.name
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description=myname,
|
description=name,
|
||||||
formatter_class=lambda prog: argparse.HelpFormatter(
|
formatter_class=lambda prog: argparse.HelpFormatter(
|
||||||
prog, max_help_position=45, width=140))
|
prog, max_help_position=45, width=140))
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-c",
|
"-c",
|
||||||
"--config",
|
"--config",
|
||||||
help=f"path to config file (default: /etc/{myname}/config.py)",
|
help=f"path to config file (default: /etc/{name}/config.py)",
|
||||||
default=f"/etc/{myname}/config.py")
|
default=f"/etc/{name}/config.py")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-d",
|
"-d",
|
||||||
"--debug",
|
"--debug",
|
||||||
@@ -129,12 +370,12 @@ def main():
|
|||||||
exclusive.add_argument(
|
exclusive.add_argument(
|
||||||
"-i",
|
"-i",
|
||||||
"--install",
|
"--install",
|
||||||
help="install systemd service file",
|
help="install service files and config",
|
||||||
action="store_true")
|
action="store_true")
|
||||||
exclusive.add_argument(
|
exclusive.add_argument(
|
||||||
"-u",
|
"-u",
|
||||||
"--uninstall",
|
"--uninstall",
|
||||||
help="uninstall systemd service file",
|
help="uninstall service files and unmodified config",
|
||||||
action="store_true")
|
action="store_true")
|
||||||
exclusive.add_argument(
|
exclusive.add_argument(
|
||||||
"-t",
|
"-t",
|
||||||
@@ -145,7 +386,7 @@ def main():
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if args.version:
|
if args.version:
|
||||||
print(f"{myname} ({version})")
|
print(f"{name} ({__version__})")
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
if args.list:
|
if args.list:
|
||||||
@@ -167,23 +408,20 @@ def main():
|
|||||||
root_logger.addHandler(ch)
|
root_logger.addHandler(ch)
|
||||||
|
|
||||||
if args.install:
|
if args.install:
|
||||||
sys.exit(install(myname))
|
sys.exit(install(name))
|
||||||
|
|
||||||
if args.uninstall:
|
if args.uninstall:
|
||||||
sys.exit(uninstall(myname))
|
sys.exit(uninstall(name))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
config = {}
|
pyinotifyd = Pyinotifyd.from_cfg_file(args.config)
|
||||||
exec(f"from {myname}.scheduler import *", config)
|
daemon = DaemonInstance(pyinotifyd)
|
||||||
exec(f"from {myname}.filemanager import *", config)
|
|
||||||
with open(args.config, "r") as c:
|
|
||||||
exec(c.read(), globals(), config)
|
|
||||||
daemon = config[f"{myname}"]
|
|
||||||
assert isinstance(daemon, Pyinotifyd), \
|
|
||||||
f"{myname}: expected {type(Pyinotifyd)}, " \
|
|
||||||
f"got {type(daemon)}"
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.exception(f"config file '{args.config}': {e}")
|
if args.debug:
|
||||||
|
logging.exception(f"config file: {e}")
|
||||||
|
else:
|
||||||
|
logging.error(f"config file: {e}")
|
||||||
|
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
if args.configtest:
|
if args.configtest:
|
||||||
@@ -192,17 +430,21 @@ def main():
|
|||||||
|
|
||||||
if args.debug:
|
if args.debug:
|
||||||
root_logger.setLevel(loglevel)
|
root_logger.setLevel(loglevel)
|
||||||
|
|
||||||
formatter = logging.Formatter(
|
formatter = logging.Formatter(
|
||||||
f"%(asctime)s - {myname}/%(name)s - %(levelname)s - %(message)s")
|
f"%(asctime)s - {name}/%(name)s - %(levelname)s - %(message)s")
|
||||||
ch.setFormatter(formatter)
|
ch.setFormatter(formatter)
|
||||||
|
|
||||||
log = logging.getLogger(myname)
|
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
for signame in ["SIGINT", "SIGTERM"]:
|
loop.add_signal_handler(
|
||||||
loop.add_signal_handler(
|
signal.SIGTERM, lambda: loop.create_task(
|
||||||
getattr(signal, signame),
|
daemon.shutdown("SIGTERM")))
|
||||||
lambda: asyncio.ensure_future(
|
loop.add_signal_handler(
|
||||||
_shutdown(signame, daemon, log)))
|
signal.SIGINT, lambda: loop.create_task(
|
||||||
|
daemon.shutdown("SIGINT")))
|
||||||
|
loop.add_signal_handler(
|
||||||
|
signal.SIGHUP, lambda: loop.create_task(
|
||||||
|
daemon.reload("SIGHUP", args.config, args.debug)))
|
||||||
|
|
||||||
daemon.start()
|
daemon.start()
|
||||||
loop.run_forever()
|
loop.run_forever()
|
||||||
|
|||||||
+103
-62
@@ -14,15 +14,101 @@
|
|||||||
# along with pyinotifyd. If not, see <http://www.gnu.org/licenses/>.
|
# along with pyinotifyd. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
|
||||||
|
import filecmp
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
SYSTEMD_PATH = "/lib/systemd/system"
|
|
||||||
|
SYSTEMD_PATHS = ["/lib/systemd/system", "/usr/lib/systemd/system"]
|
||||||
|
|
||||||
OPENRC = "/sbin/openrc"
|
OPENRC = "/sbin/openrc"
|
||||||
|
|
||||||
|
|
||||||
|
def _systemd_files(pkg_dir, name):
|
||||||
|
for path in SYSTEMD_PATHS:
|
||||||
|
if os.path.isdir(path):
|
||||||
|
break
|
||||||
|
|
||||||
|
return [
|
||||||
|
(f"{pkg_dir}/misc/systemd/{name}.service",
|
||||||
|
f"{path}/{name}.service", True)]
|
||||||
|
|
||||||
|
|
||||||
|
def _openrc_files(pkg_dir, name):
|
||||||
|
return [
|
||||||
|
(f"{pkg_dir}/misc/openrc/{name}.initd", f"/etc/init.d/{name}", True),
|
||||||
|
(f"{pkg_dir}/misc/openrc/{name}.confd", f"/etc/conf.d/{name}", False)]
|
||||||
|
|
||||||
|
|
||||||
|
def _config_files(pkg_dir, name):
|
||||||
|
return [
|
||||||
|
(f"{pkg_dir}/misc/config.py.default", f"/etc/{name}/config.py", False)]
|
||||||
|
|
||||||
|
|
||||||
|
def _install_files(files):
|
||||||
|
for src, dst, force in files:
|
||||||
|
if os.path.exists(dst):
|
||||||
|
if os.path.isdir(dst):
|
||||||
|
logging.error(
|
||||||
|
" => unable to copy file, destination path is a directory")
|
||||||
|
continue
|
||||||
|
elif not force:
|
||||||
|
logging.info(f" => file {dst} already exists")
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
logging.info(f" => install file {dst}")
|
||||||
|
shutil.copy2(src, dst)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f" => unable to install file {dst}: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def _uninstall_files(files):
|
||||||
|
for src, dst, force in files:
|
||||||
|
if not os.path.isfile(dst):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not force and not filecmp.cmp(src, dst, shallow=True):
|
||||||
|
logging.warning(
|
||||||
|
f" => keep modified file {dst}, "
|
||||||
|
f"you have to remove it manually")
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
logging.info(f" => uninstall file {dst}")
|
||||||
|
os.remove(dst)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f" => unable to uninstall file {dst}: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def _create_dir(path):
|
||||||
|
if os.path.isdir(path):
|
||||||
|
logging.info(f" => directory {path} already exists")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
logging.info(f" => create directory {path}")
|
||||||
|
os.mkdir(path)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f" => unable to create directory {path}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _delete_dir(path):
|
||||||
|
if os.path.isdir(path):
|
||||||
|
if not os.listdir(path):
|
||||||
|
try:
|
||||||
|
logging.info(f" => delete directory {path}")
|
||||||
|
os.rmdir(path)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f" => unable to delete directory {path}: {e}")
|
||||||
|
else:
|
||||||
|
logging.warning(f" => keep non-empty directory {path}")
|
||||||
|
|
||||||
|
|
||||||
def _check_root():
|
def _check_root():
|
||||||
if os.getuid() != 0:
|
if os.getuid() != 0:
|
||||||
logging.error("you need to have root privileges, please try again")
|
logging.error("you need to have root privileges, please try again")
|
||||||
@@ -32,7 +118,11 @@ def _check_root():
|
|||||||
|
|
||||||
|
|
||||||
def _check_systemd():
|
def _check_systemd():
|
||||||
systemd = os.path.isdir(SYSTEMD_PATH)
|
for path in SYSTEMD_PATHS:
|
||||||
|
systemd = os.path.isdir(path)
|
||||||
|
if systemd:
|
||||||
|
break
|
||||||
|
|
||||||
if systemd:
|
if systemd:
|
||||||
logging.info("systemd detected")
|
logging.info("systemd detected")
|
||||||
|
|
||||||
@@ -47,37 +137,6 @@ def _check_openrc():
|
|||||||
return openrc
|
return openrc
|
||||||
|
|
||||||
|
|
||||||
def _copy_missing_file(src, dst):
|
|
||||||
if os.path.exists(dst):
|
|
||||||
logging.info(f" => file {dst} already installed")
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
logging.info(f" => install file {dst}")
|
|
||||||
shutil.copy2(src, dst)
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f" => unable to install file {dst}: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
def _delete_present_file(f):
|
|
||||||
if os.path.isfile(f):
|
|
||||||
try:
|
|
||||||
logging.info(f" => uninstall file {f}")
|
|
||||||
os.remove(f)
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f" => unable to uninstall file {f}: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
def _warn_exists(path):
|
|
||||||
if os.path.isdir(path):
|
|
||||||
logging.warning(
|
|
||||||
f" => directory {path} is still present, "
|
|
||||||
f"you have to remove it manually")
|
|
||||||
else:
|
|
||||||
logging.warning(
|
|
||||||
f" => file {path} is still present, "
|
|
||||||
f"you have to remove it manually")
|
|
||||||
|
|
||||||
|
|
||||||
def install(name):
|
def install(name):
|
||||||
if not _check_root():
|
if not _check_root():
|
||||||
sys.exit(2)
|
sys.exit(2)
|
||||||
@@ -85,33 +144,16 @@ def install(name):
|
|||||||
pkg_dir = os.path.dirname(__file__)
|
pkg_dir = os.path.dirname(__file__)
|
||||||
|
|
||||||
if _check_systemd():
|
if _check_systemd():
|
||||||
dst = f"{SYSTEMD_PATH}/{name}.service"
|
_install_files(_systemd_files(pkg_dir, name))
|
||||||
src = f"{pkg_dir}/misc/systemd/{name}.service"
|
|
||||||
_copy_missing_file(src, dst)
|
|
||||||
|
|
||||||
if _check_openrc():
|
if _check_openrc():
|
||||||
files = [
|
_install_files(_openrc_files(pkg_dir, name))
|
||||||
(f"{pkg_dir}/misc/openrc/{name}.initd", f"/etc/init.d/{name}"),
|
|
||||||
(f"{pkg_dir}/misc/openrc/{name}.confd", f"/etc/conf.d/{name}")]
|
|
||||||
for src, dst in files:
|
|
||||||
_copy_missing_file(src, dst)
|
|
||||||
|
|
||||||
logging.info("install configuration file")
|
if not _create_dir(f"/etc/{name}"):
|
||||||
config_dir = f"/etc/{name}"
|
logging.error(" => unable to create config dir, giving up ...")
|
||||||
if os.path.isdir(config_dir):
|
sys.exit(3)
|
||||||
logging.info(f" => directory {config_dir} exists already")
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
logging.info(f" => create directory {config_dir}")
|
|
||||||
os.mkdir(config_dir)
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f" => unable to create directory {config_dir}: {e}")
|
|
||||||
sys.exit(3)
|
|
||||||
|
|
||||||
files = [
|
_install_files(_config_files(pkg_dir, name))
|
||||||
(f"{pkg_dir}/docs/config.py.example", f"{config_dir}/config.py")]
|
|
||||||
for src, dst in files:
|
|
||||||
_copy_missing_file(src, dst)
|
|
||||||
|
|
||||||
logging.info(f"{name} successfully installed")
|
logging.info(f"{name} successfully installed")
|
||||||
|
|
||||||
@@ -120,13 +162,12 @@ def uninstall(name):
|
|||||||
if not _check_root():
|
if not _check_root():
|
||||||
sys.exit(2)
|
sys.exit(2)
|
||||||
|
|
||||||
if _check_systemd():
|
pkg_dir = os.path.dirname(__file__)
|
||||||
_delete_present_file(f"{SYSTEMD_PATH}/{name}.service")
|
|
||||||
|
|
||||||
if _check_openrc():
|
_uninstall_files(_systemd_files(pkg_dir, name))
|
||||||
_delete_present_file(f"/etc/init.d/{name}")
|
_uninstall_files(_openrc_files(pkg_dir, name))
|
||||||
_warn_exists(f"/etc/conf.d/{name}")
|
_uninstall_files(_config_files(pkg_dir, name))
|
||||||
|
|
||||||
_warn_exists(f"/etc/{name}")
|
_delete_dir(f"/etc/{name}")
|
||||||
|
|
||||||
logging.info(f"{name} successfully uninstalled")
|
logging.info(f"{name} successfully uninstalled")
|
||||||
|
|||||||
@@ -1,83 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
####################################
|
|
||||||
# Example usage of TaskScheduler #
|
|
||||||
####################################
|
|
||||||
|
|
||||||
#async def custom_task(event, task_id):
|
|
||||||
# logging.info(f"{task_id}: execute example task: {event}")
|
|
||||||
#
|
|
||||||
#s = TaskScheduler(
|
|
||||||
# task=custom_task,
|
|
||||||
# files=True,
|
|
||||||
# dirs=False)
|
|
||||||
|
|
||||||
|
|
||||||
#####################################################
|
|
||||||
# Example usage of TaskScheduler with FileManager #
|
|
||||||
#####################################################
|
|
||||||
|
|
||||||
#rules=[{
|
|
||||||
# "action": "move",
|
|
||||||
# "src_re": r"^(?P<path>.*)",
|
|
||||||
# "dst_re": r"\g<path>.processed",
|
|
||||||
# "auto_create": True,
|
|
||||||
# "filemode": 0o755,
|
|
||||||
# "dirmode": 0o644,
|
|
||||||
# "user": "root",
|
|
||||||
# "goup": "root"}]
|
|
||||||
#
|
|
||||||
#fm = FileManager(
|
|
||||||
# rules=rules)
|
|
||||||
#
|
|
||||||
#s = TaskScheduler(
|
|
||||||
# task=fm.task,
|
|
||||||
# delay=10,
|
|
||||||
# files=True,
|
|
||||||
# dirs=False)
|
|
||||||
|
|
||||||
|
|
||||||
#####################################
|
|
||||||
# Example usage of ShellScheduler #
|
|
||||||
#####################################
|
|
||||||
|
|
||||||
#cmd = "/usr/local/bin/task.sh {maskname} {pathname} {src_pathname}"
|
|
||||||
#s = ShellScheduler(
|
|
||||||
# cmd=cmd)
|
|
||||||
|
|
||||||
|
|
||||||
###################
|
|
||||||
# Example watch #
|
|
||||||
###################
|
|
||||||
|
|
||||||
#event_map = EventMap({
|
|
||||||
# "IN_ACCESS": None,
|
|
||||||
# "IN_ATTRIB": None,
|
|
||||||
# "IN_CLOSE_NOWRITE": None,
|
|
||||||
# "IN_CLOSE_WRITE": s.schedule,
|
|
||||||
# "IN_CREATE": None,
|
|
||||||
# "IN_DELETE": s.cancel,
|
|
||||||
# "IN_DELETE_SELF": s.cancel,
|
|
||||||
# "IN_IGNORED": None,
|
|
||||||
# "IN_MODIFY": s.cancel,
|
|
||||||
# "IN_MOVE_SELF": None,
|
|
||||||
# "IN_MOVED_FROM": s.cancel,
|
|
||||||
# "IN_MOVED_TO": s.schedule,
|
|
||||||
# "IN_OPEN": None,
|
|
||||||
# "IN_Q_OVERFLOW": None,
|
|
||||||
# "IN_UNMOUNT": s.cancel})
|
|
||||||
#
|
|
||||||
#watch = Watch(
|
|
||||||
# path="/tmp",
|
|
||||||
# event_map=event_map,
|
|
||||||
# rec=True,
|
|
||||||
# auto_add=True)
|
|
||||||
|
|
||||||
|
|
||||||
########################
|
|
||||||
# Example pyinotifyd #
|
|
||||||
########################
|
|
||||||
|
|
||||||
pyinotifyd = Pyinotifyd(
|
|
||||||
watches=[],
|
|
||||||
shutdown_timeout=30)
|
|
||||||
@@ -1,193 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
# pyinotifyd is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# pyinotifyd is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with pyinotifyd. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import shutil
|
|
||||||
|
|
||||||
|
|
||||||
class Rule:
|
|
||||||
valid_actions = ["copy", "move", "delete"]
|
|
||||||
|
|
||||||
def __init__(self, action, src_re, dst_re="", auto_create=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
|
|
||||||
|
|
||||||
|
|
||||||
class FileManager:
|
|
||||||
def __init__(self, rules, logname="filemgr"):
|
|
||||||
if not isinstance(rules, list):
|
|
||||||
rules = [rules]
|
|
||||||
|
|
||||||
for rule in rules:
|
|
||||||
assert isinstance(rule, Rule), \
|
|
||||||
f"rules: expected {type(Rule)}, got {type(rule)}"
|
|
||||||
|
|
||||||
self._rules = rules
|
|
||||||
self._log = logging.getLogger((logname or __name__))
|
|
||||||
|
|
||||||
def add_rule(self, *args, **kwargs):
|
|
||||||
self._rules.append(Rule(*args, **kwargs))
|
|
||||||
|
|
||||||
async def _chmod_and_chown(self, path, mode, chown, task_id):
|
|
||||||
if mode is not None:
|
|
||||||
self._log.debug(f"{task_id}: chmod {oct(mode)} '{path}'")
|
|
||||||
os.chmod(path, mode)
|
|
||||||
|
|
||||||
if chown is not None:
|
|
||||||
changes = ""
|
|
||||||
if chown[0] is not None:
|
|
||||||
changes = chown[0]
|
|
||||||
|
|
||||||
if chown[1] is not None:
|
|
||||||
changes = f"{changes}:{chown[1]}"
|
|
||||||
|
|
||||||
self._log.debug(f"{task_id}: chown {changes} '{path}'")
|
|
||||||
shutil.chown(path, *chown)
|
|
||||||
|
|
||||||
async def _set_mode_and_owner(self, path, rule, task_id):
|
|
||||||
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 os.path.isdir(path):
|
|
||||||
await self._chmod_and_chown(path, rule.dirmode, chown, task_id)
|
|
||||||
if work_on_dirs or work_on_files:
|
|
||||||
for root, dirs, files in os.walk(path):
|
|
||||||
if work_on_dirs:
|
|
||||||
for p in [os.path.join(root, d) for d in dirs]:
|
|
||||||
await self._chmod_and_chown(
|
|
||||||
p, rule.dirmode, chown, task_id)
|
|
||||||
|
|
||||||
if work_on_files:
|
|
||||||
for p in [os.path.join(root, f) for f in files]:
|
|
||||||
await self._chmod_and_chown(
|
|
||||||
p, rule.filemode, chown, task_id)
|
|
||||||
else:
|
|
||||||
await self._chmod_and_chown(path, rule.filemode, chown, task_id)
|
|
||||||
|
|
||||||
async def task(self, event, task_id):
|
|
||||||
path = event.pathname
|
|
||||||
match = None
|
|
||||||
for rule in self._rules:
|
|
||||||
match = rule.src_re.match(path)
|
|
||||||
if match:
|
|
||||||
break
|
|
||||||
|
|
||||||
if not match:
|
|
||||||
self._log.debug(
|
|
||||||
f"{task_id}: path '{path}' matches no rule in ruleset")
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
if rule.action in ["copy", "move"]:
|
|
||||||
dst = rule.src_re.sub(rule.dst_re, path)
|
|
||||||
if not dst:
|
|
||||||
raise RuntimeError(
|
|
||||||
f"{task_id}: unable to {rule.action} '{path}', "
|
|
||||||
f"resulting destination path is empty")
|
|
||||||
|
|
||||||
if os.path.exists(dst):
|
|
||||||
raise RuntimeError(
|
|
||||||
f"{task_id}: unable to move file from '{path} "
|
|
||||||
f"to '{dst}', dstination path exists already")
|
|
||||||
|
|
||||||
dst_dir = os.path.dirname(dst)
|
|
||||||
if not os.path.isdir(dst_dir) and rule.auto_create:
|
|
||||||
self._log.info(
|
|
||||||
f"{task_id}: create directory '{dst_dir}'")
|
|
||||||
first_subdir = dst_dir
|
|
||||||
while not os.path.isdir(first_subdir):
|
|
||||||
parent = os.path.dirname(first_subdir)
|
|
||||||
if not os.path.isdir(parent):
|
|
||||||
first_subdir = parent
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
os.makedirs(dst_dir)
|
|
||||||
await self._set_mode_and_owner(first_subdir, rule, task_id)
|
|
||||||
|
|
||||||
self._log.info(
|
|
||||||
f"{task_id}: {rule.action} '{path}' to '{dst}'")
|
|
||||||
if rule.action == "copy":
|
|
||||||
if os.path.isdir(path):
|
|
||||||
shutil.copytree(path, dst)
|
|
||||||
else:
|
|
||||||
shutil.copy2(path, dst)
|
|
||||||
|
|
||||||
else:
|
|
||||||
os.rename(path, dst)
|
|
||||||
|
|
||||||
await self._set_mode_and_owner(dst, rule, task_id)
|
|
||||||
|
|
||||||
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}")
|
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
##########################
|
||||||
|
# TaskScheduler config #
|
||||||
|
##########################
|
||||||
|
|
||||||
|
#import asyncio
|
||||||
|
#import logging
|
||||||
|
#
|
||||||
|
#async def custom_job(event, task_id):
|
||||||
|
# await asyncio.sleep(1)
|
||||||
|
# logging.info(f"{task_id}: execute example task: {event}")
|
||||||
|
#
|
||||||
|
#task_sched = TaskScheduler(
|
||||||
|
# job=custom_job,
|
||||||
|
# files=True,
|
||||||
|
# dirs=False,
|
||||||
|
# delay=10,
|
||||||
|
# global_vars=globals(),
|
||||||
|
# singlejob=False)
|
||||||
|
|
||||||
|
|
||||||
|
###########################
|
||||||
|
# ShellScheduler config #
|
||||||
|
###########################
|
||||||
|
|
||||||
|
#shell_sched = ShellScheduler(
|
||||||
|
# cmd="/usr/local/bin/task.sh {maskname} {pathname} {src_pathname}",
|
||||||
|
# files=True,
|
||||||
|
# dirs=False,
|
||||||
|
# delay=10,
|
||||||
|
# singlejob=False)
|
||||||
|
|
||||||
|
|
||||||
|
#################################
|
||||||
|
# FileManagerScheduler config #
|
||||||
|
#################################
|
||||||
|
|
||||||
|
#move_rule = Rule(
|
||||||
|
# action="move",
|
||||||
|
# src_re="^/src_path/(?P<path>.*).to_move",
|
||||||
|
# dst_re="/dst_path/\g<path>.moved",
|
||||||
|
# auto_create=True,
|
||||||
|
# filemode=0o755,
|
||||||
|
# dirmode=0o644,
|
||||||
|
# user="root",
|
||||||
|
# goup="root",
|
||||||
|
# overwrite=False)
|
||||||
|
|
||||||
|
#delete_rule = Rule(
|
||||||
|
# action="delete",
|
||||||
|
# src_re="^/src_path/(?P<path>.*).to_delete",
|
||||||
|
# rec=False)
|
||||||
|
|
||||||
|
#file_sched = FileManagerScheduler(
|
||||||
|
# rules=[move_rule, delete_rule],
|
||||||
|
# files=True,
|
||||||
|
# dirs=False,
|
||||||
|
# delay=10)
|
||||||
|
|
||||||
|
|
||||||
|
#####################
|
||||||
|
# EventMap config #
|
||||||
|
#####################
|
||||||
|
|
||||||
|
#event_map = {
|
||||||
|
# "IN_ACCESS": None,
|
||||||
|
# "IN_ATTRIB": None,
|
||||||
|
# "IN_CLOSE_NOWRITE": None,
|
||||||
|
# "IN_CLOSE_WRITE": task_sched,
|
||||||
|
# "IN_CREATE": task_sched,
|
||||||
|
# "IN_DELETE": Cancel(task_sched),
|
||||||
|
# "IN_DELETE_SELF": Cancel(task_sched),
|
||||||
|
# "IN_IGNORED": None,
|
||||||
|
# "IN_MODIFY": Cancel(task_sched),
|
||||||
|
# "IN_MOVE_SELF": None,
|
||||||
|
# "IN_MOVED_FROM": Cancel(task_sched),
|
||||||
|
# "IN_MOVED_TO": task_sched,
|
||||||
|
# "IN_OPEN": None,
|
||||||
|
# "IN_Q_OVERFLOW": None,
|
||||||
|
# "IN_UNMOUNT": Cancel(task_sched)}
|
||||||
|
|
||||||
|
|
||||||
|
#######################
|
||||||
|
# pyinotifyd config #
|
||||||
|
#######################
|
||||||
|
|
||||||
|
#pyinotifyd = Pyinotifyd(shutdown_timeout=15)
|
||||||
|
#pyinotifyd.add_watch(
|
||||||
|
# path="/watched/directory",
|
||||||
|
# event_map = event_map,
|
||||||
|
# rec=True,
|
||||||
|
# auto_add=True)
|
||||||
|
|
||||||
|
|
||||||
|
################
|
||||||
|
# Log config #
|
||||||
|
################
|
||||||
|
|
||||||
|
# set global loglevel
|
||||||
|
#setLoglevel(DEBUG)
|
||||||
|
|
||||||
|
# set loglevel per logname
|
||||||
|
#setLoglevel(
|
||||||
|
# DEBUG,
|
||||||
|
# logname="daemon")
|
||||||
|
|
||||||
|
# enable syslog
|
||||||
|
#enableSyslog(
|
||||||
|
# loglevel=DEBUG,
|
||||||
|
# address="/dev/log")
|
||||||
|
|
||||||
|
# enable syslog per logname
|
||||||
|
#enableSyslog(
|
||||||
|
# loglevel=DEBUG,
|
||||||
|
# address="/dev/log",
|
||||||
|
# logname="sched")
|
||||||
@@ -4,5 +4,8 @@
|
|||||||
# USER="daemon"
|
# USER="daemon"
|
||||||
# USER="daemon:nobody"
|
# USER="daemon:nobody"
|
||||||
|
|
||||||
# Optional parameters for pymodmilter
|
# Set the shutdown timeout
|
||||||
|
# SHUTDOWN_TIMEOUT=300
|
||||||
|
|
||||||
|
# Optional command line options
|
||||||
# PYINOTIFYD_OPTS=""
|
# PYINOTIFYD_OPTS=""
|
||||||
|
|||||||
@@ -2,19 +2,16 @@
|
|||||||
|
|
||||||
user=${USER:-root}
|
user=${USER:-root}
|
||||||
pyinotifyd_opts="${PYINOTIFYD_OPTS:-}"
|
pyinotifyd_opts="${PYINOTIFYD_OPTS:-}"
|
||||||
|
shutdown_timeout="${SHUTDOWN_TIMEOUT:-300}"
|
||||||
|
|
||||||
pidfile="/run/${RC_SVCNAME}.pid"
|
pidfile="/run/${RC_SVCNAME}.pid"
|
||||||
command="/usr/bin/pyinotifyd"
|
command="/usr/bin/pyinotifyd"
|
||||||
command_args="${pyinotifyd_opts}"
|
command_args="${pyinotifyd_opts}"
|
||||||
command_background=true
|
command_background=true
|
||||||
start_stop_daemon_args="--user ${user}"
|
start_stop_daemon_args="--user ${user}"
|
||||||
|
retry="SIGTERM/${shutdown_timeout}"
|
||||||
|
|
||||||
extra_commands="configtest"
|
extra_commands="configtest reload"
|
||||||
|
|
||||||
depend() {
|
|
||||||
need net
|
|
||||||
before mta
|
|
||||||
}
|
|
||||||
|
|
||||||
checkconfig() {
|
checkconfig() {
|
||||||
output=$(${command} ${command_args} -t 2>&1)
|
output=$(${command} ${command_args} -t 2>&1)
|
||||||
@@ -44,3 +41,9 @@ stop_pre() {
|
|||||||
checkconfig || return $?
|
checkconfig || return $?
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reload() {
|
||||||
|
ebegin "Reloading ${SVCNAME}"
|
||||||
|
start-stop-daemon --signal HUP --pidfile "${pidfile}"
|
||||||
|
eend $?
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ After=fs.target
|
|||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
ExecStart=/usr/bin/pyinotifyd
|
ExecStart=/usr/bin/pyinotifyd
|
||||||
|
ExecReload=/bin/kill -HUP $MAINPID
|
||||||
TimeoutStopSec=300
|
TimeoutStopSec=300
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
|
|||||||
+393
-93
@@ -12,131 +12,223 @@
|
|||||||
# along with pyinotifyd. If not, see <http://www.gnu.org/licenses/>.
|
# along with pyinotifyd. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"TaskScheduler",
|
||||||
|
"Cancel",
|
||||||
|
"ShellScheduler",
|
||||||
|
"FileManagerRule",
|
||||||
|
"FileManagerScheduler"]
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
from inspect import iscoroutinefunction
|
||||||
from shlex import quote as shell_quote
|
from shlex import quote as shell_quote
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
|
|
||||||
class _Task:
|
class SchedulerLogger(logging.LoggerAdapter):
|
||||||
def __init__(self, event, delay, task_id, task, callback=None,
|
def process(self, msg, kwargs):
|
||||||
logname="task"):
|
if "event" in self.extra:
|
||||||
self._event = event
|
event = self.extra["event"]
|
||||||
self._path = event.pathname
|
msg = f"{msg}, mask={event.maskname}, path={event.pathname}"
|
||||||
self._delay = delay
|
|
||||||
self._task_id = task_id
|
|
||||||
self._job = task
|
|
||||||
self._callback = callback
|
|
||||||
|
|
||||||
self._task = None
|
if "id" in self.extra:
|
||||||
self._log = logging.getLogger((logname or __name__))
|
task_id = self.extra["id"]
|
||||||
|
msg = f"{msg}, task_id={task_id}"
|
||||||
|
|
||||||
async def _start(self):
|
return msg, kwargs
|
||||||
if self._delay > 0:
|
|
||||||
await asyncio.sleep(self._delay)
|
|
||||||
|
|
||||||
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")
|
|
||||||
|
|
||||||
def start(self):
|
|
||||||
if self._task is None:
|
|
||||||
self._task = asyncio.create_task(self._start())
|
|
||||||
|
|
||||||
def cancel(self):
|
|
||||||
if self._task is not None:
|
|
||||||
self._task.cancel()
|
|
||||||
self._task = None
|
|
||||||
|
|
||||||
def restart(self):
|
|
||||||
self.cancel()
|
|
||||||
self.start()
|
|
||||||
|
|
||||||
|
|
||||||
class TaskScheduler:
|
class TaskScheduler:
|
||||||
def __init__(self, task, files, dirs, delay=0, logname="sched"):
|
|
||||||
assert callable(task), \
|
|
||||||
f"task: expected callable, got {type(task)}"
|
|
||||||
self._task = task
|
|
||||||
|
|
||||||
assert isinstance(delay, int), \
|
class TaskState:
|
||||||
f"delay: expected {type(int)}, got {type(delay)}"
|
def __init__(self, task_id=None, task=None, cancelable=True):
|
||||||
self._delay = delay
|
self.id = task_id or str(uuid4())
|
||||||
|
self.task = task
|
||||||
|
self.cancelable = cancelable
|
||||||
|
|
||||||
|
def __init__(self, job, files=True, dirs=False, delay=0, logname="sched",
|
||||||
|
loop=None, global_vars={}, singlejob=False):
|
||||||
|
assert iscoroutinefunction(job), \
|
||||||
|
f"job: expected coroutine, got {type(job)}"
|
||||||
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
|
|
||||||
|
|
||||||
assert isinstance(dirs, bool), \
|
assert isinstance(dirs, bool), \
|
||||||
f"dirs: expected {type(bool)}, got {type(dirs)}"
|
f"dirs: expected {type(bool)}, got {type(dirs)}"
|
||||||
|
assert isinstance(delay, int), \
|
||||||
|
f"delay: expected {type(int)}, got {type(delay)}"
|
||||||
|
assert isinstance(global_vars, dict), \
|
||||||
|
f"global_vars: expected {type(dict)}, got {type(global_vars)}"
|
||||||
|
|
||||||
|
self._job = job
|
||||||
|
self._files = files
|
||||||
self._dirs = dirs
|
self._dirs = dirs
|
||||||
|
self._delay = delay
|
||||||
|
self._log = logging.getLogger((logname or __name__))
|
||||||
|
self._loop = (loop or asyncio.get_event_loop())
|
||||||
|
self._globals = global_vars
|
||||||
|
self._singlejob = singlejob
|
||||||
self._tasks = {}
|
self._tasks = {}
|
||||||
self._logname = (logname or __name__)
|
self._pause = False
|
||||||
self._log = logging.getLogger(self._logname)
|
|
||||||
|
|
||||||
def _task_started(self, event):
|
def pause(self):
|
||||||
path = event.pathname
|
self._log.info("pause scheduler")
|
||||||
if path in self._tasks:
|
self._pause = True
|
||||||
del self._tasks[path]
|
|
||||||
|
|
||||||
def schedule(self, event):
|
async def shutdown(self, timeout=None):
|
||||||
self._log.debug(f"received {event}")
|
self._pause = True
|
||||||
|
pending = [t.task for t in self._tasks.values()]
|
||||||
|
if pending:
|
||||||
|
if timeout is None:
|
||||||
|
self._log.info(
|
||||||
|
f"wait for {len(pending)} "
|
||||||
|
f"remaining task(s) to complete")
|
||||||
|
else:
|
||||||
|
self._log.info(
|
||||||
|
f"wait {timeout} seconds for {len(pending)} "
|
||||||
|
f"remaining task(s) to complete")
|
||||||
|
done, pending = await asyncio.wait([*pending], timeout=timeout,
|
||||||
|
loop=self._loop)
|
||||||
|
if pending:
|
||||||
|
self._log.warning(
|
||||||
|
f"shutdown timeout exceeded, "
|
||||||
|
f"cancel {len(pending)} remaining task(s)")
|
||||||
|
for task in pending:
|
||||||
|
task.cancel()
|
||||||
|
try:
|
||||||
|
await asyncio.gather(*pending, loop=self._loop)
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
self._log.info("all remainig tasks completed")
|
||||||
|
|
||||||
if (not event.dir and not self._files) or \
|
def taskindex(self, event):
|
||||||
(event.dir and not self._dirs):
|
return "singlejob" if self._singlejob else event.pathname
|
||||||
|
|
||||||
|
async def _run_job(self, event, task_state, restart=False):
|
||||||
|
logger = SchedulerLogger(self._log, {
|
||||||
|
"event": event,
|
||||||
|
"id": task_state.id})
|
||||||
|
|
||||||
|
if self._delay > 0:
|
||||||
|
task_state.task = self._loop.create_task(
|
||||||
|
asyncio.sleep(self._delay))
|
||||||
|
try:
|
||||||
|
if restart:
|
||||||
|
prefix = "re-"
|
||||||
|
else:
|
||||||
|
prefix = ""
|
||||||
|
|
||||||
|
logger.info(f"{prefix}schedule task, delay={self._delay}")
|
||||||
|
|
||||||
|
await task_state.task
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info("start task")
|
||||||
|
if self._globals:
|
||||||
|
local_vars = {"self": self,
|
||||||
|
"event": event,
|
||||||
|
"task_id": task_state.id}
|
||||||
|
task_state.task = self._loop.create_task(
|
||||||
|
eval("self._job(event, task_id)", self._globals, local_vars))
|
||||||
|
|
||||||
|
else:
|
||||||
|
task_state.task = self._loop.create_task(
|
||||||
|
self._job(event, task_state.id))
|
||||||
|
|
||||||
|
try:
|
||||||
|
task_state.cancelable = False
|
||||||
|
await task_state.task
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
logger.warning("ongoing task cancelled")
|
||||||
|
else:
|
||||||
|
logger.info("task finished")
|
||||||
|
finally:
|
||||||
|
task_index = self.taskindex(event)
|
||||||
|
del self._tasks[task_index]
|
||||||
|
|
||||||
|
async def process_event(self, event):
|
||||||
|
if not ((not event.dir and self._files) or
|
||||||
|
(event.dir and self._dirs)):
|
||||||
return
|
return
|
||||||
|
|
||||||
path = event.pathname
|
restart = False
|
||||||
maskname = event.maskname.split("|", 1)[0]
|
task_index = self.taskindex(event)
|
||||||
|
try:
|
||||||
if path in self._tasks:
|
task_state = self._tasks[task_index]
|
||||||
task = self._tasks[path]
|
except KeyError:
|
||||||
self._log.info(
|
task_state = TaskScheduler.TaskState()
|
||||||
f"received event {maskname} on '{path}', "
|
self._tasks[task_index] = task_state
|
||||||
f"re-schedule task {task.task_id} (delay={self._delay}s)")
|
|
||||||
task.restart()
|
|
||||||
else:
|
else:
|
||||||
task_id = str(uuid4())
|
logger = SchedulerLogger(self._log, {
|
||||||
self._log.info(
|
"event": event,
|
||||||
f"received event {maskname} on '{path}', "
|
"id": task_state.id})
|
||||||
f"schedule task {task_id} (delay={self._delay}s)")
|
|
||||||
task = _Task(
|
|
||||||
event, self._delay, task_id, self._task,
|
|
||||||
callback=self._task_started, logname=self._logname)
|
|
||||||
self._tasks[path] = task
|
|
||||||
task.start()
|
|
||||||
|
|
||||||
def cancel(self, event):
|
if task_state.cancelable:
|
||||||
self._log.debug(f"received {event}")
|
task_state.task.cancel()
|
||||||
|
if not self._pause:
|
||||||
|
restart = True
|
||||||
|
else:
|
||||||
|
logger.info("scheduled task cancelled")
|
||||||
|
|
||||||
path = event.pathname
|
else:
|
||||||
maskname = event.maskname.split("|", 1)[0]
|
logger.warning("skip event due to ongoing task")
|
||||||
if path in self._tasks:
|
return
|
||||||
task = self._tasks[path]
|
|
||||||
self._log.info(
|
if not self._pause:
|
||||||
f"received event {maskname} on '{path}', "
|
await self._run_job(event, task_state, restart)
|
||||||
f"cancel scheduled task {task.task_id}")
|
|
||||||
task.cancel()
|
async def process_cancel_event(self, event):
|
||||||
del self._tasks[path]
|
try:
|
||||||
|
task_index = self.taskindex(event)
|
||||||
|
task_state = self._tasks[task_index]
|
||||||
|
except KeyError:
|
||||||
|
return
|
||||||
|
|
||||||
|
logger = SchedulerLogger(self._log, {
|
||||||
|
"event": event,
|
||||||
|
"id": task_state.id})
|
||||||
|
|
||||||
|
if task_state.cancelable:
|
||||||
|
task_state.task.cancel()
|
||||||
|
logger.info("scheduled task cancelled")
|
||||||
|
task_state.task = None
|
||||||
|
logger.info(f"{task_index}")
|
||||||
|
del self._tasks[task_index]
|
||||||
|
else:
|
||||||
|
logger.warning("skip event due to ongoing task")
|
||||||
|
|
||||||
|
|
||||||
|
class Cancel:
|
||||||
|
def __init__(self, task, *args, **kwargs):
|
||||||
|
assert issubclass(type(task), TaskScheduler), \
|
||||||
|
f"task: expected {type(TaskScheduler)}, got {type(task)}"
|
||||||
|
|
||||||
|
setattr(self, "process_event", task.process_cancel_event)
|
||||||
|
|
||||||
|
def pause(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def shutdown(self, timeout=None):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ShellScheduler(TaskScheduler):
|
class ShellScheduler(TaskScheduler):
|
||||||
def __init__(self, cmd, task=None, *args, **kwargs):
|
def __init__(self, cmd, job=None, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs, job=self._shell_job)
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
super().__init__(*args, task=self.task, **kwargs)
|
async def _shell_job(self, event, task_id):
|
||||||
|
|
||||||
async def task(self, event, task_id):
|
|
||||||
maskname = event.maskname.split("|", 1)[0]
|
maskname = event.maskname.split("|", 1)[0]
|
||||||
|
|
||||||
if hasattr(event, "src_pathname"):
|
if hasattr(event, "src_pathname"):
|
||||||
src_pathname = event.src_pathname
|
src_pathname = event.src_pathname
|
||||||
else:
|
else:
|
||||||
@@ -146,6 +238,214 @@ class ShellScheduler(TaskScheduler):
|
|||||||
"{pathname}", shell_quote(event.pathname)).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}")
|
logger = SchedulerLogger(self._log, {
|
||||||
proc = await asyncio.create_subprocess_shell(cmd)
|
"event": event,
|
||||||
await proc.communicate()
|
"id": task_id})
|
||||||
|
|
||||||
|
logger.info(f"execute shell command, cmd={cmd}")
|
||||||
|
try:
|
||||||
|
proc = await asyncio.create_subprocess_shell(cmd, loop=self._loop)
|
||||||
|
await proc.communicate()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
|
||||||
|
|
||||||
|
class FileManagerRule:
|
||||||
|
valid_actions = ["copy", "move", "delete"]
|
||||||
|
|
||||||
|
def __init__(self, action, src_re, dst_re="", auto_create=False,
|
||||||
|
overwrite=False, dirmode=None, filemode=None, user=None,
|
||||||
|
group=None, rec=False):
|
||||||
|
valid = f"{', '.join(FileManagerRule.valid_actions)}"
|
||||||
|
assert action in self.valid_actions, \
|
||||||
|
f"action: expected [{valid}], got{action}"
|
||||||
|
assert isinstance(src_re, str), \
|
||||||
|
f"src_re: expected {type('')}, got {type(src_re)}"
|
||||||
|
assert isinstance(dst_re, str), \
|
||||||
|
f"dst_re: expected {type('')}, got {type(dst_re)}"
|
||||||
|
assert isinstance(auto_create, bool), \
|
||||||
|
f"auto_create: expected {type(bool)}, got {type(auto_create)}"
|
||||||
|
assert isinstance(overwrite, bool), \
|
||||||
|
f"auto_create: expected {type(bool)}, got {type(auto_create)}"
|
||||||
|
assert dirmode is None or isinstance(dirmode, int), \
|
||||||
|
f"dirmode: expected {type(int)}, got {type(dirmode)}"
|
||||||
|
assert filemode is None or isinstance(filemode, int), \
|
||||||
|
f"filemode: expected {type(int)}, got {type(filemode)}"
|
||||||
|
assert user is None or isinstance(user, str), \
|
||||||
|
f"user: expected {type('')}, got {type(user)}"
|
||||||
|
assert group is None or isinstance(group, str), \
|
||||||
|
f"group: expected {type('')}, got {type(group)}"
|
||||||
|
assert isinstance(rec, bool), \
|
||||||
|
f"rec: expected {type(bool)}, got {type(rec)}"
|
||||||
|
|
||||||
|
self.action = action
|
||||||
|
self.src_re = re.compile(src_re)
|
||||||
|
self.dst_re = dst_re
|
||||||
|
self.auto_create = auto_create
|
||||||
|
self.overwrite = overwrite
|
||||||
|
self.dirmode = dirmode
|
||||||
|
self.filemode = filemode
|
||||||
|
self.user = user
|
||||||
|
self.group = group
|
||||||
|
self.rec = rec
|
||||||
|
|
||||||
|
|
||||||
|
class FileManagerScheduler(TaskScheduler):
|
||||||
|
def __init__(self, rules, job=None, *args, **kwargs):
|
||||||
|
super().__init__(
|
||||||
|
*args, **kwargs, job=self._manager_job, singlejob=False)
|
||||||
|
|
||||||
|
if not isinstance(rules, list):
|
||||||
|
rules = [rules]
|
||||||
|
|
||||||
|
for rule in rules:
|
||||||
|
assert isinstance(rule, FileManagerRule), \
|
||||||
|
f"rules: expected {type(FileManagerRule)}, got {type(rule)}"
|
||||||
|
|
||||||
|
self._rules = rules
|
||||||
|
|
||||||
|
def _get_rule_by_event(self, event):
|
||||||
|
rule = None
|
||||||
|
for r in self._rules:
|
||||||
|
if r.src_re.match(event.pathname):
|
||||||
|
rule = r
|
||||||
|
break
|
||||||
|
|
||||||
|
return rule
|
||||||
|
|
||||||
|
async def process_event(self, event):
|
||||||
|
if not ((not event.dir and self._files) or
|
||||||
|
(event.dir and self._dirs)):
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._get_rule_by_event(event):
|
||||||
|
await super().process_event(event)
|
||||||
|
else:
|
||||||
|
logger = SchedulerLogger(self._log, {"event": event})
|
||||||
|
logger.debug("no rule in ruleset matches")
|
||||||
|
|
||||||
|
async def _chmod_and_chown(self, path, mode, chown, logger=None):
|
||||||
|
logger = (logger or self._log)
|
||||||
|
|
||||||
|
if mode is not None:
|
||||||
|
logger.debug(f"chmod {oct(mode)}")
|
||||||
|
os.chmod(path, mode)
|
||||||
|
|
||||||
|
if chown is not None:
|
||||||
|
changes = ""
|
||||||
|
if chown[0] is not None:
|
||||||
|
changes = chown[0]
|
||||||
|
|
||||||
|
if chown[1] is not None:
|
||||||
|
changes = f"{changes}:{chown[1]}"
|
||||||
|
|
||||||
|
logger.debug(f"chown {changes}")
|
||||||
|
shutil.chown(path, *chown)
|
||||||
|
|
||||||
|
async def _set_mode_and_owner(self, path, rule, logger=None):
|
||||||
|
logger = (logger or self._log)
|
||||||
|
|
||||||
|
if (rule.user is rule.group is None):
|
||||||
|
chown = None
|
||||||
|
else:
|
||||||
|
chown = (rule.user, rule.group)
|
||||||
|
|
||||||
|
if os.path.isdir(path):
|
||||||
|
mode = rule.dirmode
|
||||||
|
else:
|
||||||
|
mode = rule.filemode
|
||||||
|
|
||||||
|
await self._chmod_and_chown(path, mode, chown, logger)
|
||||||
|
|
||||||
|
if not os.path.isdir(path):
|
||||||
|
return
|
||||||
|
|
||||||
|
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:
|
||||||
|
for root, dirs, files in os.walk(path):
|
||||||
|
if work_on_dirs:
|
||||||
|
for p in [os.path.join(root, d) for d in dirs]:
|
||||||
|
await self._chmod_and_chown(
|
||||||
|
p, rule.dirmode, chown, logger)
|
||||||
|
|
||||||
|
if work_on_files:
|
||||||
|
for p in [os.path.join(root, f) for f in files]:
|
||||||
|
await self._chmod_and_chown(
|
||||||
|
p, rule.filemode, chown, logger)
|
||||||
|
|
||||||
|
async def _manager_job(self, event, task_id):
|
||||||
|
rule = self._get_rule_by_event(event)
|
||||||
|
if not rule:
|
||||||
|
return
|
||||||
|
|
||||||
|
logger = SchedulerLogger(self._log, {"id": task_id})
|
||||||
|
|
||||||
|
try:
|
||||||
|
path = event.pathname
|
||||||
|
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")
|
||||||
|
|
||||||
|
if os.path.exists(dst) and not rule.overwrite:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"unable to {rule.action} file from '{path} "
|
||||||
|
f"to '{dst}', path already exists")
|
||||||
|
|
||||||
|
dst_dir = os.path.dirname(dst)
|
||||||
|
if not os.path.isdir(dst_dir) and rule.auto_create:
|
||||||
|
logger.info(f"create directory '{dst_dir}'")
|
||||||
|
first_subdir = dst_dir
|
||||||
|
while not os.path.isdir(first_subdir):
|
||||||
|
parent = os.path.dirname(first_subdir)
|
||||||
|
if not os.path.isdir(parent):
|
||||||
|
first_subdir = parent
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.makedirs(dst_dir)
|
||||||
|
await self._set_mode_and_owner(
|
||||||
|
first_subdir, rule, logger)
|
||||||
|
except Exception as e:
|
||||||
|
raise RuntimeError(e)
|
||||||
|
|
||||||
|
logger.info(f"{rule.action} '{path}' to '{dst}'")
|
||||||
|
|
||||||
|
try:
|
||||||
|
if rule.action == "copy":
|
||||||
|
if os.path.isdir(path):
|
||||||
|
shutil.copytree(path, dst)
|
||||||
|
else:
|
||||||
|
shutil.copy2(path, dst)
|
||||||
|
|
||||||
|
else:
|
||||||
|
os.rename(path, dst)
|
||||||
|
|
||||||
|
await self._set_mode_and_owner(dst, rule, logger)
|
||||||
|
except Exception as e:
|
||||||
|
raise RuntimeError(e)
|
||||||
|
|
||||||
|
elif rule.action == "delete":
|
||||||
|
logger.info(f"{rule.action} '{path}'")
|
||||||
|
try:
|
||||||
|
if os.path.isdir(path):
|
||||||
|
if rule.rec:
|
||||||
|
shutil.rmtree(path)
|
||||||
|
else:
|
||||||
|
shutil.rmdir(path)
|
||||||
|
|
||||||
|
else:
|
||||||
|
os.remove(path)
|
||||||
|
except Exception as e:
|
||||||
|
raise RuntimeError(e)
|
||||||
|
|
||||||
|
except RuntimeError as e:
|
||||||
|
logger.error(e)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(e)
|
||||||
|
|||||||
@@ -1,115 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
# pyinotifyd is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# pyinotifyd is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with pyinotifyd. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import pyinotify
|
|
||||||
|
|
||||||
|
|
||||||
class EventMap:
|
|
||||||
flags = {
|
|
||||||
**pyinotify.EventsCodes.OP_FLAGS,
|
|
||||||
**pyinotify.EventsCodes.EVENT_FLAGS}
|
|
||||||
|
|
||||||
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)}"
|
|
||||||
for flag in EventMap.flags:
|
|
||||||
self.set(flag, default_task)
|
|
||||||
|
|
||||||
if event_map is not None:
|
|
||||||
assert isinstance(event_map, dict), \
|
|
||||||
f"event_map: expected {type(dict)}, got {type(event_map)}"
|
|
||||||
for flag, task in event_map.items():
|
|
||||||
self.set(flag, task)
|
|
||||||
|
|
||||||
def items(self):
|
|
||||||
return self._map.items()
|
|
||||||
|
|
||||||
def set(self, flag, values):
|
|
||||||
assert flag in EventMap.flags, \
|
|
||||||
f"event_map: invalid flag: {flag}"
|
|
||||||
if values is not None:
|
|
||||||
if not isinstance(values, list):
|
|
||||||
values = [values]
|
|
||||||
|
|
||||||
for value in values:
|
|
||||||
assert callable(value), \
|
|
||||||
f"event_map: {flag}: expected callable, got {type(value)}"
|
|
||||||
|
|
||||||
self._map[flag] = values
|
|
||||||
elif flag in self._map:
|
|
||||||
del self._map[flag]
|
|
||||||
|
|
||||||
|
|
||||||
class _TaskList:
|
|
||||||
def __init__(self, tasks=[]):
|
|
||||||
if not isinstance(tasks, list):
|
|
||||||
tasks = [tasks]
|
|
||||||
|
|
||||||
self._tasks = tasks
|
|
||||||
|
|
||||||
def add(self, task):
|
|
||||||
self._tasks.append(task)
|
|
||||||
|
|
||||||
def remove(self, task):
|
|
||||||
self._tasks.remove(task)
|
|
||||||
|
|
||||||
def execute(self, event):
|
|
||||||
for task in self._tasks:
|
|
||||||
task(event)
|
|
||||||
|
|
||||||
|
|
||||||
class Watch:
|
|
||||||
def __init__(self, path, event_map, rec=False, auto_add=False):
|
|
||||||
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):
|
|
||||||
self.event_map = EventMap(event_map)
|
|
||||||
else:
|
|
||||||
raise AssertionError(
|
|
||||||
f"event_map: expected {type(EventMap)} or {type(dict)}, "
|
|
||||||
f"got {type(event_map)}")
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
def event_notifier(self, wm, loop=asyncio.get_event_loop()):
|
|
||||||
handler = pyinotify.ProcessEvent()
|
|
||||||
mask = False
|
|
||||||
for flag, values in self.event_map.items():
|
|
||||||
setattr(handler, f"process_{flag}", _TaskList(values).execute)
|
|
||||||
if mask:
|
|
||||||
mask = mask | EventMap.flags[flag]
|
|
||||||
else:
|
|
||||||
mask = EventMap.flags[flag]
|
|
||||||
|
|
||||||
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)
|
|
||||||
@@ -18,7 +18,7 @@ setup(name = "pyinotifyd",
|
|||||||
# 3 - Alpha
|
# 3 - Alpha
|
||||||
# 4 - Beta
|
# 4 - Beta
|
||||||
# 5 - Production/Stable
|
# 5 - Production/Stable
|
||||||
"Development Status :: 3 - Alpha",
|
"Development Status :: 5 - Production/Stable",
|
||||||
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
|
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
|
||||||
"Operating System :: OS Independent",
|
"Operating System :: OS Independent",
|
||||||
"Programming Language :: Python",
|
"Programming Language :: Python",
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import pyinotifyd
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
sys.exit(
|
|
||||||
pyinotifyd.main()
|
|
||||||
)
|
|
||||||
Reference in New Issue
Block a user