- apparently this switch enables the application by default
[smartoffice] / kde-cloudstorage / kde-integration / syncme
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 #
4 # Tool for maintaining synchronisation links between directories
5
6 import sys
7 import os
8 import signal
9 import errno
10 import time
11 import optparse
12 import xml.dom.minidom
13 import inotifyx
14
15 class Link:
16         def __init__(self, sourcedir, targetdir):
17                 self.sourcedir = sourcedir
18                 self.targetdir = targetdir
19                 self.versioned = None
20
21 class LinkCollection:
22         def __init__(self, configdir):
23                 self.configdir = configdir
24                 self.links = []
25
26         def load(self):
27                 config = os.path.join(self.configdir, "syncme-links.xml")
28                 try:
29                         f = open(config)
30                 except:
31                         return
32                 dom = xml.dom.minidom.parseString(" ".join(f.readlines()))
33                 f.close()
34
35                 if dom.documentElement.localName != "links":
36                         print >>sys.stderr, "Warning: Configuration file '%s' is misformatted." % config
37                         return
38
39                 linksxmllist = dom.documentElement.getElementsByTagName("link")
40                 for linksxml in linksxmllist:
41                         link = Link(linksxml.getAttribute("sourcedir"), linksxml.getAttribute("targetdir"))
42                         link.versioned = (linksxml.getAttribute("versioned") == "true")
43                         self.links.append(link)
44
45         def save(self):
46                 dom = xml.dom.minidom.Document()
47                 root = dom.createElement("links")
48                 dom.appendChild(root)
49                 for link in self.links:
50                         linkxml = dom.createElement("link")
51                         linkxml.setAttribute("sourcedir", link.sourcedir)
52                         linkxml.setAttribute("targetdir", link.targetdir)
53                         linkxml.setAttribute("versioned", ("false", "true")[int(link.versioned)])
54                         root.appendChild(linkxml)
55
56                 try:
57                         os.makedirs(self.configdir)
58                 except os.error, e:
59                         if e.errno != errno.EEXIST:
60                                 raise
61
62                 config = os.path.join(self.configdir, "syncme-links.xml")
63                 f = open(config, "w")
64                 dom.writexml(f, addindent=" ", newl="\n")
65                 f.close()
66
67 class Syncer():
68         def __init__(self, sourcedir, targetdir, versioned):
69                 self.sourcedir = sourcedir
70                 self.targetdir = targetdir
71                 # FIXME: versioning is ignored for now (as is adaptation etc.)
72
73         def sync(self):
74                 print "[syncme-daemon (%s->%s)] start" % (self.sourcedir, self.targetdir)
75                 rsynckde=False
76                 if os.getenv("DESKTOP_SESSION") == "kde-plasma":
77                         # FIXME: would be more flexible to just call and check exit status?
78                         if os.path.isfile("/usr/bin/rsync-kde") or os.path.isfile("/usr/local/bin/rsync-kde"):
79                                 rsynckde=True
80                 if rsynckde:
81                         os.system("rsync-kde '%s' '%s'" % (self.sourcedir, self.targetdir))
82                 else:
83                         os.system("rsync -az '%s' '%s'" % (self.sourcedir, self.targetdir))
84                 print "[syncme-daemon (%s->%s)] finish" % (self.sourcedir, self.targetdir)
85
86 class Daemon():
87         def __init__(self):
88                 pass
89
90         def launch(self):
91                 logfile = os.path.join(links.configdir, "daemon.log")
92                 log = open(logfile, "a")
93                 sys.stdout = log
94                 sys.stdout = os.fdopen(sys.stdout.fileno(), "w", 0)
95                 print "** Daemon launch at %s" % time.ctime()
96
97                 notifyeventmask = inotifyx.IN_DELETE | inotifyx.IN_CLOSE_WRITE
98                 notifier = inotifyx.init()
99                 configwatcher = inotifyx.add_watch(notifier, os.path.join(links.configdir, "syncme-links.xml"), notifyeventmask)
100
101                 syncers = {}
102                 watchers = {}
103                 for link in links.links:
104                         syncer = Syncer(link.sourcedir, link.targetdir, link.versioned)
105                         if not syncers.has_key(link.sourcedir):
106                                 syncers[link.sourcedir] = []
107                         syncers[link.sourcedir].append(syncer)
108                 for sourcedir in syncers.keys():
109                         sourcedirwatcher = inotifyx.add_watch(notifier, sourcedir, notifyeventmask)
110                         watchers[sourcedirwatcher] = sourcedir
111
112                 for syncer in sum(syncers.values(), []):
113                         syncer.sync()
114
115                 halt_daemon = False
116                 while not halt_daemon:
117                         #time.sleep(60)
118                         events = inotifyx.get_events(notifier)
119                         for event in events:
120                                 if event.wd == configwatcher:
121                                         print "[notification] Configuration has changed, reload!"
122                                         # FIXME: Perform reloading, merge with existing config to avoid unneeded sync runs!
123                                 else:
124                                         sourcedir = watchers[event.wd]
125                                         print "[notification] Sourcedir [%s] has changed, run syncer!" % sourcedir
126                                         for syncer in syncers[sourcedir]:
127                                                 syncer.sync()
128
129                 inotifyx.rm_watch(notifier, configwatcher)
130                 for watcher in watcher.keys():
131                         inotifyx.rm_watch(notifier, watcher)
132                 os.close(notifier)
133
134                 self.terminate(os.getpid())
135
136         def terminate(self, pid):
137                 os.kill(pid, signal.SIGTERM)
138                 pidfile = os.path.join(links.configdir, ".pid")
139                 os.unlink(pidfile)
140
141         def writepid(self, pid):
142                 pidfile = os.path.join(links.configdir, ".pid")
143                 f = open(pidfile, "w")
144                 print >>f, pid
145                 f.close()
146
147         def readpid(self):
148                 pidfile = os.path.join(links.configdir, ".pid")
149                 try:
150                         f = open(pidfile)
151                 except:
152                         return -1
153                 pid = f.read().strip()
154                 f.close()
155                 return int(pid)
156
157 links = LinkCollection(os.path.expanduser('~') + "/.syncme")
158 links.load()
159
160 def daemon_start():
161         if not os.path.exists(os.path.join(os.path.expanduser('~'), ".syncme")):
162                 links.save()
163
164         d = Daemon()
165         pid = d.readpid()
166         if pid > 0:
167                 print >>sys.stderr, "Error: Daemon already running."
168                 sys.exit(-1)
169         pid = os.fork()
170         if pid == 0:
171                 d = Daemon()
172                 d.launch()
173                 sys.exit(0)
174         elif pid > 0:
175                 d = Daemon()
176                 d.writepid(pid)
177                 sys.exit(0)
178         else:
179                 print >>sys.stderr, "Error: Cannot fork."
180                 sys.exit(-1)
181
182 def daemon_stop():
183         d = Daemon()
184         pid = d.readpid()
185         if pid < 0:
186                 print >>sys.stderr, "Error: Daemon not running."
187                 sys.exit(-1)
188         d.terminate(pid)
189
190 def daemon_status():
191         d = Daemon()
192         pid = d.readpid()
193         if pid < 0:
194                 print "Daemon is not running."
195         elif pid > 0:
196                 print "Daemon is running."
197
198                 if not os.path.exists(os.path.join("/proc", str(pid))):
199                         print "Warning: The system state appears to be invalid."
200                 # FIXME: add more pid comparison logic to detect stale pidfiles, also for start/stop
201
202 def links_list():
203         source_label = "Source directory"
204         target_label = "Target directory"
205         option_label = "Options"
206
207         source_maxlen = max([len(link.sourcedir) for link in links.links] + [len(source_label)])
208         target_maxlen = max([len(link.targetdir) for link in links.links] + [len(target_label)])
209
210         print source_label, " " * (source_maxlen - len(source_label)) + "|",
211         print target_label, " " * (target_maxlen - len(target_label)) + "|",
212         print option_label
213         print "-" * source_maxlen,
214         print "+",
215         print "-" * target_maxlen,
216         print "+",
217         print "-" * len(option_label)
218
219         for link in links.links:
220                 print link.sourcedir, " " * (source_maxlen - len(link.sourcedir)) + "|",
221                 print link.targetdir, " " * (target_maxlen - len(link.targetdir)) + "|",
222                 print ("", "versioned")[int(link.versioned)]
223
224 def links_add(sourcedir, destdir, versioned):
225         link = Link(sourcedir, destdir)
226         link.versioned = (versioned == True)
227         links.links.append(link)
228         links.save()
229
230 parser = optparse.OptionParser()
231 parser.add_option("-v", "--versioned", action="store_true", help="Synchronisation should be versioned")
232 parser.add_option("-H", "--help-commands", action="store_true", help="List available tool commands")
233 (options, args) = parser.parse_args()
234
235 if options.help_commands:
236         print "Daemon control commands:"
237         print "* start:  Start the synchronisation routines asynchronously"
238         print "* stop:   Stop the synchronisation routines"
239         print "* status: Show if the daemon is already running"
240         print ""
241         print "Synchronisation links maintenance commands:"
242         print "* list:   Show all links and their status"
243         print "* add:    Add an additional link to be synchronised"
244         print "          (Syntax: add [--versioned] <sourcedir> <destdir>)"
245         sys.exit(0)
246
247 if len(args) == 0:
248         print >>sys.stderr, "Error: Needs a command, see --help-commands."
249         sys.exit(1)
250
251 cmd = args[0]
252
253 if cmd == "start":
254         if len(args) > 1:
255                 print >>sys.stderr, "Error: Superfluous arguments."
256                 sys.exit(1)
257         daemon_start()
258 elif cmd == "stop":
259         if len(args) > 1:
260                 print >>sys.stderr, "Error: Superfluous arguments."
261                 sys.exit(1)
262         daemon_stop()
263 elif cmd == "status":
264         if len(args) > 1:
265                 print >>sys.stderr, "Error: Superfluous arguments."
266                 sys.exit(1)
267         daemon_status()
268 elif cmd == "list":
269         if len(args) > 1:
270                 print >>sys.stderr, "Error: Superfluous arguments."
271                 sys.exit(1)
272         links_list()
273 elif cmd == "add":
274         if len(args) != 3:
275                 print >>sys.stderr, "Error: Incorrect number of arguments."
276                 sys.exit(1)
277         links_add(args[1], args[2], options.versioned)
278 else:
279         print >>sys.stderr, "Error: '%s' is not a valid command, see --help-commands." % cmd
280         sys.exit(1)
281