コード例 #1
0
    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})
コード例 #2
0
    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})
コード例 #3
0
 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})
コード例 #4
0
    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
コード例 #5
0
    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]})
コード例 #6
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})
コード例 #7
0
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})
コード例 #8
0
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})
コード例 #9
0
                    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})

コード例 #10
0
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
コード例 #11
0
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})
コード例 #12
0
            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)