diff options
author | Lars Wirzenius <liw@liw.fi> | 2009-12-03 16:21:24 +0200 |
---|---|---|
committer | Lars Wirzenius <liw@liw.fi> | 2009-12-03 16:21:24 +0200 |
commit | e36c72a3e6b877837382a3e1e85417117383bdbc (patch) | |
tree | 082f183d19247e132475b230590a73e1c12505ae | |
parent | 07bbfa9da73a716ea2df764a339b26829b545071 (diff) | |
download | obnam-e36c72a3e6b877837382a3e1e85417117383bdbc.tar.gz |
Wrote a class to represent filesystem entry metadata. Only covers
stat(2) results for now.
MetadataObjects can set metadata via a 'metadata' keyword argument to
initializer, or the set_from_metadata method.
Wrote a class to represent a non-directory file.
Virtual filesystems now have a chown method. It is untested, since testing
it properly would require running as root, and I don't feel like mocking
this for LocalFS. Not implemented in SftpFS.
-rw-r--r-- | obnamlib/__init__.py | 6 | ||||
-rw-r--r-- | obnamlib/metadata.py | 111 | ||||
-rw-r--r-- | obnamlib/metadata_tests.py | 138 | ||||
-rw-r--r-- | obnamlib/obj.py | 6 | ||||
-rw-r--r-- | obnamlib/obj_tests.py | 6 | ||||
-rw-r--r-- | obnamlib/objs.py | 24 | ||||
-rw-r--r-- | obnamlib/vfs.py | 3 | ||||
-rw-r--r-- | obnamlib/vfs_local.py | 3 | ||||
-rw-r--r-- | without-tests | 1 |
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 |