デモサイト
推奨ブラウザは Google Chrome です。
アクセスに時間がかかる場合がありますので、あらかじめご了承ください。
ファイルの内容をサーバに保存するなどの処理は行っていませんが、サーバを経由しますので、機密情報等をアップロードしないようにお願いいたします。
ソースコードはこちらです。
モチベーション
FastAPI でファイルのリクエストを学習しているときに思いつきました。
概要
ユーザがアップロードしたファイルを別のディレクトリにダウンロードします。
ソースコード
ファイル構造は以下のようになっています。
app
├── main.py (送られてきたファイルをそのまま返却する処理)
├── static
│ └── js
│ └── index.js (ファイルをアップロードしてダウンロードする処理)
└── templates
└── index.html (ファイルをアップロードする画面)
送られてきたファイルをそのまま返却する処理
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
でアップロードされたファイルを読み取り、ストリームレスポンスに流しています。
ファイルをアップロードする画面
<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 メソッドで送られます。
ファイルをアップロードしてダウンロードする処理
(() => {
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 で終わるものをわざわざ複雑に作るのもたまには楽しいですね。