Commits

Thomas Waldmann  committed e642031 Draft

initial commit

  • Participants

Comments (0)

Files changed (2)

File rsync_backup.py

+# -*- coding: ascii
+"""
+rsync-based backup with space-efficient hardlinks,
+multiple generations, extra token for daily, weekly,
+monthly, ... backups.
+
+author: Thomas Waldmann
+license: BSD
+"""
+
+import os
+from os.path import exists, abspath
+import sys
+import errno
+import subprocess
+import shutil
+
+RSYNC = "rsync -avH --link-dest=%(link_dst)s %(src)s/ %(dst)s/"
+
+
+def join(path, n, token):
+    if n == 0:
+        # there is only ONE "0" directory, no token appended
+        token = ""
+    return os.path.join(path, "%d%s" % (n, token))
+
+
+def rsync(src, dst, link_dst):
+    """backup contents of src directory into dst directory"""
+    cmd = RSYNC % dict(src=abspath(src),
+                       dst=abspath(dst),
+                       link_dst=abspath(link_dst))
+    rc = subprocess.call(cmd.split())
+    return rc
+
+
+def dirshift(path, n, token):
+    """shift the directories below the base path:
+       N-1 -> N (kill it later)
+       N-2 -> N-1
+       ...
+       0 -> 1
+       create an empty 0
+    """
+    for i in range(n, 0, -1):
+        try:
+            os.rename(join(path, i-1, token),
+                      join(path, i, token))
+        except OSError as err:
+            if err.errno != errno.ENOENT:
+                # ignore if directory does not exist,
+                # otherwise raise
+                raise
+    os.mkdir(join(path, 0, token))
+    del_dir = join(path, n, token)
+    if exists(del_dir):
+        shutil.rmtree(del_dir)
+
+
+def backup(src, dst, n, token=""):
+    """do a backup from src to dst, shifting dirs first"""
+    dirshift(dst, n, token)
+    return rsync(src, join(dst, 0, token), join(dst, 1, token))
+
+
+if __name__ == "__main__":
+    if len(sys.argv) != 5:
+        print "rsb N token src dst"
+        sys.exit(1)
+    n, token, src, dst = sys.argv[1:]
+    rc = backup(src, dst, int(n), token)
+    sys.exit(rc)
+

File test_rsync_backup.py

+import os
+import tempfile
+from os.path import join, exists
+
+from rsync_backup import backup, rsync, dirshift
+
+
+def rf(fname):
+    """read the content of a file and return it"""
+    with open(fname, "rb") as f:
+        return f.read()
+
+def wf(fname, content):
+    """write content into a file"""
+    with open(fname, "wb") as f:
+        f.write(content)
+
+def test_flat():
+    src = tempfile.mkdtemp()
+    dst = tempfile.mkdtemp()
+    wf(join(src, "1"), "eins")
+    wf(join(src, "2"), "zwei")
+    rc = rsync(src, dst, src)
+    assert rc == 0
+    assert rf(join(dst, "1")) == "eins"
+    assert rf(join(dst, "2")) == "zwei"
+
+
+def test_recursive():
+    src = tempfile.mkdtemp()
+    dst = tempfile.mkdtemp()
+    wf(join(src, "1"), "eins")
+    src_sub = join(src, "sub")
+    os.mkdir(src_sub)
+    wf(join(src_sub, "2"), "zwei")
+    rc = rsync(src, dst, src)
+    assert rc == 0
+    assert rf(join(dst, "1")) == "eins"
+    assert rf(join(dst, "sub", "2")) == "zwei"
+
+def test_dirshift():
+    tmp = tempfile.mkdtemp()
+    N = 10
+    for i in range(N): # 0..N-1
+        os.mkdir(join(tmp, str(i)))
+        wf(join(tmp, str(i), str(i)), "")
+    dirshift(tmp, N, "")
+    assert exists(join(tmp, "0"))
+    for i in range(1, N): # 1..N-1
+        assert exists(join(tmp, str(i), str(i-1)))
+    assert not exists(join(tmp, str(N)))
+
+def test_backup():
+    src = tempfile.mkdtemp()
+    dst = tempfile.mkdtemp()
+    N = 3
+    backup(src, dst, N)
+    assert os.listdir(join(dst, "0")) == []
+    wf(join(src, "1"), "eins")
+    wf(join(src, "2"), "zwei")
+    backup(src, dst, N)
+    assert sorted(os.listdir(join(dst, "0"))) == ["1", "2"]
+    assert rf(join(dst, "0", "1")) == "eins"
+    assert rf(join(dst, "0", "2")) == "zwei"
+    wf(join(src, "1"), "einseins")
+    backup(src, dst, N)
+    assert sorted(os.listdir(join(dst, "0"))) == ["1", "2"]
+    assert rf(join(dst, "0", "1")) == "einseins"
+    assert rf(join(dst, "0", "2")) == "zwei"
+    st = os.stat(join(dst, "0", "1"))
+    assert st.st_nlink == 1
+    st = os.stat(join(dst, "0", "2"))
+    assert st.st_nlink == 2
+    os.remove(join(src, "2"))
+    backup(src, dst, N)
+    assert sorted(os.listdir(join(dst, "0"))) == ["1"]
+    assert rf(join(dst, "0", "1")) == "einseins"
+    st = os.stat(join(dst, "1", "2"))
+    assert st.st_nlink == 2
+