Neoteroi / BlackSheep

Fast ASGI web framework for Python
https://www.neoteroi.dev/blacksheep/
MIT License
1.8k stars 75 forks source link

chunked response landing page example #345

Closed TGlock closed 1 year ago

TGlock commented 1 year ago

Need an example of file response that performs like serve_files.

@app.router.get(“/{page}”) def landing(request: Request) -> Response:

page = request.route_values[“page”]

How to Map page to a file and send back async file response with etag etc headers ?

The goal is to allow for approx 10 routes with a single parameter that return the main landing pages of the app. It’s not a SPA.

thanks in advance - probably missing the obvious

🚀 Feature Request

RobertoPrevato commented 1 year ago

Interesting question. I didn't think of documenting this, but I'll add an example to the docs site.

You can achieve that functionality this way, using the get_response_for_file. The example below also includes a Cache-Control response header with max-age=120 seconds.

from blacksheep import Application, Request, Response
from blacksheep.server.files.dynamic import get_response_for_file

app = Application()

@app.router.get("/{page}")
def landing(request: Request, page: str) -> Response:
    # In this case, page can be any file which is a direct child of the CWD
    # TODO: validate the input value because otherwise the user can download anything,
    # for example also application settings file!
    try:
        return get_response_for_file(app.files_handler, request, page, 120)
    except FileNotFoundError:
        return Response(404)

if __name__ == "__main__":
    import uvicorn

    uvicorn.run(app, port=44555)

That function handles automatically chunked encoding, ETag headers, HTTP 304 Not Modified (AFAIK, you need to specify a cache-control of at least 1 second for browsers to use etags and if-none-match), and even range-requests. Range requests are important if you are serving big files because you get out of the box support for resumable downloads (browsers can pause and resume downloads thanks to those), and if you are serving video or audio files, the clients can seek specific points of them.

In the example above, the request handler can return any file in the CWD, but not in any sub-folder. Be careful: you need to validate the input parameter.

If you want a function that can serve files in sub-folders, you would use a catch-all route with a star "*":

@app.router.get("/*")
def landing_any(request: Request) -> Response:
    # In this case, the returned file can be anything file inside the CWD or any of
    # its descendant folders!
    assert request.route_values is not None  # optional assertion
    page = request.route_values["tail"]
    try:
        return get_response_for_file(app.files_handler, request, page, 120)
    except FileNotFoundError:
        return Response(404)

Note: the function is synchronous because it returns an instance of Response, but the content is read asynchronously from the file system. The function returning chunked portions of the files is an asynchronous iterator.

RobertoPrevato commented 1 year ago

Anyway, judging by what you wrote, I understand you want to serve HTML views. For this, I recommend using Controller and returning dynamic views. I mean using the features described here: https://www.neoteroi.dev/blacksheep/mvc-project-template/