/usr/bin/svn-fast-backup is in subversion-tools 1.9.3-2ubuntu1.1.
This file is owned by root:root, with mode 0o755.
The actual contents of the file can be viewed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 | #!/usr/bin/python
# svn-fast-backup: use rsync snapshots for very fast FSFS repository backup.
# Multiple FSFS backups share data via hardlinks, meaning old backups are
# almost free, since a newer revision of a repository is almost a complete
# superset of an older revision.
# This is good for replacing incremental log-dump+restore-style backups
# because it is just as space-conserving and even faster; there is no
# inter-backup state (old backups are essentially caches); each backup
# directory is self-contained. It keeps the same interface as svn-hot-backup
# (if you use --force), but only works for FSFS repositories.
# Author: Karl Chen <quarl@quarl.org>
## quarl 2005-08-17 initial version
## quarl 2005-09-01 refactor, documentation; new options: --force, --keep,
## --simulate, --trace
# $HeadURL: http://svn.apache.org/repos/asf/subversion/branches/1.6.x/contrib/server-side/svn-fast-backup $
# $LastChangedRevision: 923804 $
# $LastChangedDate: 2010-03-16 15:22:28 +0000 (Tue, 16 Mar 2010) $
# $LastChangedBy: cmpilato $
# Originally based on svn-hot-backup.py, whose copyright notice states:
# ====================================================================
# Copyright (c) 2000-2004 CollabNet. All rights reserved.
#
# This software is licensed as described in the file COPYING, which
# you should have received as part of this distribution. The terms
# are also available at http://subversion.tigris.org/license-1.html.
# If newer versions of this license are posted there, you may use a
# newer version instead, at your option.
#
# This software consists of voluntary contributions made by many
# individuals. For exact contribution history, see the revision
# history and logs, available at http://subversion.tigris.org/.
# ====================================================================
######################################################################
import sys, os, re
import getopt
import subprocess # python2.4
######################################################################
# Global Settings
svnlook = "svnlook" # Path to svnlook
svnadmin = "svnadmin" # Path to svnadmin
rsync = "rsync" # Path to rsync
######################################################################
# Command line arguments
def usage():
raise SystemExit("""Syntax: %s [OPTIONS] repos_path backup_dir
Makes a hot backup of a Subversion FSFS repository at REPOS_PATH to
BACKUP_DIR/repos-rev.
If a previous version exists, make hard links of its files using rsync.
As multiple FSFS backups share data via hardlinks, old backups use
almost no space, since a newer revision of a repository is almost a complete
superset of an older revision (excluding direct repository modifications).
Keeps up to N backups and deletes the rest. (N includes the current backup.)
OPTIONS:
-h, --help This screen
-q, --quiet Quieter than usual
-k, --keep=N Keep N backups instead of 64
-k, --keep=all Keep all backups (never delete any)
-f, --force Make a new backup even if one with current revision exists
-t, --trace Show actions
-s, --simulate Don't perform actions
""" %sys.argv[0])
class Options: pass
def default_options():
options = Options()
options.force = False
options.trace = False
options.simulate = False
options.quiet = False
options.keep = 64 # Number of backups to keep around
return options
def parse_commandline():
options = default_options()
try:
opts, args = getopt.getopt(sys.argv[1:], 'qhk:fts', ['quiet', 'help', 'keep=', 'force',
'trace', 'simulate'])
except getopt.GetoptError, e:
print >>sys.stderr, "Error:", e
usage()
for (o,a) in opts:
if o == '-h' or o == '--help':
usage()
elif o == '-q' or o == '--quiet':
options.quiet = True
elif o == '-f' or o == '--force':
options.force = True
elif o == '-t' or o == '--trace':
options.trace = True
elif o == '-s' or o == '--simulate':
options.simulate = True
elif o == '-k' or o == '--keep':
if a.strip().lower() == 'all':
options.keep = 0
else:
options.keep = int(a)
else:
raise Exception("Internal error")
if len(args) != 2:
usage()
# Path to repository
options.repo_dir = args[0]
# Where to store the repository backup. The backup will be placed in a
# *subdirectory* of this location, named after the youngest revision.
options.backup_dir = os.path.abspath(args[1])
options.repo = os.path.basename(os.path.abspath(options.repo_dir))
return options
def comparator(a, b):
# We pass in filenames so there is never a case where they are equal.
regexp = re.compile("-(?P<revision>[0-9]+)(-(?P<increment>[0-9]+))?$")
matcha = regexp.search(a)
matchb = regexp.search(b)
reva = int(matcha.groupdict()['revision'])
revb = int(matchb.groupdict()['revision'])
if (reva < revb):
return -1
elif (reva > revb):
return 1
else:
inca = matcha.groupdict()['increment']
incb = matchb.groupdict()['increment']
if not inca:
return -1
elif not incb:
return 1;
elif (int(inca) < int(incb)):
return -1
else:
return 1
def pipe(command):
return subprocess.Popen(command, stdout=subprocess.PIPE).communicate()[0].strip()
def readfile(filename):
try:
return open(filename).read().strip()
except:
return ''
def runcmd(cmd):
if options.trace:
print >>sys.stderr, '#', cmd
if options.simulate:
return 0
return subprocess.call(cmd)
def deltree(path):
runcmd(['rm', '-r', path])
def get_youngest_revision():
if readfile(os.path.join('db', 'fs-type')) != 'fsfs':
raise SystemExit("Path '%s' doesn't contain a FSFS repository"%options.repo_dir)
return pipe([svnlook,"youngest","."])
def list_repo_backups():
'''Return a sorted list of backups for this repository.'''
regexp = re.compile(options.repo + "-[0-9]+(-[0-9]+)?$")
directory_list = [x for x in os.listdir(options.backup_dir) if regexp.match(x)]
directory_list.sort(comparator)
return directory_list
def delete_old_backups():
if options.keep <= 0:
return
for item in list_repo_backups()[:-options.keep]:
old_backup_subdir = os.path.join(options.backup_dir, item)
print " Removing old backup: ", old_backup_subdir
deltree(old_backup_subdir)
def find_next_backup_name(youngest):
# If there is already a backup of this revision, then append the next
# highest increment to the path. We still need to do a backup because the
# repository might have changed despite no new revision having been
# created. We find the highest increment and add one rather than start
# from 1 and increment because the starting increments may have already
# been removed due to options.keep.
regexp = re.compile(options.repo + "-" + youngest + "(-(?P<increment>[0-9]+))?$")
directory_list = os.listdir(options.backup_dir)
young_list = [ x for x in directory_list if regexp.match(x) ]
young_list.sort(comparator)
if not young_list:
return "%s-%s" %(options.repo, youngest)
# Backups for this revision exist already.
if not options.force:
if not options.quiet:
print "Backup already exists at",young_list[-1]
raise SystemExit
increment = int(regexp.match(young_list[-1]).groupdict()['increment'] or '0')
return "%s-%s-%d" %(options.repo, youngest, increment+1)
def do_rsync_backup():
youngest = get_youngest_revision()
if not options.quiet:
print "Beginning hot backup of '%s' (youngest revision is %s)..." %(options.repo, youngest),
backup_subdir = os.path.join(options.backup_dir, find_next_backup_name(youngest))
backup_tmpdir = backup_subdir + '.tmp'
if os.path.exists(backup_tmpdir):
raise SystemExit("%s: Backup in progress? '%s' exists -- aborting."%(sys.argv[0],backup_tmpdir))
if not options.simulate:
os.mkdir(backup_tmpdir) # ensures atomicity
if os.path.exists(backup_subdir):
# Check again after doing mkdir (which serves as a mutex acquire) --
# just in case another process just finished the same backup.
if not options.quiet:
print "Backup already exists at",backup_subdir
raise SystemExit
previous_backups = list_repo_backups()
### Use rsync to make a copy.
# We need to copy the 'current' file first.
# Don't copy the transactions/ directory.
# See http://svn.apache.org/repos/asf/subversion/trunk/notes/fsfs
rsync_dest = os.path.join(backup_tmpdir,'')
# copy db/current. -R tells rsync to use relative pathnames.
if runcmd([rsync, '-aR', 'db/current', rsync_dest]):
raise Exception("%s: rsync failed" %sys.argv[0])
# Now copy everything else.
cmd = [rsync, '-a',
'--exclude', 'db/current',
'--exclude', 'db/transactions/*',
'--exclude', 'db/log.*',
'.', rsync_dest]
# If there's a previous backup, make hard links against the latest.
if previous_backups:
cmd += ['--link-dest', os.path.join(options.backup_dir, previous_backups[-1])]
if runcmd(cmd):
raise Exception("%s: rsync failed" %sys.argv[0])
# Rename to final name.
if not options.simulate:
os.rename(backup_tmpdir, backup_subdir)
print "Finished backup to", backup_subdir
options = parse_commandline()
os.chdir(options.repo_dir)
do_rsync_backup()
delete_old_backups()
|