CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/next/lib/share/proxy/api.ts
Views: 687
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
}> {
81
const pool = getPool("long");
82
const { rows } = await pool.query(
83
"SELECT name, value FROM server_settings WHERE name='github_username' OR name='github_token'"
84
);
85
let result: { github_username?: string; github_token?: string } = {};
86
for (const row of rows) {
87
result[row.name] = row.value;
88
}
89
return result;
90
}
91
92
export async function api(path: string): Promise<any> {
93
const url = `https://api.github.com/${path}`;
94
const options: any = {};
95
const { github_username, github_token } = await credentials();
96
if (github_username && github_token) {
97
options.headers = new Headers({
98
Authorization: "Basic " + encode(`${github_username}:${github_token}`),
99
"Content-Type": "application/json",
100
});
101
}
102
//console.log(options);
103
const response = await fetch(url, options);
104
//console.log(response.headers);
105
const data: any = await response.json();
106
//console.log({ url, response });
107
if (data.message) {
108
throw Error(`${data.message} (see ${data.documentation_url})`);
109
}
110
return data;
111
}
112
113
// Use the github api to get the contents of a path on github.
114
// We are planning to use this just to get directory listings,
115
// since individual files have their content base64 encoded, etc.,
116
// and that has to be much slower than just grabbing the
117
// file form raw (and also only works up to 1MB according to
118
// github docs).
119
// How to do auth + fetch with node: https://stackoverflow.com/questions/43842793/basic-authentication-with-fetch
120
export async function contents(
121
githubOrg: string,
122
githubRepo: string,
123
segments: string[]
124
): Promise<GithubFile[]> {
125
let ref, path;
126
if (segments.length == 0) {
127
ref = ""; // the default;
128
path = ""; // root
129
} else {
130
// tree/[ref]/[path ...]
131
ref = segments[1];
132
path = join(...segments.slice(2));
133
}
134
const result = await api(
135
`repos/${githubOrg}/${githubRepo}/contents/${path}${
136
ref ? "?ref=" + ref : ""
137
}`
138
);
139
if (result.name != null) {
140
throw Error(
141
"only use contents to get directory listing, not to get file contents"
142
);
143
}
144
return result;
145
}
146
147
export async function defaultBranch(
148
githubOrg: string,
149
githubRepo: string
150
): Promise<string> {
151
return (await api(`repos/${githubOrg}/${githubRepo}`)).default_branch;
152
}
153
154
// Return all the repositories in a GitHub organization or user:
155
export async function repos(githubOrg: string): Promise<{ name: string }[]> {
156
let result;
157
try {
158
result = await api(`orgs/${githubOrg}/repos`);
159
} catch (err) {
160
result = await api(`users/${githubOrg}/repos`);
161
}
162
return result
163
.filter((repo) => !repo.private)
164
.map((repo) => {
165
return {
166
isdir: true,
167
name: repo.name,
168
mtime: new Date(repo.updated_at).valueOf(),
169
url: `/github/${githubOrg}/${repo.name}`,
170
};
171
});
172
}
173
174
export async function fileInGist(gistId: string): Promise<string> {
175
const info = await api(`gists/${gistId}`);
176
for (const filename in info.files) {
177
return filename;
178
}
179
throw Error("no files in the gist");
180
}
181
182