]> arthur.barton.de Git - bup.git/blobdiff - lib/bup/vfs.py
vfs: change /save/latest back to a symlink to the latest save
[bup.git] / lib / bup / vfs.py
index 1c67577f000b6c458c781edd7086e050fe1d57e5..e36df64d936347eb288f01a67ee1aba49abd7049 100644 (file)
@@ -16,7 +16,6 @@ case.  Any item.meta Metadata instances must not be modified directly.
 Make a copy to modify via item.meta.copy() if needed, or call
 copy_item().
 
-
 The want_meta argument is advisory for calls that accept it, and it
 may not be honored.  Callers must be able to handle an item.meta value
 that is either an instance of Metadata or an integer mode, perhaps
@@ -49,8 +48,8 @@ item.coid.
 
 from __future__ import absolute_import, print_function
 from collections import namedtuple
-from errno import ELOOP, ENOENT, ENOTDIR
-from itertools import chain, dropwhile, groupby, izip, tee
+from errno import EINVAL, ELOOP, ENOENT, ENOTDIR
+from itertools import chain, dropwhile, groupby, tee
 from random import randrange
 from stat import S_IFDIR, S_IFLNK, S_IFREG, S_ISDIR, S_ISLNK, S_ISREG
 from time import localtime, strftime
@@ -95,12 +94,25 @@ def _normal_or_chunked_file_size(repo, oid):
         _, obj_t, size = next(it)
     return ofs + sum(len(b) for b in it)
 
+def _skip_chunks_before_offset(tree, offset):
+    prev_ent = next(tree, None)
+    if not prev_ent:
+        return tree
+    ent = None
+    for ent in tree:
+        ent_ofs = int(ent[1], 16)
+        if ent_ofs > offset:
+            return chain([prev_ent, ent], tree)
+        if ent_ofs == offset:
+            return chain([ent], tree)
+        prev_ent = ent
+    return [prev_ent]
+
 def _tree_chunks(repo, tree, startofs):
     "Tree should be a sequence of (name, mode, hash) as per tree_decode()."
     assert(startofs >= 0)
     # name is the chunk's hex offset in the original file
-    tree = dropwhile(lambda (_1, name, _2): int(name, 16) < startofs, tree)
-    for mode, name, oid in tree:
+    for mode, name, oid in _skip_chunks_before_offset(tree, startofs):
         ofs = int(name, 16)
         skipmore = startofs - ofs
         if skipmore < 0:
@@ -163,18 +175,19 @@ class _FileReader(object):
         return self._size
         
     def seek(self, ofs):
-        if ofs < 0:
-            raise IOError(errno.EINVAL, 'Invalid argument')
-        if ofs > self._compute_size():
-            raise IOError(errno.EINVAL, 'Invalid argument')
+        if ofs < 0 or ofs > self._compute_size():
+            raise IOError(EINVAL, 'Invalid seek offset: %d' % ofs)
         self.ofs = ofs
 
     def tell(self):
         return self.ofs
 
     def read(self, count=-1):
+        size = self._compute_size()
+        if self.ofs >= size:
+            return ''
         if count < 0:
-            count = self._compute_size() - self.ofs
+            count = size - self.ofs
         if not self.reader or self.reader.ofs != self.ofs:
             self.reader = _ChunkReader(self._repo, self.oid, self.ofs)
         try:
@@ -223,6 +236,7 @@ def _decompose_path(path):
 
 Item = namedtuple('Item', ('meta', 'oid'))
 Chunky = namedtuple('Chunky', ('meta', 'oid'))
+FakeLink = namedtuple('FakeLink', ('meta', 'target'))
 Root = namedtuple('Root', ('meta'))
 Tags = namedtuple('Tags', ('meta'))
 RevList = namedtuple('RevList', ('meta', 'oid'))
@@ -238,8 +252,8 @@ _tags = Tags(meta=default_dir_mode)
 ### vfs cache
 
 ### A general purpose shared cache with (currently) cheap random
-### eviction.  There is currently no weighting so a single commit item
-### is just as likely to be evicted as an entire "rev-list".  See
+### eviction.  At the moment there is no weighting so a single commit
+### item is just as likely to be evicted as an entire "rev-list".  See
 ### is_valid_cache_key for a description of the expected content.
 
 _cache = {}
@@ -254,26 +268,29 @@ def clear_cache():
 def is_valid_cache_key(x):
     """Return logically true if x looks like it could be a valid cache key
     (with respect to structure).  Current valid cache entries:
-      commit_oid -> commit
-      commit_oid + ':r' -> rev-list
-         i.e. rev-list -> {'.', commit, '2012...', next_commit, ...}
+      res:... -> resolution
+      itm:OID -> Commit
+      rvl:OID -> {'.', commit, '2012...', next_commit, ...}
     """
     # Suspect we may eventually add "(container_oid, name) -> ...", and others.
     x_t = type(x)
     if x_t is bytes:
-        if len(x) == 20:
+        tag = x[:4]
+        if tag in ('itm:', 'rvl:') and len(x) == 24:
             return True
-        if len(x) == 22 and x.endswith(b':r'):
+        if tag == 'res:':
             return True
 
 def cache_get(key):
     global _cache
-    assert is_valid_cache_key(key)
+    if not is_valid_cache_key(key):
+        raise Exception('invalid cache key: ' + repr(key))
     return _cache.get(key)
 
 def cache_notice(key, value):
     global _cache, _cache_keys, _cache_max_items
-    assert is_valid_cache_key(key)
+    if not is_valid_cache_key(key):
+        raise Exception('invalid cache key: ' + repr(key))
     if key in _cache:
         return
     if len(_cache) < _cache_max_items:
@@ -286,19 +303,19 @@ def cache_notice(key, value):
     _cache_keys[victim_i] = key
     _cache[key] = value
 
-
 def cache_get_commit_item(oid, need_meta=True):
     """Return the requested tree item if it can be found in the cache.
     When need_meta is true don't return a cached item that only has a
     mode."""
     # tree might be stored independently, or as '.' with its entries.
-    item = cache_get(oid)
+    commit_key = b'itm:' + oid
+    item = cache_get(commit_key)
     if item:
         if not need_meta:
             return item
         if isinstance(item.meta, Metadata):
             return item
-    entries = cache_get(oid + b':r')
+    entries = cache_get(b'rvl:' + oid)
     if entries:
         return entries['.']
 
@@ -307,7 +324,6 @@ def cache_get_revlist_item(oid, need_meta=True):
     if commit:
         return RevList(oid=oid, meta=commit.meta)
 
-
 def copy_item(item):
     """Return a completely independent copy of item, such that
     modifications will not affect the original.
@@ -383,6 +399,8 @@ def readlink(repo, item):
         target = item.meta.symlink_target
         if target:
             return target
+    elif isinstance(item, FakeLink):
+        return item.target
     return _readlink(repo, item.oid)
 
 def _compute_item_size(repo, item):
@@ -429,7 +447,8 @@ def _commit_item_from_oid(repo, oid, require_meta):
         meta = _find_treeish_oid_metadata(repo, commit.oid)
         if meta:
             commit = commit._replace(meta=meta)
-    cache_notice(oid, commit)
+    commit_key = b'itm:' + oid
+    cache_notice(commit_key, commit)
     return commit
 
 def _revlist_item_from_oid(repo, oid, require_meta):
@@ -602,7 +621,8 @@ def _item_for_rev(rev):
     if item:
         return item
     item = Commit(meta=default_dir_mode, oid=tree_oid, coid=coid)
-    cache_notice(item.coid, item)
+    commit_key = b'itm:' + coid
+    cache_notice(commit_key, item)
     return item
 
 def cache_commit(repo, oid):
@@ -619,13 +639,14 @@ def cache_commit(repo, oid):
     revs = None  # Don't disturb the tees
     rev_names = _reverse_suffix_duplicates(_name_for_rev(x) for x in rev_names)
     rev_items = (_item_for_rev(x) for x in rev_items)
-    latest = None
+    tip = None
     for item in rev_items:
-        latest = latest or item
         name = next(rev_names)
+        tip = tip or (name, item)
         entries[name] = item
-    entries['latest'] = latest
-    cache_notice(latest.coid + b':r', entries)
+    entries['latest'] = FakeLink(meta=default_symlink_mode, target=tip[0])
+    revlist_key = b'rvl:' + tip[1].coid
+    cache_notice(revlist_key, entries)
     return entries
 
 def revlist_items(repo, oid, names):
@@ -639,7 +660,8 @@ def revlist_items(repo, oid, names):
 
     # For now, don't worry about the possibility of the contents being
     # "too big" for the cache.
-    entries = cache_get(oid + b':r')
+    revlist_key = b'rvl:' + oid
+    entries = cache_get(revlist_key)
     if not entries:
         entries = cache_commit(repo, oid)
 
@@ -734,24 +756,19 @@ def contents(repo, item, names=None, want_meta=True):
     assert repo
     assert S_ISDIR(item_mode(item))
     item_t = type(item)
-
     if item_t in real_tree_types:
         it = repo.cat(item.oid.encode('hex'))
-        _, obj_type, size = next(it)
+        _, obj_t, size = next(it)
         data = ''.join(it)
-        if obj_type == 'tree':
-            if want_meta:
-                item_gen = tree_items_with_meta(repo, item.oid, data, names)
-            else:
-                item_gen = tree_items(item.oid, data, names)
-        elif obj_type == 'commit':
-            if want_meta:
-                item_gen = tree_items_with_meta(repo, item.oid, tree_data, names)
-            else:
-                item_gen = tree_items(item.oid, tree_data, names)
-        else:
+        if obj_t != 'tree':
             for _ in it: pass
-            raise Exception('unexpected git ' + obj_type)
+            # Note: it shouldn't be possible to see an Item with type
+            # 'commit' since a 'commit' should always produce a Commit.
+            raise Exception('unexpected git ' + obj_t)
+        if want_meta:
+            item_gen = tree_items_with_meta(repo, item.oid, data, names)
+        else:
+            item_gen = tree_items(item.oid, data, names)
     elif item_t == RevList:
         item_gen = revlist_items(repo, item.oid, names)
     elif item_t == Root:
@@ -764,6 +781,18 @@ def contents(repo, item, names=None, want_meta=True):
         yield x
 
 def _resolve_path(repo, path, parent=None, want_meta=True, deref=False):
+    cache_key = b'res:%d%d%d:%s\0%s' \
+                % (bool(want_meta), bool(deref), repo.id(),
+                   ('/'.join(x[0] for x in parent) if parent else ''),
+                   '/'.join(path))
+    resolution = cache_get(cache_key)
+    if resolution:
+        return resolution
+
+    def notice_resolution(r):
+        cache_notice(cache_key, r)
+        return r
+
     def raise_dir_required_but_not_dir(path, parent, past):
         raise IOError(ENOTDIR,
                       "path %r%s resolves to non-directory %r"
@@ -789,10 +818,10 @@ def _resolve_path(repo, path, parent=None, want_meta=True, deref=False):
         deref = True
     if not future:  # path was effectively '.' or '/'
         if is_absolute:
-            return (('', _root),)
+            return notice_resolution((('', _root),))
         if parent:
-            return tuple(parent)
-        return [('', _root)]
+            return notice_resolution(tuple(parent))
+        return notice_resolution((('', _root),))
     if is_absolute:
         past = [('', _root)]
     else:
@@ -802,7 +831,7 @@ def _resolve_path(repo, path, parent=None, want_meta=True, deref=False):
         if not future:
             if must_be_dir and not S_ISDIR(item_mode(past[-1][1])):
                 raise_dir_required_but_not_dir(path, parent, past)
-            return tuple(past)
+            return notice_resolution(tuple(past))
         segment = future.pop()
         if segment == '..':
             assert len(past) > 0
@@ -823,7 +852,7 @@ def _resolve_path(repo, path, parent=None, want_meta=True, deref=False):
                 past[-1] = parent_name, parent_item
             if not item:
                 past.append((segment, None),)
-                return tuple(past)
+                return notice_resolution(tuple(past))
             mode = item_mode(item)
             if not S_ISLNK(mode):
                 if not S_ISDIR(mode):
@@ -837,7 +866,7 @@ def _resolve_path(repo, path, parent=None, want_meta=True, deref=False):
                                       terminus=past)
                     if must_be_dir:
                         raise_dir_required_but_not_dir(path, parent, past)
-                    return tuple(past)
+                    return notice_resolution(tuple(past))
                 # It's treeish
                 if want_meta and type(item) in real_tree_types:
                     dir_meta = _find_treeish_oid_metadata(repo, item.oid)
@@ -857,7 +886,7 @@ def _resolve_path(repo, path, parent=None, want_meta=True, deref=False):
                 is_absolute, _, target_future = _decompose_path(target)
                 if is_absolute:
                     if not target_future:  # path was effectively '/'
-                        return (('', _root),)
+                        return notice_resolution((('', _root),))
                     past = [('', _root)]
                     future = target_future
                 else:
@@ -964,7 +993,10 @@ def augment_item_meta(repo, item, include_size=False):
     meta.mode = m
     meta.uid = meta.gid = meta.atime = meta.mtime = meta.ctime = 0
     if S_ISLNK(m):
-        target = _readlink(repo, item.oid)
+        if isinstance(item, FakeLink):
+            target = item.target
+        else:
+            target = _readlink(repo, item.oid)
         meta.symlink_target = target
         meta.size = len(target)
     elif include_size: