summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--obnamlib/__init__.py6
-rw-r--r--obnamlib/metadata.py111
-rw-r--r--obnamlib/metadata_tests.py138
-rw-r--r--obnamlib/obj.py6
-rw-r--r--obnamlib/obj_tests.py6
-rw-r--r--obnamlib/objs.py24
-rw-r--r--obnamlib/vfs.py3
-rw-r--r--obnamlib/vfs_local.py3
-rw-r--r--without-tests1
9 files changed, 297 insertions, 1 deletions
diff --git a/obnamlib/__init__.py b/obnamlib/__init__.py
index ad0dd289..29062a4d 100644
--- a/obnamlib/__init__.py
+++ b/obnamlib/__init__.py
@@ -26,5 +26,9 @@ from interp import Interpreter
from pluginbase import ObnamPlugin
from vfs import VirtualFileSystem, VfsFactory
from vfs_local import LocalFS
-from obj import BackupObject, TYPE_ID, TYPE_ID_LIST, TYPE_INT, TYPE_STR
+from metadata import read_metadata, set_metadata, Metadata
+from obj import (BackupObject, TYPE_ID, TYPE_ID_LIST, TYPE_INT, TYPE_STR,
+ MetadataObject)
+from objs import *
+from store import Store
from app import App
diff --git a/obnamlib/metadata.py b/obnamlib/metadata.py
new file mode 100644
index 00000000..2a33ad40
--- /dev/null
+++ b/obnamlib/metadata.py
@@ -0,0 +1,111 @@
+# Copyright (C) 2009 Lars Wirzenius
+#
+# 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, see <http://www.gnu.org/licenses/>.
+
+
+import grp
+import os
+import pwd
+
+
+class Metadata(object):
+
+ '''Represent metadata for a filesystem entry.
+
+ The metadata for a filesystem entry (file, directory, device, ...)
+ consists of its stat(2) result, plus ACL and xattr.
+
+ This class represents them as fields.
+
+ We do not store all stat(2) fields. Here's a commentary on all fields:
+
+ field? stored? why
+
+ st_atime yes mutt compars atime with mtime to see if msg is new
+ st_blksize no no way to restore, not useful backed up
+ st_blocks yes used to see if restore should create holes in file
+ st_ctime no no way to restore, not useful backed up
+ st_dev yes used to restore hardlinks
+ st_gid yes used to restore group ownership
+ st_ino yes used to restore hardlinks
+ st_mode yes used to restore permissions
+ st_mtime yes used to restore mtime
+ st_nlink yes used to restore hardlinks
+ st_rdev no no use (correct me if I'm wrong about this)
+ st_size yes user needs it to see size of file in backup
+ st_uid yes used to restored ownership
+
+ Additionally, the fields 'groupname' and 'username' are stored. They
+ contain the textual names that correspond to st_gid and st_uid. When
+ restoring, the names will be preferred by default.
+
+ '''
+
+ def __init__(self):
+ self.st_atime = None
+ self.st_blocks = None
+ self.st_dev = None
+ self.st_gid = None
+ self.st_ino = None
+ self.st_mode = None
+ self.st_mtime = None
+ self.st_nlink = None
+ self.st_size = None
+ self.st_uid = None
+ self.groupname = None
+ self.username = None
+
+
+def read_metadata(fs, filename, getpwuid=None, getgrgid=None):
+ '''Return object detailing metadata for a filesystem entry.'''
+ metadata = Metadata()
+ stat_result = fs.lstat(filename)
+ for field in dir(metadata):
+ if field.startswith('st_'):
+ setattr(metadata, field, getattr(stat_result, field))
+
+ getpwuid = getpwuid or pwd.getpwuid
+ try:
+ metadata.groupname = getgrgid(metadata.st_gid)[0]
+ except KeyError:
+ metadata.groupname = None
+
+ getgrgid = getgrgid or grp.getgrgid
+ try:
+ metadata.username = getpwuid(metadata.st_uid)[0]
+ except KeyError:
+ metadata.username = None
+
+ return metadata
+
+
+def set_metadata(fs, filename, metadata, getuid=None):
+ '''Set metadata for a filesystem entry.
+
+ We only set metadata that can sensibly be set: st_atime, st_mode,
+ st_mtime. We also attempt to set ownership (st_gid, st_uid), but
+ only if we're running as root. We ignore the username, groupname
+ fields: we assume the caller will change st_uid, st_gid accordingly
+ if they want to mess with things. This makes the user take care
+ of error situations and looking up user preferences.
+
+ '''
+
+ fs.lutimes(filename, metadata.st_atime, metadata.st_mtime)
+ fs.chmod(filename, metadata.st_mode)
+
+ getuid = getuid or os.getuid
+ if getuid() == 0:
+ fs.chown(filename, metadata.st_uid, metadata.st_gid)
+
diff --git a/obnamlib/metadata_tests.py b/obnamlib/metadata_tests.py
new file mode 100644
index 00000000..7dd1acd0
--- /dev/null
+++ b/obnamlib/metadata_tests.py
@@ -0,0 +1,138 @@
+# Copyright (C) 2009 Lars Wirzenius
+#
+# 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, see <http://www.gnu.org/licenses/>.
+
+
+import os
+import stat
+import tempfile
+import unittest
+
+import obnamlib
+
+
+class FakeFS(object):
+
+ def __init__(self):
+ self.st_atime = 1
+ self.st_blocks = 2
+ self.st_dev = 3
+ self.st_gid = 4
+ self.st_ino = 5
+ self.st_mode = 6
+ self.st_mtime = 7
+ self.st_nlink = 8
+ self.st_size = 9
+ self.st_uid = 10
+ self.groupname = 'group'
+ self.username = 'user'
+
+ def lstat(self, filename):
+ return self
+
+ def getpwuid(self, uid):
+ return (self.username, None, self.st_uid, self.st_gid,
+ None, None, None)
+
+ def getgrgid(self, gid):
+ return (self.groupname, None, self.st_gid, None)
+
+ def fail_getpwuid(self, uid):
+ raise KeyError(uid)
+
+ def fail_getgrgid(self, gid):
+ raise KeyError(gid)
+
+
+class ReadMetadataTests(unittest.TestCase):
+
+ def setUp(self):
+ self.fakefs = FakeFS()
+
+ def test_returns_stat_fields_correctly(self):
+ metadata = obnamlib.read_metadata(self.fakefs, 'foo',
+ getpwuid=self.fakefs.getpwuid,
+ getgrgid=self.fakefs.getgrgid)
+ fields = ['st_atime', 'st_blocks', 'st_dev', 'st_gid', 'st_ino',
+ 'st_mode', 'st_mtime', 'st_nlink', 'st_size', 'st_uid',
+ 'groupname', 'username']
+ for field in fields:
+ self.assertEqual(getattr(metadata, field),
+ getattr(self.fakefs, field),
+ field)
+
+ def test_reads_username_as_None_if_lookup_fails(self):
+ metadata = obnamlib.read_metadata(self.fakefs, 'foo',
+ getpwuid=self.fakefs.fail_getpwuid,
+ getgrgid=self.fakefs.fail_getgrgid)
+ self.assertEqual(metadata.username, None)
+
+
+class SetMetadataTests(unittest.TestCase):
+
+ def setUp(self):
+ self.metadata = obnamlib.Metadata()
+ self.metadata.st_atime = 12765
+ self.metadata.st_mode = 42 | stat.S_IFREG
+ self.metadata.st_mtime = 10**9
+ self.metadata.st_uid = 1234
+ self.metadata.st_gid = 5678
+
+ fd, self.filename = tempfile.mkstemp()
+ os.close(fd)
+
+ self.fs = obnamlib.LocalFS('/')
+ self.fs.connect()
+
+ self.uid_set = None
+ self.gid_set = None
+ self.fs.chown = self.fake_chown
+
+ obnamlib.set_metadata(self.fs, self.filename, self.metadata)
+
+ self.st = os.stat(self.filename)
+
+ def tearDown(self):
+ self.fs.close()
+ os.remove(self.filename)
+
+ def fake_chown(self, filename, uid, gid):
+ self.uid_set = uid
+ self.gid_set = gid
+
+ def test_sets_atime(self):
+ self.assertEqual(self.st.st_atime, self.metadata.st_atime)
+
+ def test_sets_mode(self):
+ self.assertEqual(self.st.st_mode, self.metadata.st_mode)
+
+ def test_sets_mtime(self):
+ self.assertEqual(self.st.st_mtime, self.metadata.st_mtime)
+
+ def test_does_not_set_uid_when_not_running_as_root(self):
+ self.assertEqual(self.st.st_uid, os.getuid())
+
+ def test_does_not_set_gid_when_not_running_as_root(self):
+ self.assertEqual(self.st.st_gid, os.getgid())
+
+ def test_sets_uid_when_running_as_root(self):
+ obnamlib.set_metadata(self.fs, self.filename, self.metadata,
+ getuid=lambda: 0)
+ self.assertEqual(self.uid_set, self.metadata.st_uid)
+
+ def test_sets_gid_when_running_as_root(self):
+ obnamlib.set_metadata(self.fs, self.filename, self.metadata,
+ getuid=lambda: 0)
+ self.assertEqual(self.gid_set, self.metadata.st_gid)
+
diff --git a/obnamlib/obj.py b/obnamlib/obj.py
index 6d8219d5..d575fe46 100644
--- a/obnamlib/obj.py
+++ b/obnamlib/obj.py
@@ -97,5 +97,11 @@ class MetadataObject(BackupObject):
stat_fields = ('st_mtime',)
for stat_field in stat_fields:
self.values[stat_field] = (TYPE_INT, None)
+ if 'metadata' in kwargs:
+ self.set_from_metadata(kwargs['metadata'])
+ del kwargs['metadata']
self.set_from_kwargs(**kwargs)
+ def set_from_metadata(self, metadata):
+ self.st_mtime = int(metadata.st_mtime)
+
diff --git a/obnamlib/obj_tests.py b/obnamlib/obj_tests.py
index 89d55d8e..1dfde1d1 100644
--- a/obnamlib/obj_tests.py
+++ b/obnamlib/obj_tests.py
@@ -122,3 +122,9 @@ class MetadataObjectTests(unittest.TestCase):
obj = TestMetadataObject(st_mtime=123)
self.assertEqual(obj.st_mtime, 123)
+ def test_sets_metadata_via_initializer_argument(self):
+ metadata = obnamlib.Metadata()
+ metadata.st_mtime = 12765
+ obj = TestMetadataObject(metadata=metadata)
+ self.assertEqual(obj.st_mtime, metadata.st_mtime)
+
diff --git a/obnamlib/objs.py b/obnamlib/objs.py
new file mode 100644
index 00000000..dd03c62e
--- /dev/null
+++ b/obnamlib/objs.py
@@ -0,0 +1,24 @@
+# Copyright (C) 2009 Lars Wirzenius
+#
+# 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, see <http://www.gnu.org/licenses/>.
+
+
+from obnamlib import (BackupObject, TYPE_ID, TYPE_ID_LIST, TYPE_INT, TYPE_STR,
+ MetadataObject)
+
+
+class File(MetadataObject):
+
+ fields = (('basename', TYPE_STR),)
+
diff --git a/obnamlib/vfs.py b/obnamlib/vfs.py
index 829778e3..6616ca8b 100644
--- a/obnamlib/vfs.py
+++ b/obnamlib/vfs.py
@@ -84,6 +84,9 @@ class VirtualFileSystem(object):
def lstat(self, relative_path):
"""Like os.lstat."""
+ def chown(self, relative_path, uid, gid):
+ '''Like os.chown.'''
+
def chmod(self, relative_path, mode):
"""Like os.chmod."""
diff --git a/obnamlib/vfs_local.py b/obnamlib/vfs_local.py
index 42a00892..f15cd980 100644
--- a/obnamlib/vfs_local.py
+++ b/obnamlib/vfs_local.py
@@ -51,6 +51,9 @@ class LocalFS(obnamlib.VirtualFileSystem):
def lstat(self, relative_path):
return os.lstat(self.join(relative_path))
+ def chown(self, relative_path, uid, gid): # pragma: no cover
+ os.chown(self.join(relative_path), uid, gid)
+
def chmod(self, relative_path, mode):
os.chmod(self.join(relative_path), mode)
diff --git a/without-tests b/without-tests
index ae81b9cb..3a17f75f 100644
--- a/without-tests
+++ b/without-tests
@@ -4,6 +4,7 @@
./obnamlib/status.py
./obnamlib/vfs.py
./obnamlib/vfs_sftp.py
+./obnamlib/objs.py
./obnamlib/plugins/foo_plugin.py
./obnamlib/plugins/terminal_status_plugin.py
./test-plugins/hello_plugin.py