ファイルのストリーミング強制保存をクロスオリジンでも実現させるService Workerの裏技ぽい使い方
やりたいこと
同一オリジンでないURLをブラウザで開かずにファイルとして保存させたい。そして大きいファイルでもダウロードできるようにBlobとかBlob URLとかを使わないで解決したい。 勘違いされやすいところ: Access-Control-Allow-Origin: *がなくてもダウンロードという意味ではない。
デモ動画
https://youtu.be/hvdaJ-Kq0OI
ダウンロード中に円状に進捗が出ていることからストリーミングしながらダウンロードできていることが分かる。 4.8MBなのに遅いのはデモ動画撮ったネットワーク環境が悪かったから。この手法で遅くなっているわけではないことに注意
このページではその技術をなるべく余計なものを排除して載せることが目的。
ネットワークなしで表示とか
早く表示するとか
とかが思いつくが、
内部でクロスオリジンのサイトをfetch()したり、
大雑把な仕組み
Download ボタンが押される。
GET /swdownload?url=https://nwtgck.github.io...-scala.mp4&filename=myvideo.mp4 のリクエストが出る
JavaScriptで<a href="mp4動画のURL" target="_blank">が動的に生成されて.click()されることでGETリクエストされる。 event.respondWith(fetch(mp4動画のURL))でレスポンスが変える
つまり、ファイルは/index.htmlと/service-worker.jsしかないにもかかわらず、Service Workerが/swdownloadというパスに対するリクエストにレスポンスを返せるようなHTTPサーバーのように振る舞う。 実際のコード
git cloneしてpython3 -m http.serverとか使ってlocalhost:8000を立ててService Workerが動くようにして試せば良い。 code:index.html
<html>
<head></head>
<body>
<script>
// Register Service Worker
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js');
});
}
function download() {
// Get values from inputs
const url = window.target_url.value;
const filename = window.filename.value;
const a = document.createElement('a');
a.href = /swdonwload?url=${encodeURIComponent(url)}&filename=${filename};
a.target = '_blank';
a.click();
}
</script>
<input type="text" id="target_url" placeholder="URL" size="100"><br>
<input type="text" id="filename" placeholder="File name"><br>
<button onclick="download()">Download</button>
</body>
</html>
code:service-worker.js
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
if (url.pathname === '/swdonwload') {
const targetUrl = url.searchParams.get('url');
const filename = url.searchParams.get('filename');
event.respondWith((async () => {
const res = await fetch(targetUrl);
headers.set('Content-Disposition', attachment; filename="${filename}");
const downloadableRes = new Response(res.body, {
headers
});
return downloadableRes;
})());
}
});
対応ブラウザ
対応をJavaScript側で確認したい
Service Workerに/swdownload-supportにアクセスされたら決まったResponse()返すようにして、その/swdownload-supportに向かってUI側のJavaScriptでfetch()とかしてちゃんとレスポンスが返ってくるかどうかで判定すれば良いと思う。 Piping UIでは呼び出し側の関係でretry回数があるが、ロジックとしてはシンプルにリトライはなし良いと思う。