0

I've been studying this example project. Very concise and to the point, and a pleasant surprise, too: a Save File Dialog (a standard desktop File Dialog, opened from Chrome).

The responsible code:

src/html.py:

@app.post('/download')
def form_post(request: Request, num: int = Form(...), multiply_by_2: bool = Form(False), action: str = Form(...)):
    if action == 'convert':
        result = spell_number(num, multiply_by_2)
        return templates.TemplateResponse('download.html', context={'request': request, 'result': result, 'num': num})
    elif action == 'download':
        # Requires aiofiles
        result = spell_number(num, multiply_by_2)
        filepath = save_to_text(result, num)
        return FileResponse(filepath, media_type='application/octet-stream', filename='{}.txt'.format(num))

templates/download.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Sample Form</title>
</head>
<body>
<form method="post">
    <input type="number" name="num" value="{{ num }}"/>
    <input type="checkbox" id="multiply_by_2" name="multiply_by_2" value="True">
    <label for="multiply_by_2">Multiply by 2&nbsp;</label>
    <input type="submit" name="action" value="convert">
    <input type="submit" name="action" value="download">
</form>
<p>Result: {{ result }}</p>
</body>
</html>

I can't see any hint on a File Dialog in FileResponse, much less on the Save File Dialog, which pops up. I want the Open File Dialog too, by the way. I tried to research it, without success.

How does it work?

UPD, to make myself clearer.

I'm playing with something like this:

from tkinter import Tk
from tkinter.filedialog import askopenfilename
...
@app.post("/open")
def form_post(
    request: Request,
    action: str = Form("open"),
):
    if action == "open":
        root = Tk()
        root.withdraw()
        # ensure the file dialog pops to the top window
        root.wm_attributes('-topmost', 1)
        fname = askopenfilename(parent=root)
        print(f"Chosen file: {fname}")
        return templates.TemplateResponse("open.html", context={"request": request})
    elif action == "save":
        # Requires aiofiles
        return FileResponse(
            "templates/legacy/download.html",
            media_type="text/html",
            filename="download.html",
        )

For now, button save makes use of the system Save File Dialog, while button open employs tkinter's Open Dialog. It will do, because the whole thing is merely an application with a Web UI. Still, it looks and feels a bit ridiculous.

Is there a way to make the browser serve the Open File Dialg?

Alexey Orlov
  • 2,412
  • 3
  • 27
  • 46

1 Answers1

1

The file dialog is what Chrome shows for that kind of response (application/octet-stream). The server side framework isn't responsible for creating the save file dialog; it just provides a response and the browser does what it usually does for the that kind of response.

You can use the Content-Disposition header in your response to indicate to the browser that it should show a download dialog instead of the content directly (if it supports the mime type natively). The Content-Disposition header also allows you to provide a default file name for the save dialog (but only a file name - not a path).

A open file dialog would be the responsibility of the HTML, by using <input type="file" name="name_matching_fastapi_parameter">. You can then tell FastAPI that you expect that a parameter is a file with the UploadFile type.

From the FastAPI reference:

from fastapi import FastAPI, File, UploadFile

app = FastAPI()


@app.post("/files/")
async def create_file(file: bytes = File(...)):
    return {"file_size": len(file)}


@app.post("/uploadfile/")
async def create_upload_file(file: UploadFile = File(...)):
    return {"filename": file.filename}
MatsLindh
  • 49,529
  • 4
  • 53
  • 84
  • Thank you very much, @MatsLindh! So `application/octet-stream` causes *Save File Dialog*. I'm very sorry, but I can't find the MIME type which would cause *Open File Dialog*. Is there such a type? – Alexey Orlov Sep 26 '21 at 08:21
  • I see that MIME type is responsible for the file type only (as it should). I still can't see how to order an *Open File Dialog*. – Alexey Orlov Sep 26 '21 at 08:38
  • That would be something you do in your HTML; when the user clicks the "Browse" button on the `input type="file"` element, a file selection dialog will open up. You can't do this from your FastAPI code; it has to be done in the HTML itself. – MatsLindh Sep 26 '21 at 19:38
  • I put the `` into the template and got the *Open Dialog* all right. Still, I can't collect the file name. Surprisingly, the howto info, especially relevant to FastAPI, is scarce. – Alexey Orlov Sep 28 '21 at 03:44
  • If you look at [the page for UploadFile as reference in the answer](https://fastapi.tiangolo.com/tutorial/request-files/#uploadfile), you'll find that it exposes a `filename` attribute: _filename: A str with the original file name that was uploaded (e.g. myimage.jpg)._ – MatsLindh Sep 28 '21 at 07:37
  • Yes, `UploadFile` exposes `filename`. It's the name of a file already opened and loaded into memory, however. In this paradigm there is no way to get just a file name/path, because the identity of a local file is of no use on a remote server. Am I correct? – Alexey Orlov Oct 02 '21 at 09:00
  • Correct, FastAPI knows nothing about (and can't do anything with) whatever your browser is doing. It only receives the file data and certain metadata (such as the original file name) from the browser when the browser submits the file through an http post. There's the `FileReader` interface you can access through Javascript if you want to do other processing - but the same limitation applies; you can't select/open random files on a user's computer, the user has to actively select the file themelves: https://developer.mozilla.org/en-US/docs/Web/API/FileReader – MatsLindh Oct 02 '21 at 19:42