|
|
<!doctype html> |
|
|
<html lang="ja"> |
|
|
<head> |
|
|
<meta charset="utf-8" /> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" /> |
|
|
<title>URL → showSaveFilePicker でダウンロード</title> |
|
|
<style> |
|
|
body {font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; padding: 2rem; background:#f7fafc; color:#0f172a} |
|
|
.card {background:#fff; padding:1.25rem; border-radius:10px; box-shadow:0 6px 18px rgba(2,6,23,0.06); max-width:720px} |
|
|
label {display:block; font-weight:600; margin-bottom:0.25rem} |
|
|
input[type=text] {width:100%; padding:0.5rem; border:1px solid #e6edf3; border-radius:6px} |
|
|
button {margin-top:0.75rem; padding:0.6rem 1rem; border-radius:8px; border:0; background:#2563eb; color:white; font-weight:600} |
|
|
pre {background:#0b1220; color:#e6f2ff; padding:0.75rem; border-radius:6px; overflow:auto} |
|
|
.note {margin-top:0.75rem; color:#374151} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<div class="card"> |
|
|
<h1>URL を入力してファイルを保存(showSaveFilePicker)</h1> |
|
|
<p>指定した URL を fetch して、ブラウザのファイル保存ダイアログで保存します。<br>(対応ブラウザ:Chromium ベースの最新ブラウザなど)</p> |
|
|
|
|
|
<label for="url">ダウンロードする URL</label> |
|
|
<input id="url" type="text" placeholder="https://example.com/path/to/file.png" value="" /> |
|
|
|
|
|
<label for="suggest">保存時のファイル名(空欄なら URL から推測)</label> |
|
|
<input id="suggest" type="text" placeholder="自動推測されます(例: file.png)" /> |
|
|
|
|
|
<div> |
|
|
<button id="ok">OK — ダウンロード開始</button> |
|
|
</div> |
|
|
|
|
|
<div class="note" id="status" aria-live="polite"></div> |
|
|
|
|
|
<h3>使い方メモ</h3> |
|
|
<ul> |
|
|
<li>クロスオリジン(CORS)が許可されていないサーバーからは fetch に失敗します。サーバー側で Access-Control-Allow-Origin を設定するか、プロキシ経由で取得してください。</li> |
|
|
<li>showSaveFilePicker は一部のブラウザでしか動作しません。存在しない場合は代替処理(a.download)にフォールバックします。</li> |
|
|
</ul> |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
function setStatus(text) { |
|
|
document.getElementById('status').textContent = text; |
|
|
} |
|
|
|
|
|
async function downloadFromUrl(url, suggestedName) { |
|
|
setStatus('fetch を開始します…'); |
|
|
|
|
|
const response = await fetch(url); |
|
|
if (!response.ok) throw new Error('HTTP エラー: ' + response.status); |
|
|
|
|
|
const blob = await response.blob(); |
|
|
|
|
|
|
|
|
let filename = suggestedName && suggestedName.trim() ? suggestedName.trim() : null; |
|
|
const cd = response.headers.get('content-disposition'); |
|
|
if (!filename && cd) { |
|
|
const match = cd.match(/filename\*?=(?:UTF-8'')?"?([^";]+)"?/i); |
|
|
if (match) filename = decodeURIComponent(match[1]); |
|
|
} |
|
|
|
|
|
|
|
|
if (!filename) { |
|
|
try { |
|
|
const u = new URL(url); |
|
|
const name = u.pathname.split('/').filter(Boolean).pop(); |
|
|
if (name) filename = name; |
|
|
} catch (e) { |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
if (!filename) filename = 'download'; |
|
|
|
|
|
|
|
|
if (window.showSaveFilePicker) { |
|
|
setStatus('ファイル保存ダイアログを開いています…'); |
|
|
const opts = {suggestedName: filename}; |
|
|
|
|
|
const handle = await window.showSaveFilePicker(opts); |
|
|
const writable = await handle.createWritable(); |
|
|
|
|
|
await writable.write(blob); |
|
|
await writable.close(); |
|
|
setStatus('保存完了: ' + handle.name); |
|
|
} else { |
|
|
|
|
|
setStatus('showSaveFilePicker が使えないため、フォールバックでダウンロードします…'); |
|
|
const a = document.createElement('a'); |
|
|
const objectUrl = URL.createObjectURL(blob); |
|
|
a.href = objectUrl; |
|
|
a.download = filename; |
|
|
document.body.appendChild(a); |
|
|
a.click(); |
|
|
a.remove(); |
|
|
setTimeout(() => URL.revokeObjectURL(objectUrl), 5000); |
|
|
setStatus('ダウンロード開始(フォールバック): ' + filename); |
|
|
} |
|
|
} |
|
|
|
|
|
document.getElementById('ok').addEventListener('click', async () => { |
|
|
const btn = document.getElementById('ok'); |
|
|
const url = document.getElementById('url').value.trim(); |
|
|
const suggested = document.getElementById('suggest').value.trim(); |
|
|
if (!url) return setStatus('URL を入力してください。'); |
|
|
|
|
|
btn.disabled = true; |
|
|
try { |
|
|
await downloadFromUrl(url, suggested); |
|
|
} catch (err) { |
|
|
console.error(err); |
|
|
setStatus('エラー: ' + (err.message || err)); |
|
|
|
|
|
if (err instanceof TypeError) { |
|
|
setStatus((document.getElementById('status').textContent || '') + '\n注意: CORS によるブロックやネットワークエラーの可能性があります。'); |
|
|
} |
|
|
} finally { |
|
|
btn.disabled = false; |
|
|
} |
|
|
}); |
|
|
</script> |
|
|
</body> |
|
|
</html> |