[
  {
    "path": "LICENSE",
    "content": "Copyright (c) 2010 Greggory Hernandez\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE."
  },
  {
    "path": "README.md",
    "content": "See jobs.yml for proper configuration syntax\n\nDependencies: python, python-pyinotify, python-yaml\n\nIn Ubuntu (and Debian):\n\n\tsudo apt-get install python python-pyinotify python-yaml\n\nmake sure watcher.py is marked as executable\n\n\tchmod +x watcher.py\n\n\nstart the daemon with:\n\n\t./watcher.py start\n\n\nstop it with:\n\n\t./watcher.py stop\n\n\nrestart it with:\n\n\t./watcher.py restart\n\n\nThe first time you start it (if you haven't done it yourself) it will\ncreate ~/.watcher and ~/.watcher/jobs.yml and then it will yell at\nyou. You need to edit ~/.watcher/jobs.yml to setup folders to watch.\nYou'll find a jobs.yml in the same directory as this README. Use that\nas an example. It should be pretty simple.\n\nIf you edit ~/.watcher/jobs.yml you must restart the daemon for it to\nreload the configuration file. It'd make sense for me to set up\nwatcher to watch the config file. That'll be coming soon.\n\nProblems? greggory.hz@gmail.com\n\nHave fun.\n"
  },
  {
    "path": "jobs.yml",
    "content": "# Copyright (c) 2010 Greggory Hernandez\n\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to deal\n# in the Software without restriction, including without limitation the rights\n# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n\n# The above copyright notice and this permission notice shall be included in\n# all copies or substantial portions of the Software.\n\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n# THE SOFTWARE.\n\n# ---------------------------END COPYRIGHT--------------------------------------\n\n# ---------------------------Eclusion feature added by Brunus ---------------------------------\n# Hi added an exclusion feature, something realy needed to monitor a website tree for exemple\n# brunus.v@gmail.com or @brunus_V on tweeter\n# ---------------------------------------------------------------------------------------------\n\n# This is a sample jobs file. Yours should go in ~/.watcher/jobs.yml\n# if you run watcher.py start, this file and folder will be created\n\njob1:\n  # a generic label for a job.  Currently not used make it whatever you want\n  label: Watch /var/www for added or removed files\n\n  # directory or file to watch.  Probably should be abs path.\n  watch: /var/www\n  # directories or files to exclude\n  exclude: ['/var/www/site1/cache', '/var/www/site2/cache']\n\n  # list of events to watch for.\n  # supported events:\n  # 'access' - File was accessed (read) (*)\n  # 'atrribute_change' - Metadata changed (permissions, timestamps, extended attributes, etc.) (*)\n  # 'write_close' - File opened for writing was closed (*)\n  # 'nowrite_close' - File not opened for writing was closed (*)\n  # 'create' - File/directory created in watched directory (*)\n  # 'delete' - File/directory deleted from watched directory (*)\n  # 'self_delete' - Watched file/directory was itself deleted\n  # 'modify' - File was modified (*)\n  # 'self_move' - Watched file/directory was itself moved\n  # 'move_from' - File moved out of watched directory (*)\n  # 'move_to' - File moved into watched directory (*)\n  # 'open' - File was opened (*)\n  # 'all' - Any of the above events are fired\n  # 'move' - A combination of 'move_from' and 'move_to'\n  # 'close' - A combination of 'write_close' and 'nowrite_close'\n  #\n  # When monitoring a directory, the events marked with an asterisk (*) above\n  # can occur for files in the directory, in which case the name field in the\n  # returned event data identifies the name of the file within the directory.\n  events: ['create', 'move_from', 'move_to', 'delete', 'modify']\n\n  # TODO:\n  # this currently isn't implemented, but this is where support will be added for:\n  # IN_DONT_FOLLOW, IN_ONESHOT, IN_ONLYDIR and IN_NO_LOOP\n  # There will be further documentation on these once they are implmented\n  options: []\n\n  # if true, watcher will monitor directories recursively for changes\n  recursive: true\n\n  # the command to run. Can be any command. It's run as whatever user started watcher.\n  # The following wildards may be used inside command specification:\n  # $$ dollar sign\n  # $watched watched filesystem path (see above)\n  # $filename event-related file name\n  # $tflags event flags (textually)\n  # $nflags event flags (numerically)\n  # $dest_file this will manage recursion better if included as the dest (especially when copying or similar)\n  #     if $dest_file was left out of the command below, Watcher won't properly\n  #     handle newly created directories when watching recursively. It's fine\n  #     to leave out when recursive is false or you won't be creating new\n  #     directories.\n  # $src_path is only used in move_to and is the corresponding path from move_from\n  # $src_rel_path [needs doc]\n  # $datetime output date and time of the event, format is : Y-m-d H:M:S\n  # command: cp -r $filename /home/user/Documents/$dest_file # $src_path\n  command: echo $datetime $filename $tflags # $src_path\n"
  },
  {
    "path": "watcher.py",
    "content": "#!/usr/bin/env python3\n# Copyright (c) 2010 Greggory Hernandez\n\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to deal\n# in the Software without restriction, including without limitation the rights\n# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n\n# The above copyright notice and this permission notice shall be included in\n# all copies or substantial portions of the Software.\n\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n# THE SOFTWARE.\n\n### BEGIN INIT INFO\n# Provides:          watcher.py\n# Required-Start:    $remote_fs $syslog\n# Required-Stop:     $remote_fs $syslog\n# Default-Start:     2 3 4 5\n# Default-Stop:      0 1 6\n# Short-Description: Monitor directories for file changes\n# Description:       Monitor directories specified in /etc/watcher.ini for\n#                    changes using the Kernel's inotify mechanism and run\n#                    jobs when files or directories change\n### END INIT INFO\n\nimport sys, os, time, atexit\nfrom signal import SIGTERM\nimport pyinotify\nimport sys, os\nimport datetime\nimport subprocess\nfrom types import *\nfrom string import Template\nimport configparser\nimport argparse\n\nclass Daemon:\n    \"\"\"\n    A generic daemon class\n\n    Usage: subclass the Daemon class and override the run method\n    \"\"\"\n    def __init__(self, pidfile, stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'):\n        self.stdin = stdin\n        self.stdout = stdout\n        self.stderr = stderr\n        self.pidfile = pidfile\n\n    def daemonize(self):\n        \"\"\"\n        do the UNIX double-fork magic, see Stevens' \"Advanced Programming in the\n        UNIX Environment\" for details (ISBN 0201563177)\n        http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16\n        \"\"\"\n        try:\n            pid = os.fork()\n            if pid > 0:\n                #exit first parent\n                sys.exit(0)\n        except OSError as e:\n            sys.stderr.write(\"fork #1 failed: %d (%s)\\n\" % (e.errno, e.strerror))\n            sys.exit(1)\n\n        # decouple from parent environment\n        os.chdir(\"/\")\n        os.setsid()\n        os.umask(0)\n\n        # do second fork\n        try:\n            pid = os.fork()\n            if pid > 0:\n                # exit from second parent\n                sys.exit(0)\n        except OSError as e:\n            sys.stderr.write(\"fork #2 failed: %d (%s)\\n\" % (e.errno, e.strerror))\n            sys.exit(1)\n\n        #redirect standard file descriptors\n        sys.stdout.flush()\n        sys.stderr.flush()\n        si = open(self.stdin, 'r')\n        so = open(self.stdout, 'wb')\n        se = open(self.stderr, 'wb', 0)\n        os.dup2(si.fileno(), sys.stdin.fileno())\n        os.dup2(so.fileno(), sys.stdout.fileno())\n        os.dup2(se.fileno(), sys.stderr.fileno())\n\n        #write pid file\n        atexit.register(self.delpid)\n        pid = str(os.getpid())\n        open(self.pidfile, 'w+').write(\"%s\\n\" % pid)\n\n    def delpid(self):\n        os.remove(self.pidfile)\n\n    def start(self):\n        \"\"\"\n        Start the daemon\n        \"\"\"\n        # Check for a pidfile to see if the daemon already runs\n        try:\n            pf = open(self.pidfile, 'r')\n            pid = int(pf.read().strip())\n            pf.close()\n        except IOError:\n            pid = None\n\n        if pid:\n            message = \"pidfile %s already exists. Daemon already running?\\n\"\n            sys.stderr.write(message % self.pidfile)\n            sys.exit(1)\n\n        # Start the Daemon\n        self.daemonize()\n        self.run()\n\n    def stop(self):\n        \"\"\"\n        Stop the daemon\n        \"\"\"\n        # get the pid from the pidfile\n        try:\n            pf = open(self.pidfile, 'r')\n            pid = int(pf.read().strip())\n            pf.close()\n        except IOError:\n            pid = None\n\n        if not pid:\n            message = \"pidfile %s does not exist. Daemon not running?\\n\"\n            sys.stderr.write(message % self.pidfile)\n            return # not an error in a restart\n\n        # Try killing the daemon process\n        try:\n            while 1:\n                os.kill(pid, SIGTERM)\n                time.sleep(0.1)\n        except OSError as err:\n            err = str(err)\n            if err.find(\"No such process\") > 0:\n                if os.path.exists(self.pidfile):\n                    os.remove(self.pidfile)\n            else:\n                print(str(err))\n                sys.exit(1)\n\n    def restart(self):\n        \"\"\"\n        Restart the daemon\n        \"\"\"\n        self.stop()\n        self.start()\n\n    def status(self):\n        try:\n            pf = open(self.pidfile, 'r')\n            pid = int(pf.read().strip())\n            pf.close()\n        except IOError:\n            pid = None\n            \n        if pid:\n            print(\"service running\")\n            sys.exit(0)\n        if not pid:\n            print(\"service not running\")\n            sys.exit(3)\n\n    def run(self):\n        \"\"\"\n        You should override this method when you subclass Daemon. It will be called after the process has been\n        daemonized by start() or restart().\n        \"\"\"\n\nclass EventHandler(pyinotify.ProcessEvent):\n    def __init__(self, command):\n        pyinotify.ProcessEvent.__init__(self)\n        self.command = command\n\n    # from http://stackoverflow.com/questions/35817/how-to-escape-os-system-calls-in-python\n    def shellquote(self,s):\n        s = str(s)\n        return \"'\" + s.replace(\"'\", \"'\\\\''\") + \"'\"\n\n    def runCommand(self, event):\n        t = Template(self.command)\n        command = t.substitute(watched=self.shellquote(event.path),\n                               filename=self.shellquote(event.pathname),\n                               tflags=self.shellquote(event.maskname),\n                               nflags=self.shellquote(event.mask),\n                               cookie=self.shellquote(event.cookie if hasattr(event, \"cookie\") else 0))\n        try:\n            os.system(command)\n        except OSError as err:\n            print(\"Failed to run command '%s' %s\" % (command, str(err)))\n\n    def process_IN_ACCESS(self, event):\n        print(\"Access: \", event.pathname)\n        self.runCommand(event)\n\n    def process_IN_ATTRIB(self, event):\n        print(\"Attrib: \", event.pathname)\n        self.runCommand(event)\n\n    def process_IN_CLOSE_WRITE(self, event):\n        print(\"Close write: \", event.pathname)\n        self.runCommand(event)\n\n    def process_IN_CLOSE_NOWRITE(self, event):\n        print(\"Close nowrite: \", event.pathname)\n        self.runCommand(event)\n\n    def process_IN_CREATE(self, event):\n        print(\"Creating: \", event.pathname)\n        self.runCommand(event)\n\n    def process_IN_DELETE(self, event):\n        print(\"Deleteing: \", event.pathname)\n        self.runCommand(event)\n\n    def process_IN_MODIFY(self, event):\n        print(\"Modify: \", event.pathname)\n        self.runCommand(event)\n\n    def process_IN_MOVE_SELF(self, event):\n        print(\"Move self: \", event.pathname)\n        self.runCommand(event)\n\n    def process_IN_MOVED_FROM(self, event):\n        print(\"Moved from: \", event.pathname)\n        self.runCommand(event)\n\n    def process_IN_MOVED_TO(self, event):\n        print(\"Moved to: \", event.pathname)\n        self.runCommand(event)\n\n    def process_IN_OPEN(self, event):\n        print(\"Opened: \", event.pathname)\n        self.runCommand(event)\n\nclass WatcherDaemon(Daemon):\n\n    def __init__(self, config):\n        self.stdin   = '/dev/null'\n        self.stdout  = config.get('DEFAULT','logfile')\n        self.stderr  = config.get('DEFAULT','logfile')\n        self.pidfile = config.get('DEFAULT','pidfile')\n        self.config  = config\n\n    def run(self):\n        log('Daemon started')\n        wdds      = []\n        notifiers = []\n\n        # read jobs from config file\n        for section in self.config.sections():\n            log(section + \": \" + self.config.get(section,'watch'))\n            # get the basic config info\n            mask      = self._parseMask(self.config.get(section,'events').split(','))\n            folder    = self.config.get(section,'watch')\n            recursive = self.config.getboolean(section,'recursive')\n            autoadd   = self.config.getboolean(section,'autoadd')\n            excluded  = self.config.get(section,'excluded')\n            command   = self.config.get(section,'command')\n\n            # Exclude directories right away if 'excluded' regexp is set\n            # Example https://github.com/seb-m/pyinotify/blob/master/python2/examples/exclude.py\n            if excluded.strip() == '':   # if 'excluded' is empty or whitespaces only\n                excl = None\n            else:\n                excl = pyinotify.ExcludeFilter(excluded.split(','))\n\n            wm = pyinotify.WatchManager()\n            handler = EventHandler(command)\n\n            wdds.append(wm.add_watch(folder, mask, rec=recursive, auto_add=autoadd, exclude_filter=excl))\n\n            # BUT we need a new ThreadNotifier so I can specify a different\n            # EventHandler instance for each job\n            # this means that each job has its own thread as well (I think)\n            notifiers.append(pyinotify.ThreadedNotifier(wm, handler))\n\n        # now we need to start ALL the notifiers.\n        # TODO: load test this ... is having a thread for each a problem?\n        for notifier in notifiers:\n            notifier.start()\n\n\n    def _parseMask(self, masks):\n        ret = False;\n\n        for mask in masks:\n            mask = mask.strip()\n\n            if 'access' == mask:\n                ret = self._addMask(pyinotify.IN_ACCESS, ret)\n            elif 'attribute_change' == mask:\n                ret = self._addMask(pyinotify.IN_ATTRIB, ret)\n            elif 'write_close' == mask:\n                ret = self._addMask(pyinotify.IN_CLOSE_WRITE, ret)\n            elif 'nowrite_close' == mask:\n                ret = self._addMask(pyinotify.IN_CLOSE_NOWRITE, ret)\n            elif 'create' == mask:\n                ret = self._addMask(pyinotify.IN_CREATE, ret)\n            elif 'delete' == mask:\n                ret = self._addMask(pyinotify.IN_DELETE, ret)\n            elif 'self_delete' == mask:\n                ret = self._addMask(pyinotify.IN_DELETE_SELF, ret)\n            elif 'modify' == mask:\n                ret = self._addMask(pyinotify.IN_MODIFY, ret)\n            elif 'self_move' == mask:\n                ret = self._addMask(pyinotify.IN_MOVE_SELF, ret)\n            elif 'move_from' == mask:\n                ret = self._addMask(pyinotify.IN_MOVED_FROM, ret)\n            elif 'move_to' == mask:\n                ret = self._addMask(pyinotify.IN_MOVED_TO, ret)\n            elif 'open' == mask:\n                ret = self._addMask(pyinotify.IN_OPEN, ret)\n            elif 'all' == mask:\n                m = pyinotify.IN_ACCESS | pyinotify.IN_ATTRIB | pyinotify.IN_CLOSE_WRITE | \\\n                    pyinotify.IN_CLOSE_NOWRITE | pyinotify.IN_CREATE | pyinotify.IN_DELETE | \\\n                    pyinotify.IN_DELETE_SELF | pyinotify.IN_MODIFY | pyinotify.IN_MOVE_SELF | \\\n                    pyinotify.IN_MOVED_FROM | pyinotify.IN_MOVED_TO | pyinotify.IN_OPEN\n                ret = self._addMask(m, ret)\n            elif 'move' == mask:\n                ret = self._addMask(pyinotify.IN_MOVED_FROM | pyinotify.IN_MOVED_TO, ret)\n            elif 'close' == mask:\n                ret = self._addMask(pyinotify.IN_CLOSE_WRITE | pyinotify.IN_CLOSE_NOWRITE, ret)\n\n        return ret\n\n    def _addMask(self, new_option, current_options):\n        if not current_options:\n            return new_option\n        else:\n            return current_options | new_option\n\n\n\ndef log(msg):\n    sys.stdout.write(\"%s %s\\n\" % ( str(datetime.datetime.now()), msg ))\n\n\nif __name__ == \"__main__\":\n    # Parse commandline arguments\n    parser = argparse.ArgumentParser(\n                description='A daemon to monitor changes within specified directories and run commands on these changes.',\n             )\n    parser.add_argument('-c','--config',\n                        action='store',\n                        help='Path to the config file (default: %(default)s)')\n    parser.add_argument('command',\n                        action='store',\n                        choices=['start','stop','restart','status','debug'],\n                        help='What to do. Use debug to start in the foreground')\n    args = parser.parse_args()\n\n    # Parse the config file\n    config = configparser.ConfigParser()\n    if(args.config):\n        confok = config.read(args.config)\n    else:\n        confok = config.read(['/etc/watcher.ini', os.path.expanduser('~/.watcher.ini')]);\n\n    if(not confok):\n        sys.stderr.write(\"Failed to read config file. Try -c parameter\\n\")\n        sys.exit(4);\n\n    # Initialize the daemon\n    daemon = WatcherDaemon(config)\n\n    # Execute the command\n    if 'start' == args.command:\n        daemon.start()\n    elif 'stop' == args.command:\n        daemon.stop()\n    elif 'restart' == args.command:\n        daemon.restart()\n    elif 'status' == args.command:\n        daemon.status()\n    elif 'debug' == args.command:\n        daemon.run()\n    else:\n        print(\"Unkown Command\")\n        sys.exit(2)\n    sys.exit(0)\n"
  }
]