def contains(self, qname, uid=None): """ Determine whether the property given by C{qname} is stored in an extended attribute of the wrapped path. @param qname: The property to look up as a two-tuple of namespace URI and local name. @param uid: The per-user identifier for per user properties. @return: C{True} if the property exists, C{False} otherwise. """ key = self._encode(qname, uid) try: self.attrs.get(key) except KeyError: return False except IOError, e: if e.errno in _ATTR_MISSING or e.errno == errno.ENOENT: return False raise HTTPError(StatusResponse( statusForFailure(Failure()), "Unable to read property: %s" % (key,) ))
def get(self, qname, uid=None): """ Retrieve the value of a property stored as an extended attribute on the wrapped path. @param qname: The property to retrieve as a two-tuple of namespace URI and local name. @param uid: The per-user identifier for per user properties. @raise HTTPError: If there is no value associated with the given property. @return: A L{WebDAVDocument} representing the value associated with the given property. """ try: data = self.attrs.get(self._encode(qname, uid)) except KeyError: raise HTTPError(StatusResponse( responsecode.NOT_FOUND, "No such property: %s" % (encodeXMLName(*qname),) )) except IOError, e: if e.errno in _ATTR_MISSING or e.errno == errno.ENOENT: raise HTTPError(StatusResponse( responsecode.NOT_FOUND, "No such property: %s" % (encodeXMLName(*qname),) )) else: raise HTTPError(StatusResponse( statusForFailure(Failure()), "Unable to read property: %s" % (encodeXMLName(*qname),) ))
def _check_exception(self, exception, result): try: raise exception except Exception: failure = Failure() status = statusForFailure(failure) self.failUnless( status == result, "Failure %r (%s) generated incorrect status code: %s != %s" % (failure, failure.value, status, result)) else: raise AssertionError("We shouldn't be here.")
def _check_exception(self, exception, result): try: raise exception except Exception: failure = Failure() status = statusForFailure(failure) self.failUnless( status == result, "Failure %r (%s) generated incorrect status code: %s != %s" % (failure, failure.value, status, result) ) else: raise AssertionError("We shouldn't be here.")
def add(self, recipient, what, reqstatus=None, calendar=None, suppressErrorLog=False): """ Add a response. @param recipient: the recipient for this response. @param what: a status code or a L{Failure} for the given recipient. @param status: the iTIP request-status for the given recipient. @param calendar: the calendar data for the given recipient response. @param suppressErrorLog: whether to suppress a log message for errors; primarily this is used when trying to process a VFREEBUSY over iMIP, which isn't supported. """ if type(what) is int: code = what error = None message = responsecode.RESPONSES[code] elif isinstance(what, Failure): code = statusForFailure(what) error = self.errorForFailure(what) message = messageForFailure(what) else: raise AssertionError("Unknown data type: {}".format(what, )) if self.recipient_mapper is not None: recipient = self.recipient_mapper(recipient) if not suppressErrorLog and code > 400: # Error codes only self.log.error( "Error during {method} for {r}: {msg}", method=self.method, r=recipient, msg=message, ) details = ScheduleResponseQueue.ScheduleResonseDetails( self.recipient_element(davxml.HRef.fromString(recipient)) if self.recipient_uses_href else self.recipient_element.fromString(recipient), self.request_status_element(reqstatus), calendar, error, self.response_description_element(message) if message is not None else None, ) self.responses.append(details)
def _namedPropertiesForResource(request, props, resource, forbidden=False): """ Return the specified properties on the specified resource. @param request: the L{IRequest} for the current request. @param props: a list of property elements or qname tuples for the properties of interest. @param resource: the L{DAVResource} for the targeted resource. @return: a map of OK and NOT FOUND property values. """ properties_by_status = { responsecode.OK: [], responsecode.FORBIDDEN: [], responsecode.NOT_FOUND: [], } for property in props: if isinstance(property, element.WebDAVElement): qname = property.qname() else: qname = property if forbidden: properties_by_status[responsecode.FORBIDDEN].append( propertyName(qname)) else: props = (yield resource.listProperties(request)) if qname in props: try: prop = (yield resource.readProperty(qname, request)) properties_by_status[responsecode.OK].append(prop) except: f = Failure() log.error( "Error reading property %r for resource %s: %s" % (qname, request.uri, f.value)) status = statusForFailure( f, "getting property: %s" % (qname, )) if status not in properties_by_status: properties_by_status[status] = [] properties_by_status[status].append( propertyName(qname)) else: properties_by_status[responsecode.NOT_FOUND].append( propertyName(qname)) returnValue(properties_by_status)
def list(self, uid=None, filterByUID=True): """ Enumerate the property names stored in extended attributes of the wrapped path. @param uid: The per-user identifier for per user properties. @return: A C{list} of property names as two-tuples of namespace URI and local name. """ prefix = self.deadPropertyXattrPrefix try: attrs = iter(self.attrs) except IOError, e: if e.errno == errno.ENOENT: return [] raise HTTPError( StatusResponse(statusForFailure(Failure()), "Unable to list properties: %s", (self.resource.fp.path,)) )
def mkcollection(filepath): """ Perform a X{MKCOL} on the given filepath. @param filepath: the L{FilePath} of the collection resource to create. @raise HTTPError: (containing an appropriate response) if the operation fails. @return: a deferred response with a status code of L{responsecode.CREATED} if the destination already exists, or L{responsecode.NO_CONTENT} if the destination was created by the X{MKCOL} operation. """ try: os.mkdir(filepath.path) # Remove stat info from filepath because we modified it filepath.changed() except: raise HTTPError(statusForFailure( Failure(), "creating directory in MKCOL: %s" % (filepath.path,) )) return succeed(responsecode.CREATED)
def delete(self, qname, uid=None): """ Remove the extended attribute from the wrapped path which stores the property given by C{qname}. @param uid: The per-user identifier for per user properties. @param qname: The property to delete as a two-tuple of namespace URI and local name. """ key = self._encode(qname, uid) try: try: self.attrs.remove(key) except KeyError: pass except IOError, e: if e.errno not in _ATTR_MISSING: raise except: raise HTTPError(StatusResponse(statusForFailure(Failure()), "Unable to delete property: %s", (key,)))
def list(self, uid=None, filterByUID=True): """ Enumerate the property names stored in extended attributes of the wrapped path. @param uid: The per-user identifier for per user properties. @return: A C{list} of property names as two-tuples of namespace URI and local name. """ prefix = self.deadPropertyXattrPrefix try: attrs = iter(self.attrs) except IOError, e: if e.errno == errno.ENOENT: return [] raise HTTPError( StatusResponse(statusForFailure(Failure()), "Unable to list properties: %s", (self.resource.fp.path, )))
def mkcollection(filepath): """ Perform a X{MKCOL} on the given filepath. @param filepath: the L{FilePath} of the collection resource to create. @raise HTTPError: (containing an appropriate response) if the operation fails. @return: a deferred response with a status code of L{responsecode.CREATED} if the destination already exists, or L{responsecode.NO_CONTENT} if the destination was created by the X{MKCOL} operation. """ try: os.mkdir(filepath.path) # Remove stat info from filepath because we modified it filepath.changed() except: raise HTTPError( statusForFailure( Failure(), "creating directory in MKCOL: %s" % (filepath.path, ))) return succeed(responsecode.CREATED)
def add(self, recipient, what, reqstatus=None, calendar=None, suppressErrorLog=False): """ Add a response. @param recipient: the recipient for this response. @param what: a status code or a L{Failure} for the given recipient. @param status: the iTIP request-status for the given recipient. @param calendar: the calendar data for the given recipient response. @param suppressErrorLog: whether to suppress a log message for errors; primarily this is used when trying to process a VFREEBUSY over iMIP, which isn't supported. """ if type(what) is int: code = what error = None message = responsecode.RESPONSES[code] elif isinstance(what, Failure): code = statusForFailure(what) error = self.errorForFailure(what) message = messageForFailure(what) else: raise AssertionError("Unknown data type: {}".format(what,)) if self.recipient_mapper is not None: recipient = self.recipient_mapper(recipient) if not suppressErrorLog and code > 400: # Error codes only self.log.error("Error during {method} for {r}: {msg}", method=self.method, r=recipient, msg=message, ) details = ScheduleResponseQueue.ScheduleResonseDetails( self.recipient_element(davxml.HRef.fromString(recipient)) if self.recipient_uses_href else self.recipient_element.fromString(recipient), self.request_status_element(reqstatus), calendar, error, self.response_description_element(message) if message is not None else None, ) self.responses.append(details)
def _namedPropertiesForResource(request, props, resource): """ Return the specified properties on the specified resource. @param request: the L{IRequest} for the current request. @param props: a list of property elements or qname tuples for the properties of interest. @param resource: the L{DAVFile} for the targetted resource. @return: a map of OK and NOT FOUND property values. """ properties_by_status = { responsecode.OK : [], responsecode.NOT_FOUND : [], } for property in props: if isinstance(property, element.WebDAVElement): qname = property.qname() else: qname = property props = waitForDeferred(resource.listProperties(request)) yield props props = props.getResult() if qname in props: try: prop = waitForDeferred(resource.readProperty(qname, request)) yield prop prop = prop.getResult() properties_by_status[responsecode.OK].append(prop) except: f = Failure() status = statusForFailure(f, "getting property: %s" % (qname,)) if status != responsecode.NOT_FOUND: log.error("Error reading property %r for resource %s: %s" % (qname, request.uri, f.value)) if status not in properties_by_status: properties_by_status[status] = [] properties_by_status[status].append(propertyName(qname)) else: properties_by_status[responsecode.NOT_FOUND].append(propertyName(qname)) yield properties_by_status
def delete(self, qname, uid=None): """ Remove the extended attribute from the wrapped path which stores the property given by C{qname}. @param uid: The per-user identifier for per user properties. @param qname: The property to delete as a two-tuple of namespace URI and local name. """ key = self._encode(qname, uid) try: try: self.attrs.remove(key) except KeyError: pass except IOError, e: if e.errno not in _ATTR_MISSING: raise except: raise HTTPError( StatusResponse(statusForFailure(Failure()), "Unable to delete property: %s", (key, )))
def get(self, qname, uid=None): """ Retrieve the value of a property stored as an extended attribute on the wrapped path. @param qname: The property to retrieve as a two-tuple of namespace URI and local name. @param uid: The per-user identifier for per user properties. @raise HTTPError: If there is no value associated with the given property. @return: A L{WebDAVDocument} representing the value associated with the given property. """ try: data = self.attrs.get(self._encode(qname, uid)) except KeyError: raise HTTPError( StatusResponse( responsecode.NOT_FOUND, "No such property: %s" % (encodeXMLName(*qname), ))) except IOError, e: if e.errno in _ATTR_MISSING or e.errno == errno.ENOENT: raise HTTPError( StatusResponse( responsecode.NOT_FOUND, "No such property: %s" % (encodeXMLName(*qname), ))) else: raise HTTPError( StatusResponse( statusForFailure(Failure()), "Unable to read property: %s" % (encodeXMLName(*qname), )))
def contains(self, qname, uid=None): """ Determine whether the property given by C{qname} is stored in an extended attribute of the wrapped path. @param qname: The property to look up as a two-tuple of namespace URI and local name. @param uid: The per-user identifier for per user properties. @return: C{True} if the property exists, C{False} otherwise. """ key = self._encode(qname, uid) try: self.attrs.get(key) except KeyError: return False except IOError, e: if e.errno in _ATTR_MISSING or e.errno == errno.ENOENT: return False raise HTTPError( StatusResponse(statusForFailure(Failure()), "Unable to read property: %s" % (key, )))
def copy(source_filepath, destination_filepath, destination_uri, depth): """ Perform a X{COPY} from the given source and destination filepaths. This will perform a X{DELETE} on the destination if necessary; the caller should check and handle the X{overwrite} header before calling L{copy} (as in L{COPYMOVE.prepareForCopy}). @param source_filepath: a L{FilePath} for the file to copy from. @param destination_filepath: a L{FilePath} for the file to copy to. @param destination_uri: the URI of the destination resource. @param depth: the recursion X{Depth} for the X{COPY} operation, which must be one of "0", "1", or "infinity". @raise HTTPError: (containing a response with a status code of L{responsecode.BAD_REQUEST}) if C{depth} is not "0", "1" or "infinity". @raise HTTPError: (containing an appropriate response) if the operation fails. If C{source_filepath} is a directory, the response will be a L{MultiStatusResponse}. @return: a deferred response with a status code of L{responsecode.CREATED} if the destination already exists, or L{responsecode.NO_CONTENT} if the destination was created by the X{COPY} operation. """ if source_filepath.isfile(): # # Copy the file # log.info("Copying file %s to %s" % (source_filepath.path, destination_filepath.path)) try: source_file = source_filepath.open() except: raise HTTPError(statusForFailure( Failure(), "opening file for reading: %s" % (source_filepath.path,) )) source_stream = FileStream(source_file) response = waitForDeferred(put(source_stream, destination_filepath, destination_uri)) yield response try: response = response.getResult() finally: source_stream.close() source_file.close() checkResponse(response, "put", responsecode.NO_CONTENT, responsecode.CREATED) yield response return elif source_filepath.isdir(): if destination_filepath.exists(): # # Delete the destination # response = waitForDeferred(delete(destination_uri, destination_filepath)) yield response response = response.getResult() checkResponse(response, "delete", responsecode.NO_CONTENT) success_code = responsecode.NO_CONTENT else: success_code = responsecode.CREATED # # Copy the directory # log.info("Copying directory %s to %s" % (source_filepath.path, destination_filepath.path)) source_basename = source_filepath.path destination_basename = destination_filepath.path errors = ResponseQueue(source_basename, "COPY", success_code) if destination_filepath.parent().isdir(): if os.path.islink(source_basename): link_destination = os.readlink(source_basename) if link_destination[0] != os.path.sep: link_destination = os.path.join(source_basename, link_destination) try: os.symlink(destination_basename, link_destination) except: errors.add(source_basename, Failure()) else: try: os.mkdir(destination_basename) except: raise HTTPError(statusForFailure( Failure(), "creating directory %s" % (destination_basename,) )) if depth == "0": yield success_code return else: raise HTTPError(StatusResponse( responsecode.CONFLICT, "Parent collection for destination %s does not exist" % (destination_uri,) )) # # Recursive copy # # FIXME: When we report errors, do we report them on the source URI # or on the destination URI? We're using the source URI here. # # FIXME: defer the walk? source_basename_len = len(source_basename) def paths(basepath, subpath): source_path = os.path.join(basepath, subpath) assert source_path.startswith(source_basename) destination_path = os.path.join(destination_basename, source_path[source_basename_len+1:]) return source_path, destination_path for dir, subdirs, files in os.walk(source_filepath.path, topdown=True): for filename in files: source_path, destination_path = paths(dir, filename) if not os.path.isdir(os.path.dirname(destination_path)): errors.add(source_path, responsecode.NOT_FOUND) else: response = waitForDeferred(copy(FilePath(source_path), FilePath(destination_path), destination_uri, depth)) yield response response = response.getResult() checkResponse(response, "copy", responsecode.CREATED, responsecode.NO_CONTENT) for subdir in subdirs: source_path, destination_path = paths(dir, subdir) log.info("Copying directory %s to %s" % (source_path, destination_path)) if not os.path.isdir(os.path.dirname(destination_path)): errors.add(source_path, responsecode.CONFLICT) else: if os.path.islink(source_path): link_destination = os.readlink(source_path) if link_destination[0] != os.path.sep: link_destination = os.path.join(source_path, link_destination) try: os.symlink(destination_path, link_destination) except: errors.add(source_path, Failure()) else: try: os.mkdir(destination_path) except: errors.add(source_path, Failure()) yield errors.response() return else: log.error("Unable to COPY to non-file: %s" % (source_filepath.path,)) raise HTTPError(StatusResponse( responsecode.FORBIDDEN, "The requested resource exists but is not backed by a regular file." ))
def _namedPropertiesForResource(request, props, resource, calendar=None, timezone=None, vcard=None, isowner=True, dataAllowed=True, forbidden=False): """ Return the specified properties on the specified resource. @param request: the L{IRequest} for the current request. @param props: a list of property elements or qname tuples for the properties of interest. @param resource: the L{CalDAVResource} for the targeted resource. @param calendar: the L{Component} for the calendar for the resource. This may be None if the calendar has not already been read in, in which case the resource will be used to get the calendar if needed. @param timezone: the L{Component} the VTIMEZONE to use for floating/all-day. @param vcard: the L{Component} for the vcard for the resource. This may be None if the vcard has not already been read in, in which case the resource will be used to get the vcard if needed. @param isowner: C{True} if the authorized principal making the request is the DAV:owner, C{False} otherwise. @param dataAllowed: C{True} if calendar/address data is allowed to be returned, C{False} otherwise. @param forbidden: if C{True} then return 403 status for all properties, C{False} otherwise. @return: a map of OK and NOT FOUND property values. """ properties_by_status = { responsecode.OK : [], responsecode.FORBIDDEN : [], responsecode.NOT_FOUND : [], } # Look for Prefer header first, then try Brief prefer = request.headers.getHeader("prefer", {}) returnMinimal = any([key == "return" and value == "minimal" for key, value, _ignore_args in prefer]) if not returnMinimal: returnMinimal = request.headers.getHeader("brief", False) for property in props: if isinstance(property, element.WebDAVElement): qname = property.qname() else: qname = property if forbidden: properties_by_status[responsecode.FORBIDDEN].append(propertyName(qname)) continue if isinstance(property, caldavxml.CalendarData): if dataAllowed: # Handle private events access restrictions if calendar is None: calendar = (yield resource.componentForUser()) filtered = HiddenInstanceFilter().filter(calendar) filtered = PrivateEventFilter(resource.accessMode, isowner).filter(filtered) filtered = CalendarDataFilter(property, timezone).filter(filtered) propvalue = CalendarData.fromCalendar(filtered, format=property.content_type) properties_by_status[responsecode.OK].append(propvalue) else: properties_by_status[responsecode.FORBIDDEN].append(propertyName(qname)) continue if isinstance(property, carddavxml.AddressData): if dataAllowed: if vcard is None: vcard = (yield resource.vCard()) filtered = AddressDataFilter(property).filter(vcard) propvalue = AddressData.fromAddress(filtered, format=property.content_type) properties_by_status[responsecode.OK].append(propvalue) else: properties_by_status[responsecode.FORBIDDEN].append(propertyName(qname)) continue has = (yield resource.hasProperty(property, request)) if has: try: prop = (yield resource.readProperty(property, request)) if prop is not None: properties_by_status[responsecode.OK].append(prop) elif not returnMinimal: properties_by_status[responsecode.NOT_FOUND].append(propertyName(qname)) except HTTPError: f = Failure() status = statusForFailure(f, "getting property: %s" % (qname,)) if status not in properties_by_status: properties_by_status[status] = [] if not returnMinimal or status != responsecode.NOT_FOUND: properties_by_status[status].append(propertyName(qname)) elif not returnMinimal: properties_by_status[responsecode.NOT_FOUND].append(propertyName(qname)) returnValue(properties_by_status)
def _namedPropertiesForResource(request, props, resource, calendar=None, timezone=None, vcard=None, isowner=True): """ Return the specified properties on the specified resource. @param request: the L{IRequest} for the current request. @param props: a list of property elements or qname tuples for the properties of interest. @param resource: the L{CalDAVResource} for the targeted resource. @param calendar: the L{Component} for the calendar for the resource. This may be None if the calendar has not already been read in, in which case the resource will be used to get the calendar if needed. @param timezone: the L{Component} the VTIMEZONE to use for floating/all-day. @param vcard: the L{Component} for the vcard for the resource. This may be None if the vcard has not already been read in, in which case the resource will be used to get the vcard if needed. @param isowner: C{True} if the authorized principal making the request is the DAV:owner, C{False} otherwise. @return: a map of OK and NOT FOUND property values. """ properties_by_status = { responsecode.OK : [], responsecode.NOT_FOUND : [], } # Look for Prefer header first, then try Brief prefer = request.headers.getHeader("prefer", {}) returnMinimal = any([key == "return" and value == "minimal" for key, value, _ignore_args in prefer]) if not returnMinimal: returnMinimal = request.headers.getHeader("brief", False) for property in props: if isinstance(property, caldavxml.CalendarData): # Handle private events access restrictions if calendar is None: calendar = (yield resource.iCalendarForUser(request)) filtered = HiddenInstanceFilter().filter(calendar) filtered = PrivateEventFilter(resource.accessMode, isowner).filter(filtered) filtered = CalendarDataFilter(property, timezone).filter(filtered) propvalue = CalendarData.fromCalendar(filtered, format=property.content_type) properties_by_status[responsecode.OK].append(propvalue) continue if isinstance(property, carddavxml.AddressData): if vcard is None: vcard = (yield resource.vCard()) filtered = AddressDataFilter(property).filter(vcard) propvalue = AddressData.fromAddress(filtered, format=property.content_type) properties_by_status[responsecode.OK].append(propvalue) continue if isinstance(property, element.WebDAVElement): qname = property.qname() else: qname = property has = (yield resource.hasProperty(property, request)) if has: try: prop = (yield resource.readProperty(property, request)) if prop is not None: properties_by_status[responsecode.OK].append(prop) elif not returnMinimal: properties_by_status[responsecode.NOT_FOUND].append(propertyName(qname)) except HTTPError: f = Failure() status = statusForFailure(f, "getting property: %s" % (qname,)) if status not in properties_by_status: properties_by_status[status] = [] if not returnMinimal or status != responsecode.NOT_FOUND: properties_by_status[status].append(propertyName(qname)) elif not returnMinimal: properties_by_status[responsecode.NOT_FOUND].append(propertyName(qname)) returnValue(properties_by_status)
def report_DAV__expand_property(self, request, expand_property): """ Generate an expand-property REPORT. (RFC 3253, section 3.8) TODO: for simplicity we will only support one level of expansion. """ # Verify root element if not isinstance(expand_property, element.ExpandProperty): raise ValueError( "%s expected as root element, not %s." % (element.ExpandProperty.sname(), expand_property.sname())) # Only handle Depth: 0 depth = request.headers.getHeader("depth", "0") if depth != "0": log.error("Non-zero depth is not allowed: %s" % (depth, )) raise HTTPError( StatusResponse(responsecode.BAD_REQUEST, "Depth %s not allowed" % (depth, ))) # # Get top level properties to expand and make sure we only have one level # properties = {} for property in expand_property.children: namespace = property.attributes.get("namespace", dav_namespace) name = property.attributes.get("name", "") # Make sure children have no children props_to_find = [] for child in property.children: if child.children: log.error( "expand-property REPORT only supports single level expansion" ) raise HTTPError( StatusResponse( responsecode.NOT_IMPLEMENTED, "expand-property REPORT only supports single level expansion" )) child_namespace = child.attributes.get("namespace", dav_namespace) child_name = child.attributes.get("name", "") props_to_find.append((child_namespace, child_name)) properties[(namespace, name)] = props_to_find # # Generate the expanded responses status for each top-level property # properties_by_status = { responsecode.OK: [], responsecode.NOT_FOUND: [], } filteredaces = None lastParent = None for qname in properties.iterkeys(): try: prop = (yield self.readProperty(qname, request)) # Form the PROPFIND-style DAV:prop element we need later props_to_return = element.PropertyContainer(*properties[qname]) # Now dereference any HRefs responses = [] for href in prop.children: if isinstance(href, element.HRef): # Locate the Href resource and its parent resource_uri = str(href) child = (yield request.locateResource(resource_uri)) if not child or not child.exists(): responses.append( element.StatusResponse( href, element.Status.fromResponseCode( responsecode.NOT_FOUND))) continue parent = (yield request.locateResource( parentForURL(resource_uri))) # Check privileges on parent - must have at least DAV:read try: yield parent.checkPrivileges(request, (element.Read(), )) except AccessDeniedError: responses.append( element.StatusResponse( href, element.Status.fromResponseCode( responsecode.FORBIDDEN))) continue # Cache the last parent's inherited aces for checkPrivileges optimization if lastParent != parent: lastParent = parent # Do some optimisation of access control calculation by determining any inherited ACLs outside of # the child resource loop and supply those to the checkPrivileges on each child. filteredaces = ( yield parent.inheritedACEsforChildren(request)) # Check privileges - must have at least DAV:read try: yield child.checkPrivileges( request, (element.Read(), ), inherited_aces=filteredaces) except AccessDeniedError: responses.append( element.StatusResponse( href, element.Status.fromResponseCode( responsecode.FORBIDDEN))) continue # Now retrieve all the requested properties on the HRef resource yield prop_common.responseForHref( request, responses, href, child, prop_common.propertyListForResource, props_to_return, ) prop.children = responses properties_by_status[responsecode.OK].append(prop) except: f = Failure() log.error( "Error reading property {qname} for resource {req}: {failure}", qname=qname, req=request.uri, failure=f.value) status = statusForFailure(f, "getting property: %s" % (qname, )) if status not in properties_by_status: properties_by_status[status] = [] properties_by_status[status].append(propertyName(qname)) # Build the overall response propstats = [ element.PropertyStatus( element.PropertyContainer(*properties_by_status[pstatus]), element.Status.fromResponseCode(pstatus)) for pstatus in properties_by_status if properties_by_status[pstatus] ] returnValue( MultiStatusResponse( (element.PropertyStatusResponse(element.HRef(request.uri), *propstats), )))
if hasattr(resource, "hasProperty"): has = waitForDeferred( resource.hasProperty(property, request)) yield has has = has.getResult() else: has = False if has: try: resource_property = waitForDeferred( resource.readProperty(property, request)) yield resource_property resource_property = resource_property.getResult() except: f = Failure() status = statusForFailure( f, "getting property: %s" % (property, )) if status not in properties_by_status: properties_by_status[status] = [] if not returnMinimal or status != responsecode.NOT_FOUND: properties_by_status[status].append( propertyName(property)) else: if resource_property is not None: properties_by_status[responsecode.OK].append( resource_property) elif not returnMinimal: properties_by_status[ responsecode.NOT_FOUND].append( propertyName(property)) elif not returnMinimal: properties_by_status[responsecode.NOT_FOUND].append(
def delete(uri, filepath, depth="infinity"): """ Perform a X{DELETE} operation on the given URI, which is backed by the given filepath. @param filepath: the L{FilePath} to delete. @param depth: the recursion X{Depth} for the X{DELETE} operation, which must be "infinity". @raise HTTPError: (containing a response with a status code of L{responsecode.BAD_REQUEST}) if C{depth} is not "infinity". @raise HTTPError: (containing an appropriate response) if the delete operation fails. If C{filepath} is a directory, the response will be a L{MultiStatusResponse}. @return: a deferred response with a status code of L{responsecode.NO_CONTENT} if the X{DELETE} operation succeeds. """ # # Remove the file(s) # # FIXME: defer if filepath.isdir(): # # RFC 2518, section 8.6 says that we must act as if the Depth header is # set to infinity, and that the client must omit the Depth header or set # it to infinity, meaning that for collections, we will delete all # members. # # This seems somewhat at odds with the notion that a bad request should # be rejected outright; if the client sends a bad depth header, the # client is broken, and RFC 2518, section 8 suggests that a bad request # should be rejected... # # Let's play it safe for now and ignore broken clients. # if depth != "infinity": msg = ("Client sent illegal depth header value for DELETE: %s" % (depth, )) log.error(msg) raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, msg)) # # Recursive delete # # RFC 2518, section 8.6 says that if we get an error deleting a resource # other than the collection in the request-URI, that we must respond # with a multi-status response containing error statuses for each # resource that we fail to delete. It also says we should not return # no-content (success) status, which means that we should continue after # errors, rather than aborting right away. This is interesting in that # it's different from how most operating system tools act (eg. rm) when # recursive filsystem deletes fail. # uri_path = urllib.unquote(urlsplit(uri)[2]) if uri_path[-1] == "/": uri_path = uri_path[:-1] log.info("Deleting directory %s" % (filepath.path, )) # NOTE: len(uri_path) is wrong if os.sep is not one byte long... meh. request_basename = filepath.path[:-len(uri_path)] errors = ResponseQueue(request_basename, "DELETE", responsecode.NO_CONTENT) # FIXME: defer this for dir, subdirs, files in os.walk(filepath.path, topdown=False): for filename in files: path = os.path.join(dir, filename) try: os.remove(path) except: errors.add(path, Failure()) for subdir in subdirs: path = os.path.join(dir, subdir) if os.path.islink(path): try: os.remove(path) except: errors.add(path, Failure()) else: try: os.rmdir(path) except: errors.add(path, Failure()) try: os.rmdir(filepath.path) except: raise HTTPError( statusForFailure(Failure(), "deleting directory: %s" % (filepath.path, ))) response = errors.response() else: # # Delete a file; much simpler, eh? # log.info("Deleting file %s" % (filepath.path, )) try: os.remove(filepath.path) except: raise HTTPError( statusForFailure(Failure(), "deleting file: %s" % (filepath.path, ))) response = responsecode.NO_CONTENT # Remove stat info for filepath since we deleted the backing file filepath.changed() return succeed(response)
def put(stream, filepath, uri=None): """ Perform a PUT of the given data stream into the given filepath. @param stream: the stream to write to the destination. @param filepath: the L{FilePath} of the destination file. @param uri: the URI of the destination resource. If the destination exists, if C{uri} is not C{None}, perform a X{DELETE} operation on the destination, but if C{uri} is C{None}, delete the destination directly. Note that whether a L{put} deletes the destination directly vs. performing a X{DELETE} on the destination affects the response returned in the event of an error during deletion. Specifically, X{DELETE} on collections must return a L{MultiStatusResponse} under certain circumstances, whereas X{PUT} isn't required to do so. Therefore, if the caller expects X{DELETE} semantics, it must provide a valid C{uri}. @raise HTTPError: (containing an appropriate response) if the operation fails. @return: a deferred response with a status code of L{responsecode.CREATED} if the destination already exists, or L{responsecode.NO_CONTENT} if the destination was created by the X{PUT} operation. """ log.info("Writing to file %s" % (filepath.path, )) if filepath.exists(): if uri is None: try: if filepath.isdir(): rmdir(filepath.path) else: os.remove(filepath.path) except: raise HTTPError( statusForFailure(Failure(), "writing to file: %s" % (filepath.path, ))) else: response = waitForDeferred(delete(uri, filepath)) yield response response = response.getResult() checkResponse(response, "delete", responsecode.NO_CONTENT) success_code = responsecode.NO_CONTENT else: success_code = responsecode.CREATED # # Write the contents of the request stream to resource's file # try: resource_file = filepath.open("w") except: raise HTTPError( statusForFailure( Failure(), "opening file for writing: %s" % (filepath.path, ))) try: x = waitForDeferred(readIntoFile(stream, resource_file)) yield x x.getResult() except: raise HTTPError( statusForFailure(Failure(), "writing to file: %s" % (filepath.path, ))) # Remove stat info from filepath since we modified the backing file filepath.changed() yield success_code
def copy(source_filepath, destination_filepath, destination_uri, depth): """ Perform a X{COPY} from the given source and destination filepaths. This will perform a X{DELETE} on the destination if necessary; the caller should check and handle the X{overwrite} header before calling L{copy} (as in L{COPYMOVE.prepareForCopy}). @param source_filepath: a L{FilePath} for the file to copy from. @param destination_filepath: a L{FilePath} for the file to copy to. @param destination_uri: the URI of the destination resource. @param depth: the recursion X{Depth} for the X{COPY} operation, which must be one of "0", "1", or "infinity". @raise HTTPError: (containing a response with a status code of L{responsecode.BAD_REQUEST}) if C{depth} is not "0", "1" or "infinity". @raise HTTPError: (containing an appropriate response) if the operation fails. If C{source_filepath} is a directory, the response will be a L{MultiStatusResponse}. @return: a deferred response with a status code of L{responsecode.CREATED} if the destination already exists, or L{responsecode.NO_CONTENT} if the destination was created by the X{COPY} operation. """ if source_filepath.isfile(): # # Copy the file # log.info("Copying file %s to %s" % (source_filepath.path, destination_filepath.path)) try: source_file = source_filepath.open() except: raise HTTPError( statusForFailure( Failure(), "opening file for reading: %s" % (source_filepath.path, ))) source_stream = FileStream(source_file) response = waitForDeferred( put(source_stream, destination_filepath, destination_uri)) yield response try: response = response.getResult() finally: source_stream.close() source_file.close() checkResponse(response, "put", responsecode.NO_CONTENT, responsecode.CREATED) yield response return elif source_filepath.isdir(): if destination_filepath.exists(): # # Delete the destination # response = waitForDeferred( delete(destination_uri, destination_filepath)) yield response response = response.getResult() checkResponse(response, "delete", responsecode.NO_CONTENT) success_code = responsecode.NO_CONTENT else: success_code = responsecode.CREATED # # Copy the directory # log.info("Copying directory %s to %s" % (source_filepath.path, destination_filepath.path)) source_basename = source_filepath.path destination_basename = destination_filepath.path errors = ResponseQueue(source_basename, "COPY", success_code) if destination_filepath.parent().isdir(): if os.path.islink(source_basename): link_destination = os.readlink(source_basename) if link_destination[0] != os.path.sep: link_destination = os.path.join(source_basename, link_destination) try: os.symlink(destination_basename, link_destination) except: errors.add(source_basename, Failure()) else: try: os.mkdir(destination_basename) except: raise HTTPError( statusForFailure( Failure(), "creating directory %s" % (destination_basename, ))) if depth == "0": yield success_code return else: raise HTTPError( StatusResponse( responsecode.CONFLICT, "Parent collection for destination %s does not exist" % (destination_uri, ))) # # Recursive copy # # FIXME: When we report errors, do we report them on the source URI # or on the destination URI? We're using the source URI here. # # FIXME: defer the walk? source_basename_len = len(source_basename) def paths(basepath, subpath): source_path = os.path.join(basepath, subpath) assert source_path.startswith(source_basename) destination_path = os.path.join( destination_basename, source_path[source_basename_len + 1:]) return source_path, destination_path for dir, subdirs, files in os.walk(source_filepath.path, topdown=True): for filename in files: source_path, destination_path = paths(dir, filename) if not os.path.isdir(os.path.dirname(destination_path)): errors.add(source_path, responsecode.NOT_FOUND) else: response = waitForDeferred( copy(FilePath(source_path), FilePath(destination_path), destination_uri, depth)) yield response response = response.getResult() checkResponse(response, "copy", responsecode.CREATED, responsecode.NO_CONTENT) for subdir in subdirs: source_path, destination_path = paths(dir, subdir) log.info("Copying directory %s to %s" % (source_path, destination_path)) if not os.path.isdir(os.path.dirname(destination_path)): errors.add(source_path, responsecode.CONFLICT) else: if os.path.islink(source_path): link_destination = os.readlink(source_path) if link_destination[0] != os.path.sep: link_destination = os.path.join( source_path, link_destination) try: os.symlink(destination_path, link_destination) except: errors.add(source_path, Failure()) else: try: os.mkdir(destination_path) except: errors.add(source_path, Failure()) yield errors.response() return else: log.error("Unable to COPY to non-file: %s" % (source_filepath.path, )) raise HTTPError( StatusResponse( responsecode.FORBIDDEN, "The requested resource exists but is not backed by a regular file." ))
def put(stream, filepath, uri=None): """ Perform a PUT of the given data stream into the given filepath. @param stream: the stream to write to the destination. @param filepath: the L{FilePath} of the destination file. @param uri: the URI of the destination resource. If the destination exists, if C{uri} is not C{None}, perform a X{DELETE} operation on the destination, but if C{uri} is C{None}, delete the destination directly. Note that whether a L{put} deletes the destination directly vs. performing a X{DELETE} on the destination affects the response returned in the event of an error during deletion. Specifically, X{DELETE} on collections must return a L{MultiStatusResponse} under certain circumstances, whereas X{PUT} isn't required to do so. Therefore, if the caller expects X{DELETE} semantics, it must provide a valid C{uri}. @raise HTTPError: (containing an appropriate response) if the operation fails. @return: a deferred response with a status code of L{responsecode.CREATED} if the destination already exists, or L{responsecode.NO_CONTENT} if the destination was created by the X{PUT} operation. """ log.info("Writing to file %s" % (filepath.path,)) if filepath.exists(): if uri is None: try: if filepath.isdir(): rmdir(filepath.path) else: os.remove(filepath.path) except: raise HTTPError(statusForFailure( Failure(), "writing to file: %s" % (filepath.path,) )) else: response = waitForDeferred(delete(uri, filepath)) yield response response = response.getResult() checkResponse(response, "delete", responsecode.NO_CONTENT) success_code = responsecode.NO_CONTENT else: success_code = responsecode.CREATED # # Write the contents of the request stream to resource's file # try: resource_file = filepath.open("w") except: raise HTTPError(statusForFailure( Failure(), "opening file for writing: %s" % (filepath.path,) )) try: x = waitForDeferred(readIntoFile(stream, resource_file)) yield x x.getResult() except: raise HTTPError(statusForFailure( Failure(), "writing to file: %s" % (filepath.path,) )) # Remove stat info from filepath since we modified the backing file filepath.changed() yield success_code
def delete(uri, filepath, depth="infinity"): """ Perform a X{DELETE} operation on the given URI, which is backed by the given filepath. @param filepath: the L{FilePath} to delete. @param depth: the recursion X{Depth} for the X{DELETE} operation, which must be "infinity". @raise HTTPError: (containing a response with a status code of L{responsecode.BAD_REQUEST}) if C{depth} is not "infinity". @raise HTTPError: (containing an appropriate response) if the delete operation fails. If C{filepath} is a directory, the response will be a L{MultiStatusResponse}. @return: a deferred response with a status code of L{responsecode.NO_CONTENT} if the X{DELETE} operation succeeds. """ # # Remove the file(s) # # FIXME: defer if filepath.isdir(): # # RFC 2518, section 8.6 says that we must act as if the Depth header is # set to infinity, and that the client must omit the Depth header or set # it to infinity, meaning that for collections, we will delete all # members. # # This seems somewhat at odds with the notion that a bad request should # be rejected outright; if the client sends a bad depth header, the # client is broken, and RFC 2518, section 8 suggests that a bad request # should be rejected... # # Let's play it safe for now and ignore broken clients. # if depth != "infinity": msg = ("Client sent illegal depth header value for DELETE: %s" % (depth,)) log.error(msg) raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, msg)) # # Recursive delete # # RFC 2518, section 8.6 says that if we get an error deleting a resource # other than the collection in the request-URI, that we must respond # with a multi-status response containing error statuses for each # resource that we fail to delete. It also says we should not return # no-content (success) status, which means that we should continue after # errors, rather than aborting right away. This is interesting in that # it's different from how most operating system tools act (eg. rm) when # recursive filsystem deletes fail. # uri_path = urllib.unquote(urlsplit(uri)[2]) if uri_path[-1] == "/": uri_path = uri_path[:-1] log.info("Deleting directory %s" % (filepath.path,)) # NOTE: len(uri_path) is wrong if os.sep is not one byte long... meh. request_basename = filepath.path[:-len(uri_path)] errors = ResponseQueue(request_basename, "DELETE", responsecode.NO_CONTENT) # FIXME: defer this for dir, subdirs, files in os.walk(filepath.path, topdown=False): for filename in files: path = os.path.join(dir, filename) try: os.remove(path) except: errors.add(path, Failure()) for subdir in subdirs: path = os.path.join(dir, subdir) if os.path.islink(path): try: os.remove(path) except: errors.add(path, Failure()) else: try: os.rmdir(path) except: errors.add(path, Failure()) try: os.rmdir(filepath.path) except: raise HTTPError(statusForFailure( Failure(), "deleting directory: %s" % (filepath.path,) )) response = errors.response() else: # # Delete a file; much simpler, eh? # log.info("Deleting file %s" % (filepath.path,)) try: os.remove(filepath.path) except: raise HTTPError(statusForFailure( Failure(), "deleting file: %s" % (filepath.path,) )) response = responsecode.NO_CONTENT # Remove stat info for filepath since we deleted the backing file filepath.changed() return succeed(response)
responsecode.NOT_FOUND : [], } if search_properties == "all": properties_to_enumerate = (yield resource.listAllprop(request)) else: properties_to_enumerate = search_properties for property in properties_to_enumerate: has = (yield resource.hasProperty(property, request)) if has: try: resource_property = (yield resource.readProperty(property, request)) except: f = Failure() status = statusForFailure(f, "getting property: %s" % (property,)) if status not in properties_by_status: properties_by_status[status] = [] if not returnMinimal or status != responsecode.NOT_FOUND: properties_by_status[status].append(propertyName(property)) else: if resource_property is not None: properties_by_status[responsecode.OK].append(resource_property) elif not returnMinimal and search_properties != "all": properties_by_status[responsecode.NOT_FOUND].append(propertyName(property)) elif not returnMinimal and search_properties != "all": properties_by_status[responsecode.NOT_FOUND].append(propertyName(property)) propstats = [] for status in properties_by_status:
def report_DAV__expand_property(self, request, expand_property): """ Generate an expand-property REPORT. (RFC 3253, section 3.8) TODO: for simplicity we will only support one level of expansion. """ # Verify root element if not isinstance(expand_property, element.ExpandProperty): raise ValueError("%s expected as root element, not %s." % (element.ExpandProperty.sname(), expand_property.sname())) # Only handle Depth: 0 depth = request.headers.getHeader("depth", "0") if depth != "0": log.error("Non-zero depth is not allowed: %s" % (depth,)) raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Depth %s not allowed" % (depth,))) # # Get top level properties to expand and make sure we only have one level # properties = {} for property in expand_property.children: namespace = property.attributes.get("namespace", dav_namespace) name = property.attributes.get("name", "") # Make sure children have no children props_to_find = [] for child in property.children: if child.children: log.error("expand-property REPORT only supports single level expansion") raise HTTPError(StatusResponse( responsecode.NOT_IMPLEMENTED, "expand-property REPORT only supports single level expansion" )) child_namespace = child.attributes.get("namespace", dav_namespace) child_name = child.attributes.get("name", "") props_to_find.append((child_namespace, child_name)) properties[(namespace, name)] = props_to_find # # Generate the expanded responses status for each top-level property # properties_by_status = { responsecode.OK : [], responsecode.NOT_FOUND : [], } filteredaces = None lastParent = None for qname in properties.iterkeys(): try: prop = (yield self.readProperty(qname, request)) # Form the PROPFIND-style DAV:prop element we need later props_to_return = element.PropertyContainer(*properties[qname]) # Now dereference any HRefs responses = [] for href in prop.children: if isinstance(href, element.HRef): # Locate the Href resource and its parent resource_uri = str(href) child = (yield request.locateResource(resource_uri)) if not child or not child.exists(): responses.append(element.StatusResponse(href, element.Status.fromResponseCode(responsecode.NOT_FOUND))) continue parent = (yield request.locateResource(parentForURL(resource_uri))) # Check privileges on parent - must have at least DAV:read try: yield parent.checkPrivileges(request, (element.Read(),)) except AccessDeniedError: responses.append(element.StatusResponse(href, element.Status.fromResponseCode(responsecode.FORBIDDEN))) continue # Cache the last parent's inherited aces for checkPrivileges optimization if lastParent != parent: lastParent = parent # Do some optimisation of access control calculation by determining any inherited ACLs outside of # the child resource loop and supply those to the checkPrivileges on each child. filteredaces = (yield parent.inheritedACEsforChildren(request)) # Check privileges - must have at least DAV:read try: yield child.checkPrivileges(request, (element.Read(),), inherited_aces=filteredaces) except AccessDeniedError: responses.append(element.StatusResponse(href, element.Status.fromResponseCode(responsecode.FORBIDDEN))) continue # Now retrieve all the requested properties on the HRef resource yield prop_common.responseForHref( request, responses, href, child, prop_common.propertyListForResource, props_to_return, ) prop.children = responses properties_by_status[responsecode.OK].append(prop) except: f = Failure() log.error( "Error reading property {qname} for resource {req}: {failure}", qname=qname, req=request.uri, failure=f.value ) status = statusForFailure(f, "getting property: %s" % (qname,)) if status not in properties_by_status: properties_by_status[status] = [] properties_by_status[status].append(propertyName(qname)) # Build the overall response propstats = [ element.PropertyStatus( element.PropertyContainer(*properties_by_status[pstatus]), element.Status.fromResponseCode(pstatus) ) for pstatus in properties_by_status if properties_by_status[pstatus] ] returnValue(MultiStatusResponse((element.PropertyStatusResponse(element.HRef(request.uri), *propstats),)))