Permalink
Browse files

Add inosync

  • Loading branch information...
1 parent 30d1159 commit 6dbb756c934e2bae41f13b3a98e100d4b7d781d0 @nelhage committed Sep 17, 2012
Showing with 219 additions and 0 deletions.
  1. +219 −0 inosync
View
219 inosync
@@ -0,0 +1,219 @@
+#!/usr/bin/python
+# vim: set fileencoding=utf-8 ts=2 sw=2 expandtab :
+
+import os,sys
+import subprocess
+from optparse import OptionParser,make_option
+from time import sleep
+from pyinotify import *
+import logging
+
+__author__ = "Benedikt Böhm"
+__copyright__ = "Copyright (c) 2007-2008 Benedikt Böhm <bb@xnull.de>"
+__version__ = 0,2,1
+
+OPTION_LIST = [
+ make_option(
+ "-c", dest = "config",
+ default = "/etc/inosync/default.py",
+ metavar = "FILE",
+ help = "load configuration from FILE"),
+ make_option(
+ "-d", dest = "daemonize",
+ action = "store_true",
+ default = False,
+ help = "daemonize %prog"),
+ make_option(
+ "-p", dest = "pretend",
+ action = "store_true",
+ default = False,
+ help = "do not actually call rsync"),
+ make_option(
+ "-v", dest = "verbose",
+ action = "store_true",
+ default = False,
+ help = "print debugging information"),
+]
+
+ALL_EVENTS = [
+ "IN_ACCESS",
+ "IN_ATTRIB",
+ "IN_CLOSE_WRITE",
+ "IN_CLOSE_NOWRITE",
+ "IN_CREATE",
+ "IN_DELETE",
+ "IN_DELETE_SELF",
+ "IN_MODIFY",
+ "IN_MOVED_FROM",
+ "IN_MOVED_TO",
+ "IN_OPEN"
+]
+
+DEFAULT_EVENTS = [
+ "IN_CLOSE_WRITE",
+ "IN_CREATE",
+ "IN_DELETE",
+ "IN_MOVED_FROM",
+ "IN_MOVED_TO",
+ "IN_MODIFY"
+]
+
+class RsyncEvent(ProcessEvent):
+ pretend = None
+ dirty = True
+
+ def __init__(self, pretend=False):
+ self.pretend = pretend
+
+ def sync(self):
+ if not self.dirty:
+ return
+ logging.info("syncing...")
+ args = [config.rsync, "-ltrp", "--delete"]
+ args.extend(config.extra_opts)
+ args.append("--bwlimit=%s" % config.rspeed)
+ if config.logfile:
+ args.append("--log-file=%s" % config.logfile)
+ if "excludes" in dir(config):
+ for exclude in config.excludes:
+ args.append("--exclude=%s" % exclude)
+ args.append(config.wpath)
+ for node in config.rnodes:
+ nodecmd = args + [node]
+ if self.pretend:
+ logging.info("would execute `%s'" % (' '.join(nodecmd)))
+ else:
+ logging.debug("executing... `%s'" % (' '.join(nodecmd)))
+ subprocess.call(nodecmd)
+ self.dirty = False
+ logging.info("done")
+
+ def process_default(self, event):
+ logging.debug("caught %s on %s" % \
+ (event.maskname, event.pathname))
+ if not event.maskname in config.emask:
+ logging.debug("ignoring %s on %s" % \
+ (event.maskname, event.pathname))
+ return
+ self.dirty = True
+
+def daemonize():
+ try:
+ pid = os.fork()
+ except OSError, e:
+ raise Exception, "%s [%d]" % (e.strerror, e.errno)
+
+ if (pid == 0):
+ os.setsid()
+ try:
+ pid = os.fork()
+ except OSError, e:
+ raise Exception, "%s [%d]" % (e.strerror, e.errno)
+ if (pid == 0):
+ os.chdir('/')
+ os.umask(0)
+ else:
+ os._exit(0)
+ else:
+ os._exit(0)
+
+ os.open("/dev/null", os.O_RDWR)
+ os.dup2(0, 1)
+ os.dup2(0, 2)
+
+ return 0
+
+def load_config(filename):
+ if not os.path.isfile(filename):
+ raise RuntimeError, "configuration file does not exist: %s" % filename
+
+ configdir = os.path.dirname(filename)
+ configfile = os.path.basename(filename)
+
+ if configfile.endswith(".py"):
+ configfile = configfile[0:-3]
+
+ sys.path.append(configdir)
+ exec("import %s as __config__" % configfile)
+ sys.path.remove(configdir)
+
+ global config
+ config = __config__
+
+ if not "wpath" in dir(config):
+ raise RuntimeError, "no watch path given"
+ if not os.path.isdir(config.wpath):
+ raise RuntimeError, "watch path does not exist: %s" % config.wpath
+ if not os.path.isabs(config.wpath):
+ config.wpath = os.path.abspath(config.wpath)
+
+ if not "rnodes" in dir(config) or len(config.rnodes) < 1:
+ raise RuntimeError, "no remote nodes given"
+
+ if not "rspeed" in dir(config) or config.rspeed < 0:
+ config.rspeed = 0
+
+ if not "emask" in dir(config):
+ config.emask = DEFAULT_EVENTS
+ for event in config.emask:
+ if not event in ALL_EVENTS:
+ raise RuntimeError, "invalid inotify event: %s" % event
+
+ if not "edelay" in dir(config):
+ config.edelay = 10
+ if config.edelay < 1:
+ raise RuntimeError, "event delay needs to be greater than 1"
+
+ if not "logfile" in dir(config):
+ config.logfile = None
+
+ if not "rsync" in dir(config):
+ config.rsync = "/usr/bin/rsync"
+ if not os.path.isabs(config.rsync):
+ raise RuntimeError, "rsync path needs to be absolute"
+ if not os.path.isfile(config.rsync):
+ raise RuntimeError, "rsync binary does not exist: %s" % config.rsync
+ if not "extra_opts" in dir(config):
+ config.extra_opts = []
+
+ logging.basicConfig(level=logging.INFO)
+
+def main():
+ version = ".".join(map(str, __version__))
+ parser = OptionParser(option_list=OPTION_LIST,version="%prog " + version)
+ (options, args) = parser.parse_args()
+
+ if len(args) > 0:
+ parser.error("too many arguments")
+
+ load_config(options.config)
+
+ if options.daemonize:
+ daemonize()
+
+ wm = WatchManager()
+ ev = RsyncEvent(options.pretend)
+ notifier = Notifier(wm, ev)
+ wds = wm.add_watch(config.wpath, EventsCodes.ALL_FLAGS['ALL_EVENTS'],
+ rec = True, auto_add = True)
+
+ logging.debug("starting initial synchronization on %s" % config.wpath)
+ ev.sync()
+ logging.debug("initial synchronization on %s done" % config.wpath)
+ notifier._timeout = 0
+ logging.info("resuming normal operations on %s" % config.wpath)
+ while True:
+ try:
+ notifier.process_events()
+ if notifier.check_events():
+ notifier.read_events()
+ ev.sync()
+ sleep(config.edelay)
+ except KeyboardInterrupt:
+ notifier.stop()
+ break
+
+ sys.exit(0)
+
+if __name__ == "__main__":
+ main()

0 comments on commit 6dbb756

Please sign in to comment.