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 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 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 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." ))