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