This file is indexed.

/usr/bin/svn-fast-backup is in subversion-tools 1.6.17dfsg-3ubuntu3.

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()