1 | #!/usr/bin/env python |
---|
2 | # |
---|
3 | # svn-backup-dumps.py -- Create dumpfiles to backup a subversion repository. |
---|
4 | # |
---|
5 | # ==================================================================== |
---|
6 | # Copyright (c) 2006-2009 CollabNet. All rights reserved. |
---|
7 | # |
---|
8 | # This software is licensed as described in the file COPYING, which |
---|
9 | # you should have received as part of this distribution. The terms |
---|
10 | # are also available at http://subversion.tigris.org/license-1.html. |
---|
11 | # If newer versions of this license are posted there, you may use a |
---|
12 | # newer version instead, at your option. |
---|
13 | # |
---|
14 | # This software consists of voluntary contributions made by many |
---|
15 | # individuals. For exact contribution history, see the revision |
---|
16 | # history and logs, available at http://subversion.tigris.org/. |
---|
17 | # ==================================================================== |
---|
18 | # |
---|
19 | # This script creates dump files from a subversion repository. |
---|
20 | # It is intended for use in cron jobs and post-commit hooks. |
---|
21 | # |
---|
22 | # The basic operation modes are: |
---|
23 | # 1. Create a full dump (revisions 0 to HEAD). |
---|
24 | # 2. Create incremental dumps containing at most N revisions. |
---|
25 | # 3. Create incremental single revision dumps (for use in post-commit). |
---|
26 | # |
---|
27 | # All dump files are prefixed with the basename of the repository. All |
---|
28 | # examples below assume that the repository '/srv/svn/repos/src' is |
---|
29 | # dumped so all dumpfiles start with 'src'. |
---|
30 | # |
---|
31 | # Optional functionality: |
---|
32 | # 4. Create gzipped dump files. |
---|
33 | # 5. Create bzipped dump files. |
---|
34 | # 6. Transfer the dumpfile to another host using ftp. |
---|
35 | # 7. Transfer the dumpfile to another host using smb. |
---|
36 | # |
---|
37 | # See also 'svn-backup-dumps.py -h'. |
---|
38 | # |
---|
39 | # |
---|
40 | # 1. Create a full dump (revisions 0 to HEAD). |
---|
41 | # |
---|
42 | # svn-backup-dumps.py <repos> <dumpdir> |
---|
43 | # |
---|
44 | # <repos> Path to the repository. |
---|
45 | # <dumpdir> Directory for storing the dump file. |
---|
46 | # |
---|
47 | # This creates a dump file named 'src.000000-NNNNNN.svndmp.gz' |
---|
48 | # where NNNNNN is the revision number of HEAD. |
---|
49 | # |
---|
50 | # 2. Create incremental dumps containing at most N revisions. |
---|
51 | # |
---|
52 | # svn-backup-dumps.py -c <count> <repos> <dumpdir> |
---|
53 | # |
---|
54 | # <count> Count of revisions per dump file. |
---|
55 | # <repos> Path to the repository. |
---|
56 | # <dumpdir> Directory for storing the dump file. |
---|
57 | # |
---|
58 | # When started the first time with a count of 1000 and if HEAD is |
---|
59 | # at 2923 it creates the following files: |
---|
60 | # |
---|
61 | # src.000000-000999.svndmp.gz |
---|
62 | # src.001000-001999.svndmp.gz |
---|
63 | # src.002000-002923.svndmp.gz |
---|
64 | # |
---|
65 | # Say the next time HEAD is at 3045 it creates these two files: |
---|
66 | # |
---|
67 | # src.002000-002999.svndmp.gz |
---|
68 | # src.003000-003045.svndmp.gz |
---|
69 | # |
---|
70 | # |
---|
71 | # 3. Create incremental single revision dumps (for use in post-commit). |
---|
72 | # |
---|
73 | # svn-backup-dumps.py -r <revnr> <repos> <dumpdir> |
---|
74 | # |
---|
75 | # <revnr> A revision number. |
---|
76 | # <repos> Path to the repository. |
---|
77 | # <dumpdir> Directory for storing the dump file. |
---|
78 | # |
---|
79 | # This creates a dump file named 'src.NNNNNN.svndmp.gz' where |
---|
80 | # NNNNNN is the revision number of HEAD. |
---|
81 | # |
---|
82 | # |
---|
83 | # 4. Create gzipped dump files. |
---|
84 | # |
---|
85 | # svn-backup-dumps.py -z ... |
---|
86 | # |
---|
87 | # ... More options, see 1-3, 6, 7. |
---|
88 | # |
---|
89 | # |
---|
90 | # 5. Create bzipped dump files. |
---|
91 | # |
---|
92 | # svn-backup-dumps.py -b ... |
---|
93 | # |
---|
94 | # ... More options, see 1-3, 6, 7. |
---|
95 | # |
---|
96 | # |
---|
97 | # 6. Transfer the dumpfile to another host using ftp. |
---|
98 | # |
---|
99 | # svn-backup-dumps.py -t ftp:<host>:<user>:<password>:<path> ... |
---|
100 | # |
---|
101 | # <host> Name of the FTP host. |
---|
102 | # <user> Username on the remote host. |
---|
103 | # <password> Password for the user. |
---|
104 | # <path> Subdirectory on the remote host. |
---|
105 | # ... More options, see 1-5. |
---|
106 | # |
---|
107 | # If <path> contains the string '%r' it is replaced by the |
---|
108 | # repository name (basename of the repository path). |
---|
109 | # |
---|
110 | # |
---|
111 | # 7. Transfer the dumpfile to another host using smb. |
---|
112 | # |
---|
113 | # svn-backup-dumps.py -t smb:<share>:<user>:<password>:<path> ... |
---|
114 | # |
---|
115 | # <share> Name of an SMB share in the form '//host/share'. |
---|
116 | # <user> Username on the remote host. |
---|
117 | # <password> Password for the user. |
---|
118 | # <path> Subdirectory of the share. |
---|
119 | # ... More options, see 1-5. |
---|
120 | # |
---|
121 | # If <path> contains the string '%r' it is replaced by the |
---|
122 | # repository name (basename of the repository path). |
---|
123 | # |
---|
124 | # |
---|
125 | # |
---|
126 | # TODO: |
---|
127 | # - find out how to report smbclient errors |
---|
128 | # - improve documentation |
---|
129 | # |
---|
130 | |
---|
131 | __version = "0.5" |
---|
132 | |
---|
133 | import sys |
---|
134 | import os |
---|
135 | if os.name != "nt": |
---|
136 | import fcntl |
---|
137 | import select |
---|
138 | import gzip |
---|
139 | import os.path |
---|
140 | from optparse import OptionParser |
---|
141 | from ftplib import FTP |
---|
142 | from subprocess import Popen, PIPE |
---|
143 | |
---|
144 | try: |
---|
145 | import bz2 |
---|
146 | have_bz2 = True |
---|
147 | except ImportError: |
---|
148 | have_bz2 = False |
---|
149 | |
---|
150 | |
---|
151 | class SvnBackupOutput: |
---|
152 | |
---|
153 | def __init__(self, abspath, filename): |
---|
154 | self.__filename = filename |
---|
155 | self.__absfilename = os.path.join(abspath, filename) |
---|
156 | |
---|
157 | def open(self): |
---|
158 | pass |
---|
159 | |
---|
160 | def write(self, data): |
---|
161 | pass |
---|
162 | |
---|
163 | def close(self): |
---|
164 | pass |
---|
165 | |
---|
166 | def get_filename(self): |
---|
167 | return self.__filename |
---|
168 | |
---|
169 | def get_absfilename(self): |
---|
170 | return self.__absfilename |
---|
171 | |
---|
172 | |
---|
173 | class SvnBackupOutputPlain(SvnBackupOutput): |
---|
174 | |
---|
175 | def __init__(self, abspath, filename): |
---|
176 | SvnBackupOutput.__init__(self, abspath, filename) |
---|
177 | |
---|
178 | def open(self): |
---|
179 | self.__ofd = open(self.get_absfilename(), "wb") |
---|
180 | |
---|
181 | def write(self, data): |
---|
182 | self.__ofd.write(data) |
---|
183 | |
---|
184 | def close(self): |
---|
185 | self.__ofd.close() |
---|
186 | |
---|
187 | |
---|
188 | class SvnBackupOutputGzip(SvnBackupOutput): |
---|
189 | |
---|
190 | def __init__(self, abspath, filename): |
---|
191 | SvnBackupOutput.__init__(self, abspath, filename + ".gz") |
---|
192 | |
---|
193 | def open(self): |
---|
194 | self.__compressor = gzip.GzipFile(filename=self.get_absfilename(), |
---|
195 | mode="wb") |
---|
196 | |
---|
197 | def write(self, data): |
---|
198 | self.__compressor.write(data) |
---|
199 | |
---|
200 | def close(self): |
---|
201 | self.__compressor.flush() |
---|
202 | self.__compressor.close() |
---|
203 | |
---|
204 | |
---|
205 | class SvnBackupOutputBzip2(SvnBackupOutput): |
---|
206 | |
---|
207 | def __init__(self, abspath, filename): |
---|
208 | SvnBackupOutput.__init__(self, abspath, filename + ".bz2") |
---|
209 | |
---|
210 | def open(self): |
---|
211 | self.__compressor = bz2.BZ2Compressor() |
---|
212 | self.__ofd = open(self.get_absfilename(), "wb") |
---|
213 | |
---|
214 | def write(self, data): |
---|
215 | self.__ofd.write(self.__compressor.compress(data)) |
---|
216 | |
---|
217 | def close(self): |
---|
218 | self.__ofd.write(self.__compressor.flush()) |
---|
219 | self.__ofd.close() |
---|
220 | |
---|
221 | |
---|
222 | class SvnBackupException(Exception): |
---|
223 | |
---|
224 | def __init__(self, errortext): |
---|
225 | self.errortext = errortext |
---|
226 | |
---|
227 | def __str__(self): |
---|
228 | return self.errortext |
---|
229 | |
---|
230 | class SvnBackup: |
---|
231 | |
---|
232 | def __init__(self, options, args): |
---|
233 | # need 3 args: progname, reposname, dumpdir |
---|
234 | if len(args) != 3: |
---|
235 | if len(args) < 3: |
---|
236 | raise SvnBackupException("too few arguments, specify repospath and dumpdir.") |
---|
237 | else: |
---|
238 | raise SvnBackupException("too many arguments, specify repospath and dumpdir only.") |
---|
239 | self.__repospath = args[1] |
---|
240 | self.__dumpdir = args[2] |
---|
241 | # check repospath |
---|
242 | rpathparts = os.path.split(self.__repospath) |
---|
243 | if len(rpathparts[1]) == 0: |
---|
244 | # repospath without trailing slash |
---|
245 | self.__repospath = rpathparts[0] |
---|
246 | if not os.path.exists(self.__repospath): |
---|
247 | raise SvnBackupException("repos '%s' does not exist." % self.__repospath) |
---|
248 | if not os.path.isdir(self.__repospath): |
---|
249 | raise SvnBackupException("repos '%s' is not a directory." % self.__repospath) |
---|
250 | for subdir in [ "db", "conf", "hooks" ]: |
---|
251 | dir = os.path.join(self.__repospath, "db") |
---|
252 | if not os.path.isdir(dir): |
---|
253 | raise SvnBackupException("repos '%s' is not a repository." % self.__repospath) |
---|
254 | rpathparts = os.path.split(self.__repospath) |
---|
255 | self.__reposname = rpathparts[1] |
---|
256 | if self.__reposname in [ "", ".", ".." ]: |
---|
257 | raise SvnBackupException("couldn't extract repos name from '%s'." % self.__repospath) |
---|
258 | # check dumpdir |
---|
259 | if not os.path.exists(self.__dumpdir): |
---|
260 | raise SvnBackupException("dumpdir '%s' does not exist." % self.__dumpdir) |
---|
261 | elif not os.path.isdir(self.__dumpdir): |
---|
262 | raise SvnBackupException("dumpdir '%s' is not a directory." % self.__dumpdir) |
---|
263 | # set options |
---|
264 | self.__rev_nr = options.rev |
---|
265 | self.__count = options.cnt |
---|
266 | self.__quiet = options.quiet |
---|
267 | self.__deltas = options.deltas |
---|
268 | self.__zip = options.zip |
---|
269 | self.__overwrite = False |
---|
270 | self.__overwrite_all = False |
---|
271 | if options.overwrite > 0: |
---|
272 | self.__overwrite = True |
---|
273 | if options.overwrite > 1: |
---|
274 | self.__overwrite_all = True |
---|
275 | self.__transfer = None |
---|
276 | if options.transfer != None: |
---|
277 | self.__transfer = options.transfer.split(":") |
---|
278 | if len(self.__transfer) != 5: |
---|
279 | if len(self.__transfer) < 5: |
---|
280 | raise SvnBackupException("too few fields for transfer '%s'." % self.__transfer) |
---|
281 | else: |
---|
282 | raise SvnBackupException("too many fields for transfer '%s'." % self.__transfer) |
---|
283 | if self.__transfer[0] not in [ "ftp", "smb" ]: |
---|
284 | raise SvnBackupException("unknown transfer method '%s'." % self.__transfer[0]) |
---|
285 | |
---|
286 | def set_nonblock(self, fileobj): |
---|
287 | fd = fileobj.fileno() |
---|
288 | n = fcntl.fcntl(fd, fcntl.F_GETFL) |
---|
289 | fcntl.fcntl(fd, fcntl.F_SETFL, n|os.O_NONBLOCK) |
---|
290 | |
---|
291 | def exec_cmd(self, cmd, output=None, printerr=False): |
---|
292 | if os.name == "nt": |
---|
293 | return self.exec_cmd_nt(cmd, output, printerr) |
---|
294 | else: |
---|
295 | return self.exec_cmd_unix(cmd, output, printerr) |
---|
296 | |
---|
297 | def exec_cmd_unix(self, cmd, output=None, printerr=False): |
---|
298 | try: |
---|
299 | proc = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=False) |
---|
300 | except: |
---|
301 | return (256, "", "Popen failed (%s ...):\n %s" % (cmd[0], |
---|
302 | str(sys.exc_info()[1]))) |
---|
303 | stdout = proc.stdout |
---|
304 | stderr = proc.stderr |
---|
305 | self.set_nonblock(stdout) |
---|
306 | self.set_nonblock(stderr) |
---|
307 | readfds = [ stdout, stderr ] |
---|
308 | selres = select.select(readfds, [], []) |
---|
309 | bufout = "" |
---|
310 | buferr = "" |
---|
311 | while len(selres[0]) > 0: |
---|
312 | for fd in selres[0]: |
---|
313 | buf = fd.read(16384) |
---|
314 | if len(buf) == 0: |
---|
315 | readfds.remove(fd) |
---|
316 | elif fd == stdout: |
---|
317 | if output: |
---|
318 | output.write(buf) |
---|
319 | else: |
---|
320 | bufout += buf |
---|
321 | else: |
---|
322 | if printerr: |
---|
323 | sys.stdout.write("%s " % buf) |
---|
324 | else: |
---|
325 | buferr += buf |
---|
326 | if len(readfds) == 0: |
---|
327 | break |
---|
328 | selres = select.select(readfds, [], []) |
---|
329 | rc = proc.wait() |
---|
330 | if printerr: |
---|
331 | print("") |
---|
332 | return (rc, bufout, buferr) |
---|
333 | |
---|
334 | def exec_cmd_nt(self, cmd, output=None, printerr=False): |
---|
335 | try: |
---|
336 | proc = Popen(cmd, stdout=PIPE, stderr=None, shell=False) |
---|
337 | except: |
---|
338 | return (256, "", "Popen failed (%s ...):\n %s" % (cmd[0], |
---|
339 | str(sys.exc_info()[1]))) |
---|
340 | stdout = proc.stdout |
---|
341 | bufout = "" |
---|
342 | buferr = "" |
---|
343 | buf = stdout.read(16384) |
---|
344 | while len(buf) > 0: |
---|
345 | if output: |
---|
346 | output.write(buf) |
---|
347 | else: |
---|
348 | bufout += buf |
---|
349 | buf = stdout.read(16384) |
---|
350 | rc = proc.wait() |
---|
351 | return (rc, bufout, buferr) |
---|
352 | |
---|
353 | def get_head_rev(self): |
---|
354 | cmd = [ "svnlook", "youngest", self.__repospath ] |
---|
355 | r = self.exec_cmd(cmd) |
---|
356 | if r[0] == 0 and len(r[2]) == 0: |
---|
357 | return int(r[1].strip()) |
---|
358 | else: |
---|
359 | print(r[2]) |
---|
360 | return -1 |
---|
361 | |
---|
362 | def transfer_ftp(self, absfilename, filename): |
---|
363 | rc = False |
---|
364 | try: |
---|
365 | host = self.__transfer[1] |
---|
366 | user = self.__transfer[2] |
---|
367 | passwd = self.__transfer[3] |
---|
368 | destdir = self.__transfer[4].replace("%r", self.__reposname) |
---|
369 | ftp = FTP(host, user, passwd) |
---|
370 | ftp.cwd(destdir) |
---|
371 | ifd = open(absfilename, "rb") |
---|
372 | ftp.storbinary("STOR %s" % filename, ifd) |
---|
373 | ftp.quit() |
---|
374 | rc = len(ifd.read(1)) == 0 |
---|
375 | ifd.close() |
---|
376 | except Exception, e: |
---|
377 | raise SvnBackupException("ftp transfer failed:\n file: '%s'\n error: %s" % \ |
---|
378 | (absfilename, str(e))) |
---|
379 | return rc |
---|
380 | |
---|
381 | def transfer_smb(self, absfilename, filename): |
---|
382 | share = self.__transfer[1] |
---|
383 | user = self.__transfer[2] |
---|
384 | passwd = self.__transfer[3] |
---|
385 | if passwd == "": |
---|
386 | passwd = "-N" |
---|
387 | destdir = self.__transfer[4].replace("%r", self.__reposname) |
---|
388 | cmd = ("smbclient", share, "-U", user, passwd, "-D", destdir, |
---|
389 | "-c", "put %s %s" % (absfilename, filename)) |
---|
390 | r = self.exec_cmd(cmd) |
---|
391 | rc = r[0] == 0 |
---|
392 | if not rc: |
---|
393 | print(r[2]) |
---|
394 | return rc |
---|
395 | |
---|
396 | def transfer(self, absfilename, filename): |
---|
397 | if self.__transfer == None: |
---|
398 | return |
---|
399 | elif self.__transfer[0] == "ftp": |
---|
400 | self.transfer_ftp(absfilename, filename) |
---|
401 | elif self.__transfer[0] == "smb": |
---|
402 | self.transfer_smb(absfilename, filename) |
---|
403 | else: |
---|
404 | print("unknown transfer method '%s'." % self.__transfer[0]) |
---|
405 | |
---|
406 | def create_dump(self, checkonly, overwrite, fromrev, torev=None): |
---|
407 | revparam = "%d" % fromrev |
---|
408 | r = "%06d" % fromrev |
---|
409 | if torev != None: |
---|
410 | revparam += ":%d" % torev |
---|
411 | r += "-%06d" % torev |
---|
412 | filename = "%s.%s.svndmp" % (self.__reposname, r) |
---|
413 | output = None |
---|
414 | if self.__zip: |
---|
415 | if self.__zip == "gzip": |
---|
416 | output = SvnBackupOutputGzip(self.__dumpdir, filename) |
---|
417 | else: |
---|
418 | output = SvnBackupOutputBzip2(self.__dumpdir, filename) |
---|
419 | else: |
---|
420 | output = SvnBackupOutputPlain(self.__dumpdir, filename) |
---|
421 | absfilename = output.get_absfilename() |
---|
422 | realfilename = output.get_filename() |
---|
423 | if checkonly: |
---|
424 | return os.path.exists(absfilename) |
---|
425 | elif os.path.exists(absfilename): |
---|
426 | if overwrite: |
---|
427 | print("overwriting " + absfilename) |
---|
428 | else: |
---|
429 | print("%s already exists." % absfilename) |
---|
430 | return True |
---|
431 | else: |
---|
432 | print("writing " + absfilename) |
---|
433 | cmd = [ "svnadmin", "dump", |
---|
434 | "--incremental", "-r", revparam, self.__repospath ] |
---|
435 | if self.__quiet: |
---|
436 | cmd[2:2] = [ "-q" ] |
---|
437 | if self.__deltas: |
---|
438 | cmd[2:2] = [ "--deltas" ] |
---|
439 | output.open() |
---|
440 | r = self.exec_cmd(cmd, output, True) |
---|
441 | output.close() |
---|
442 | rc = r[0] == 0 |
---|
443 | if rc: |
---|
444 | self.transfer(absfilename, realfilename) |
---|
445 | return rc |
---|
446 | |
---|
447 | def export_single_rev(self): |
---|
448 | return self.create_dump(False, self.__overwrite, self.__rev_nr) |
---|
449 | |
---|
450 | def export(self): |
---|
451 | headrev = self.get_head_rev() |
---|
452 | if headrev == -1: |
---|
453 | return False |
---|
454 | if self.__count is None: |
---|
455 | return self.create_dump(False, self.__overwrite, 0, headrev) |
---|
456 | baserev = headrev - (headrev % self.__count) |
---|
457 | rc = True |
---|
458 | cnt = self.__count |
---|
459 | fromrev = baserev - cnt |
---|
460 | torev = baserev - 1 |
---|
461 | while fromrev >= 0 and rc: |
---|
462 | if self.__overwrite_all or \ |
---|
463 | not self.create_dump(True, False, fromrev, torev): |
---|
464 | rc = self.create_dump(False, self.__overwrite_all, |
---|
465 | fromrev, torev) |
---|
466 | fromrev -= cnt |
---|
467 | torev -= cnt |
---|
468 | else: |
---|
469 | fromrev = -1 |
---|
470 | if rc: |
---|
471 | rc = self.create_dump(False, self.__overwrite, baserev, headrev) |
---|
472 | return rc |
---|
473 | |
---|
474 | def execute(self): |
---|
475 | if self.__rev_nr != None: |
---|
476 | return self.export_single_rev() |
---|
477 | else: |
---|
478 | return self.export() |
---|
479 | |
---|
480 | |
---|
481 | if __name__ == "__main__": |
---|
482 | usage = "usage: svn-backup-dumps.py [options] repospath dumpdir" |
---|
483 | parser = OptionParser(usage=usage, version="%prog "+__version) |
---|
484 | if have_bz2: |
---|
485 | parser.add_option("-b", |
---|
486 | action="store_const", const="bzip2", |
---|
487 | dest="zip", default=None, |
---|
488 | help="compress the dump using bzip2.") |
---|
489 | parser.add_option("--deltas", |
---|
490 | action="store_true", |
---|
491 | dest="deltas", default=False, |
---|
492 | help="pass --deltas to svnadmin dump.") |
---|
493 | parser.add_option("-c", |
---|
494 | action="store", type="int", |
---|
495 | dest="cnt", default=None, |
---|
496 | help="count of revisions per dumpfile.") |
---|
497 | parser.add_option("-o", |
---|
498 | action="store_const", const=1, |
---|
499 | dest="overwrite", default=0, |
---|
500 | help="overwrite files.") |
---|
501 | parser.add_option("-O", |
---|
502 | action="store_const", const=2, |
---|
503 | dest="overwrite", default=0, |
---|
504 | help="overwrite all files.") |
---|
505 | parser.add_option("-q", |
---|
506 | action="store_true", |
---|
507 | dest="quiet", default=False, |
---|
508 | help="quiet.") |
---|
509 | parser.add_option("-r", |
---|
510 | action="store", type="int", |
---|
511 | dest="rev", default=None, |
---|
512 | help="revision number for single rev dump.") |
---|
513 | parser.add_option("-t", |
---|
514 | action="store", type="string", |
---|
515 | dest="transfer", default=None, |
---|
516 | help="transfer dumps to another machine "+ |
---|
517 | "(s.a. --help-transfer).") |
---|
518 | parser.add_option("-z", |
---|
519 | action="store_const", const="gzip", |
---|
520 | dest="zip", |
---|
521 | help="compress the dump using gzip.") |
---|
522 | parser.add_option("--help-transfer", |
---|
523 | action="store_true", |
---|
524 | dest="help_transfer", default=False, |
---|
525 | help="shows detailed help for the transfer option.") |
---|
526 | (options, args) = parser.parse_args(sys.argv) |
---|
527 | if options.help_transfer: |
---|
528 | print("Transfer help:") |
---|
529 | print("") |
---|
530 | print(" FTP:") |
---|
531 | print(" -t ftp:<host>:<user>:<password>:<dest-path>") |
---|
532 | print("") |
---|
533 | print(" SMB (using smbclient):") |
---|
534 | print(" -t smb:<share>:<user>:<password>:<dest-path>") |
---|
535 | print("") |
---|
536 | sys.exit(0) |
---|
537 | rc = False |
---|
538 | try: |
---|
539 | backup = SvnBackup(options, args) |
---|
540 | rc = backup.execute() |
---|
541 | except SvnBackupException, e: |
---|
542 | print("svn-backup-dumps.py: %s" % e) |
---|
543 | if rc: |
---|
544 | print("Everything OK.") |
---|
545 | sys.exit(0) |
---|
546 | else: |
---|
547 | print("An error occured!") |
---|
548 | sys.exit(1) |
---|
549 | |
---|
550 | # vim:et:ts=4:sw=4 |
---|