summaryrefslogtreecommitdiff
path: root/tools/backup
diff options
context:
space:
mode:
authorLorry <lorry@roadtrain.codethink.co.uk>2012-08-22 14:29:52 +0100
committerLorry <lorry@roadtrain.codethink.co.uk>2012-08-22 14:29:52 +0100
commitf1bdf13786f0752c0846cf36f0d91e4fc6747929 (patch)
tree4223b2035bf2240d681a53822808b3c7f687b905 /tools/backup
downloadsubversion-tarball-f1bdf13786f0752c0846cf36f0d91e4fc6747929.tar.gz
Tarball conversion
Diffstat (limited to 'tools/backup')
-rwxr-xr-xtools/backup/hot-backup.py.in354
1 files changed, 354 insertions, 0 deletions
diff --git a/tools/backup/hot-backup.py.in b/tools/backup/hot-backup.py.in
new file mode 100755
index 0000000..20dd794
--- /dev/null
+++ b/tools/backup/hot-backup.py.in
@@ -0,0 +1,354 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# hot-backup.py: perform a "hot" backup of a Subversion repository
+# and clean any old Berkeley DB logfiles after the
+# backup completes, if the repository backend is
+# Berkeley DB.
+#
+# Subversion is a tool for revision control.
+# See http://subversion.apache.org for more information.
+#
+# ====================================================================
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+# ====================================================================
+
+# $HeadURL: http://svn.apache.org/repos/asf/subversion/branches/1.7.x/tools/backup/hot-backup.py.in $
+# $LastChangedDate: 2010-08-20 04:30:52 +0000 (Fri, 20 Aug 2010) $
+# $LastChangedBy: cmpilato $
+# $LastChangedRevision: 987379 $
+
+######################################################################
+
+import sys, os, getopt, stat, re, time, shutil, subprocess
+
+######################################################################
+# Global Settings
+
+# Path to svnlook utility
+svnlook = r"@SVN_BINDIR@/svnlook"
+
+# Path to svnadmin utility
+svnadmin = r"@SVN_BINDIR@/svnadmin"
+
+# Default number of backups to keep around (0 for "keep them all")
+num_backups = int(os.environ.get("SVN_HOTBACKUP_BACKUPS_NUMBER", 64))
+
+# Archive types/extensions
+archive_map = {
+ 'gz' : ".tar.gz",
+ 'bz2' : ".tar.bz2",
+ 'zip' : ".zip",
+ 'zip64' : ".zip"
+ }
+
+# Chmod recursively on a whole subtree
+def chmod_tree(path, mode, mask):
+ for dirpath, dirs, files in os.walk(path):
+ for name in dirs + files:
+ fullname = os.path.join(dirpath, name)
+ if not os.path.islink(fullname):
+ new_mode = (os.stat(fullname)[stat.ST_MODE] & ~mask) | mode
+ os.chmod(fullname, new_mode)
+
+# For clearing away read-only directories
+def safe_rmtree(dirname, retry=0):
+ "Remove the tree at DIRNAME, making it writable first"
+ def rmtree(dirname):
+ chmod_tree(dirname, 0666, 0666)
+ shutil.rmtree(dirname)
+
+ if not os.path.exists(dirname):
+ return
+
+ if retry:
+ for delay in (0.5, 1, 2, 4):
+ try:
+ rmtree(dirname)
+ break
+ except:
+ time.sleep(delay)
+ else:
+ rmtree(dirname)
+ else:
+ rmtree(dirname)
+
+######################################################################
+# Command line arguments
+
+def usage(out = sys.stdout):
+ scriptname = os.path.basename(sys.argv[0])
+ out.write(
+"""USAGE: %s [OPTIONS] REPOS_PATH BACKUP_PATH
+
+Create a backup of the repository at REPOS_PATH in a subdirectory of
+the BACKUP_PATH location, named after the youngest revision.
+
+Options:
+ --archive-type=FMT Create an archive of the backup. FMT can be one of:
+ bz2 : Creates a bzip2 compressed tar file.
+ gz : Creates a gzip compressed tar file.
+ zip : Creates a compressed zip file.
+ zip64: Creates a zip64 file (can be > 2GB).
+ --num-backups=N Number of prior backups to keep around (0 to keep all).
+ --verify Verify the backup.
+ --help -h Print this help message and exit.
+
+""" % (scriptname,))
+
+
+try:
+ opts, args = getopt.gnu_getopt(sys.argv[1:], "h?", ["archive-type=",
+ "num-backups=",
+ "verify",
+ "help"])
+except getopt.GetoptError, e:
+ sys.stderr.write("ERROR: %s\n\n" % e)
+ sys.stderr.flush()
+ usage(sys.stderr)
+ sys.exit(2)
+
+archive_type = None
+verify_copy = False
+
+for o, a in opts:
+ if o == "--archive-type":
+ archive_type = a
+ elif o == "--num-backups":
+ num_backups = int(a)
+ elif o == "--verify":
+ verify_copy = True
+ elif o in ("-h", "--help", "-?"):
+ usage()
+ sys.exit()
+
+if len(args) != 2:
+ sys.stderr.write("ERROR: only two arguments allowed.\n\n")
+ sys.stderr.flush()
+ usage(sys.stderr)
+ sys.exit(2)
+
+# Path to repository
+repo_dir = args[0]
+repo = os.path.basename(os.path.abspath(repo_dir))
+
+# Where to store the repository backup. The backup will be placed in
+# a *subdirectory* of this location, named after the youngest
+# revision.
+backup_dir = args[1]
+
+# Added to the filename regexp, set when using --archive-type.
+ext_re = ""
+
+# Do we want to create an archive of the backup
+if archive_type:
+ if archive_type in archive_map:
+ # Additionally find files with the archive extension.
+ ext_re = "(" + re.escape(archive_map[archive_type]) + ")?"
+ else:
+ sys.stderr.write("Unknown archive type '%s'.\n\n\n" % archive_type)
+ sys.stderr.flush()
+ usage(sys.stderr)
+ sys.exit(2)
+
+
+######################################################################
+# Helper functions
+
+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]+))?" +
+ ext_re + "$")
+ 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 get_youngest_revision():
+ """Examine the repository REPO_DIR using the svnlook binary
+ specified by SVNLOOK, and return the youngest revision."""
+
+ p = subprocess.Popen([svnlook, 'youngest', repo_dir],
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ infile, outfile, errfile = p.stdin, p.stdout, p.stderr
+
+ stdout_lines = outfile.readlines()
+ stderr_lines = errfile.readlines()
+ outfile.close()
+ infile.close()
+ errfile.close()
+
+ if stderr_lines:
+ raise Exception("Unable to find the youngest revision for repository '%s'"
+ ": %s" % (repo_dir, stderr_lines[0].rstrip()))
+
+ return stdout_lines[0].strip()
+
+######################################################################
+# Main
+
+print("Beginning hot backup of '"+ repo_dir + "'.")
+
+
+### Step 1: get the youngest revision.
+
+try:
+ youngest = get_youngest_revision()
+except Exception, e:
+ sys.stderr.write("%s\n" % e)
+ sys.stderr.flush()
+ sys.exit(1)
+
+print("Youngest revision is %s" % youngest)
+
+
+### Step 2: Find next available backup path
+
+backup_subdir = os.path.join(backup_dir, repo + "-" + 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 num_backups.
+
+regexp = re.compile("^" + repo + "-" + youngest +
+ "(-(?P<increment>[0-9]+))?" + ext_re + "$")
+directory_list = os.listdir(backup_dir)
+young_list = [x for x in directory_list if regexp.search(x)]
+if young_list:
+ young_list.sort(comparator)
+ increment = regexp.search(young_list.pop()).groupdict()['increment']
+ if increment:
+ backup_subdir = os.path.join(backup_dir, repo + "-" + youngest + "-"
+ + str(int(increment) + 1))
+ else:
+ backup_subdir = os.path.join(backup_dir, repo + "-" + youngest + "-1")
+
+### Step 3: Ask subversion to make a hot copy of a repository.
+### copied last.
+
+print("Backing up repository to '" + backup_subdir + "'...")
+err_code = subprocess.call([svnadmin, "hotcopy", repo_dir,
+ backup_subdir, "--clean-logs"])
+if err_code != 0:
+ sys.stderr.write("Unable to backup the repository.\n")
+ sys.stderr.flush()
+ sys.exit(err_code)
+else:
+ print("Done.")
+
+### Step 4: Verify the hotcopy
+if verify_copy:
+ print("Verifying backup...")
+ err_code = subprocess.call([svnadmin, "verify", "--quiet", backup_subdir])
+ if err_code != 0:
+ sys.stderr.write("Backup verification failed.\n")
+ sys.stderr.flush()
+ sys.exit(err_code)
+ else:
+ print("Done.")
+
+### Step 5: Make an archive of the backup if required.
+if archive_type:
+ archive_path = backup_subdir + archive_map[archive_type]
+ err_msg = ""
+
+ print("Archiving backup to '" + archive_path + "'...")
+ if archive_type == 'gz' or archive_type == 'bz2':
+ try:
+ import tarfile
+ tar = tarfile.open(archive_path, 'w:' + archive_type)
+ tar.add(backup_subdir, os.path.basename(backup_subdir))
+ tar.close()
+ except ImportError, e:
+ err_msg = "Import failed: " + str(e)
+ err_code = -2
+ except tarfile.TarError, e:
+ err_msg = "Tar failed: " + str(e)
+ err_code = -3
+
+ elif archive_type == 'zip' or archive_type == 'zip64':
+ try:
+ import zipfile
+
+ def add_to_zip(zp, root, dirname, names):
+ root = os.path.join(root, '')
+
+ for file in names:
+ path = os.path.join(dirname, file)
+ if os.path.isfile(path):
+ zp.write(path, path[len(root):])
+ elif os.path.isdir(path) and os.path.islink(path):
+ for dirpath, dirs, files in os.walk(path):
+ add_to_zip(zp, path, dirpath, dirs + files)
+
+ zp = zipfile.ZipFile(archive_path, 'w', zipfile.ZIP_DEFLATED, archive_type == 'zip64')
+ for dirpath, dirs, files in os.walk(backup_subdir):
+ add_to_zip(zp, backup_dir, dirpath, dirs + files)
+ zp.close()
+ except ImportError, e:
+ err_msg = "Import failed: " + str(e)
+ err_code = -4
+ except zipfile.error, e:
+ err_msg = "Zip failed: " + str(e)
+ err_code = -5
+
+
+ if err_code != 0:
+ sys.stderr.write("Unable to create an archive for the backup.\n%s\n" % err_msg)
+ sys.stderr.flush()
+ sys.exit(err_code)
+ else:
+ print("Archive created, removing backup '" + backup_subdir + "'...")
+ safe_rmtree(backup_subdir, 1)
+
+### Step 6: finally, remove all repository backups other than the last
+### NUM_BACKUPS.
+
+if num_backups > 0:
+ regexp = re.compile("^" + repo + "-[0-9]+(-[0-9]+)?" + ext_re + "$")
+ directory_list = os.listdir(backup_dir)
+ old_list = [x for x in directory_list if regexp.search(x)]
+ old_list.sort(comparator)
+ del old_list[max(0,len(old_list)-num_backups):]
+ for item in old_list:
+ old_backup_item = os.path.join(backup_dir, item)
+ print("Removing old backup: " + old_backup_item)
+ if os.path.isdir(old_backup_item):
+ safe_rmtree(old_backup_item, 1)
+ else:
+ os.remove(old_backup_item)