📤

GitHub API でファイルを読み書きする

2021/12/22に公開
この記事は すごくなりたいがくせいぐるーぷ Advent Calendar 2021 22日目の記事です。

この記事では、Octokitを用いてGitHub API v3を呼び出し、GitHub上のファイルを読み書きしていきます。

この記事の内容はこのリポジトリに記載しています。
https://github.com/e-chan1007/1222-github-api

認証

公開リポジトリに対する読み取り操作には認可が必要ありません。書き込み操作を行う場合には、Octokitクラスのコンストラクタなどで以下の指定をしてください。

1. Personal Access Tokenを使う場合

GitHubの SettingsDeveloper settingsPersonal access tokens から、Personal Access Tokenを発行します。当然ですが、最低限 repo スコープを有効化する必要があります。
Octokitクラスのコンストラクタで、 authオプションにPersonal Access Tokenを指定します。

import { Octokit } from "octokit";

new Octokit({ auth: "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" });

2. OAuth Appとして認証する場合

OAuth 2を使って発行したアクセストークンも、同様に authオプションに指定します。

new Octokit({ auth: "gho_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" });

OAuthの認可手順についてはこちらから。
https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps

なお、OAuthAppクラスを使うことでもOAuth関連の操作を行うことができます。

3. GitHub Appとして認証する

https://docs.github.com/en/developers/apps/building-github-apps/authenticating-with-github-apps
GitHub Appとして認証するときは、秘密鍵が必要になります。
Appクラスを経由してGitHub Appとして認証したOctokitクラスのインスタンスを取得します。

インストールIDがわかっている場合

await new App({ 
  appId: 99999999, // AppのID
  privateKey: "-----BEGIN RSA PRIVATE KEY-----\n ..." // 秘密鍵
}).getInstallationOctokit(999999); // インストールID

getInstallationOctokitPromise<Octokit>であるため、await することで Octokitクラスのインスタンスを取得します。
インストールIDについては上記サイトの通りにAPIを呼び出したり、次のApp#eachInstallationから抽出したりすることができます。

ユーザーのIDなどがわかっている場合

// パターン1. コールバック関数
new App({ 
  appId: 99999999, // AppのID
  privateKey: "-----BEGIN RSA PRIVATE KEY-----\n ..." // 秘密鍵
}).eachInstallation(({ installation, octokit }) => {
  if(installation.account.login === "ユーザーID") {
    // [ユーザーID]にインストールされたAppとして認証したoctokit
  }
});

// パターン2. AsyncIterable
const iterator = new App({ 
  appId: 99999999, // AppのID
  privateKey: "-----BEGIN RSA PRIVATE KEY-----\n ..." // 秘密鍵
}).eachInstallation.iterator();

for await (const installation of iterator) {
  if(installation.account.login === "ユーザーID") {
    // [ユーザーID]にインストールされたAppとして認証したoctokit
  }
}

App#eachInstallationによってインストールされた各Appに対して操作が行えます。また、App#eachInstallation.iteratorAsyncIterableであるため、for-await-ofでのループ処理もできます。

ファイルを読む

const owner = "e-chan1007"; // 所有者(ユーザー/組織)
const repo = "1222-github-api"; // リポジトリ
const barnch = "main"; // ブランチ

const latestCommit = (await octokit.rest.repos.getBranch({ owner, repo, branch })).data.commit;
const files = (await octokit.rest.git.getTree({ owner, repo, tree_sha: latestCommit.sha })).data.tree;
const blob = (await octokit.rest.git.getBlob({ owner, repo, file_sha: files.find(file => file.path === "README.md")?.sha! })).data;
const content = Buffer.from(blob.content, "base64").toString("utf-8");
  1. 最終コミットのハッシュを取得
  2. 最終コミットに紐付いたツリーを取得
  3. ツリーから任意のファイルのハッシュを取得
  4. ファイルを取得
  5. base64で送られたファイルの内容をデコードする

(ここでは最終コミットとしますが、特定のコミットを対象にする場合はそのハッシュを指定してください。)

ファイルを書き込む

const owner = "e-chan1007"; // 所有者(ユーザー/組織)
const repo = "1222-github-api"; // リポジトリ
const barnch = "main"; // ブランチ

const createdBlob = (await octokit.rest.git.createBlob({
  owner,
  repo,
  content: Buffer.from("Hello GitHub API With Base64 Encoded!", "utf-8").toString("base64"),
  encoding: "base64"
})).data;

const latestCommit = (await octokit.rest.repos.getBranch({ owner, repo, branch })).data.commit;

const createdTree = (await octokit.rest.git.createTree({
  owner,
  repo,
  tree: [{
    type: "blob",
    path: "test.txt",
    mode: "100644",
    content: "Hello GitHub API!"
  }, {
    type: "blob",
    path: "base64/test.txt",
    mode: "100644",
    sha: createdBlob.sha
  }],
  base_tree: latestCommit.sha
})).data;

const createdCommit = (await octokit.rest.git.createCommit({
  owner,
  repo,
  message: "Test Commit with GitHub API",
  tree: createdTree.sha,
  parents: [latestCommit.sha],
})).data;

await octokit.rest.git.updateRef({
  owner,
  repo,
  ref: `heads/${target.branch}`,
  sha: createdCommit.sha
});
  1. (ファイルをアップロード)
  2. 最終コミットのハッシュを取得
  3. 最終コミットをもとにツリーを作成
  4. 最終コミットに繋がるようにコミット
  5. refs/heads/ブランチが最新コミットを指すようにする

ファイルのアップロードについては、事前に行うことも、ツリーの作成と同時に行うこともできます。事前に行う場合はツリーの作成時にハッシュを、同時に行う場合は内容を直接指定します。
(おそらく)バイナリをアップロードする場合は事前にbase64エンコードしてアップロードします。
平文の場合はどちらでも可能です。(コード中ではbase64エンコード済)

ツリーの作成時に、base_treeにもとにするツリー(コミット)のハッシュを指定します。そうすることで、base_treeにあったファイルはそのままに、treeパラメーターにあるファイルで上書きor追加します。
一方で、base_treeを指定しない場合、treeパラメーターにないファイルは削除されたとみなされます。

treeで指定 treeで未指定
base_treeを指定 上書き&新規作成 保持
base_tree未指定 上書き&新規作成 削除

削除を伴う場合には、base_treeを指定せずに、保持するすべてのファイルを指定する必要があるようです。

ツリーの作成時にmodeを指定していますが、これはファイルの種類を示しています。通常のファイルには100644を指定しておきます。その他のパラメーターについて、詳しくはこちらから。
https://docs.github.com/en/rest/reference/git#create-a-tree--parameters

ちなみに、GitHubではPGP署名がされたコミットにverifiedという表示がつきます。GitHub APIを用いる場合、GitHub Appとして認証を行うと署名済コミットとして扱われます。他にも、コミット時にsignatureパラメーターを指定することもできます。

さいごに

以上、GitHub上のファイルをGitHub APIを利用して読み書きする方法を紹介しました。GitHubをデータベースとしたサービスの開発などに使えそうですね。
少し手順が複雑な面もありますが、ぜひご自身のリポジトリで試してみてください。

Discussion