FastUI icon indicating copy to clipboard operation
FastUI copied to clipboard

is there a way to create an end point for downloading a file?

Open fmrib00 opened this issue 1 year ago • 5 comments

fmrib00 avatar Mar 11 '24 12:03 fmrib00

Here's an example of a basic endpoint which you can submit a file to. Submit the form/FormData, with a field named uploaded that contains your file and submit it to this endpoint.

import pathlib

@router.post("/api/media_upload")
async def media_upload(uploaded: UploadFile):
    save_path = pathlib.Path("some/path")
    file_path = save_path / (uploaded.filename or "my-backup-file-name.docx")
    try:
        contents = await uploaded.read()
        file_path.write_bytes(contents)
    except Exception as e:
        print(e)
    return PlainTextResponse(f"File {file_path.as_posix()} saved")

zboyles avatar Mar 15 '24 07:03 zboyles

Thanks for the help but I am interested in getting an endpoint for doing the opposite - returning (downloading) a file instead of uploading one within fastui framework. Any idea?

fmrib00 avatar Mar 15 '24 07:03 fmrib00

That makes me think of the server component ServerLoad although I have no experience with it yet. In the Recorder component I'm putting together, I have a typescript function that conditionally executes on the client-side browser, if the python FastUI component has save_download=True. I'll provide that typescript code below. It's blob data is set dynamically during runtime, it's supplied by the browser API MediaRecorder class, and it functions as expected.

  const handleDownloadRecording = (blob: Blob): void => {
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.style.display = 'none';
    a.href = url;
    a.download = 'recording.webm';
    document.body.appendChild(a);
    a.click();
    window.URL.revokeObjectURL(url);
  }

Is that closer to what you're talking about? Or are you thinking about a separate FastUI download component? Just thinking out loud, couldn't you use the Form component? Then again, maybe there's no way for you to route the file to the client rather than the endpoint. Then again, I could just be confusing myself about it now 😃

Can you provide a step by step of what you want/expect to happen and include your best guess which environment that step takes place (typescript react / python FastUI). If it's python FastUI, it would help if you also included if it's in the endpoint vs the initial instance layout code too.

Maybe what you need is a custom FileDownload component where the file Blob is set dynamically, I'm not sure if this is possible with the existing ServerLoad or not but that too is worth investigating.

zboyles avatar Mar 16 '24 06:03 zboyles

I would like a new FastUI component for that, something like DownloadEvent, so that I can create an endpoint like this: @router.post("/download") async def download() -> list[AnyComponent]:

	file = gen_file()   // returns a io.BytesIO object or a file handle maybe
	file_name = ...

return [c.DownloadEvent(file_handle = file, name = file_name)] 

this will result in a file download in broswer....

fmrib00 avatar Mar 16 '24 07:03 fmrib00

Oh okay, I was making it much more complex in my mind. 😃

The good news, what you're looking to do is possible with the current version of FastUI. It looks like the mistake you're making has to do with thinking about everything you want, all at once. At least that's how I've made similar mistakes. For example, your endpoint is trying to do 2 things; it's trying to generate the file and the return type list[AnyComponent] is saying it wants to return the UI. Sometimes it's helpful to remind yourself that the UI is served independent from anything in the python backend.

In the specific case adding download links to dynamically created files, split the dynamic file generation and the return of that file from the UI element Link which is pointing to the file. Below I've created a gen_file function to mock dynamically generating the file bytes and filename. I split the demonstration into two endpoints for clarity but there's no reason you can have a single endpoint that uses something like query string params to conditionally return different files or to be used in the dynamic generation of a file. In fact, I'll include a 3rd endpoint that serves both.

I've added a 3rd endpoint which uses a path parameter to determine if the file is returned from the bytes stream or if a static tempfile is generated and returned. The gen_file function allows for dynamic content and a custom filename now too. There are example links for each of the options, including the dynamically determined file creation using the same endpoint and a path parameter of stream or static, and an optional query string parameter to override the name. Note that the GoToEvent has an attribute for query which I believe will convert a dict to query string parameters but you'll need to play with that to verify how it works.


import io
import tempfile
from fastapi import FastAPI
from fastapi.responses import FileResponse, StreamingResponse

app = FastAPI()

@app.get('/', response_model=FastUI, response_model_exclude_none=True)
def components_view() -> list[AnyComponent]:
    return [
        c.Page(
            components=[
                c.Heading(text='Download Links', level=2),
                c.LinkList(
                    links=[
                        c.Link(
                            components=[c.Text(text='Download File (Stream)')],
                            on_click=GoToEvent(url='/api/dl_file_stream', target='_blank'),
                        ),
                        c.Link(
                            components=[c.Text(text='Download File (Static)')],
                            on_click=GoToEvent(url='/api/dl_file_static', target='_blank'),
                        ),
                        c.Link(
                            components=[c.Text(text='Download Dynamic File (Stream)')],
                            on_click=GoToEvent(url='/api/dl_file/stream', target='_blank'),
                        ),
                        c.Link(
                            components=[c.Text(text='Download Dynamic File (Static)')],
                            on_click=GoToEvent(url='/api/dl_file/static', target='_blank'),
                        ),
                        c.Link(
                            components=[c.Text(text='Download Dynamic File-Customized Name (Stream)')],
                            on_click=GoToEvent(url='/api/dl_file/stream?filename=my_stream_generated_file.txt', target='_blank'),
                        ),
                        c.Link(
                            components=[c.Text(text='Download Dynamic File-Customized Name (Static)')],
                            on_click=GoToEvent(url='/api/dl_file/static?filename=my_static_tempfile_generated_file.txt', target='_blank'),
                        ),
                    ],
                ),
            ]
        )
    ]


def gen_file(extra_text: str | None = None, override_filename: str | None = None) -> tuple[io.BytesIO, str]:
    content = f"This is the content of the file.\n{extra_text or 'The End'}"
    file_bytes = io.BytesIO(content.encode())
    filename = override_filename or "my_generated_file.txt"
    return file_bytes, filename

@app.get("/api/dl_file/{source}")
async def dl_file(source: str | None = None, filename: str | None = None):
    file_bytes, filename = gen_file(f"I was generated from '{source}' path parameter.", filename)
    if source == 'stream':
        async def iterfile():  # Generator function
            yield file_bytes.getvalue()

        response = StreamingResponse(iterfile(), media_type="application/octet-stream")
        response.headers["Content-Disposition"] = f"attachment; filename={filename}"
        return response 
    elif source == 'static':
        with tempfile.NamedTemporaryFile(delete=False) as temp_file:  
            temp_file.write(file_bytes.getvalue())

        return FileResponse(temp_file.name, filename=filename, media_type="application/octet-stream")
    return {"detail": "Supported path query values are 'stream' and 'static'."}


@app.get("/api/dl_file_static")
async def dl_file_static():
    file_bytes, filename = gen_file()

    with tempfile.NamedTemporaryFile(delete=False) as temp_file:  
        temp_file.write(file_bytes.getvalue())

    return FileResponse(temp_file.name, filename=filename, media_type="application/octet-stream") 

@app.get("/api/dl_file_stream")
async def dl_file_stream():
    file_bytes, filename = gen_file()

    async def iterfile():  # Generator function
        yield file_bytes.getvalue()

    response = StreamingResponse(iterfile(), media_type="application/octet-stream")
    response.headers["Content-Disposition"] = f"attachment; filename={filename}"
    return response 

zboyles avatar Mar 16 '24 12:03 zboyles

it worked really nicely! Thanks so much for help!

fmrib00 avatar Mar 17 '24 12:03 fmrib00

Pleased you sorted it :+1:.

samuelcolvin avatar Mar 17 '24 12:03 samuelcolvin

I found some issue about this moment. The site lose headers from user, and can't using user: Annotated[User | None, Depends(User.from_request_opt)] in download get. But if i use router link site back "Request Error Response not valid JSON"

Pleased you sorted it 👍.

Nosikmov avatar Jun 05 '24 06:06 Nosikmov