Path: blob/master/src/packages/next/lib/share/proxy/api.ts
5738 views
/*1api.github.com is very nice to use to get info, but23"Unauthenticated clients can make **60 requests per hour**."4https://docs.github.com/en/rest/guides/getting-started-with-the-rest-api56So it's completely useless for our purposes without authentication.78"When authenticating, you should see your rate limit bumped to 5,000 requests an hour,"910which is also hopefully sufficient, but worrisome.1112Thoughts:1314- Since all rendering could be done client side, I could actually have15the client browser grab content instead of the server, then render there to16massively reduce api load, although even users could quickly hit "60 requests17per hour", so the api still wouldn't help.1819- If we do hit the 5K/hour limit, maybe we can use more than one api key?2021- Upgrading to enterprise doesn't increase this much.2223- We could switch to mirroring and cloning files locally, and that might24work around this problem in practice, but be a lot of work. We'll see.252627Get at https://github.com/settings/tokens28*/2930// these are provided by nextjs: https://nextjs.org/blog/next-9-4#improved-built-in-fetch-support31declare var fetch, Headers;3233import { encode } from "base-64";34import { join } from "path";35import getPool from "@cocalc/database/pool";3637// We don't allow just fetching content that is arbitrarily large, since that could cause38// the server to just run out of memory. However, we want this to reasonably big.39export const RAW_MAX_SIZE_BYTES = 10000000; // 10MB4041// TODO: we will also have a raw blob or stream or something for serving up images, etc.,42export async function rawText(43githubOrg: string,44githubRepo: string,45segments: string[],46): Promise<string> {47const url = rawURL(githubOrg, githubRepo, segments);48//console.log("raw:", { url });49return await (await fetch(url, { size: RAW_MAX_SIZE_BYTES })).text();50}5152function rawURL(53githubOrg: string,54githubRepo: string,55segments: string[],56): string {57return `https://raw.githubusercontent.com/${githubOrg}/${githubRepo}/${join(58...segments.slice(1),59)}`;60}6162interface GithubFile {63name: string;64path: string;65sha: string;66size: number;67url: string;68html_url: string;69git_url: string;70download_url: string;71type: "file" | "dir";72content: string;73encoding: string;74}7576async function credentials(): Promise<{77github_username?: string;78github_token?: string;79github_block?: string;80}> {81const pool = getPool("long");82const { rows } = await pool.query(83"SELECT name, value FROM server_settings WHERE name='github_username' OR name='github_token' OR name='github_block'",84);85let result: {86github_username?: string;87github_token?: string;88github_block?: string;89} = {};90for (const row of rows) {91result[row.name] = row.value;92}93return result;94}9596function isBlocked(path: string, github_block?: string) {97if (!github_block) {98return false;99}100const path1 = path.toLowerCase();101for (const x of github_block.split(",")) {102const y = x.trim().toLowerCase();103if (path1.includes(y)) {104return true;105}106}107return false;108}109110export async function api(path: string): Promise<any> {111const url = `https://api.github.com/${path}`;112const options: any = {};113const { github_username, github_token, github_block } = await credentials();114if (isBlocked(path, github_block)) {115throw Error(116`Path '${path}' is blocked by the site admins. If you think this is a mistake, please contact support.`,117);118}119if (github_username && github_token) {120options.headers = new Headers({121Authorization: "Basic " + encode(`${github_username}:${github_token}`),122"Content-Type": "application/json",123});124}125//console.log(options);126const response = await fetch(url, options);127//console.log(response.headers);128const data: any = await response.json();129//console.log({ url, response });130if (data.message) {131throw Error(`${data.message} (see ${data.documentation_url})`);132}133return data;134}135136// Use the github api to get the contents of a path on github.137// We are planning to use this just to get directory listings,138// since individual files have their content base64 encoded, etc.,139// and that has to be much slower than just grabbing the140// file form raw (and also only works up to 1MB according to141// github docs).142// How to do auth + fetch with node: https://stackoverflow.com/questions/43842793/basic-authentication-with-fetch143export async function contents(144githubOrg: string,145githubRepo: string,146segments: string[],147): Promise<GithubFile[]> {148let ref, path;149if (segments.length == 0) {150ref = ""; // the default;151path = ""; // root152} else {153// tree/[ref]/[path ...]154ref = segments[1];155path = join(...segments.slice(2));156}157const result = await api(158`repos/${githubOrg}/${githubRepo}/contents/${path}${159ref ? "?ref=" + ref : ""160}`,161);162if (result.name != null) {163throw Error(164"only use contents to get directory listing, not to get file contents",165);166}167return result;168}169170export async function defaultBranch(171githubOrg: string,172githubRepo: string,173): Promise<string> {174return (await api(`repos/${githubOrg}/${githubRepo}`)).default_branch;175}176177// Return all the repositories in a GitHub organization or user:178export async function repos(githubOrg: string): Promise<{ name: string }[]> {179let result;180try {181result = await api(`orgs/${githubOrg}/repos`);182} catch (err) {183result = await api(`users/${githubOrg}/repos`);184}185return result186.filter((repo) => !repo.private)187.map((repo) => {188return {189isdir: true,190name: repo.name,191mtime: new Date(repo.updated_at).valueOf(),192url: `/github/${githubOrg}/${repo.name}`,193};194});195}196197export async function fileInGist(gistId: string): Promise<string> {198const info = await api(`gists/${gistId}`);199for (const filename in info.files) {200return filename;201}202throw Error("no files in the gist");203}204205206