|
@@ -5,6 +5,7 @@ from .constants import ITEM_KEYS
|
|
|
from .helpers import safe_encode, safe_decode
|
|
|
from .helpers import bigint_to_int, int_to_bigint
|
|
|
from .helpers import StableDict
|
|
|
+from .helpers import format_file_size
|
|
|
|
|
|
cdef extern from "_item.c":
|
|
|
object _object_to_optr(object obj)
|
|
@@ -184,19 +185,22 @@ class Item(PropDict):
|
|
|
|
|
|
part = PropDict._make_property('part', int)
|
|
|
|
|
|
- def get_size(self, hardlink_masters=None, memorize=False, compressed=False, from_chunks=False):
|
|
|
+ def get_size(self, hardlink_masters=None, memorize=False, compressed=False, from_chunks=False, consider_ids=None):
|
|
|
"""
|
|
|
Determine the (uncompressed or compressed) size of this item.
|
|
|
|
|
|
- For hardlink slaves, the size is computed via the hardlink master's
|
|
|
- chunk list, if available (otherwise size will be returned as 0).
|
|
|
-
|
|
|
- If memorize is True, the computed size value will be stored into the item.
|
|
|
+ :param hardlink_masters: If given, the size of hardlink slaves is computed via the hardlink master's chunk list,
|
|
|
+ otherwise size will be returned as 0.
|
|
|
+ :param memorize: Whether the computed size value will be stored into the item.
|
|
|
+ :param compressed: Whether the compressed or uncompressed size will be returned.
|
|
|
+ :param from_chunks: If true, size is computed from chunks even if a precomputed value is available.
|
|
|
+ :param consider_ids: Returns the size of the given ids only.
|
|
|
"""
|
|
|
attr = 'csize' if compressed else 'size'
|
|
|
assert not (compressed and memorize), 'Item does not have a csize field.'
|
|
|
+ assert not (consider_ids is not None and memorize), "Can't store size when considering only certain ids"
|
|
|
try:
|
|
|
- if from_chunks:
|
|
|
+ if from_chunks or consider_ids is not None:
|
|
|
raise AttributeError
|
|
|
size = getattr(self, attr)
|
|
|
except AttributeError:
|
|
@@ -226,7 +230,10 @@ class Item(PropDict):
|
|
|
chunks, _ = hardlink_masters.get(master, (None, None))
|
|
|
if chunks is None:
|
|
|
return 0
|
|
|
- size = sum(getattr(ChunkListEntry(*chunk), attr) for chunk in chunks)
|
|
|
+ if consider_ids is not None:
|
|
|
+ size = sum(getattr(ChunkListEntry(*chunk), attr) for chunk in chunks if chunk.id in consider_ids)
|
|
|
+ else:
|
|
|
+ size = sum(getattr(ChunkListEntry(*chunk), attr) for chunk in chunks)
|
|
|
# if requested, memorize the precomputed (c)size for items that have an own chunks list:
|
|
|
if memorize and having_chunks:
|
|
|
setattr(self, attr, size)
|
|
@@ -251,6 +258,21 @@ class Item(PropDict):
|
|
|
def from_optr(self, optr):
|
|
|
return _optr_to_object(optr)
|
|
|
|
|
|
+ @classmethod
|
|
|
+ def create_deleted(cls, path):
|
|
|
+ return cls(deleted=True, chunks=[], mode=0, path=path)
|
|
|
+
|
|
|
+ def is_link(self):
|
|
|
+ return self._is_type(stat.S_ISLNK)
|
|
|
+
|
|
|
+ def is_dir(self):
|
|
|
+ return self._is_type(stat.S_ISDIR)
|
|
|
+
|
|
|
+ def _is_type(self, typetest):
|
|
|
+ try:
|
|
|
+ return typetest(self.mode)
|
|
|
+ except AttributeError:
|
|
|
+ return False
|
|
|
|
|
|
|
|
|
class EncryptedKey(PropDict):
|
|
@@ -359,62 +381,119 @@ class ManifestItem(PropDict):
|
|
|
config = PropDict._make_property('config', dict)
|
|
|
item_keys = PropDict._make_property('item_keys', tuple)
|
|
|
|
|
|
- def compare_link(item1, item2):
|
|
|
- # These are the simple link cases. For special cases, e.g. if a
|
|
|
- # regular file is replaced with a link or vice versa, it is
|
|
|
- # indicated in compare_mode instead.
|
|
|
- if item1.get('deleted'):
|
|
|
+class ItemDiff:
|
|
|
+ """
|
|
|
+ Comparison of two items from different archives.
|
|
|
+
|
|
|
+ The items may have different paths and still be considered equal (e.g. for renames).
|
|
|
+ It does not include extended or time attributes in the comparison.
|
|
|
+ """
|
|
|
+
|
|
|
+ def __init__(self, item1, item2, chunk_iterator1, chunk_iterator2, numeric_owner=False, can_compare_chunk_ids=False):
|
|
|
+ self._item1 = item1
|
|
|
+ self._item2 = item2
|
|
|
+ self._numeric_owner = numeric_owner
|
|
|
+ self._can_compare_chunk_ids = can_compare_chunk_ids
|
|
|
+ self.equal = self._equal(chunk_iterator1, chunk_iterator2)
|
|
|
+
|
|
|
+ def __repr__(self):
|
|
|
+ if self.equal:
|
|
|
+ return 'equal'
|
|
|
+
|
|
|
+ changes = []
|
|
|
+
|
|
|
+ if self._item1.is_link() or self._item2.is_link():
|
|
|
+ changes.append(self._link_string())
|
|
|
+
|
|
|
+ if 'chunks' in self._item1 and 'chunks' in self._item2:
|
|
|
+ changes.append(self._content_string())
|
|
|
+
|
|
|
+ if self._item1.is_dir() or self._item2.is_dir():
|
|
|
+ changes.append(self._dir_string())
|
|
|
+
|
|
|
+ if not (self._item1.get('deleted') or self._item2.get('deleted')):
|
|
|
+ changes.append(self._owner_string())
|
|
|
+ changes.append(self._mode_string())
|
|
|
+
|
|
|
+ return ' '.join((x for x in changes if x))
|
|
|
+
|
|
|
+ def _equal(self, chunk_iterator1, chunk_iterator2):
|
|
|
+ # if both are deleted, there is nothing at path regardless of what was deleted
|
|
|
+ if self._item1.get('deleted') and self._item2.get('deleted'):
|
|
|
+ return True
|
|
|
+
|
|
|
+ attr_list = ['deleted', 'mode', 'source']
|
|
|
+ attr_list += ['uid', 'gid'] if self._numeric_owner else ['user', 'group']
|
|
|
+ for attr in attr_list:
|
|
|
+ if self._item1.get(attr) != self._item2.get(attr):
|
|
|
+ return False
|
|
|
+
|
|
|
+ if 'mode' in self._item1: # mode of item1 and item2 is equal
|
|
|
+ if (self._item1.is_link() and 'source' in self._item1 and 'source' in self._item2
|
|
|
+ and self._item1.source != self._item2.source):
|
|
|
+ return False
|
|
|
+
|
|
|
+ if 'chunks' in self._item1 and 'chunks' in self._item2:
|
|
|
+ return self._content_equal(chunk_iterator1, chunk_iterator2)
|
|
|
+
|
|
|
+ return True
|
|
|
+
|
|
|
+ def _link_string(self):
|
|
|
+ if self._item1.get('deleted'):
|
|
|
return 'added link'
|
|
|
- if item2.get('deleted'):
|
|
|
+ if self._item2.get('deleted'):
|
|
|
return 'removed link'
|
|
|
- if 'source' in item1 and 'source' in item2 and item1.source != item2.source:
|
|
|
+ if 'source' in self._item1 and 'source' in self._item2 and self._item1.source != self._item2.source:
|
|
|
return 'changed link'
|
|
|
|
|
|
-def compare_content(path, item1, item2):
|
|
|
- if contents_changed(item1, item2):
|
|
|
- if item1.get('deleted'):
|
|
|
- return 'added {:>13}'.format(format_file_size(sum_chunk_size(item2)))
|
|
|
- if item2.get('deleted'):
|
|
|
- return 'removed {:>11}'.format(format_file_size(sum_chunk_size(item1)))
|
|
|
- if not can_compare_chunk_ids:
|
|
|
+ def _content_string(self):
|
|
|
+ if self._item1.get('deleted'):
|
|
|
+ return ('added {:>13}'.format(format_file_size(self._item2.get_size())))
|
|
|
+ if self._item2.get('deleted'):
|
|
|
+ return ('removed {:>11}'.format(format_file_size(self._item1.get_size())))
|
|
|
+ if not self._can_compare_chunk_ids:
|
|
|
return 'modified'
|
|
|
- chunk_ids1 = {c.id for c in item1.chunks}
|
|
|
- chunk_ids2 = {c.id for c in item2.chunks}
|
|
|
+ chunk_ids1 = {c.id for c in self._item1.chunks}
|
|
|
+ chunk_ids2 = {c.id for c in self._item2.chunks}
|
|
|
added_ids = chunk_ids2 - chunk_ids1
|
|
|
removed_ids = chunk_ids1 - chunk_ids2
|
|
|
- added = sum_chunk_size(item2, added_ids)
|
|
|
- removed = sum_chunk_size(item1, removed_ids)
|
|
|
- return '{:>9} {:>9}'.format(format_file_size(added, precision=1, sign=True),
|
|
|
- format_file_size(-removed, precision=1, sign=True))
|
|
|
+ added = self._item2.get_size(consider_ids=added_ids)
|
|
|
+ removed = self._item1.get_size(consider_ids=removed_ids)
|
|
|
+ return ('{:>9} {:>9}'.format(format_file_size(added, precision=1, sign=True),
|
|
|
+ format_file_size(-removed, precision=1, sign=True)))
|
|
|
|
|
|
- def compare_directory(item1, item2):
|
|
|
- if item2.get('deleted') and not item1.get('deleted'):
|
|
|
+ def _dir_string(self):
|
|
|
+ if self._item2.get('deleted') and not self._item1.get('deleted'):
|
|
|
return 'removed directory'
|
|
|
- if item1.get('deleted') and not item2.get('deleted'):
|
|
|
+ if self._item1.get('deleted') and not self._item2.get('deleted'):
|
|
|
return 'added directory'
|
|
|
|
|
|
- def compare_owner(item1, item2):
|
|
|
- user1, group1 = get_owner(item1)
|
|
|
- user2, group2 = get_owner(item2)
|
|
|
- if user1 != user2 or group1 != group2:
|
|
|
- return '[{}:{} -> {}:{}]'.format(user1, group1, user2, group2)
|
|
|
+ def _owner_string(self):
|
|
|
+ u_attr, g_attr = ('uid', 'gid') if self._numeric_owner else ('user', 'group')
|
|
|
+ u1, g1 = self._item1.get(u_attr), self._item1.get(g_attr)
|
|
|
+ u2, g2 = self._item2.get(u_attr), self._item2.get(g_attr)
|
|
|
+ if (u1, g1) != (u2, g2):
|
|
|
+ return '[{}:{} -> {}:{}]'.format(u1, g1, u2, g2)
|
|
|
|
|
|
- def compare_mode(item1, item2):
|
|
|
- if item1.mode != item2.mode:
|
|
|
- return '[{} -> {}]'.format(get_mode(item1), get_mode(item2))
|
|
|
+ def _mode_string(self):
|
|
|
+ if 'mode' in self._item1 and 'mode' in self._item2 and self._item1.mode != self._item2.mode:
|
|
|
+ return '[{} -> {}]'.format(stat.filemode(self._item1.mode), stat.filemode(self._item2.mode))
|
|
|
|
|
|
- def contents_changed(item1, item2):
|
|
|
- if can_compare_chunk_ids:
|
|
|
- return item1.chunks != item2.chunks
|
|
|
- if sum_chunk_size(item1) != sum_chunk_size(item2):
|
|
|
- return True
|
|
|
- chunk_ids1 = [c.id for c in item1.chunks]
|
|
|
- chunk_ids2 = [c.id for c in item2.chunks]
|
|
|
- return not fetch_and_compare_chunks(chunk_ids1, chunk_ids2, archive1, archive2)
|
|
|
+ def _content_equal(self, chunk_iterator1, chunk_iterator2):
|
|
|
+ if self._can_compare_chunk_ids:
|
|
|
+ return self._item1.chunks == self._item2.chunks
|
|
|
+ if self._item1.get_size() != self._item2.get_size():
|
|
|
+ return False
|
|
|
+ return ItemDiff._chunk_content_equal(chunk_iterator1, chunk_iterator2)
|
|
|
|
|
|
@staticmethod
|
|
|
- def compare_chunk_contents(chunks1, chunks2):
|
|
|
- """Compare two chunk iterators (like returned by :meth:`.DownloadPipeline.fetch_many`)"""
|
|
|
+ def _chunk_content_equal(chunks1, chunks2):
|
|
|
+ """
|
|
|
+ Compare chunk content and return True if they are identical.
|
|
|
+
|
|
|
+ The chunks must be given as chunk iterators (like returned by :meth:`.DownloadPipeline.fetch_many`).
|
|
|
+ """
|
|
|
+
|
|
|
end = object()
|
|
|
alen = ai = 0
|
|
|
blen = bi = 0
|