def deserialize(rv, peer, text, translator, serializer, filter=None, debug=False): items = [] trans = translator(rv) inbound, extra = serializer.deserialize(rv, text) for alias, rsExternal in inbound.items(): uuid = trans.getUUIDForAlias(alias) if uuid: item = rv.findUUID(uuid) else: item = None if rsExternal is not None: if item is not None: # Item already exists if not pim.has_stamp(item, shares.SharedItem): shares.SharedItem(item).add() shared = shares.SharedItem(item) state = shared.getPeerState(peer) rsInternal= eim.RecordSet(trans.exportItem(item)) else: # Item doesn't exist yet state = shares.State(itsView=rv, peer=peer) rsInternal = eim.RecordSet() dSend, dApply, pending = state.merge(rsInternal, rsExternal, isDiff=False, filter=filter, debug=debug) state.updateConflicts(item) if dApply: logger.debug("Applying: %s %s", uuid, dApply) trans.startImport() trans.importRecords(dApply) trans.finishImport() uuid = trans.getUUIDForAlias(alias) if uuid: item = rv.findUUID(uuid) else: item = None if item is not None and item.isLive(): if not pim.has_stamp(item, shares.SharedItem): shares.SharedItem(item).add() shares.SharedItem(item).addPeerState(state, peer) items.append(item) return items
def outbound(peers, item, filter=None, debug=False): rv = peers[0].itsView if filter is None: filter = lambda rs: rs else: filter = filter.sync_filter # At some point, which serializer and translator to use should be # configurable serializer = eimml.EIMMLSerializer # only using class methods trans = translator.SharingTranslator(rv) rsInternal = {} items = [item] version = str(item.itsVersion) if pim.has_stamp(item, pim.EventStamp): for mod in pim.EventStamp(item).modifications or []: # modifications that have been changed purely by # auto-triage shouldn't have recordsets created for them if not (isinstance(mod, pim.Note) and pim.EventStamp(mod).isTriageOnlyModification() and pim.EventStamp(mod).simpleAutoTriage() == mod._triageStatus): items.append(mod) for item in items: alias = trans.getAliasForItem(item) rsInternal[alias] = filter(eim.RecordSet(trans.exportItem(item))) if not pim.has_stamp(item, shares.SharedItem): shares.SharedItem(item).add() shared = shares.SharedItem(item) # Abort if pending conflicts if shared.conflictingStates: raise errors.ConflictsPending(_(u"Conflicts pending.")) for peer in peers: state = shared.getPeerState(peer) # Set agreed state to what we have locally state.agreed = rsInternal[alias] # Repository identifier: if rv.repository is not None: repoId = rv.repository.getSchemaInfo()[0].str16() else: repoId = "" text = serializer.serialize(rv, rsInternal, rootName="item", repo=repoId, version=version) return text
def verify(self): if self.item is None: self.discard() return False if self.pendingRemoval: # Someone else removed this item from share.contents peer = self.state.peer if peer and isinstance(peer, Share): if self.item in peer.contents: # local item still in share.contents, still conflict return True # item no longer in share.contents, no longer in conflict self.discard() return False # Some non-Share peer, keep it a pending removal conflict return True # generate records for local item translator = self.state.getTranslator() items = [self.item] if self.item.hasLocalAttributeValue('inheritFrom'): # need to check if the conflict is actually on the master items.append(self.item.inheritFrom) for item in items: rs = eim.RecordSet(translator.exportItem(item)) if self.change.exclusions: # the conflict is a record removal change = list(self.change.exclusions)[0] # search rs for matching key for r in rs.inclusions: if r.getKey() == change.getKey(): # record still applies to local item, still a conflict return True # didn't find a match, so the record has been removed on the # item, thus this is no longer a conflict else: # the conflict is on a field change change = list(self.change.inclusions)[0] for r in rs.inclusions: if r.getKey() == change.getKey(): # found matching record, now see if local values match # pending change for field in type(r).__fields__: changedField = getattr(change, field.name) if changedField != nc: if getattr(r, field.name) != changedField: # mismatch, conflict still valid return True # pending changes all match local item, no more conflict self.discard() return False
def exportFile(rv, path, collection, activity=None, translatorClass=translator.SharingTranslator, serializerClass=ics.ICSSerializer, filters=None, debug=False): if filters is None: filter = lambda rs: rs else: filter = eim.Filter(None, u'Temporary filter') for uri in filters: filter += eim.lookupSchemaURI(uri) filter = filter.sync_filter trans = translatorClass(rv) trans.startImport() total = len(collection) if activity: activity.update(totalWork=total) outbound = {} for item in collection: if (isinstance(item, pim.Note) and pim.EventStamp(item).isTriageOnlyModification()): continue # skip triage-only modifications alias = trans.getAliasForItem(item) outbound[alias] = filter(eim.RecordSet(trans.exportItem(item))) if activity: activity.update(work=1, msg=_(u"Exporting items...")) text = serializerClass.serialize(rv, outbound, name=collection.displayName, monolithic=True) output = open(path, "wb") output.write(text) output.close() if activity: activity.update(totalWork=None, msg=_(u"Exporting complete."))
def serialize(rv, items, translator, serializer, filter=None, debug=False): if filter is None: filter = lambda rs: rs else: filter = filter.sync_filter trans = translator(rv) rsInternal = { } for item in items: alias = trans.getAliasForItem(item) rsInternal[alias] = filter(eim.RecordSet(trans.exportItem(item))) if not pim.has_stamp(item, shares.SharedItem): shares.SharedItem(item).add() shared = shares.SharedItem(item) text = serializer.serialize(rv, rsInternal) return text
def merge(self, rsInternal, inbound=None, isDiff=True, filter=None, readOnly=False, debug=False): assert isinstance(rsInternal, eim.RecordSet) if filter is None: filter = lambda rs: rs else: filter = filter.sync_filter pending = self.pending agreed = self.agreed # We need to set rsExternal to equal the entire external state # If we're passing in a diff, apply it to agreed + pending if isDiff: rsExternal = agreed + pending + (inbound or eim.Diff()) elif isinstance(inbound, eim.Diff): rsExternal = eim.RecordSet() + inbound else: rsExternal = inbound or eim.RecordSet() assert isinstance(rsInternal, eim.RecordSet) internalDiff = filter(rsInternal - agreed) externalDiff = filter(rsExternal - agreed) ncd = internalDiff | externalDiff msgs = list() add = msgs.append add("----------- Beginning merge") add(" old agreed: %s" % agreed) add(" old pending: %s" % pending) add(" rsInternal: %s" % rsInternal) add(" internalDiff: %s" % internalDiff) add(" rsExternal: %s" % rsExternal) add(" externalDiff: %s" % externalDiff) add(" ncd: %s" % ncd) if readOnly: # This allows a read-only subscriber to maintain local changes # that only conflict when someone else makes a conflicting change, # and not *every* time they sync. # We don't want internal changes from # reaching self.agreed or dSend. We *do* want to be alerted to # conflicts between internal and external changes, however. To # do this, we generate a recordset representing how things would # be if the external changes won, and another recordset # representing how things would be if the internal changes won, # and we diff the two. extWins = agreed + internalDiff + pending + externalDiff intWins = agreed + pending + externalDiff + internalDiff add(" extWins: %s" % extWins) add(" intWins: %s" % intWins) pending = self.pending = filter(extWins - intWins) agreed = self.agreed = filter(rsExternal) dSend = eim.Diff() else: agreed = self.agreed = agreed + ncd # add(" agreed+=ncd: %s" % agreed) dSend = self._cleanDiff(rsExternal, ncd) # add(" dSend cleanDiff: %s" % dSend) rsExternal += dSend # add(" rsExternal+=dSend: %s" % rsExternal) pending = self.pending = filter(rsExternal - agreed) dApply = self._cleanDiff(rsInternal, ncd) add(" - - - - Results - - - - ") add(" dSend: %s" % dSend) add(" dApply: %s" % dApply) add(" new agreed: %s" % agreed) add(" new pending: %s" % pending) add(" ----------- Merge complete") if pending: msgs.insert(0, "Conflict detected:") doLog = logger.info if (debug or pending) else logger.debug for msg in msgs: doLog(msg) return dSend, dApply, pending
def getAgreed(self): if hasattr(self, '_agreed'): return eim.RecordSet(cPickle.loads(self._agreed)) else: return eim.RecordSet()
def inbound(peer, text, filter=None, allowDeletion=False, debug=False): logger.info("itemcentric.inbound(%s)", str(peer)) if isinstance(peer, list): # Peer email address items can have several that match. We pass in # a list so this code can reuse the one (if any) already associated # to the inbound item. peers = peer else: peers = [peer] rv = peers[0].itsView # At some point, which serializer and translator to use should be # configurable serializer = eimml.EIMMLSerializer # only using class methods trans = translator.SharingTranslator(rv) inbound, extra = serializer.deserialize(rv, text, helperView=rv) peerRepoId = extra.get('repo', None) peerItemVersion = int(extra.get('version', '-1')) itemToReturn = None aliases = inbound.keys() aliases.sort() for alias in aliases: rsExternal = inbound[alias] uuid = trans.getUUIDForAlias(alias) if uuid: item = rv.findUUID(uuid) else: item = None if rsExternal is not None: if item is not None and not getattr(item, '_fake', False): # Item already exists if not pim.has_stamp(item, shares.SharedItem): shares.SharedItem(item).add() shared = shares.SharedItem(item) if not getattr(shared, 'peerStates', False): # has no peer states yet, use the first peer in list peer = peers[0] else: for state in shared.peerStates: peerUUID = shared.peerStates.getAlias(state) for peer in peers: if peer.itsUUID.str16() == peerUUID: # peer matches break else: # no match; use the first one passed in peer = peers[0] state = shared.getPeerState(peer) rsInternal = eim.RecordSet(trans.exportItem(item)) else: # Item doesn't exist yet peer = peers[0] state = shares.State(itsView=rv, peer=peer) rsInternal = eim.RecordSet() if state.peerRepoId and (peerRepoId != state.peerRepoId): # This update is not from the peer repository we last saw. # Treat the update is entirely new state.clear() if uuid is not None: masterAlias, recurrenceID = utility.splitUUID(rv, alias) if masterAlias != alias and not state.agreed: # This is a new inbound modification state.agreed += eim.RecordSet( recordset_conduit.getInheritRecords( rsExternal.inclusions, alias)) state.peerRepoId = peerRepoId # Only process recordsets whose version is greater than the last one # we say. In the case of null-repository-view testing, versions are # always stuck at zero, so process those as well. if ((peerItemVersion == 0) or (peerItemVersion > state.peerItemVersion)): state.peerItemVersion = peerItemVersion dSend, dApply, pending = state.merge(rsInternal, rsExternal, isDiff=False, filter=filter, debug=debug) state.autoResolve(rsInternal, dApply, dSend) state.updateConflicts(item) if dApply: logger.debug("Applying: %s %s", uuid, dApply) trans.startImport() trans.importRecords(dApply) trans.finishImport() uuid = trans.getUUIDForAlias(alias) if uuid: item = rv.findUUID(uuid) else: item = None if item is not None and item.isLive(): if not pim.has_stamp(item, shares.SharedItem): shares.SharedItem(item).add() shares.SharedItem(item).addPeerState(state, peer) if itemToReturn is None: # the item to return should always be a master, not # a modification itemToReturn = getattr(item, 'inheritFrom', item) else: logger.info("Out-of-sequence update for %s", uuid) raise errors.OutOfSequence( "Update %d arrived after %d" % (peerItemVersion, state.peerItemVersion)) else: # Deletion if item is not None: # Remove the state if pim.has_stamp(item, shares.SharedItem): shared = shares.SharedItem(item) shared.removePeerState(peer) # Remove the item (if allowed) if allowDeletion: logger.debug("Deleting item: %s", uuid) item.delete(True) item = None return itemToReturn