forked from platypush/platypush
[#289] Converted backend.file.monitor
into a runnable plugin.
Closes: #289
This commit is contained in:
parent
d484a34c00
commit
1843ab224b
16 changed files with 231 additions and 184 deletions
|
@ -11,7 +11,6 @@ Backends
|
||||||
platypush/backend/button.flic.rst
|
platypush/backend/button.flic.rst
|
||||||
platypush/backend/camera.pi.rst
|
platypush/backend/camera.pi.rst
|
||||||
platypush/backend/chat.telegram.rst
|
platypush/backend/chat.telegram.rst
|
||||||
platypush/backend/file.monitor.rst
|
|
||||||
platypush/backend/foursquare.rst
|
platypush/backend/foursquare.rst
|
||||||
platypush/backend/github.rst
|
platypush/backend/github.rst
|
||||||
platypush/backend/google.fit.rst
|
platypush/backend/google.fit.rst
|
||||||
|
@ -28,7 +27,6 @@ Backends
|
||||||
platypush/backend/nextcloud.rst
|
platypush/backend/nextcloud.rst
|
||||||
platypush/backend/nfc.rst
|
platypush/backend/nfc.rst
|
||||||
platypush/backend/nodered.rst
|
platypush/backend/nodered.rst
|
||||||
platypush/backend/ping.rst
|
|
||||||
platypush/backend/pushbullet.rst
|
platypush/backend/pushbullet.rst
|
||||||
platypush/backend/redis.rst
|
platypush/backend/redis.rst
|
||||||
platypush/backend/scard.rst
|
platypush/backend/scard.rst
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
``file.monitor``
|
|
||||||
==================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.backend.file.monitor
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``ping``
|
|
||||||
==========================
|
|
||||||
|
|
||||||
.. automodule:: platypush.backend.ping
|
|
||||||
:members:
|
|
5
docs/source/platypush/plugins/file.monitor.rst
Normal file
5
docs/source/platypush/plugins/file.monitor.rst
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
``file.monitor``
|
||||||
|
================
|
||||||
|
|
||||||
|
.. automodule:: platypush.plugins.file.monitor
|
||||||
|
:members:
|
|
@ -1,5 +0,0 @@
|
||||||
``ping``
|
|
||||||
===================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.message.response.ping
|
|
||||||
:members:
|
|
|
@ -33,6 +33,7 @@ Plugins
|
||||||
platypush/plugins/esp.rst
|
platypush/plugins/esp.rst
|
||||||
platypush/plugins/ffmpeg.rst
|
platypush/plugins/ffmpeg.rst
|
||||||
platypush/plugins/file.rst
|
platypush/plugins/file.rst
|
||||||
|
platypush/plugins/file.monitor.rst
|
||||||
platypush/plugins/foursquare.rst
|
platypush/plugins/foursquare.rst
|
||||||
platypush/plugins/google.calendar.rst
|
platypush/plugins/google.calendar.rst
|
||||||
platypush/plugins/google.drive.rst
|
platypush/plugins/google.drive.rst
|
||||||
|
|
|
@ -11,7 +11,6 @@ Responses
|
||||||
platypush/responses/chat.telegram.rst
|
platypush/responses/chat.telegram.rst
|
||||||
platypush/responses/google.drive.rst
|
platypush/responses/google.drive.rst
|
||||||
platypush/responses/pihole.rst
|
platypush/responses/pihole.rst
|
||||||
platypush/responses/ping.rst
|
|
||||||
platypush/responses/printer.cups.rst
|
platypush/responses/printer.cups.rst
|
||||||
platypush/responses/qrcode.rst
|
platypush/responses/qrcode.rst
|
||||||
platypush/responses/ssh.rst
|
platypush/responses/ssh.rst
|
||||||
|
|
|
@ -1,127 +0,0 @@
|
||||||
from typing import Iterable, Dict, Union, Any
|
|
||||||
|
|
||||||
from watchdog.observers import Observer
|
|
||||||
|
|
||||||
from platypush.backend import Backend
|
|
||||||
from .entities.handlers import EventHandler
|
|
||||||
from .entities.resources import MonitoredResource, MonitoredPattern, MonitoredRegex
|
|
||||||
|
|
||||||
|
|
||||||
class FileMonitorBackend(Backend):
|
|
||||||
"""
|
|
||||||
This backend monitors changes to local files and directories using the Watchdog API.
|
|
||||||
"""
|
|
||||||
|
|
||||||
class EventHandlerFactory:
|
|
||||||
"""
|
|
||||||
Create a file system event handler from a string, dictionary or ``MonitoredResource`` resource.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def from_resource(
|
|
||||||
resource: Union[str, Dict[str, Any], MonitoredResource]
|
|
||||||
) -> EventHandler:
|
|
||||||
if isinstance(resource, str):
|
|
||||||
resource = MonitoredResource(resource)
|
|
||||||
elif isinstance(resource, dict):
|
|
||||||
if 'regexes' in resource or 'ignore_regexes' in resource:
|
|
||||||
resource = MonitoredRegex(**resource)
|
|
||||||
elif (
|
|
||||||
'patterns' in resource
|
|
||||||
or 'ignore_patterns' in resource
|
|
||||||
or 'ignore_directories' in resource
|
|
||||||
):
|
|
||||||
resource = MonitoredPattern(**resource)
|
|
||||||
else:
|
|
||||||
resource = MonitoredResource(**resource)
|
|
||||||
|
|
||||||
return EventHandler.from_resource(resource)
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self, paths: Iterable[Union[str, Dict[str, Any], MonitoredResource]], **kwargs
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
:param paths: List of paths to monitor. Paths can either be expressed in any of the following ways:
|
|
||||||
|
|
||||||
- Simple strings. In this case, paths will be interpreted as absolute references to a file or a directory
|
|
||||||
to monitor. Example:
|
|
||||||
|
|
||||||
.. code-block:: yaml
|
|
||||||
|
|
||||||
backend.file.monitor:
|
|
||||||
paths:
|
|
||||||
# Monitor changes on the /tmp folder
|
|
||||||
- /tmp
|
|
||||||
# Monitor changes on /etc/passwd
|
|
||||||
- /etc/passwd
|
|
||||||
|
|
||||||
- Path with monitoring properties expressed as a key-value object. Example showing the supported attributes:
|
|
||||||
|
|
||||||
.. code-block:: yaml
|
|
||||||
|
|
||||||
backend.file.monitor:
|
|
||||||
paths:
|
|
||||||
# Monitor changes on the /tmp folder and its subfolders
|
|
||||||
- path: /tmp
|
|
||||||
recursive: True
|
|
||||||
|
|
||||||
- Path with pattern-based search criteria for the files to monitor and exclude. Example:
|
|
||||||
|
|
||||||
.. code-block:: yaml
|
|
||||||
|
|
||||||
backend.file.monitor:
|
|
||||||
paths:
|
|
||||||
# Recursively monitor changes on the ~/my-project folder that include all
|
|
||||||
# *.py files, excluding those whose name starts with tmp_ and
|
|
||||||
# all the files contained in the __pycache__ folders
|
|
||||||
- path: ~/my-project
|
|
||||||
recursive: True
|
|
||||||
patterns:
|
|
||||||
- "*.py"
|
|
||||||
ignore_patterns:
|
|
||||||
- "tmp_*"
|
|
||||||
ignore_directories:
|
|
||||||
- "__pycache__"
|
|
||||||
|
|
||||||
- Path with regex-based search criteria for the files to monitor and exclude. Example:
|
|
||||||
|
|
||||||
.. code-block:: yaml
|
|
||||||
|
|
||||||
backend.file.monitor:
|
|
||||||
paths:
|
|
||||||
# Recursively monitor changes on the ~/my-images folder that include all
|
|
||||||
# the files matching a JPEG extension in case-insensitive mode,
|
|
||||||
# excluding those whose name starts with tmp_ and
|
|
||||||
# all the files contained in the __MACOSX folders
|
|
||||||
- path: ~/my-images
|
|
||||||
recursive: True
|
|
||||||
case_sensitive: False
|
|
||||||
regexes:
|
|
||||||
- '.*\\.jpe?g$'
|
|
||||||
ignore_patterns:
|
|
||||||
- '^tmp_.*'
|
|
||||||
ignore_directories:
|
|
||||||
- '__MACOSX'
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
super().__init__(**kwargs)
|
|
||||||
self._observer = Observer()
|
|
||||||
|
|
||||||
for path in paths:
|
|
||||||
handler = self.EventHandlerFactory.from_resource(path)
|
|
||||||
self._observer.schedule(
|
|
||||||
handler, handler.resource.path, recursive=handler.resource.recursive
|
|
||||||
)
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
super().run()
|
|
||||||
self.logger.info('Initializing file monitor backend')
|
|
||||||
self._observer.start()
|
|
||||||
self.wait_stop()
|
|
||||||
|
|
||||||
def on_stop(self):
|
|
||||||
self.logger.info('Stopping file monitor backend')
|
|
||||||
self._observer.stop()
|
|
||||||
self._observer.join()
|
|
||||||
self.logger.info('Stopped file monitor backend')
|
|
|
@ -1,18 +0,0 @@
|
||||||
manifest:
|
|
||||||
events:
|
|
||||||
platypush.message.event.file.FileSystemCreateEvent: if a resource is created.
|
|
||||||
platypush.message.event.file.FileSystemDeleteEvent: if a resource is removed.
|
|
||||||
platypush.message.event.file.FileSystemModifyEvent: if a resource is modified.
|
|
||||||
install:
|
|
||||||
apk:
|
|
||||||
- py3-watchdog
|
|
||||||
apt:
|
|
||||||
- python3-watchdog
|
|
||||||
dnf:
|
|
||||||
- python-watchdog
|
|
||||||
pacman:
|
|
||||||
- python-watchdog
|
|
||||||
pip:
|
|
||||||
- watchdog
|
|
||||||
package: platypush.backend.file.monitor
|
|
||||||
type: backend
|
|
|
@ -382,6 +382,30 @@ backend.http:
|
||||||
# - https://api.quantamagazine.org/feed/
|
# - https://api.quantamagazine.org/feed/
|
||||||
###
|
###
|
||||||
|
|
||||||
|
###
|
||||||
|
# # The file monitor plugin can be used to track modifications to the
|
||||||
|
# # filesystem - for example, when a file or a directory is modified, created
|
||||||
|
# # or removed.
|
||||||
|
#
|
||||||
|
# file.monitor:
|
||||||
|
# paths:
|
||||||
|
# # Recursively monitor changes on the
|
||||||
|
# # ~/my-images folder that include all the files
|
||||||
|
# # matching a JPEG extension in case-insensitive
|
||||||
|
# # mode, excluding those whose name starts with
|
||||||
|
# # tmp_ and all the files contained in the
|
||||||
|
# # __MACOSX folders
|
||||||
|
# - path: ~/my-images
|
||||||
|
# recursive: true
|
||||||
|
# case_sensitive: false
|
||||||
|
# regexes:
|
||||||
|
# - '.*\\.jpe?g$'
|
||||||
|
# ignore_patterns:
|
||||||
|
# - '^tmp_.*'
|
||||||
|
# ignore_directories:
|
||||||
|
# - '__MACOSX'
|
||||||
|
###
|
||||||
|
|
||||||
###
|
###
|
||||||
# # Example configuration of a weather plugin
|
# # Example configuration of a weather plugin
|
||||||
#
|
#
|
||||||
|
|
140
platypush/plugins/file/monitor/__init__.py
Normal file
140
platypush/plugins/file/monitor/__init__.py
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
from typing import Iterable, Dict, Optional, Union, Any
|
||||||
|
|
||||||
|
from watchdog.observers import Observer
|
||||||
|
|
||||||
|
from platypush.plugins import RunnablePlugin
|
||||||
|
|
||||||
|
from .entities.handlers import EventHandler
|
||||||
|
from .entities.resources import MonitoredResource, MonitoredPattern, MonitoredRegex
|
||||||
|
|
||||||
|
|
||||||
|
def event_handler_from_resource(
|
||||||
|
resource: Union[str, Dict[str, Any], MonitoredResource]
|
||||||
|
) -> Optional[EventHandler]:
|
||||||
|
"""
|
||||||
|
Create a file system event handler from a string, dictionary or
|
||||||
|
``MonitoredResource`` resource.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if isinstance(resource, str):
|
||||||
|
res = MonitoredResource(resource)
|
||||||
|
elif isinstance(resource, dict):
|
||||||
|
if 'regexes' in resource or 'ignore_regexes' in resource:
|
||||||
|
res = MonitoredRegex(**resource)
|
||||||
|
elif (
|
||||||
|
'patterns' in resource
|
||||||
|
or 'ignore_patterns' in resource
|
||||||
|
or 'ignore_directories' in resource
|
||||||
|
):
|
||||||
|
res = MonitoredPattern(**resource)
|
||||||
|
else:
|
||||||
|
res = MonitoredResource(**resource)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return EventHandler.from_resource(res)
|
||||||
|
|
||||||
|
|
||||||
|
class FileMonitorPlugin(RunnablePlugin):
|
||||||
|
"""
|
||||||
|
A plugin to monitor changes to files and directories.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, paths: Iterable[Union[str, Dict[str, Any], MonitoredResource]], **kwargs
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
:param paths: List of paths to monitor. Paths can either be expressed
|
||||||
|
in any of the following ways:
|
||||||
|
|
||||||
|
- Simple strings. In this case, paths will be interpreted as
|
||||||
|
absolute references to a file or a directory to monitor.
|
||||||
|
Example:
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
file.monitor:
|
||||||
|
paths:
|
||||||
|
# Monitor changes on the /tmp folder
|
||||||
|
- /tmp
|
||||||
|
# Monitor changes on /etc/passwd
|
||||||
|
- /etc/passwd
|
||||||
|
|
||||||
|
- Path with monitoring properties expressed as a key-value
|
||||||
|
object. Example showing the supported attributes:
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
file.monitor:
|
||||||
|
paths:
|
||||||
|
# Monitor changes on the /tmp folder and its
|
||||||
|
# subfolders
|
||||||
|
- path: /tmp
|
||||||
|
recursive: True
|
||||||
|
|
||||||
|
- Path with pattern-based search criteria for the files to monitor
|
||||||
|
and exclude. Example:
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
file.monitor:
|
||||||
|
paths:
|
||||||
|
# Recursively monitor changes on the
|
||||||
|
# ~/my-project folder that include all *.py
|
||||||
|
# files, excluding those whose name starts with
|
||||||
|
# tmp_ and all the files contained in the
|
||||||
|
# __pycache__ folders
|
||||||
|
- path: ~/my-project
|
||||||
|
recursive: True
|
||||||
|
patterns:
|
||||||
|
- "*.py"
|
||||||
|
ignore_patterns:
|
||||||
|
- "tmp_*"
|
||||||
|
ignore_directories:
|
||||||
|
- "__pycache__"
|
||||||
|
|
||||||
|
- Path with regex-based search criteria for the files to
|
||||||
|
monitor and exclude. Example:
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
file.monitor:
|
||||||
|
paths:
|
||||||
|
# Recursively monitor changes on the
|
||||||
|
# ~/my-images folder that include all the files
|
||||||
|
# matching a JPEG extension in case-insensitive
|
||||||
|
# mode, excluding those whose name starts with
|
||||||
|
# tmp_ and all the files contained in the
|
||||||
|
# __MACOSX folders
|
||||||
|
- path: ~/my-images
|
||||||
|
recursive: True
|
||||||
|
case_sensitive: False
|
||||||
|
regexes:
|
||||||
|
- '.*\\.jpe?g$'
|
||||||
|
ignore_patterns:
|
||||||
|
- '^tmp_.*'
|
||||||
|
ignore_directories:
|
||||||
|
- '__MACOSX'
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self._observer = Observer()
|
||||||
|
|
||||||
|
for path in paths:
|
||||||
|
handler = event_handler_from_resource(path)
|
||||||
|
if not handler:
|
||||||
|
continue
|
||||||
|
|
||||||
|
self._observer.schedule(
|
||||||
|
handler, handler.resource.path, recursive=handler.resource.recursive
|
||||||
|
)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self._observer.stop()
|
||||||
|
self._observer.join()
|
||||||
|
super().stop()
|
||||||
|
|
||||||
|
def main(self):
|
||||||
|
self._observer.start()
|
||||||
|
self.wait_stop()
|
|
@ -1,17 +1,31 @@
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from watchdog.events import FileSystemEventHandler, PatternMatchingEventHandler, RegexMatchingEventHandler
|
from watchdog.events import (
|
||||||
|
FileSystemEventHandler,
|
||||||
|
PatternMatchingEventHandler,
|
||||||
|
RegexMatchingEventHandler,
|
||||||
|
)
|
||||||
|
|
||||||
from platypush.backend.file.monitor.entities.resources import MonitoredResource, MonitoredPattern, MonitoredRegex
|
|
||||||
from platypush.context import get_bus
|
from platypush.context import get_bus
|
||||||
from platypush.message.event.file import FileSystemModifyEvent, FileSystemCreateEvent, FileSystemDeleteEvent
|
from platypush.message.event.file import (
|
||||||
|
FileSystemModifyEvent,
|
||||||
|
FileSystemCreateEvent,
|
||||||
|
FileSystemDeleteEvent,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .resources import (
|
||||||
|
MonitoredResource,
|
||||||
|
MonitoredPattern,
|
||||||
|
MonitoredRegex,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class EventHandler(FileSystemEventHandler):
|
class EventHandler(FileSystemEventHandler):
|
||||||
"""
|
"""
|
||||||
Base class for Watchdog event handlers.
|
Base class for Watchdog event handlers.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, resource: MonitoredResource, **kwargs):
|
def __init__(self, resource: MonitoredResource, **kwargs):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
resource.path = os.path.expanduser(resource.path)
|
resource.path = os.path.expanduser(resource.path)
|
||||||
|
@ -20,7 +34,8 @@ class EventHandler(FileSystemEventHandler):
|
||||||
def _should_ignore_event(self, event) -> bool:
|
def _should_ignore_event(self, event) -> bool:
|
||||||
ignore_dirs = [
|
ignore_dirs = [
|
||||||
os.path.expanduser(
|
os.path.expanduser(
|
||||||
_dir if os.path.expanduser(_dir).strip('/').startswith(self.resource.path)
|
_dir
|
||||||
|
if os.path.expanduser(_dir).strip('/').startswith(self.resource.path)
|
||||||
else os.path.join(self.resource.path, _dir)
|
else os.path.join(self.resource.path, _dir)
|
||||||
)
|
)
|
||||||
for _dir in getattr(self.resource, 'ignore_directories', [])
|
for _dir in getattr(self.resource, 'ignore_directories', [])
|
||||||
|
@ -41,8 +56,7 @@ class EventHandler(FileSystemEventHandler):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if ignore_regexes and any(
|
if ignore_regexes and any(
|
||||||
re.match(regex, event.src_path)
|
re.match(regex, event.src_path) for regex in (ignore_patterns or [])
|
||||||
for regex in ignore_patterns
|
|
||||||
):
|
):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -51,7 +65,9 @@ class EventHandler(FileSystemEventHandler):
|
||||||
def _on_event(self, event, output_event_type):
|
def _on_event(self, event, output_event_type):
|
||||||
if self._should_ignore_event(event):
|
if self._should_ignore_event(event):
|
||||||
return
|
return
|
||||||
get_bus().post(output_event_type(path=event.src_path, is_directory=event.is_directory))
|
get_bus().post(
|
||||||
|
output_event_type(path=event.src_path, is_directory=event.is_directory)
|
||||||
|
)
|
||||||
|
|
||||||
def on_created(self, event):
|
def on_created(self, event):
|
||||||
self._on_event(event, FileSystemCreateEvent)
|
self._on_event(event, FileSystemCreateEvent)
|
||||||
|
@ -62,7 +78,7 @@ class EventHandler(FileSystemEventHandler):
|
||||||
def on_modified(self, event):
|
def on_modified(self, event):
|
||||||
self._on_event(event, FileSystemModifyEvent)
|
self._on_event(event, FileSystemModifyEvent)
|
||||||
|
|
||||||
def on_moved(self, event):
|
def on_moved(self, _):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -78,21 +94,27 @@ class PatternEventHandler(EventHandler, PatternMatchingEventHandler):
|
||||||
"""
|
"""
|
||||||
Event handler for file patterns.
|
Event handler for file patterns.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, resource: MonitoredPattern):
|
def __init__(self, resource: MonitoredPattern):
|
||||||
super().__init__(resource=resource,
|
super().__init__(
|
||||||
|
resource=resource,
|
||||||
patterns=resource.patterns,
|
patterns=resource.patterns,
|
||||||
ignore_patterns=resource.ignore_patterns,
|
ignore_patterns=resource.ignore_patterns,
|
||||||
ignore_directories=resource.ignore_directories,
|
ignore_directories=resource.ignore_directories,
|
||||||
case_sensitive=resource.case_sensitive)
|
case_sensitive=resource.case_sensitive,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class RegexEventHandler(EventHandler, RegexMatchingEventHandler):
|
class RegexEventHandler(EventHandler, RegexMatchingEventHandler):
|
||||||
"""
|
"""
|
||||||
Event handler for regex-based file patterns.
|
Event handler for regex-based file patterns.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, resource: MonitoredRegex):
|
def __init__(self, resource: MonitoredRegex):
|
||||||
super().__init__(resource=resource,
|
super().__init__(
|
||||||
|
resource=resource,
|
||||||
regexes=resource.regexes,
|
regexes=resource.regexes,
|
||||||
ignore_regexes=resource.ignore_regexes,
|
ignore_regexes=resource.ignore_regexes,
|
||||||
ignore_directories=resource.ignore_directories,
|
ignore_directories=resource.ignore_directories,
|
||||||
case_sensitive=resource.case_sensitive)
|
case_sensitive=resource.case_sensitive,
|
||||||
|
)
|
18
platypush/plugins/file/monitor/manifest.yaml
Normal file
18
platypush/plugins/file/monitor/manifest.yaml
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
manifest:
|
||||||
|
events:
|
||||||
|
- platypush.message.event.file.FileSystemCreateEvent
|
||||||
|
- platypush.message.event.file.FileSystemDeleteEvent
|
||||||
|
- platypush.message.event.file.FileSystemModifyEvent
|
||||||
|
install:
|
||||||
|
apk:
|
||||||
|
- py3-watchdog
|
||||||
|
apt:
|
||||||
|
- python3-watchdog
|
||||||
|
dnf:
|
||||||
|
- python-watchdog
|
||||||
|
pacman:
|
||||||
|
- python-watchdog
|
||||||
|
pip:
|
||||||
|
- watchdog
|
||||||
|
package: platypush.plugins.file.monitor
|
||||||
|
type: plugin
|
Loading…
Reference in a new issue