korefile
Version:
File System API for Local/GitHub.
312 lines (300 loc) • 9.73 kB
text/typescript
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
});
}
};
};