summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile10
-rw-r--r--_obnammodule.c92
-rw-r--r--obnamlib/__init__.py4
-rw-r--r--obnamlib/vfs.py182
-rw-r--r--obnamlib/vfs_local.py150
-rw-r--r--obnamlib/vfs_local_tests.py219
-rw-r--r--obnamlib/vfs_sftp.py202
-rw-r--r--setup.py28
-rw-r--r--without-tests3
9 files changed, 889 insertions, 1 deletions
diff --git a/Makefile b/Makefile
index 0abb36ef..bdedb9f7 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,12 @@
-all:
+PYTHON = python
+
+all: _obnam.so
+
+_obnam.so: _obnammodule.c
+ if $(PYTHON) setup.py build > setup.log 2>&1; then \
+ rm setup.log; else cat setup.log; exit 1; fi
+ cp build/lib*/*.so .
+ rm -rf build
check:
python -m CoverageTestRunner --ignore-missing-from=without-tests
diff --git a/_obnammodule.c b/_obnammodule.c
new file mode 100644
index 00000000..829bb817
--- /dev/null
+++ b/_obnammodule.c
@@ -0,0 +1,92 @@
+/*
+ * _obnammodule.c -- Python extensions for Obna
+ *
+ * Copyright (C) 2008, 2009 Lars Wirzenius <liw@liw.fi>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+
+/*
+ * This is a Python extension module written for Obnam, the backup
+ * software.
+ *
+ * This module provides a way to call the posix_fadvise function from
+ * Python. Obnam uses this to use set the POSIX_FADV_SEQUENTIAL and
+ * POSIX_FADV_DONTNEED flags, to make sure the kernel knows that it will
+ * read files sequentially and that the data does not need to be cached.
+ * This makes Obnam not trash the disk buffer cache, which is nice.
+ */
+
+
+#include <Python.h>
+
+
+#define _XOPEN_SOURCE 600
+#include <fcntl.h>
+#include <stdio.h>
+
+
+static PyObject *
+fadvise_dontneed(PyObject *self, PyObject *args)
+{
+ int fd;
+ /* Can't use off_t for offset and len, since PyArg_ParseTuple
+ doesn't know it. */
+ unsigned long long offset;
+ unsigned long long len;
+ int ret;
+
+ if (!PyArg_ParseTuple(args, "iLL", &fd, &offset, &len))
+ return NULL;
+ ret = posix_fadvise(fd, offset, len, POSIX_FADV_DONTNEED);
+ return Py_BuildValue("i", ret);
+}
+
+
+static PyObject *
+lutimes_wrapper(PyObject *self, PyObject *args)
+{
+ int ret;
+ long atime;
+ long mtime;
+ struct timeval tv[2];
+ const char *filename;
+
+ if (!PyArg_ParseTuple(args, "sll", &filename, &atime, &mtime))
+ return NULL;
+ tv[0].tv_sec = atime;
+ tv[0].tv_usec = 0;
+ tv[1].tv_sec = mtime;
+ tv[1].tv_usec = 0;
+ ret = lutimes(filename, tv);
+ return Py_BuildValue("i", ret);
+}
+
+
+static PyMethodDef methods[] = {
+ {"fadvise_dontneed", fadvise_dontneed, METH_VARARGS,
+ "Call posix_fadvise(2) with POSIX_FADV_DONTNEED argument."},
+ {"lutimes", lutimes_wrapper, METH_VARARGS,
+ "lutimes(2) wrapper; args are filename, atime, and mtime."},
+ {NULL, NULL, 0, NULL} /* Sentinel */
+};
+
+
+PyMODINIT_FUNC
+init_obnam(void)
+{
+ (void) Py_InitModule("_obnam", methods);
+}
diff --git a/obnamlib/__init__.py b/obnamlib/__init__.py
index 59b177d6..6e7c29f4 100644
--- a/obnamlib/__init__.py
+++ b/obnamlib/__init__.py
@@ -14,6 +14,10 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
+import _obnam
+
from hooks import Hook, HookManager
from cfg import Configuration
from interp import Interpreter
+from vfs import VirtualFileSystem, VfsFactory
+from vfs_local import LocalFS
diff --git a/obnamlib/vfs.py b/obnamlib/vfs.py
new file mode 100644
index 00000000..829778e3
--- /dev/null
+++ b/obnamlib/vfs.py
@@ -0,0 +1,182 @@
+# Copyright (C) 2008 Lars Wirzenius <liw@liw.fi>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+
+import os
+import urlparse
+
+import obnamlib
+
+
+class VirtualFileSystem(object):
+
+ """A virtual filesystem interface.
+
+ The backup program needs to access both local and remote files.
+ To make it easier to support all kinds of files both locally and
+ remotely, we use a custom virtual filesystem interface so that
+ all filesystem access is done the same way. This way, we can
+ easily support user data and backup stores in any combination of
+ local and remote filesystems.
+
+ This class defines the interface for such virtual filesystems.
+ Sub-classes will actually implement the interface.
+
+ When a VFS is instantiated, it is bound to a base URL. When
+ accessing the virtual filesystem, all paths are then given
+ relative to the base URL. The Unix syntax for files is used
+ for the relative paths: directory components separated by
+ slashes, and an initial slash indicating the root of the
+ filesystem (in this case, the base URL).
+
+ """
+
+ def __init__(self, baseurl):
+ self.baseurl = baseurl
+
+ def connect(self):
+ """Connect to filesystem."""
+
+ def close(self):
+ """Close connection to filesystem."""
+
+ def listdir(self, relative_path):
+ """Return list of basenames of entities at relative_path."""
+
+ def lock(self, lockname):
+ """Create a lock file with the given name."""
+
+ def unlock(self, lockname):
+ """Remove a lock file."""
+
+ def exists(self, relative_path):
+ """Does the file or directory exist?"""
+
+ def isdir(self, relative_path):
+ """Is it a directory?"""
+
+ def mkdir(self, relative_path):
+ """Create a directory.
+
+ Parent directories must already exist.
+
+ """
+
+ def makedirs(self, relative_path):
+ """Create a directory, and missing parents."""
+
+ def remove(self, relative_path):
+ """Remove a file."""
+
+ def lstat(self, relative_path):
+ """Like os.lstat."""
+
+ def chmod(self, relative_path, mode):
+ """Like os.chmod."""
+
+ def lutimes(self, relative_path, atime, mtime):
+ """Like lutimes(2)."""
+
+ def link(self, existing_path, new_path):
+ """Like os.link."""
+
+ def readlink(self, symlink):
+ """Like os.readlink."""
+
+ def symlink(self, source, destination):
+ """Like os.symlink."""
+
+ def open(self, relative_path, mode):
+ """Open a file, like the builtin open() or file() function.
+
+ The return value is a file object like the ones returned
+ by the builtin open() function.
+
+ """
+
+ def cat(self, relative_path):
+ """Return the contents of a file."""
+
+ def write_file(self, relative_path, contents):
+ """Write a new file.
+
+ The file must not yet exist. The file is written atomically,
+ so that the given name will only exist when the file is
+ completely written.
+
+ Any directories in relative_path will be created if necessary.
+
+ """
+
+ def overwrite_file(self, relative_path, contents):
+ """Like write_file, but overwrites existing file.
+
+ The old file isn't immediately lost, it gets renamed with
+ a backup suffix.
+
+ """
+
+ def depth_first(self, top, prune=None):
+ """Walk a directory tree depth-first, except for unwanted subdirs.
+
+ This is, essentially, 'os.walk(top, topdown=False)', except that
+ if the prune argument is set, we call it before descending to
+ sub-directories to allow it to remove any directories and files
+ the caller does not want to know about.
+
+ If set, prune must be a function that gets three arguments (current
+ directory, list of sub-directory names, list of files in directory),
+ and must modify the two lists _in_place_. For example:
+
+ def prune(dirname, dirnames, filenames):
+ if ".bzr" in dirnames:
+ dirnames.remove(".bzr")
+
+ The dirnames and filenames lists contain basenames, relative to
+ dirname.
+
+ top is relative to VFS root, and so is the returned directory name.
+
+ """
+
+ names = self.listdir(top)
+ dirs = []
+ nondirs = []
+ for name in names:
+ if self.isdir(os.path.join(top, name)):
+ dirs.append(name)
+ else:
+ nondirs.append(name)
+ if prune:
+ prune(top, dirs, nondirs)
+ for name in dirs:
+ path = os.path.join(top, name)
+ for x in self.depth_first(path, prune=prune):
+ yield x
+ yield top, dirs, nondirs
+
+
+class VfsFactory:
+
+ """Create new instances of VirtualFileSystem."""
+
+ def new(self, url):
+ """Create a new VFS appropriate for a given URL."""
+ scheme, netloc, path, params, query, fragment = urlparse.urlparse(url)
+ if scheme == "sftp":
+ return obnamlib.SftpFS(url)
+ else:
+ return obnamlib.LocalFS(url)
diff --git a/obnamlib/vfs_local.py b/obnamlib/vfs_local.py
new file mode 100644
index 00000000..42a00892
--- /dev/null
+++ b/obnamlib/vfs_local.py
@@ -0,0 +1,150 @@
+# Copyright (C) 2008 Lars Wirzenius <liw@liw.fi>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+
+import errno
+import logging
+import os
+import tempfile
+
+import obnamlib
+
+
+class LocalFS(obnamlib.VirtualFileSystem):
+
+ """A VFS implementation for local filesystems."""
+
+ chunk_size = 1024 * 1024
+
+ def lock(self, lockname):
+ try:
+ self.write_file(lockname, "")
+ except OSError, e:
+ if e.errno == errno.EEXIST:
+ raise obnamlib.Exception("Lock %s already exists" % lockname)
+ else:
+ raise
+
+ def unlock(self, lockname):
+ if self.exists(lockname):
+ self.remove(lockname)
+
+ def join(self, relative_path):
+ return os.path.join(self.baseurl, relative_path.lstrip("/"))
+
+ def remove(self, relative_path):
+ os.remove(self.join(relative_path))
+
+ def lstat(self, relative_path):
+ return os.lstat(self.join(relative_path))
+
+ def chmod(self, relative_path, mode):
+ os.chmod(self.join(relative_path), mode)
+
+ def lutimes(self, relative_path, atime, mtime):
+ obnamlib._obnam.lutimes(self.join(relative_path), atime, mtime)
+
+ def link(self, existing, new):
+ os.link(self.join(existing), self.join(new))
+
+ def readlink(self, relative_path):
+ return os.readlink(self.join(relative_path))
+
+ def symlink(self, existing, new):
+ os.symlink(existing, self.join(new))
+
+ def open(self, relative_path, mode):
+ return file(self.join(relative_path), mode)
+
+ def exists(self, relative_path):
+ return os.path.exists(self.join(relative_path))
+
+ def isdir(self, relative_path):
+ return os.path.isdir(self.join(relative_path))
+
+ def mkdir(self, relative_path):
+ os.mkdir(self.join(relative_path))
+
+ def makedirs(self, relative_path):
+ os.makedirs(self.join(relative_path))
+
+ def cat(self, relative_path):
+ logging.debug("LocalFS: Reading %s" % relative_path)
+ f = self.open(relative_path, "r")
+ chunks = []
+ while True:
+ chunk = f.read(self.chunk_size)
+ if not chunk:
+ break
+ chunks.append(chunk)
+# self.progress["bytes-received"] += len(chunk)
+ f.close()
+ data = "".join(chunks)
+ logging.debug("LocalFS: %s had %d bytes" % (relative_path, len(data)))
+ return data
+
+ def write_file(self, relative_path, contents):
+ logging.debug("LocalFS: Writing %s (%d)" %
+ (relative_path, len(contents)))
+ path = self.join(relative_path)
+ dirname = os.path.dirname(path)
+ if not os.path.exists(dirname):
+ os.makedirs(dirname)
+ fd, name = tempfile.mkstemp(dir=dirname)
+ pos = 0
+ while pos < len(contents):
+ chunk = contents[pos:pos+self.chunk_size]
+ os.write(fd, chunk)
+# self.progress["bytes-sent"] += len(chunk)
+ pos += len(chunk)
+ os.close(fd)
+ try:
+ os.link(name, path)
+ except OSError:
+ os.remove(name)
+ raise
+ os.remove(name)
+ logging.debug("LocalFS: write_file updates bytes-sent")
+
+ def overwrite_file(self, relative_path, contents):
+ logging.debug("LocalFS: Over-writing %s (%d)" %
+ (relative_path, len(contents)))
+ path = self.join(relative_path)
+ dirname = os.path.dirname(path)
+ fd, name = tempfile.mkstemp(dir=dirname)
+ pos = 0
+ while pos < len(contents):
+ chunk = contents[pos:pos+self.chunk_size]
+ os.write(fd, chunk)
+# self.progress["bytes-sent"] += len(chunk)
+ pos += len(chunk)
+ os.close(fd)
+
+ # Rename existing to have a .bak suffix. If _that_ file already
+ # exists, remove that.
+ bak = path + ".bak"
+ try:
+ os.remove(bak)
+ except OSError:
+ pass
+ try:
+ os.link(path, bak)
+ except OSError:
+ pass
+ os.rename(name, path)
+
+ def listdir(self, dirname):
+ return os.listdir(self.join(dirname))
diff --git a/obnamlib/vfs_local_tests.py b/obnamlib/vfs_local_tests.py
new file mode 100644
index 00000000..37d22dfb
--- /dev/null
+++ b/obnamlib/vfs_local_tests.py
@@ -0,0 +1,219 @@
+# Copyright (C) 2008 Lars Wirzenius <liw@liw.fi>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+
+import errno
+import os
+import shutil
+import tempfile
+import unittest
+
+import obnamlib
+
+
+class LocalFSTests(unittest.TestCase):
+
+ def setUp(self):
+ self.dirname = tempfile.mkdtemp()
+ self.fs = obnamlib.LocalFS(self.dirname)
+
+ def tearDown(self):
+ self.fs.close()
+ shutil.rmtree(self.dirname)
+
+ def test_joins_relative_path_ok(self):
+ self.assertEqual(self.fs.join("foo"),
+ os.path.join(self.dirname, "foo"))
+
+ def test_join_treats_absolute_path_as_relative(self):
+ self.assertEqual(self.fs.join("/foo"),
+ os.path.join(self.dirname, "foo"))
+
+ def test_creates_lock_file(self):
+ self.fs.lock("lock")
+ self.assert_(self.fs.exists("lock"))
+ self.assert_(os.path.exists(os.path.join(self.dirname, "lock")))
+
+ def test_second_lock_fails(self):
+ self.fs.lock("lock")
+ self.assertRaises(Exception, self.fs.lock, "lock")
+
+ def test_lock_raises_oserror_without_eexist(self):
+ def raise_it(relative_path, contents):
+ e = OSError()
+ e.errno = errno.EAGAIN
+ raise e
+ self.fs.write_file = raise_it
+ self.assertRaises(OSError, self.fs.lock, "foo")
+
+ def test_unlock_removes_lock(self):
+ self.fs.lock("lock")
+ self.fs.unlock("lock")
+ self.assertFalse(self.fs.exists("lock"))
+ self.assertFalse(os.path.exists(os.path.join(self.dirname, "lock")))
+
+ def test_exists_returns_false_for_nonexistent_file(self):
+ self.assertFalse(self.fs.exists("foo"))
+
+ def test_exists_returns_true_for_existing_file(self):
+ file(os.path.join(self.dirname, "foo"), "w").close()
+ self.assert_(self.fs.exists("foo"))
+
+ def test_isdir_returns_false_for_nonexistent_file(self):
+ self.assertFalse(self.fs.isdir("foo"))
+
+ def test_isdir_returns_false_for_nondir(self):
+ file(os.path.join(self.dirname, "foo"), "w").close()
+ self.assertFalse(self.fs.isdir("foo"))
+
+ def test_isdir_returns_true_for_existing_dir(self):
+ os.mkdir(os.path.join(self.dirname, "foo"))
+ self.assert_(self.fs.isdir("foo"))
+
+ def test_mkdir_creates_directory(self):
+ self.fs.mkdir("foo")
+ self.assert_(os.path.isdir(os.path.join(self.dirname, "foo")))
+
+ def test_mkdir_raises_oserror_if_directory_exists(self):
+ self.assertRaises(OSError, self.fs.mkdir, ".")
+
+ def test_mkdir_raises_oserror_if_parent_does_not_exist(self):
+ self.assertRaises(OSError, self.fs.mkdir, "foo/bar")
+
+ def test_makedirs_creates_directory_when_parent_exists(self):
+ self.fs.makedirs("foo")
+ self.assert_(os.path.isdir(os.path.join(self.dirname, "foo")))
+
+ def test_makedirs_creates_directory_when_parent_does_not_exist(self):
+ self.fs.makedirs("foo/bar")
+ self.assert_(os.path.isdir(os.path.join(self.dirname, "foo/bar")))
+
+ def test_lstat_returns_result(self):
+ self.assert_(self.fs.lstat("."))
+
+ def test_chmod_sets_permissions_correctly(self):
+ self.fs.mkdir("foo")
+ self.fs.chmod("foo", 0777)
+ self.assertEqual(self.fs.lstat("foo").st_mode & 0777, 0777)
+
+ def test_lutimes_sets_times_correctly(self):
+ self.fs.mkdir("foo")
+ self.fs.lutimes("foo", 1, 2)
+ self.assertEqual(self.fs.lstat("foo").st_atime, 1)
+ self.assertEqual(self.fs.lstat("foo").st_mtime, 2)
+
+ def test_link_creates_hard_link(self):
+ f = self.fs.open("foo", "w")
+ f.write("foo")
+ f.close()
+ self.fs.link("foo", "bar")
+ st1 = self.fs.lstat("foo")
+ st2 = self.fs.lstat("bar")
+ self.assertEqual(st1, st2)
+
+ def test_symlink_creates_soft_link(self):
+ self.fs.symlink("foo", "bar")
+ target = os.readlink(os.path.join(self.dirname, "bar"))
+ self.assertEqual(target, "foo")
+
+ def test_readlink_reads_link_target(self):
+ self.fs.symlink("foo", "bar")
+ self.assertEqual(self.fs.readlink("bar"), "foo")
+
+ def test_opens_existing_file_ok(self):
+ file(os.path.join(self.dirname, "foo"), "w").close()
+ self.assert_(self.fs.open("foo", "w"))
+
+ def test_open_fails_for_nonexistent_file(self):
+ self.assertRaises(IOError, self.fs.open, "foo", "r")
+
+ def test_cat_reads_existing_file_ok(self):
+ file(os.path.join(self.dirname, "foo"), "w").write("bar")
+ self.assertEqual(self.fs.cat("foo"), "bar")
+
+ def test_cat_fails_for_nonexistent_file(self):
+ self.assertRaises(IOError, self.fs.cat, "foo")
+
+ def test_write_file_writes_file_ok(self):
+ self.fs.write_file("foo", "bar")
+ self.assertEqual(self.fs.cat("foo"), "bar")
+
+ def test_write_fails_if_file_exists_already(self):
+ file(os.path.join(self.dirname, "foo"), "w").write("bar")
+ self.assertRaises(OSError, self.fs.write_file, "foo", "foobar")
+
+ def test_write_creates_missing_directories(self):
+ self.fs.write_file("foo/bar", "yo")
+ self.assertEqual(self.fs.cat("foo/bar"), "yo")
+
+ def test_write_leaves_existing_file_intact(self):
+ file(os.path.join(self.dirname, "foo"), "w").write("bar")
+ try:
+ self.fs.write_file("foo", "foobar")
+ except OSError:
+ pass
+ self.assertEqual(self.fs.cat("foo"), "bar")
+
+ def test_overwrite_creates_new_file_ok(self):
+ self.fs.overwrite_file("foo", "bar")
+ self.assertEqual(self.fs.cat("foo"), "bar")
+
+ def test_overwrite_renames_existing_file(self):
+ self.fs.write_file("foo", "bar")
+ self.fs.overwrite_file("foo", "foobar")
+ self.assert_(self.fs.exists("foo.bak"))
+
+ def test_overwrite_removes_existing_bak_file(self):
+ self.fs.write_file("foo", "bar")
+ self.fs.write_file("foo.bak", "baz")
+ self.fs.overwrite_file("foo", "foobar")
+ self.assertEqual(self.fs.cat("foo.bak"), "bar")
+
+ def test_overwrite_replaces_existing_file(self):
+ self.fs.write_file("foo", "bar")
+ self.fs.overwrite_file("foo", "foobar")
+ self.assertEqual(self.fs.cat("foo"), "foobar")
+
+
+class DepthFirstTests(unittest.TestCase):
+
+ def setUp(self):
+ self.root = tempfile.mkdtemp()
+ self.dirs = ["foo", "foo/bar", "foobar"]
+ self.dirs = [os.path.join(self.root, x) for x in self.dirs]
+ for dir in self.dirs:
+ os.mkdir(dir)
+ self.dirs.insert(0, self.root)
+ self.fs = obnamlib.LocalFS("/")
+
+ def tearDown(self):
+ shutil.rmtree(self.root)
+
+ def testFindsAllDirs(self):
+ dirs = [x[0] for x in self.fs.depth_first(self.root)]
+ self.failUnlessEqual(sorted(dirs), sorted(self.dirs))
+
+ def prune(self, dirname, dirnames, filenames):
+ if "foo" in dirnames:
+ dirnames.remove("foo")
+
+ def testFindsAllDirsExceptThePrunedOne(self):
+ correct = [x
+ for x in self.dirs
+ if not x.endswith("/foo") and not "/foo/" in x]
+ dirs = [x[0]
+ for x in self.fs.depth_first(self.root, prune=self.prune)]
+ self.failUnlessEqual(sorted(dirs), sorted(correct))
diff --git a/obnamlib/vfs_sftp.py b/obnamlib/vfs_sftp.py
new file mode 100644
index 00000000..cf862132
--- /dev/null
+++ b/obnamlib/vfs_sftp.py
@@ -0,0 +1,202 @@
+# Copyright (C) 2009 Lars Wirzenius <liw@liw.fi>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+
+import errno
+import getpass
+import logging
+import os
+import pwd
+import stat
+import urlparse
+
+import paramiko
+
+import obnamlib
+
+
+class SftpFS(obnamlib.VirtualFileSystem):
+
+ """A VFS implementation for SFTP."""
+
+ def __init__(self, baseurl, progress):
+ obnamlib.VirtualFileSystem.__init__(self, baseurl, progress)
+ self.first_lutimes = True
+
+ def connect(self):
+ user = host = port = path = None
+ scheme, netloc, path, query, fragment = urlparse.urlsplit(self.baseurl)
+ assert scheme == "sftp", "wrong scheme in %s" % self.baseurl
+ if "@" in netloc:
+ user, netloc = netloc.split("@", 1)
+ else:
+ user = self.get_username()
+ if ":" in netloc:
+ host, port = netloc.split(":", 1)
+ port = int(port)
+ else:
+ host = netloc
+ port = 22
+ if path.startswith("/~/"):
+ path = path[3:]
+ self.basepath = path
+ self.transport = paramiko.Transport((host, port))
+ self.transport.connect()
+ self.check_host_key(host)
+ self.authenticate(user)
+ self.sftp = paramiko.SFTPClient.from_transport(self.transport)
+
+ def get_username(self):
+ return pwd.getpwuid(os.getuid()).pw_name
+
+ def check_host_key(self, hostname):
+ key = self.transport.get_remote_server_key()
+ known_hosts = os.path.expanduser('~/.ssh/known_hosts')
+ keys = paramiko.util.load_host_keys(known_hosts)
+ if hostname not in keys:
+ raise obnamlib.Exception("Host key for %s not found" % hostname)
+ elif not keys[hostname].has_key(key.get_name()):
+ raise obnamlib.Exception("Unknown host key for %s" % hostname)
+ elif keys[hostname][key.get_name()] != key:
+ print '*** WARNING: Host key has changed!!!'
+ raise obnamlib.Exception("Host key has changed for %s" % hostname)
+
+ def authenticate(self, username):
+ if self.authenticate_via_agent(username):
+ return
+ raise obnamlib.Exception("Can't authenticate to SSH server.")
+
+ def authenticate_via_agent(self, username):
+ agent = paramiko.Agent()
+ agent_keys = agent.get_keys()
+ for key in agent_keys:
+ try:
+ self.transport.auth_publickey(username, key)
+ return True
+ except paramiko.SSHException:
+ pass
+ return False
+
+ def close(self):
+ self.transport.close()
+
+ def join(self, relative_path):
+ return os.path.join(self.basepath, relative_path.lstrip("/"))
+
+ def listdir(self, relative_path):
+ return self.sftp.listdir(self.join(relative_path))
+
+ def lock(self, lockname):
+ try:
+ self.write_file(lockname, "")
+ except IOError, e:
+ if e.errno == errno.EEXIST:
+ raise obnamlib.Exception("Lock %s already exists" % lockname)
+ else:
+ raise
+
+ def unlock(self, lockname):
+ if self.exists(lockname):
+ self.remove(lockname)
+
+ def remove(self, relative_path):
+ self.sftp.remove(self.join(relative_path))
+
+ def lstat(self, relative_path):
+ return self.sftp.lstat(self.join(relative_path))
+
+ def chmod(self, relative_path, mode):
+ self.sftp.chmod(self.join(relative_path), mode)
+
+ def lutimes(self, relative_path, atime, mtime):
+ # FIXME: This does not work for symlinks!
+ # Sftp does not have a way of doing that. This means if the restore
+ # target is over sftp, symlinks and their targets will have wrong
+ # mtimes.
+ if self.first_lutimes:
+ logging.warning("lutimes used over SFTP, this does not work "
+ "against symlinks (warning appears only first "
+ "time)")
+ self.first_lutimes = False
+ self.sftp.utime(self.join(relative_path), (atime, mtime))
+
+ def link(self, existing, new):
+ raise obnamlib.Exception("Cannot link on SFTP. Sorry.")
+
+ def readlink(self, relative_path):
+ return self.sftp.readlink(self.join(relative_path))
+
+ def symlink(self, existing, new):
+ self.sftp.symlink(existing, self.join(new))
+
+ def open(self, relative_path, mode):
+ return self.sftp.file(self.join(relative_path), mode)
+
+ def exists(self, relative_path):
+ try:
+ self.lstat(relative_path)
+ return True
+ except IOError:
+ return False
+
+ def isdir(self, relative_path):
+ try:
+ st = self.lstat(relative_path)
+ except IOError:
+ return False
+ return stat.S_ISDIR(st.st_mode)
+
+ def mkdir(self, relative_path):
+ self.sftp.mkdir(self.join(relative_path))
+
+ def makedirs(self, relative_path):
+ if self.isdir(relative_path):
+ return
+ parent = os.path.dirname(relative_path)
+ if parent and parent != relative_path:
+ self.makedirs(parent)
+ self.mkdir(relative_path)
+
+ def cat(self, relative_path):
+ f = self.open(relative_path, "r")
+ chunks = []
+ while True:
+ # 32 KiB is the chunk size that gives me the fastest speed
+ # for sftp transfers. I don't know why the size matters.
+ chunk = f.read(32 * 1024)
+ if not chunk:
+ break
+ chunks.append(chunk)
+ self.progress["bytes-received"] += len(chunk)
+ f.close()
+ return "".join(chunks)
+
+ def write_helper(self, relative_path, mode, contents):
+ self.makedirs(os.path.dirname(relative_path))
+ f = self.open(relative_path, mode)
+ chunk_size = 32 * 1024
+ for pos in range(0, len(contents), chunk_size):
+ chunk = contents[pos:pos + chunk_size]
+ f.write(chunk)
+ self.progress["bytes-sent"] += len(chunk)
+ f.close()
+
+ def write_file(self, relative_path, contents):
+ self.write_helper(relative_path, 'wx', contents)
+
+ def overwrite_file(self, relative_path, contents):
+ self.write_helper(relative_path, 'w', contents)
+
diff --git a/setup.py b/setup.py
new file mode 100644
index 00000000..f2d11dfb
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,28 @@
+#!/usr/bin/python
+# Copyright (C) 2008 Lars Wirzenius <liw@liw.fi>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from distutils.core import setup, Extension
+
+setup(name='obnam',
+ version='0.0.0',
+ description='Backup software',
+ author='Lars Wirzenius',
+ author_email='liw@liw.fi',
+ url='http://braawi.org/obnam.html',
+ packages=['obnamlib'],
+ ext_modules=[Extension('_obnam', sources=['_obnammodule.c'])],
+ )
diff --git a/without-tests b/without-tests
index af7fc51f..3024c259 100644
--- a/without-tests
+++ b/without-tests
@@ -1,2 +1,5 @@
+./setup.py
./obnamlib/__init__.py
./obnamlib/status.py
+./obnamlib/vfs.py
+./obnamlib/vfs_sftp.py