UNPKG

korefile

Version:

File System API for Local/GitHub.

312 lines (300 loc) 9.73 kB
import { Octokit } from "@octokit/rest"; import { KoreFileAdaptor } from "./KoreFileAdaptor"; import { encode as arrayBufferToBase64 } from "base64-arraybuffer"; // https://github.com/octokit/rest.js/issues/1971 import type { components } from "@octokit/openapi-types"; import { fromBase64 } from "js-base64"; type GetRepoContentResponseDataFile = components["schemas"]["content-file"]; const debug = require("debug")("korefile"); const GITHUB_API_TOKEN = typeof process === "object" && process.env.GITHUB_API_TOKEN; export interface GitCommitPushOptions { owner: string; repo: string; files: { path: string; content: string | ArrayBuffer; }[]; ref: string; forceUpdate?: boolean; commitMessage?: string; token?: string; // or process.env.GITHUB_API_TOKEN } const getReferenceCommit = function (octokit: Octokit, options: Pick<GitCommitPushOptions, "owner" | "repo" | "ref">) { return octokit.git .getRef({ owner: options.owner, repo: options.repo, ref: options.ref }) .then((res) => { debug("getReferenceCommit Response: %O", res); return { referenceCommitSha: res.data.object.sha }; }); }; const createTree = function ( octokit: Octokit, options: Pick<GitCommitPushOptions, "owner" | "repo" | "files">, data: { referenceCommitSha: string } ) { const promises = options.files.map((file) => { if (typeof file.content === "string") { return octokit.git .createBlob({ owner: options.owner, repo: options.repo, content: file.content, encoding: "utf-8" }) .then((blob: any) => { debug("createBlob:text"); return { sha: blob.data.sha, path: file.path, mode: "100644", type: "blob" } as const; }); } else if (ArrayBuffer.isView(file.content)) { return octokit.git .createBlob({ owner: options.owner, repo: options.repo, content: arrayBufferToBase64(file.content), encoding: "base64" }) .then((blob: any) => { debug("createBlob:buffer"); return { sha: blob.data.sha, path: file.path, mode: "100644", type: "blob" } as const; }); } throw new Error(`This file can not handled: ${file}`); }); return Promise.all(promises).then((files) => { debug("files: %O", files); return octokit.git .createTree({ owner: options.owner, repo: options.repo, tree: files, base_tree: data.referenceCommitSha }) .then((res) => { debug("createCommit Response: %O", res); return { ...data, newTreeSha: res.data.sha }; }); }); }; const createCommit = function ( octokit: Octokit, options: Pick<GitCommitPushOptions, "owner" | "repo" | "commitMessage">, data: any ) { return octokit.git .createCommit({ owner: options.owner, repo: options.repo, message: options.commitMessage || "commit", tree: data.newTreeSha, parents: [data.referenceCommitSha] }) .then((res) => { debug("createTree Response: %O", res); return { ...data, newCommitSha: res.data.sha }; }); }; const updateReference = function (github: Octokit, options: GitCommitPushOptions, data: any) { return github.git .updateRef({ owner: options.owner, repo: options.repo, ref: options.ref, sha: data.newCommitSha, force: options.forceUpdate }) .then((res) => { debug("updateReference Response: %O", res); }); }; export const getContent = ( github: Octokit, { owner, repo, path, ref }: { owner: string; repo: string; path: string; ref: string; } ) => { return github.repos .getContent({ owner, repo, path, ref }) .then((res) => { const data = res.data as GetRepoContentResponseDataFile; if (Array.isArray(data)) { throw new Error(`folder does not support`); } debug("getContent Response: %O", res); if (data.type !== "file") { return Promise.reject(new Error("This is not file:" + path)); } if (data.encoding === "base64") { // TODO: support binary return Promise.resolve(fromBase64(data.content)); } throw new Error("Unknown file type" + data.type + ":" + data.encoding); }); }; export const deleteFile = async ( octokit: Octokit, { owner, repo, path, ref, commitMessage }: { owner: string; repo: string; path: string; ref: string; commitMessage: string; } ) => { const response = await octokit.repos.getContent({ owner, repo, path, ref }); const { data } = response; debug("deleteFile:getContents Response: %O", data); if (Array.isArray(data)) { throw new Error(`folder does not support`); } return octokit.repos .deleteFile({ owner, repo, path, branch: ref.replace(/^refs\//, "").replace(/^heads\//, ""), sha: data.sha, message: commitMessage }) .then((res) => { debug("deleteFile:deleteFile Response: %O", res); }); }; export interface GitHubAdaptorOptions { host?: string; owner: string; repo: string; ref: string; commitMessage?: { write?: (filePath: string) => string; delete?: (filePath: string) => string; }; forceUpdate?: boolean; token?: string; // or process.env.GITHUB_API_TOKEN } export const createGitHubAdaptor = (options: GitHubAdaptorOptions): KoreFileAdaptor => { const token = options.token || GITHUB_API_TOKEN; if (!token) { throw new Error(`token is not defined`); } const octKit = new Octokit({ auth: token, type: "oauth", userAgent: "korefile" }); const filledOptions = { owner: options.owner, repo: options.repo, ref: options.ref, forceUpdate: options.forceUpdate || false }; return { readFile(filePath: string): Promise<string> { return getContent(octKit, { repo: filledOptions.repo, owner: filledOptions.owner, path: filePath, ref: filledOptions.ref }); }, writeFile(filePath: string, content: string | ArrayBuffer): Promise<void> { const withFileOption = { ...filledOptions, files: [ { path: filePath, content: content } ] }; const commitMessage = options.commitMessage && typeof options.commitMessage.write === "function" ? options.commitMessage.write(filePath) : `Update ${filePath}`; return getReferenceCommit(octKit, filledOptions) .then((data) => createTree(octKit, withFileOption, data)) .then((data) => createCommit( octKit, { ...filledOptions, commitMessage }, data ) ) .then((data) => updateReference(octKit, withFileOption, data)); }, writeFiles(files: { path: string; content: string | ArrayBuffer }[]): Promise<void> { const withFileOption = { ...filledOptions, files: files }; const commitMessage = `Update files`; return getReferenceCommit(octKit, filledOptions) .then((data) => createTree(octKit, withFileOption, data)) .then((data) => createCommit( octKit, { ...filledOptions, commitMessage }, data ) ) .then((data) => updateReference(octKit, withFileOption, data)); }, deleteFile(filePath: string): Promise<void> { const commitMessage = options.commitMessage && typeof options.commitMessage.delete === "function" ? options.commitMessage.delete(filePath) : `Delete ${filePath}`; return deleteFile(octKit, { ...filledOptions, path: filePath, commitMessage: commitMessage }); } }; };