source: valtobtest/subversion-1.6.2/tools/server-side/svn-backup-dumps.py @ 3

Last change on this file since 3 was 3, checked in by valtob, 15 years ago

subversion source 1.6.2 as test

  • Property svn:executable set to *
File size: 18.2 KB
Line 
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
133import sys
134import os
135if os.name != "nt":
136    import fcntl
137    import select
138import gzip
139import os.path
140from optparse import OptionParser
141from ftplib import FTP
142from subprocess import Popen, PIPE
143
144try:
145    import bz2
146    have_bz2 = True
147except ImportError:
148    have_bz2 = False
149
150
151class 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
173class 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
188class 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
205class 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
222class SvnBackupException(Exception):
223
224    def __init__(self, errortext):
225        self.errortext = errortext
226
227    def __str__(self):
228        return self.errortext
229
230class 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
481if __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
Note: See TracBrowser for help on using the repository browser.