◆ Github のファイルリストのファイル名やフォルダ名を Alt-Click でダウンロードできる

Github ってリポジトリ全体をまとめてダウンロードはできますが ファイルやフォルダをダウンロードできないのですよね
ファイルを 1 つずつ開いて 「Raw」 を押して実際のファイルをブラウザ開いてそれをダウンロードすることになります
1 ファイルだけダウンロードしたいなら別に気にならなかったのですが フォルダをダウンロードしたいときに自分で地道にやってられなかったので拡張機能にしました

ファイルやフォルダ一覧のテーブルの名前を Alt-Click するとダウンロードできます

ghd

Alt-Click にしたのは Alt-Click がリンク先をダウンロードするブラウザのショートカットキーだからです
通常ここを Alt-Click してもリンク先の web ページをダウンロードしてしまって意味がありません
ここを Alt-Click でフォルダやファイルがダウンロードできるようになったら便利かなと思ってこういう操作にしました


中身はすごく単純でたった 3 ファイルです

[manifest.json]
{
"manifest_version": 2,
"name": "ghd",
"version": "1",
"content_scripts": [
{
"matches": ["https://github.com/*"],
"js": ["cs.js"]
}
],
"background": {
"scripts": ["bg.js"]
},
"permissions": ["downloads", "http://raw.githubusercontent.com/*", "https://api.github.com/*"]
}

まずはマニフェストファイルです
クリック監視するのでページ内で JavaScript を動かすために content_scripts を github のページなら埋め込むように設定しています
また実際にダウンロードを行うのはバックグラウンドで行うので background にスクリプトを指定しています
permissions にはダウンロード機能とダウンロード先と API の URL を指定しています


[cs.js]
document.body.addEventListener("click", eve => {
const a = eve.target.closest(".repository-content table.files a.js-navigation-open")
if(a && eve.altKey){
eve.preventDefault()
const [owner, repo,, ref, ...path] = new URL(location).pathname.split("/").slice(1)
path.push(a.textContent)
chrome.runtime.sendMessage({owner, repo, ref, path: path.join("/")}, msg => {
alert(msg ? "complete" : "error")
})
}
}, true)

ページに埋め込む content_script がこれです
テーブルのファイル名・フォルダ名の部分を Alt キーありでクリックされた場合のみに ダウンロードするファイルやフォルダの情報をバックグラウンドに送信します


[bg.js]
chrome.runtime.onMessage.addListener((msg, sender, response) => {
const url = `https://api.github.com/repos/${msg.owner}/${msg.repo}/contents/${msg.path}?ref=${msg.ref || "master"}`
dl(url)
.then(e => response(true))
.catch(e => {console.error(e), response(false)})
return true
})


async function dl(url){
const result = await fetch(url).then(e => e.json())
for(const item of Array.isArray(result) ? result : [result]){
if(item.type === "file"){
const path = new URL(item.download_url).pathname
// chrome cannot download dotfiles
const filename = `ghd-files${path}`.replace(/\/\./g, "/!.")
console.log(`${item.download_url} --> ${filename}`)
chrome.downloads.download({url: item.download_url, filename})
}else if(item.type === "dir"){
await wait(100)
await dl(item.url)
}
}
}

async function wait(msec){
return new Promise(r => setTimeout(r, msec))
}

バッググランド処理では ページからメッセージを受け取ると API にアクセスして情報を取得します
ファイルならそのままダウンロード URL をダウンロードします
フォルダなら 再帰的に中を見ていきファイルをダウンロードします

ダウンロード先は Chrome で既定のダウンロードフォルダ内の ghd-files フォルダになります
このフォルダの中にダウンロードしたファイルのパスでフォルダが作られます

「リポジトリオーナー/リポジトリ名/ブランチやタグ/ファイルへのパス」となります

一つ不便なことに Chrome の制限で 「.」 から始まるファイル名は使えません
設定ファイルなどをダウンロードしようとするとエラーになります

なので 「.」 から始まる場合は 「!.」 に置換しています
ダウンロード後に手動でもとに戻してください