1
2
お題は不問!Qiita Engineer Festa 2024で記事投稿!
Qiita Engineer Festa20242024年7月17日まで開催中!

FastAPI を使って PC のファイルを別の場所にコピーするだけの無駄機能を作る

Posted at

デモサイト

推奨ブラウザは Google Chrome です。
アクセスに時間がかかる場合がありますので、あらかじめご了承ください。

ファイルの内容をサーバに保存するなどの処理は行っていませんが、サーバを経由しますので、機密情報等をアップロードしないようにお願いいたします。

ソースコードはこちらです。

モチベーション

FastAPI でファイルのリクエストを学習しているときに思いつきました。

概要

ユーザがアップロードしたファイルを別のディレクトリにダウンロードします。

flow.png

ソースコード

ファイル構造は以下のようになっています。

app
├── main.py (送られてきたファイルをそのまま返却する処理)
├── static
│   └── js
│       └── index.js (ファイルをアップロードしてダウンロードする処理)
└── templates
    └── index.html (ファイルをアップロードする画面)

送られてきたファイルをそのまま返却する処理

main.py
import os.path
from io import BytesIO

from fastapi import FastAPI, UploadFile
from starlette.requests import Request
from starlette.responses import StreamingResponse, HTMLResponse, Response
from starlette.staticfiles import StaticFiles
from starlette.templating import Jinja2Templates

current_dir = os.path.dirname(__file__)

app = FastAPI()
app.mount("/static", StaticFiles(directory=f"{current_dir}/static"), name="static")
templates = Jinja2Templates(directory=f"{current_dir}/templates")


@app.get("/", response_class=HTMLResponse)
def copy_file_form(request: Request) -> Response:
    return templates.TemplateResponse(request, name="index.html", context={})


@app.post("/copy_file/")
async def copy_file(file: UploadFile) -> Response:
    return StreamingResponse(
        content=BytesIO(await file.read(-1)),
        status_code=200,
        headers={"content-disposition": f"attachment; filename={file.filename}"},
    )

copy_file_form は画面表示用 API です。
copy_file でアップロードされたファイルを読み取り、ストリームレスポンスに流しています。

ファイルをアップロードする画面

templates/index.html
<html lang="en">
<head>
    <title>Muda File Copy</title>
</head>
<body>
<form id="form" action="{{ url_for('copy_file') }}" method="post" enctype="multipart/form-data">
    <input id="input-file" type="file" name="file">
    <input id="btn-copy" type="submit" value="Copy">
</form>
</body>
<script src="{{ url_for('static', path='/js/index.js') }}"></script>

フォームは copy_file 関数の /copy_file/ へ POST メソッドで送られます。

ファイルをアップロードしてダウンロードする処理

static/js/index.js
(() => {
    if (!("showSaveFilePicker" in window)) {
        alert("window.showSaveFilePicker is not implemented.")
        document.querySelectorAll("#form input").forEach(elem => {
            elem.disabled = true
        })
        return false
    }

    const showSaveFilePicker = window.showSaveFilePicker

    class FileNotSelectedException extends Error {
    }

    document.getElementById("btn-copy").addEventListener("click", async (evt) => {
        evt.preventDefault()

        let writable
        try {
            const elementById = document.getElementById("input-file");
            if (!elementById.value) {
                // noinspection ExceptionCaughtLocallyJS
                throw new FileNotSelectedException()
            }
            const saveFilePicker = await showSaveFilePicker({
                startIn: "downloads",
                suggestedName: elementById.value.split('\\').pop(),
            })
            writable = await saveFilePicker.createWritable()
        } catch (e) {
            if (e instanceof DOMException) {
                console.warn(e)
                return false
            }
            if (e instanceof FileNotSelectedException) {
                alert("File is not specified.")
                return false
            }
            alert("An error occurred. Please check console.")
        }
        if (!writable) {
            return false
        }

        let ok
        try {
            const form = document.getElementById("form")
            await fetch(form.action, {
                method: "POST",
                body: new FormData(form)
            }).then(async r => {
                await writable.write(await r.blob())
                ok = true
            }).catch(reason => {
                console.error(reason)
                alert("An error occurred. Please check console.")
            })
        } finally {
            await writable.close()
        }
        if (ok) {
            alert("Succeeded to copy a file!")
        }
    })
})()

今回はファイル保存先を決めるために window.showSaveFilePicker 関数を使用して保存ウィンドウを開きます。
保存ウィンドウを開くときに、最初はダウンロードフォルダが開くようにし、ファイル名にはアップロードしたファイルと同じファイル名が指定されているようにします。

            const saveFilePicker = await showSaveFilePicker({
                startIn: "downloads",
                suggestedName: elementById.value.split('\\').pop(),
            })

saveFilePicker から書き込み可能なファイルストリームを開いたら、最初に用意した API に POST メソッドでリクエストを送り、返ってきたボディを書き込みます。

            const form = document.getElementById("form")
            await fetch(form.action, {
                method: "POST",
                body: new FormData(form)
            }).then(async r => {
                await writable.write(await r.blob())
                ok = true
            })

現在、window.showSaveFilePicker は実験的な機能となっており、対応ブラウザが限られています。詳しくは こちら をご覧ください。

所感

(Windows) Ctrl + C -> フォルダ移動 -> Ctrl + V で終わるものをわざわざ複雑に作るのもたまには楽しいですね。

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2