コード例 #1
0
ファイル: responses.py プロジェクト: keshapps/BlackSheep
def _file(value: FileInput,
          content_type: Union[str, bytes],
          content_disposition_type: ContentDispositionType,
          file_name: str = None):
    if file_name:
        exact_file_name = ntpath.basename(file_name)
        if not exact_file_name:
            raise ValueError(
                'Invalid file name: it should be an exact file name without path, for example: "foo.txt"'
            )

        content_disposition_value = f'{content_disposition_type.value}; filename="{exact_file_name}"'
    else:
        content_disposition_value = content_disposition_type.value

    content_type = _ensure_bytes(content_type)

    if isinstance(value, str):
        # value is treated as a path
        content = StreamedContent(content_type, _get_file_provider(value))
    elif isinstance(value, BytesIO):

        async def data_provider():
            while True:
                chunk = value.read(1024 * 64)

                if not chunk:
                    break

                yield chunk
            yield b''

        content = StreamedContent(content_type, data_provider)
    elif callable(value):
        # value is treated as an async generator
        async def data_provider():
            async for chunk in value():
                yield chunk
            yield b''

        content = StreamedContent(content_type, data_provider)
    elif isinstance(value, bytes):
        content = Content(content_type, value)
    elif isinstance(value, bytearray):
        content = Content(content_type, bytes(value))
    else:
        raise ValueError(
            'Invalid value, expected one of: Callable, str, bytes, bytearray, io.BytesIO'
        )

    return Response(
        200, [(b'Content-Disposition', content_disposition_value.encode())],
        content)
コード例 #2
0
async def test_on_writing_paused_awaits(connection):
    async def dummy_body_generator() -> AsyncIterable[bytes]:
        for i in range(5):
            await asyncio.sleep(0.01)
            yield str(i).encode()

            if i > 2:
                # simulate connection pause
                connection.pause_writing()

    request = Request("POST", b"https://localhost:3000/foo", []).with_content(
        StreamedContent(b"text/plain", dummy_body_generator)
    )
    fake_transport = FakeTransport()
    connection.open = True
    connection.transport = fake_transport

    try:
        await asyncio.wait_for(connection.send(request), 0.1)
    except TimeoutError:
        pass

    assert connection.writing_paused is True
    assert fake_transport.messages == [
        b"POST /foo HTTP/1.1\r\nhost: localhost\r\ncontent-type: "
        + b"text/plain\r\ntransfer-encoding: chunked\r\n\r\n",
        b"1\r\n0\r\n",
        b"1\r\n1\r\n",
        b"1\r\n2\r\n",
        b"1\r\n3\r\n",
    ]
コード例 #3
0
async def test_connection_stops_sending_body_if_server_returns_response(connection):
    async def dummy_body_generator() -> AsyncIterable[bytes]:
        for i in range(5):
            await asyncio.sleep(0.01)
            yield str(i).encode()

            if i > 2:
                # simulate getting a bad request response from the server:
                connection.headers = get_example_headers()
                connection.parser = FakeParser(400)
                connection.on_headers_complete()
                connection.on_message_complete()

    request = Request("POST", b"https://localhost:3000/foo", []).with_content(
        StreamedContent(b"text/plain", dummy_body_generator)
    )
    fake_transport = FakeTransport()
    connection.open = True
    connection.transport = fake_transport
    response = await connection.send(request)

    assert response.status == 400
    # NB: connection stopped writing to transport before the end of the body:
    assert fake_transport.messages == [
        b"POST /foo HTTP/1.1\r\nhost: localhost\r\ncontent-type: "
        + b"text/plain\r\ntransfer-encoding: chunked\r\n\r\n",
        b"1\r\n0\r\n",
        b"1\r\n1\r\n",
        b"1\r\n2\r\n",
        b"1\r\n3\r\n",
    ]
コード例 #4
0
async def test_chunked_encoding_with_generated_content():

    async def data_generator():
        yield b'{"hello":"world",'
        yield b'"lorem":'
        yield b'"ipsum","dolor":"sit"'
        yield b',"amet":"consectetur"}'

    content = StreamedContent(b'application/json', data_generator)

    chunks = []

    async for chunk in data_generator():
        chunks.append(chunk)

    gen = (item for item in chunks)

    async for chunk in write_chunks(content):
        try:
            generator_bytes = next(gen)
        except StopIteration:
            assert chunk == b'0\r\n\r\n'
        else:
            assert chunk == hex(len(generator_bytes))[2:].encode() \
                   + b'\r\n' + generator_bytes + b'\r\n'
コード例 #5
0
ファイル: __init__.py プロジェクト: keshapps/BlackSheep
def get_response_for_file(request, resource_path: str, cache_time: int):
    stat = os.stat(resource_path)
    file_size = stat.st_size
    modified_time = stat.st_mtime
    current_etag = str(modified_time).encode()
    previous_etag = request.if_none_match

    headers = [
        (b'Last-Modified', unix_timestamp_to_datetime(modified_time)),
        (b'ETag', current_etag)
    ]

    if cache_time > 0:
        headers.append((b'Cache-Control', b'max-age=' + str(cache_time).encode()))

    if previous_etag and current_etag == previous_etag:
        return Response(304, headers, None)

    if request.method == 'HEAD':
        headers.append((b'Content-Type', get_mime_type(resource_path)))
        headers.append((b'Content-Length', str(file_size).encode()))
        return Response(200, headers, None)

    return Response(200, 
                    headers,
                    StreamedContent(get_mime_type(resource_path),
                                    get_file_data(resource_path, file_size)))
コード例 #6
0
async def test_on_connection_lost_send_throws(connection):
    async def dummy_body_generator() -> AsyncIterable[bytes]:
        for i in range(5):
            await asyncio.sleep(0.01)
            yield str(i).encode()

            if i > 2:
                # simulate connection lost
                connection.connection_lost(None)

    request = Request("POST", b"https://localhost:3000/foo", []).with_content(
        StreamedContent(b"text/plain", dummy_body_generator))
    fake_transport = FakeTransport()
    connection.open = True
    connection.transport = fake_transport

    with pytest.raises(ConnectionClosedError):
        await connection.send(request)
コード例 #7
0
ファイル: __init__.py プロジェクト: gitter-badger/BlackSheep
def get_response_for_file(
    files_handler: FilesHandler,
    request: Request,
    resource_path: str,
    cache_time: int,
    info: Optional[FileInfo] = None,
) -> Response:
    if info is None:
        info = FileInfo.from_path(resource_path)

    current_etag = info.etag.encode()
    previous_etag = request.if_none_match

    # is the client requesting a Range of bytes?
    # NB: ignored if not GET or unit cannot be handled
    requested_range = _get_requested_range(request)

    if requested_range:
        _validate_range(requested_range, info.size)

    headers = [
        (b"Last-Modified", info.modified_time.encode()),
        (b"ETag", current_etag),
        (b"Accept-Ranges", b"bytes"),
    ]

    if cache_time > 0:
        headers.append((b"Cache-Control", b"max-age=" + str(cache_time).encode()))

    if previous_etag and current_etag == previous_etag:
        # handle HTTP 304 Not Modified
        return Response(304, headers, None)

    if request.method == "HEAD":
        # NB: responses to HEAD requests don't have a body,
        # and responses with a body in BlackSheep have content-type
        # and content-length headers set automatically,
        # depending on their content; therefore here it's necessary to set
        # content-type and content-length for HEAD

        # TODO: instead of calling info.mime.encode every time, optimize using a
        # Dict[str, bytes] - optimize number to encoded string, too, using LRU
        headers.append((b"Content-Type", info.mime.encode()))
        headers.append((b"Content-Length", str(info.size).encode()))
        return Response(200, headers, None)

    status = 200
    mime = get_mime_type_from_name(resource_path).encode()

    if requested_range and is_requested_range_actual(request, info):
        # NB: the method can only be GET for range requests, so it cannot
        # happen to have response 206 partial content with HEAD
        status = 206
        boundary: Optional[bytes]

        if requested_range.is_multipart:
            # NB: multipart byteranges return the mime inside the portions
            boundary = str(uuid.uuid4()).replace("-", "").encode()
            file_type = mime
            mime = b"multipart/byteranges; boundary=" + boundary
        else:
            boundary = file_type = None
            single_part = requested_range.parts[0]
            headers.append(
                (b"Content-Range", _get_content_range_value(single_part, info.size))
            )

        content = StreamedContent(
            mime,
            get_range_file_getter(
                files_handler,
                resource_path,
                info.size,
                requested_range,
                boundary=boundary,
                file_type=file_type,
            ),
        )
    else:
        content = StreamedContent(
            mime, get_file_getter(files_handler, resource_path, info.size)
        )

    return Response(status, headers, content)
コード例 #8
0
def get_response_for_file(request: Request,
                          resource_path: str,
                          cache_time: int,
                          info: Optional[FileInfo] = None):
    if not info:
        info = FileInfo.from_path(resource_path)

    current_etag = info.etag.encode()
    previous_etag = request.if_none_match

    # is the client requesting a Range of bytes?
    # NB: ignored if not GET or unit cannot be handled
    requested_range = _get_requested_range(request)

    if requested_range:
        _validate_range(requested_range, info.size)

    headers = [(b'Last-Modified', info.modified_time.encode()),
               (b'ETag', current_etag), (b'Accept-Ranges', b'bytes')]

    if cache_time > 0:
        headers.append(
            (b'Cache-Control', b'max-age=' + str(cache_time).encode()))

    if previous_etag and current_etag == previous_etag:
        # handle HTTP 304 Not Modified
        return Response(304, headers, None)

    if request.method == 'HEAD':
        # NB: responses to HEAD requests don't have a body,
        # and responses with a body in BlackSheep have content-type and content-length headers set automatically,
        # depending on their content; therefore here it's necessary to set content-type and content-length for HEAD
        headers.append((b'Content-Type', info.mime.encode()))
        headers.append((b'Content-Length', str(info.size).encode()))
        return Response(200, headers, None)

    status = 200
    mime = get_mime_type(resource_path).encode()

    if requested_range and is_requested_range_actual(request, info):
        # NB: the method can only be GET for range requests, so it cannot happen
        # to have response 206 partial content with HEAD
        status = 206

        if requested_range.is_multipart:
            # NB: multipart byteranges return the mime inside the portions
            boundary = str(uuid.uuid4()).replace('-', '').encode()
            file_type = mime
            mime = b'multipart/byteranges; boundary=' + boundary
        else:
            boundary = file_type = None
            single_part = requested_range.parts[0]
            headers.append((b'Content-Range',
                            _get_content_range_value(single_part, info.size)))

        content = StreamedContent(
            mime,
            get_range_file_getter(resource_path,
                                  info.size,
                                  requested_range,
                                  boundary=boundary,
                                  file_type=file_type))
    else:
        content = StreamedContent(mime,
                                  get_file_getter(resource_path, info.size))

    return Response(status, headers, content)