def create(self): style = self.share.fileStyle() if style == utility.STYLE_DIRECTORY: url = self.getLocation() handle = self._getServerHandle() try: if url[-1] != '/': url += '/' # need to get resource representing the parent of the # collection we want to create # Get the parent directory of the given path: # '/dev1/foo/bar' becomes ['dev1', 'foo', 'bar'] path = url.strip('/').split('/') parentPath = path[:-1] childName = path[-1] # ['dev1', 'foo'] becomes "dev1/foo" url = "/".join(parentPath) resource = handle.getResource(url) if getattr(self, 'ticket', False): resource.ticketId = self.ticket child = self._createCollectionResource(handle, resource, childName) except zanshin.webdav.ConnectionError, err: raise errors.CouldNotConnect( _(u"Unable to connect to server: %(error)s") % {'error': err}) except M2Crypto.BIO.BIOError, err: raise errors.CouldNotConnect( _(u"Unable to connect to server: %(error)s") % {'error': err})
def _send(self, methodName, path, body=None): # Caller must check resp.status themselves handle = self._getServerHandle() extraHeaders = {} ticket = getattr(self, 'ticket', None) if ticket: extraHeaders['Ticket'] = ticket if self._allTickets: extraHeaders['X-MorseCode-Ticket'] = list(self._allTickets) syncToken = getattr(self, 'syncToken', None) if syncToken: extraHeaders['X-MorseCode-SyncToken'] = syncToken extraHeaders['Content-Type'] = 'application/eim+xml' request = zanshin.http.Request(methodName, path, extraHeaders, body) try: start = time.time() response = handle.blockUntil(handle.addRequest, request) end = time.time() if hasattr(self, 'networkTime'): self.networkTime += (end - start) return response except zanshin.webdav.ConnectionError, err: raise errors.CouldNotConnect( _(u"Unable to connect to server: %(error)s") % {'error': err})
def destroy(self): if self.exists(): resource = self._resourceFromPath("") logger.info("...removing from server: %s" % resource.path) if resource != None: try: deleteResp = self._getServerHandle().blockUntil( resource.delete) except zanshin.webdav.ConnectionError, err: raise errors.CouldNotConnect( _(u"Unable to connect to server: %(error)s") % {'error': err}) except M2Crypto.BIO.BIOError, err: raise errors.CouldNotConnect( _(u"Unable to connect to server: %(error)s") % {'error': err})
def _getPublishedShares(self, callback): path = self.path.strip("/") if path: path = "/%s" % path path = urllib.quote("%s/mc/user/%s" % (path, self.username)) handle = WebDAV.ChandlerServerHandle( self.host, self.port, username=self.username, password=waitForDeferred(self.password.decryptPassword()), useSSL=self.useSSL, repositoryView=self.itsView) extraHeaders = {} body = None request = zanshin.http.Request('GET', path, extraHeaders, body) try: resp = handle.blockUntil(handle.addRequest, request) except zanshin.webdav.ConnectionError, err: exc = errors.CouldNotConnect( _(u"Unable to connect to server: %(error)s") % {'error': err}) if callback: return callMethodInUIThread(callback, (exc, self.itsUUID, None)) else: raise exc
def exists(self): resource = self._resourceFromPath(u"") try: result = self._getServerHandle().blockUntil(resource.exists) except zanshin.error.ConnectionError, err: raise errors.CouldNotConnect( _(u"Unable to connect to server: %(error)s") % {'error': err.args[0]})
def _getResourceList(self, location): # must implement """ Return information (etags) about all resources within a collection """ resourceList = {} style = self.share.fileStyle() if style == utility.STYLE_DIRECTORY: shareCollection = self._getContainerResource() try: children = self._getServerHandle().blockUntil( shareCollection.getAllChildren) except zanshin.webdav.ConnectionError, err: raise errors.CouldNotConnect( _(u"Unable to connect to server: %(error)s") % {'error': err}) except M2Crypto.BIO.BIOError, err: raise errors.CouldNotConnect( _(u"Unable to connect to server: %(error)s") % {'error': err})
class WebDAVRecordSetConduit(ResourceRecordSetConduit, DAVConduitMixin): """ Implements the new EIM/RecordSet interface """ def sync(self, modeOverride=None, activity=None, forceUpdate=None, debug=False): startTime = time.time() self.networkTime = 0.0 stats = super(WebDAVRecordSetConduit, self).sync(modeOverride=modeOverride, activity=activity, forceUpdate=forceUpdate, debug=debug) endTime = time.time() duration = endTime - startTime logger.info("Sync took %6.2f seconds (network = %6.2f)", duration, self.networkTime) return stats def getResource(self, path): # return text, etag resource = self._resourceFromPath(path) try: start = time.time() resp = self._getServerHandle().blockUntil(resource.get) end = time.time() self.networkTime += (end - start) except twisted.internet.error.ConnectionDone, err: errors.annotate(err, _(u"Server reported incorrect Content-Length for %(itemPath)s.") % \ {"itemPath": path}, details=str(err)) raise except zanshin.webdav.ConnectionError, err: raise errors.CouldNotConnect( _(u"Unable to connect to server: %(error)s") % {'error': err})
class DAVConduitMixin(conduits.HTTPMixin): def _getSharePath(self): return "/" + self._getSettings(withPassword=False)[2] def _resourceFromPath(self, path): serverHandle = self._getServerHandle() sharePath = self._getSharePath() if sharePath == u"/": sharePath = u"" # Avoid double-slashes on next line... resourcePath = u"%s/%s" % (sharePath, self.shareName) if self.share.fileStyle() == utility.STYLE_DIRECTORY: if not resourcePath.endswith("/"): resourcePath += "/" resourcePath += path resource = serverHandle.getResource(resourcePath) if getattr(self, 'ticket', False): resource.ticketId = self.ticket return resource def exists(self): resource = self._resourceFromPath(u"") try: result = self._getServerHandle().blockUntil(resource.exists) except zanshin.error.ConnectionError, err: raise errors.CouldNotConnect( _(u"Unable to connect to server: %(error)s") % {'error': err.args[0]}) except M2Crypto.BIO.BIOError, err: raise errors.CouldNotConnect( _(u"Unable to connect to server: %(error)s") % {'error': err})
etag = child.etag # if path is empty, it's a subcollection (skip it) if path: resourceList[path] = {'data': etag} elif style == utility.STYLE_SINGLE: resource = self._getServerHandle().getResource(location) if getattr(self, 'ticket', False): resource.ticketId = self.ticket # @@@ [grant] Error handling and reporting here # are sub-optimal try: self._getServerHandle().blockUntil(resource.propfind, depth=0) except zanshin.webdav.ConnectionError, err: raise errors.CouldNotConnect( _(u"Unable to connect to server: %(error)s") % {'error': err}) except M2Crypto.BIO.BIOError, err: raise errors.CouldNotConnect( _(u"Unable to connect to server: %(error)s") % {'error': err}) except zanshin.webdav.PermissionsError, err: message = _(u"Not authorized to GET %(path)s.") % { 'path': location } raise errors.NotAllowed(message) #else: #if not exists: # raise NotFound(_(u"Path %(path)s not found") % {'path': resource.path})
class CosmoAccount(accounts.SharingAccount): # The path attribute we inherit from WebDAVAccount represents the # base path of the Cosmo installation, typically "/cosmo". The # following attributes store paths relative to WebDAVAccount.path pimPath = schema.One( schema.Text, doc='Base path on the host to use for the user-facing urls', initialValue=u'pim/collection', ) morsecodePath = schema.One( schema.Text, doc='Base path on the host to use for morsecode publishing', initialValue=u'mc/collection', ) davPath = schema.One( schema.Text, doc='Base path on the host to use for DAV publishing', initialValue=u'dav/collection', ) accountProtocol = schema.One(initialValue='Morsecode', ) accountType = schema.One(initialValue='SHARING_MORSECODE', ) # modify this only in sharing view to avoid merge conflicts unsubscribed = schema.Many( schema.Text, initialValue=set(), doc='UUIDs of unsubscribed collections the user has not acted upon') # modify this only in main view to avoid merge conflicts ignored = schema.Many( schema.Text, initialValue=set(), doc='UUIDs of unsubscribed collections the user chose to ignore') # modify this only in main view to avoid merge conflicts requested = schema.Many( schema.Text, initialValue=set(), doc='UUIDs of unsubscribed collections the user wants to restore') def publish(self, collection, displayName=None, activity=None, filters=None, overwrite=False, options=None): rv = self.itsView share = shares.Share(itsView=rv, contents=collection) shareName = collection.itsUUID.str16() conduit = CosmoConduit(itsParent=share, shareName=shareName, account=self, translator=translator.SharingTranslator, serializer=eimml.EIMMLSerializer) if filters: conduit.filters = filters share.conduit = conduit if overwrite: if activity: activity.update( totalWork=None, msg=_(u"Removing old collection from server...")) share.destroy() share.put(activity=activity) return share def getPublishedShares(self, callback=None, blocking=False): if blocking: return self._getPublishedShares(None) # don't block the current thread def startThread(repository, uuid): rv = viewpool.getView(repository) conduit = rv[uuid] try: conduit._getPublishedShares(callback) finally: viewpool.releaseView(rv) t = threading.Thread(target=startThread, args=(self.itsView.repository, self.itsUUID)) t.start() def _getPublishedShares(self, callback): path = self.path.strip("/") if path: path = "/%s" % path path = urllib.quote("%s/mc/user/%s" % (path, self.username)) handle = WebDAV.ChandlerServerHandle( self.host, self.port, username=self.username, password=waitForDeferred(self.password.decryptPassword()), useSSL=self.useSSL, repositoryView=self.itsView) extraHeaders = {} body = None request = zanshin.http.Request('GET', path, extraHeaders, body) try: resp = handle.blockUntil(handle.addRequest, request) except zanshin.webdav.ConnectionError, err: exc = errors.CouldNotConnect( _(u"Unable to connect to server: %(error)s") % {'error': err}) if callback: return callMethodInUIThread(callback, (exc, self.itsUUID, None)) else: raise exc except M2Crypto.BIO.BIOError, err: exc = errors.CouldNotConnect( _(u"Unable to connect to server: %(error)s") % {'error': err}) if callback: return callMethodInUIThread(callback, (exc, self.itsUUID, None)) else: raise exc
class CosmoConduit(recordset_conduit.DiffRecordSetConduit, conduits.HTTPMixin): morsecodePath = schema.One( schema.Text, doc='Base path on the host to use for morsecode publishing when ' 'not using an account; sharePath is the user-facing path', initialValue=u'', ) chunkSize = schema.One(schema.Integer, defaultValue=100, doc="How many items to send at once") def sync(self, modeOverride=None, activity=None, forceUpdate=None, debug=False): startTime = time.time() self.networkTime = 0.0 stats = super(CosmoConduit, self).sync(modeOverride=modeOverride, activity=activity, forceUpdate=forceUpdate, debug=debug) endTime = time.time() duration = endTime - startTime logger.info("Sync took %6.2f seconds (network = %6.2f)", duration, self.networkTime) return stats def _putChunk(self, chunk, extra): text = self.serializer.serialize(self.itsView, chunk, **extra) logger.debug("Sending to server [%s]", text) self.put(text) def putRecords(self, toSend, extra, debug=False, activity=None): # If not chunking, send the whole thing. Also, if toSend is an empty # dict, we still want to send to the server in order to create an # empty collection (hence the "or not toSend"): if self.chunkSize == 0 or not toSend: self._putChunk(toSend, extra) else: # We need to guarantee that masters are sent before modifications, # so sorting on uuid/recurrenceID: uuids = toSend.keys() uuids.sort() numUuids = len(uuids) numChunks = numUuids / self.chunkSize if numUuids % self.chunkSize: numChunks += 1 if activity: activity.update(totalWork=numChunks, workDone=0) chunk = {} chunkNum = 1 count = 0 for uuid in uuids: count += 1 chunk[uuid] = toSend[uuid] if count == self.chunkSize: if activity: activity.update(msg="Sending chunk %d of %d" % (chunkNum, numChunks)) self._putChunk(chunk, extra) if activity: activity.update(msg="Sent chunk %d of %d" % (chunkNum, numChunks), work=1) chunk = {} count = 0 chunkNum += 1 if chunk: # still have some left over if activity: activity.update(msg="Sending chunk %d of %d" % (chunkNum, numChunks)) self._putChunk(chunk, extra) if activity: activity.update(msg="Sent chunk %d of %d" % (chunkNum, numChunks), work=1) def get(self): path = self.getMorsecodePath() resp = self._send('GET', path) if resp.status == 401: raise errors.NotAllowed( _(u"Please verify your username and password"), details="Received [%s]" % resp.body) elif resp.status != 200: raise errors.SharingError("%s (HTTP status %d)" % (resp.message, resp.status), details="Received [%s]" % resp.body) syncTokenHeaders = resp.headers.getHeader('X-MorseCode-SyncToken') if syncTokenHeaders: self.syncToken = syncTokenHeaders[0] # # @@@MOR what if this header is missing? ticketTypeHeaders = resp.headers.getHeader('X-MorseCode-TicketType') if ticketTypeHeaders: ticketType = ticketTypeHeaders[0] if ticketType == 'read-write': self.share.mode = 'both' elif ticketType == 'read-only': self.share.mode = 'get' return resp.body def put(self, text): path = self.getMorsecodePath() if self.syncToken: method = 'POST' else: method = 'PUT' tries = 3 resp = self._send(method, path, text) while resp.status == 503: tries -= 1 if tries == 0: msg = _(u"Server busy. Try again later. (HTTP status 503)") raise errors.SharingError(msg) resp = self._send(method, path, text) if resp.status in (205, 423): # The collection has either been updated by someone else since # we last did a GET (205) or the collection is in the process of # being updated right now and is locked (423). In each case, our # reaction is the same -- abort the sync. # TODO: We should try to sync again soon raise errors.TokenMismatch( _(u"Collection updated by someone else.")) elif resp.status in (403, 409): # Either a collection already exists with the UUID or we've got # multiple items with the same icaluid. Need to parse the response # body to find out rootElement = self.raiseCosmoError(resp.body) # Find out if it's ours: shares = self.account.getPublishedShares(blocking=True) toRaise = errors.AlreadyExists( "%s (HTTP status %d)" % (resp.message, resp.status), details="Collection already exists on server") toRaise.mine = False for name, uuid, href, tickets, unsubscribed in shares: if uuid == self.share.contents.itsUUID.str16(): toRaise.mine = True raise toRaise elif resp.status == 401: raise errors.NotAllowed( _(u"Please verify your username and password"), details="Received [%s]" % resp.body) elif resp.status not in (201, 204): raise errors.SharingError( "%s (HTTP status %d)" % (resp.message, resp.status), details="Sent [%s], Received [%s]" % (text, resp.body)) syncTokenHeaders = resp.headers.getHeader('X-MorseCode-SyncToken') if syncTokenHeaders: self.syncToken = syncTokenHeaders[0] # # @@@MOR what if this header is missing? if method == 'PUT': ticketHeaders = resp.headers.getHeader('X-MorseCode-Ticket') if ticketHeaders: for ticketHeader in ticketHeaders: mode, ticket = ticketHeader.split('=') if mode == 'read-only': self.ticketReadOnly = ticket if mode == 'read-write': self.ticketReadWrite = ticket def raiseCosmoError(self, xmlText): """ Utility function to parse a Cosmo XML body for errors. May raise: L{errors.DuplicateIcalUids} L{errors.ForbiddenItem} Otherwise returns C{None} (unparseable XML) or an C{ElementTree} (XML parsed OK, but error unknown). """ try: rootElement = fromstring(xmlText) except: logger.exception("Couldn't parse response: %s", xmlText) return None # continue on if (rootElement is not None and rootElement.tag == "{%s}error" % mcURI): for errorsElement in rootElement: if errorsElement.tag == "{%s}no-uid-conflict" % mcURI: uuids = list() for errorElement in errorsElement: uuids.append(errorElement.text) toRaise = errors.DuplicateIcalUids( _(u"Duplicate ical UIDs detected: %(ids)s") % {'ids': ", ".join(uuids)}) toRaise.uuids = uuids raise toRaise elif errorsElement.tag == "{%s}insufficient-privileges" % mcURI: for child in errorsElement: if child.tag == "{%s}target-uuid" % mcURI: uuid = child.text try: item = self.itsView.findUUID(uuid) except ValueError: item = None displayName = getattr(item, 'displayName', '') errorText = _( u"""The server denied access to the item "%(item description)s". Try to remove it from the collection and resync.""" ) % { 'item description': displayName or uuid } toRaise = errors.ForbiddenItem(errorText) toRaise.uuid = uuid raise toRaise assert False, \ "Missing {%s}target-uuid element from XML body %s" % ( mcURI, xmlText) return rootElement def destroy(self, silent=False): path = self.getMorsecodePath() resp = self._send('DELETE', path) if not silent: if resp.status == 404: raise errors.NotFound("Collection not found at %s" % path) elif resp.status != 204: raise errors.SharingError("%s (HTTP status %d)" % (resp.message, resp.status), details="Received [%s]" % resp.body) def create(self): pass def _send(self, methodName, path, body=None): # Caller must check resp.status themselves handle = self._getServerHandle() extraHeaders = {} ticket = getattr(self, 'ticket', None) if ticket: extraHeaders['Ticket'] = ticket if self._allTickets: extraHeaders['X-MorseCode-Ticket'] = list(self._allTickets) syncToken = getattr(self, 'syncToken', None) if syncToken: extraHeaders['X-MorseCode-SyncToken'] = syncToken extraHeaders['Content-Type'] = 'application/eim+xml' request = zanshin.http.Request(methodName, path, extraHeaders, body) try: start = time.time() response = handle.blockUntil(handle.addRequest, request) end = time.time() if hasattr(self, 'networkTime'): self.networkTime += (end - start) return response except zanshin.webdav.ConnectionError, err: raise errors.CouldNotConnect( _(u"Unable to connect to server: %(error)s") % {'error': err}) except M2Crypto.BIO.BIOError, err: raise errors.CouldNotConnect( _(u"Unable to connect to server: %(error)s") % {'error': err})
response = handle.blockUntil(handle.addRequest, request) end = time.time() if hasattr(self, 'networkTime'): self.networkTime += (end - start) return response except zanshin.webdav.ConnectionError, err: raise errors.CouldNotConnect( _(u"Unable to connect to server: %(error)s") % {'error': err}) except M2Crypto.BIO.BIOError, err: raise errors.CouldNotConnect( _(u"Unable to connect to server: %(error)s") % {'error': err}) except twisted.internet.error.ConnectionLost, err: raise errors.CouldNotConnect( _(u"Lost connection to server: %(error)s") % {'error': err}) TICKET_RE = re.compile('(^|/)(pim|mc)/collection($|/)') def getLocation(self, privilege=None, morsecode=False): """ Return the user-facing url of the share """ if morsecode: f = self._getMorsecodeSettings else: f = self._getSettings (host, port, path, username, password, useSSL) = f(withPassword=False)