From ceb6b7fdeed677344aa43c3a8fe663cfca81e3df Mon Sep 17 00:00:00 2001 From: Aleksey Lim Date: Tue, 17 Apr 2012 05:13:40 +0000 Subject: Started work to keep votes related properties out of regular ones --- diff --git a/active_document/__init__.py b/active_document/__init__.py index ea88f36..14988a4 100644 --- a/active_document/__init__.py +++ b/active_document/__init__.py @@ -24,7 +24,7 @@ from active_document.env import ACCESS_CREATE, ACCESS_WRITE, ACCESS_READ, \ NotFound, Forbidden, principal from active_document.metadata import Metadata, Property, \ - AggregatorProperty, StoredProperty, ActiveProperty, \ + VoteProperty, StoredProperty, ActiveProperty, \ CounterProperty, BlobProperty, BrowsableProperty, \ active_property, active_method, Method diff --git a/active_document/document.py b/active_document/document.py index 13a4486..680db4a 100644 --- a/active_document/document.py +++ b/active_document/document.py @@ -20,7 +20,7 @@ from gettext import gettext as _ from active_document import env, util from active_document.document_class import DocumentClass from active_document.metadata import BlobProperty, StoredProperty -from active_document.metadata import AggregatorProperty, active_method +from active_document.metadata import VoteProperty, active_method from active_document.util import enforce @@ -116,9 +116,8 @@ class Document(DocumentClass): if self._record is None: self._record = self._storage.get(self.guid) orig = self._record.get(prop.name) - elif isinstance(prop, AggregatorProperty): - orig = self._storage.is_aggregated( - self.guid, prop.name, prop.value) + elif isinstance(prop, VoteProperty): + orig = self._storage.has_vote(self.guid, prop.name, prop.voter) else: raise RuntimeError(_('Property "%s" in "%s" cannot be get') % \ (prop.name, self.metadata.name)) @@ -150,8 +149,7 @@ class Document(DocumentClass): else: self.assert_access(env.ACCESS_WRITE, prop) - enforce(isinstance(prop, StoredProperty) or \ - isinstance(prop, AggregatorProperty), + enforce(isinstance(prop, StoredProperty), _('Property "%s" in "%s" cannot be set'), prop.name, self.metadata.name) @@ -250,6 +248,14 @@ class Document(DocumentClass): prop.name, self.metadata.name) return self._storage.stat_blob(self.guid, prop.name) + @active_method(cmd='vote') + def vote(self, prop): + self._vote(prop, True) + + @active_method(cmd='unvote') + def unvote(self, prop): + self._vote(prop, False) + def on_create(self, properties, cache): """Call back to call on document creation. @@ -337,3 +343,22 @@ class Document(DocumentClass): if orig is None: orig, __ = self._cache.get(prop.name, (None, None)) self._cache[prop.name] = cast(orig), cast(new) + + def _vote(self, prop, new): + prop = self.metadata[prop] + enforce(isinstance(prop, VoteProperty), + _('Cannot vote for "%s" property in "%s"'), + prop.name, self.metadata.name) + + orig = self._storage.has_vote(self.guid, prop.name, prop.voter) + if orig == new: + return + + self._storage.vote(self.guid, prop.name, prop.voter, new) + + if prop.counter: + if new: + self[prop.counter] = 1 + elif not is_new: + self[prop.counter] = -1 + self.post() diff --git a/active_document/document_class.py b/active_document/document_class.py index 92313f6..83a29cd 100644 --- a/active_document/document_class.py +++ b/active_document/document_class.py @@ -19,7 +19,7 @@ from gettext import gettext as _ from active_document import env from active_document.storage import Storage from active_document.metadata import Metadata, active_property -from active_document.metadata import ActiveProperty, AggregatorProperty +from active_document.metadata import ActiveProperty, VoteProperty from active_document.metadata import CounterProperty, StoredProperty from active_document.metadata import BrowsableProperty from active_document.util import enforce @@ -163,16 +163,16 @@ class DocumentClass(object): every object to let the caller execute urgent tasks """ - aggregated_props = [] + vote_props = [] for prop in cls.metadata.values(): - if isinstance(prop, AggregatorProperty) and prop.counter: - aggregated_props.append(prop) + if isinstance(prop, VoteProperty) and prop.counter: + vote_props.append(prop) for guid, props in cls._storage.walk(cls._index.mtime): - for prop in aggregated_props: - counter = cls._storage.count_aggregated(guid, prop.name) + for prop in vote_props: + counter = cls._storage.count_voters(guid, prop.name) if counter != props[prop.counter]: - cls._storage.put(guid, {prop.counter: counter}) + props[prop.counter] = counter cls._index.store(guid, props, None, cls._pre_store, cls._post_store) yield @@ -284,20 +284,6 @@ class DocumentClass(object): """ is_reindexing = not changes - for prop_name, new in changes.items(): - prop = cls.metadata[prop_name] - if not isinstance(prop, AggregatorProperty): - continue - orig = cls._storage.is_aggregated(guid, prop_name, new.value) - if new == orig: - if not is_new: - del changes[prop_name] - elif prop.counter: - if new: - changes[prop.counter] = 1 - elif not is_new: - changes[prop.counter] = -1 - if is_reindexing or not is_new: record = cls._storage.get(guid) for prop_name, prop in cls.metadata.items(): @@ -305,10 +291,6 @@ class DocumentClass(object): if not is_reindexing and isinstance(prop, CounterProperty): changes[prop_name] = \ record.get(prop_name) + changes[prop_name] - elif isinstance(prop, AggregatorProperty): - if is_reindexing: - changes[prop.counter] = \ - cls._storage.count_aggregated(guid, prop_name) elif isinstance(prop, StoredProperty): changes[prop_name] = record.get(prop_name) @@ -322,14 +304,4 @@ class DocumentClass(object): Method will be called in separate, index writer, thread. """ - for prop_name, new in changes.items(): - prop = cls.metadata[prop_name] - if not isinstance(prop, AggregatorProperty): - continue - if new: - cls._storage.aggregate(guid, prop_name, new.value) - elif not is_new: - cls._storage.disaggregate(guid, prop_name, new.value) - del changes[prop_name] - cls._storage.put(guid, changes) diff --git a/active_document/metadata.py b/active_document/metadata.py index f73a6d1..cbfa6c8 100644 --- a/active_document/metadata.py +++ b/active_document/metadata.py @@ -328,18 +328,19 @@ class ActiveProperty(StoredProperty): return self._boolean -class AggregatorProperty(Property, BrowsableProperty): +class VoteProperty(Property, BrowsableProperty): """Property that aggregates arbitrary values. This properties is repesented by boolean value (int in string notation) - that shows that `AggregatorProperty.value` is aggregated or not. - After setting this property, `AggregatorProperty.value` will be added or + that shows that `VoteProperty.value` is aggregated or not. + After setting this property, `VoteProperty.value` will be added or removed from the aggregatation list. """ def __init__(self, name, counter): - Property.__init__(self, name, typecast=bool, default=False) + Property.__init__(self, name, typecast=bool, default=False, + permissions=env.ACCESS_READ) self._counter = counter @property @@ -348,15 +349,9 @@ class AggregatorProperty(Property, BrowsableProperty): return self._counter @property - def value(self): + def voter(self): return env.principal.user - def encode(self, value): - return AggregatedValue(self.value, bool(value)) - - def decode(self, value): - return bool(value) - class CounterProperty(ActiveProperty): """Only index property that can be changed only by incrementing. @@ -396,22 +391,6 @@ class BlobProperty(Property): return self._mime_type -class AggregatedValue(int): - - def __new__(cls, value, enabled): - self = int.__new__(cls, int(enabled)) - self.value = value - return self - - def __cmp__(self, other): - if hasattr(other, 'value'): - # pylint: disable-msg=E1101 - diff = cmp(self.value, other.value) - if diff: - return diff - return int.__cmp__(self, other) - - class Method(object): def __init__(self, cb=None, cmd=None, http_method='GET', diff --git a/active_document/storage.py b/active_document/storage.py index 46098d0..88ff177 100644 --- a/active_document/storage.py +++ b/active_document/storage.py @@ -28,14 +28,13 @@ from gettext import gettext as _ from active_document import util, env from active_document.metadata import StoredProperty, CounterProperty -from active_document.metadata import AggregatorProperty, BlobProperty -from active_document.metadata import AggregatedValue +from active_document.metadata import VoteProperty, BlobProperty from active_document.util import enforce _PAGE_SIZE = 4096 _SEQNO_SUFFIX = '.seqno' -_AGGREGATE_SUFFIX = '.value' +_VOTE_SUFFIX = '.value' _logger = logging.getLogger('ad.storage') @@ -203,68 +202,55 @@ class Storage(object): 'sha1sum': sha1sum, } - def is_aggregated(self, guid, name, value): - """Check if specified `value` is aggregated to the `name` property. + def has_vote(self, guid, name, voter): + """Did specified `voter` make a vote for `name` property. :param guid: document's GUID to check :param name: - aggregated property name - :param value: - value to check for aggregation + vote property name + :param voter: + votter to check on behalf of :returns: - `True` if `value` is aggregated - - """ - path = self._path(guid, name + '.' + str(value) + _AGGREGATE_SUFFIX) - return AggregatedValue(value, _aggregated(path)) - - def aggregate(self, guid, name, value): - """Append specified `value` to `name` property. - - :param guid: - document's GUID to aggregate to - :param name: - aggregated property name - :param value: - value to aggregate + `True` if vote exists """ - seqno = self.metadata.next_seqno() - self._set_aggregate(guid, name, value, True, seqno, time.time()) - self._write_property(guid, 'seqno', seqno) + path = self._path(guid, name + '.' + str(voter) + _VOTE_SUFFIX) + return _has_vote(path) - def disaggregate(self, guid, name, value): - """Remove specified `value` to `name` property. + def vote(self, guid, name, voter, value): + """Change vote status. :param guid: - document's GUID to remove from + document's GUID to vote for :param name: - aggregated property name + vote property name + :param voter: + votter to change on behalf of :param value: - value to remove + vote status; `True` to vote, `False` for unvote """ seqno = self.metadata.next_seqno() - self._set_aggregate(guid, name, value, False, seqno, time.time()) + self._vote(guid, name, voter, value, seqno, time.time()) self._write_property(guid, 'seqno', seqno) - def count_aggregated(self, guid, name): - """Count number of entities aggregated to `name` property. + def count_voters(self, guid, name): + """Count number of voters. :param guid: document's GUID to count in :param name: - aggregated property name + property name to vote :returns: - integer value with number of aggregated entities + number of voters """ result = 0 - for i in glob(self._path(guid, name + '.*' + _AGGREGATE_SUFFIX)): - if _aggregated(i): + for i in glob(self._path(guid, name + '.*' + _VOTE_SUFFIX)): + if _has_vote(i): result += 1 - _logger.debug('There are %s entities in "%s" aggregated property ' \ + _logger.debug('There are %s entities in "%s" vote property ' \ 'of "%s" document in "%s"', result, name, guid, self.metadata.name) return result @@ -285,14 +271,14 @@ class Storage(object): for name, prop in self.metadata.items(): path = self._path(guid, name) - if isinstance(prop, AggregatorProperty): + if isinstance(prop, VoteProperty): values = [] - for i in glob(path + '.*' + _AGGREGATE_SUFFIX): + for i in glob(path + '.*' + _VOTE_SUFFIX): value = basename(i).split('.')[1] seqno_stat = os.stat(path + '.' + value + _SEQNO_SUFFIX) if int(seqno_stat.st_mtime) in accept_range: values.append( - ((value, _aggregated(i)), os.stat(i).st_mtime)) + ((value, _has_vote(i)), os.stat(i).st_mtime)) if values: traits[name] = values elif exists(path) and not isinstance(prop, CounterProperty): @@ -334,12 +320,16 @@ class Storage(object): path = self._ensure_path(create_stamp, guid, name) prop = self.metadata[name] - if isinstance(prop, AggregatorProperty): - for (value, aggregated), ts in diff[name]: - agg_path = path + '.' + value + _AGGREGATE_SUFFIX - if not exists(agg_path) or os.stat(agg_path).st_mtime < ts: - self._set_aggregate(guid, name, value, aggregated, - seqno, ts) + if isinstance(prop, VoteProperty): + for (voter, voted), ts in diff[name]: + vote_path = path + '.' + voter + _VOTE_SUFFIX + if not exists(vote_path) or \ + os.stat(vote_path).st_mtime < ts: + self._vote(guid, name, voter, voted, seqno, ts) + if prop.counter: + print '>', self.count_voters(guid, name) + self._write_property(guid, prop.counter, + self.count_voters(guid, name), seqno, ts) applied = True else: value, ts = diff[name] @@ -425,30 +415,29 @@ class Storage(object): name, guid, self.metadata.name) return final_size - def _set_aggregate(self, guid, name, value, aggregated, seqno, mtime=None): + def _vote(self, guid, name, voter, value, seqno, mtime=None): try: - value = str(value) - enforce(value.isalnum(), _('Aggregated value should be alnum')) - path = self._ensure_path(False, guid, name + '.' + value) - agg_path = path + _AGGREGATE_SUFFIX - if not exists(agg_path): - file(agg_path, 'w').close() + voter = str(voter) + enforce(voter.isalnum(), _('Voter value should be alnum')) + path = self._ensure_path(False, guid, name + '.' + voter) + vote_path = path + _VOTE_SUFFIX + if not exists(vote_path): + file(vote_path, 'w').close() if mtime: - os.utime(agg_path, (mtime, mtime)) + os.utime(vote_path, (mtime, mtime)) _touch_seqno(path, seqno) - mode = os.stat(agg_path).st_mode - if aggregated: + mode = os.stat(vote_path).st_mode + if value: mode |= stat.S_ISVTX else: mode &= ~stat.S_ISVTX - os.chmod(agg_path, mode) + os.chmod(vote_path, mode) except Exception, error: util.exception() - raise RuntimeError(_('Cannot change "%s" aggregatation for "%s" ' \ - 'property of "%s" in "%s": %s') % \ - (value, name, guid, self.metadata.name, error)) - _logger.debug('Changed "%s" aggregattion from "%s" of "%s" document ' \ - 'in "%s"', value, name, guid, self.metadata.name) + raise RuntimeError(_('Cannot vote for "%s" property of "%s" ' \ + 'in "%s": %s') % (name, guid, self.metadata.name, error)) + _logger.debug('"%s" voted for "%s" property of "%s" ' \ + 'document in "%s"', voter, name, guid, self.metadata.name) def _write_property(self, guid, name, value, seqno=None, mtime=None): if name == 'seqno': @@ -501,7 +490,7 @@ def _read_property(path): return json.load(f) -def _aggregated(path): +def _has_vote(path): return exists(path) and bool(os.stat(path).st_mode & stat.S_ISVTX) diff --git a/tests/units/document.py b/tests/units/document.py index a4ba85c..7e13a4a 100755 --- a/tests/units/document.py +++ b/tests/units/document.py @@ -17,8 +17,7 @@ from __init__ import tests from active_document import document, storage, env, index, document_class from active_document.document_class import active_property from active_document.metadata import StoredProperty, BlobProperty -from active_document.metadata import CounterProperty -from active_document.metadata import AggregatorProperty +from active_document.metadata import CounterProperty, VoteProperty from active_document.index import IndexWriter @@ -177,14 +176,14 @@ class DocumentTest(tests.Test): {'size': len(data), 'sha1sum': hashlib.sha1(data).hexdigest()}, doc.stat_blob('blob')) - def test_AggregatorProperty(self): + def test_VoteProperty(self): voter = ['probe'] - class Vote(AggregatorProperty): + class Vote(VoteProperty): @property - def value(self): + def voter(self): return voter[0] class Document(TestDocument): @@ -253,12 +252,12 @@ class DocumentTest(tests.Test): [(0, 0)], [(i.vote, i.counter) for i in docs]) - def test_AggregatorProperty_DoNotAggregateOnNoChanches(self): + def test_VoteProperty_DoNotAggregateOnNoChanches(self): - class Vote(AggregatorProperty): + class Vote(VoteProperty): @property - def value(self): + def voter(self): return -1 class Document(TestDocument): @@ -370,10 +369,10 @@ class DocumentTest(tests.Test): def test_crawler(self): - class Vote(AggregatorProperty): + class Vote(VoteProperty): @property - def value(self): + def voter(self): return -1 class Document(TestDocument): @@ -735,12 +734,12 @@ class DocumentTest(tests.Test): [i.counter for i in Document.find(0, 10, guid=doc_2.guid)[0]]) self.assertEqual(3, Document(doc_2.guid).counter) - def test_merge_AggregatorProperty(self): + def test_merge_VoteProperty(self): - class Vote(AggregatorProperty): + class Vote(VoteProperty): @property - def value(self): + def voter(self): pass class Document(TestDocument): @@ -763,7 +762,7 @@ class DocumentTest(tests.Test): } Document.merge(doc.guid, diff) self.assertEqual( - [(doc.guid, 1, 2)], + [(doc.guid, True, 2)], [(i.guid, i.vote, i.counter) for i in Document.find(0, 100)[0]]) def test_merge_New(self): diff --git a/tests/units/folder.py b/tests/units/folder.py index 6259e92..ebc5bb0 100755 --- a/tests/units/folder.py +++ b/tests/units/folder.py @@ -15,7 +15,6 @@ from __init__ import tests from active_document import env, folder, document, util, index_queue, sneakernet from active_document.document_class import active_property from active_document.metadata import CounterProperty, BlobProperty -from active_document.metadata import AggregatorProperty class FolderTest(tests.Test): diff --git a/tests/units/storage.py b/tests/units/storage.py index 2de752c..92faae2 100755 --- a/tests/units/storage.py +++ b/tests/units/storage.py @@ -11,7 +11,7 @@ from __init__ import tests from active_document import env from active_document.metadata import Metadata, ActiveProperty -from active_document.metadata import AggregatorProperty, BlobProperty +from active_document.metadata import VoteProperty, BlobProperty from active_document.metadata import CounterProperty from active_document.storage import Storage, _PAGE_SIZE @@ -103,39 +103,39 @@ class StorageTest(tests.Test): def test_Aggregates(self): storage = self.storage([]) - self.assertEqual(0, storage.is_aggregated('guid', 'prop', 'probe')) - self.assertEqual(0, storage.count_aggregated('guid', 'prop')) + self.assertEqual(0, storage.has_vote('guid', 'prop', 'probe')) + self.assertEqual(0, storage.count_voters('guid', 'prop')) - storage.aggregate('guid', 'prop', 'probe') - self.assertEqual(1, storage.is_aggregated('guid', 'prop', 'probe')) - self.assertEqual(1, storage.count_aggregated('guid', 'prop')) + storage.vote('guid', 'prop', 'probe', True) + self.assertEqual(1, storage.has_vote('guid', 'prop', 'probe')) + self.assertEqual(1, storage.count_voters('guid', 'prop')) - storage.aggregate('guid', 'prop', 'probe2') - self.assertEqual(1, storage.is_aggregated('guid', 'prop', 'probe')) - self.assertEqual(1, storage.is_aggregated('guid', 'prop', 'probe2')) - self.assertEqual(2, storage.count_aggregated('guid', 'prop')) + storage.vote('guid', 'prop', 'probe2', True) + self.assertEqual(1, storage.has_vote('guid', 'prop', 'probe')) + self.assertEqual(1, storage.has_vote('guid', 'prop', 'probe2')) + self.assertEqual(2, storage.count_voters('guid', 'prop')) - storage.disaggregate('guid', 'prop', 'probe') - self.assertEqual(0, storage.is_aggregated('guid', 'prop', 'probe')) - self.assertEqual(1, storage.is_aggregated('guid', 'prop', 'probe2')) - self.assertEqual(1, storage.count_aggregated('guid', 'prop')) + storage.vote('guid', 'prop', 'probe', False) + self.assertEqual(0, storage.has_vote('guid', 'prop', 'probe')) + self.assertEqual(1, storage.has_vote('guid', 'prop', 'probe2')) + self.assertEqual(1, storage.count_voters('guid', 'prop')) - storage.disaggregate('guid', 'prop', 'probe2') - self.assertEqual(0, storage.is_aggregated('guid', 'prop', 'probe')) - self.assertEqual(0, storage.is_aggregated('guid', 'prop', 'probe2')) - self.assertEqual(0, storage.count_aggregated('guid', 'prop')) + storage.vote('guid', 'prop', 'probe2', False) + self.assertEqual(0, storage.has_vote('guid', 'prop', 'probe')) + self.assertEqual(0, storage.has_vote('guid', 'prop', 'probe2')) + self.assertEqual(0, storage.count_voters('guid', 'prop')) def test_diff(self): storage = self.storage([ ActiveProperty('prop_1', slot=1), - AggregatorProperty('prop_2', 'counter'), + VoteProperty('prop_2', 'counter'), BlobProperty('prop_3'), CounterProperty('prop_4', slot=2), ]) storage.put('guid', {'prop_1': 'value', 'prop_4': '0'}) - storage.aggregate('guid', 'prop_2', 'enabled') - storage.disaggregate('guid', 'prop_2', 'disabled') + storage.vote('guid', 'prop_2', 'enabled', True) + storage.vote('guid', 'prop_2', 'disabled', False) storage.set_blob('guid', 'prop_3', StringIO('blob')) os.utime('test/gu/guid/prop_1', (1, 1)) @@ -202,7 +202,7 @@ class StorageTest(tests.Test): storage = self.storage([ ActiveProperty('guid', slot=0), ActiveProperty('prop_1', slot=1), - AggregatorProperty('prop_2', 'counter'), + VoteProperty('prop_2', 'counter'), BlobProperty('prop_3'), ]) @@ -218,9 +218,9 @@ class StorageTest(tests.Test): assert os.stat('test/gu/guid_1/prop_1').st_mtime == 1 self.assertEqual('"value"', file('test/gu/guid_1/prop_1').read()) assert os.stat('test/gu/guid_1/prop_2.enabled.value').st_mtime == 2 - assert storage.is_aggregated('guid_1', 'prop_2', 'enabled') + assert storage.has_vote('guid_1', 'prop_2', 'enabled') assert os.stat('test/gu/guid_1/prop_2.disabled.value').st_mtime == 3 - assert not storage.is_aggregated('guid_1', 'prop_2', 'disabled') + assert not storage.has_vote('guid_1', 'prop_2', 'disabled') assert os.stat('test/gu/guid_1/prop_3').st_mtime == 4 self.assertEqual('blob', file('test/gu/guid_1/prop_3').read()) @@ -235,8 +235,8 @@ class StorageTest(tests.Test): ts = int(time.time()) storage.put('guid_3', {'prop_1': 'value_2'}) - storage.disaggregate('guid_3', 'prop_2', 'enabled') - storage.aggregate('guid_3', 'prop_2', 'disabled') + storage.vote('guid_3', 'prop_2', 'enabled', False) + storage.vote('guid_3', 'prop_2', 'disabled', True) storage.set_blob('guid_3', 'prop_3', StringIO('blob_2')) diff.pop('guid') @@ -244,9 +244,9 @@ class StorageTest(tests.Test): assert os.stat('test/gu/guid_3/prop_1').st_mtime >= ts self.assertEqual('"value_2"', file('test/gu/guid_3/prop_1').read()) assert os.stat('test/gu/guid_3/prop_2.enabled.value').st_mtime >= ts - assert not storage.is_aggregated('guid_3', 'prop_2', 'enabled') + assert not storage.has_vote('guid_3', 'prop_2', 'enabled') assert os.stat('test/gu/guid_3/prop_2.disabled.value').st_mtime >= ts - assert storage.is_aggregated('guid_3', 'prop_2', 'disabled') + assert storage.has_vote('guid_3', 'prop_2', 'disabled') assert os.stat('test/gu/guid_3/prop_3').st_mtime >= ts self.assertEqual('blob_2', file('test/gu/guid_3/prop_3').read()) @@ -261,7 +261,7 @@ class StorageTest(tests.Test): def test_put_Times(self): storage = self.storage([ ActiveProperty('prop_1', slot=1), - AggregatorProperty('prop_2', 'counter'), + VoteProperty('prop_2', 'counter'), BlobProperty('prop_3'), ]) @@ -274,7 +274,7 @@ class StorageTest(tests.Test): storage.get('guid').get('seqno')) assert ts <= os.stat('test/gu/guid/prop_1').st_mtime - storage.aggregate('guid', 'prop_2', 'value') + storage.vote('guid', 'prop_2', 'value', True) self.assertEqual(2, storage.get('guid').get('seqno')) self.assertEqual( int(os.stat('test/gu/guid/prop_2.value.seqno').st_mtime), @@ -306,7 +306,7 @@ class StorageTest(tests.Test): time.sleep(1) ts += 1 - storage.disaggregate('guid', 'prop_2', 'value') + storage.vote('guid', 'prop_2', 'value', False) self.assertEqual(5, storage.get('guid').get('seqno')) self.assertEqual( int(os.stat('test/gu/guid/prop_2.value.seqno').st_mtime), -- cgit v0.9.1