forked from download-directory/download-directory.github.io
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.js
More file actions
229 lines (183 loc) · 5.87 KB
/
Copy pathindex.js
File metadata and controls
229 lines (183 loc) · 5.87 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
import saveFile from 'save-file';
import listContent from 'list-github-dir-content';
import pMap from 'p-map';
import pRetry from 'p-retry';
// Matches '/<re/po>/tree/<ref>/<dir>'
const urlParserRegex = /^[/]([^/]+)[/]([^/]+)[/]tree[/]([^/]+)[/](.*)/;
function updateStatus(status, ...extra) {
const element = document.querySelector('.status');
if (status) {
element.prepend(status + '\n');
} else {
element.textContent = status || '';
}
console.log(status, ...extra);
}
async function waitForToken() {
const input = document.querySelector('#token');
if (localStorage.token) {
input.value = localStorage.token;
} else {
const toggle = document.querySelector('#token-toggle');
toggle.checked = true;
updateStatus('Waiting for token…');
await new Promise(resolve => {
input.addEventListener('input', function handler() {
if (input.checkValidity()) {
toggle.checked = false;
resolve();
input.removeEventListener('input', handler);
}
});
});
}
}
async function fetchRepoInfo(repo) {
const response = await fetch(`https://api.github.com/repos/${repo}`,
localStorage.token ? {
headers: {
Authorization: `Bearer ${localStorage.token}`,
},
} : {},
);
switch (response.status) {
case 401: {
updateStatus('⚠ The token provided is invalid or has been revoked.', {token: localStorage.token});
throw new Error('Invalid token');
}
case 403: {
// See https://developer.github.com/v3/#rate-limiting
if (response.headers.get('X-RateLimit-Remaining') === '0') {
updateStatus('⚠ Your token rate limit has been exceeded.', {token: localStorage.token});
throw new Error('Rate limit exceeded');
}
break;
}
case 404: {
updateStatus('⚠ Repository was not found.', {repo});
throw new Error('Repository not found');
}
default:
}
if (!response.ok) {
updateStatus('⚠ Could not obtain repository data from the GitHub API.', {repo, response});
throw new Error('Fetch error');
}
return response.json();
}
async function getZIP() {
const JSZip = await import('jszip');
return new JSZip();
}
function escapeFilepath(path) {
return path.replaceAll('#', '%23');
}
async function init() {
const zipPromise = getZIP();
let user;
let repository;
let ref;
let dir;
const input = document.querySelector('#token');
if (localStorage.token) {
input.value = localStorage.token;
}
input.addEventListener('input', () => {
if (input.checkValidity()) {
localStorage.token = input.value;
}
});
try {
const query = new URLSearchParams(location.search);
const parsedUrl = new URL(query.get('url'));
[, user, repository, ref, dir] = urlParserRegex.exec(parsedUrl.pathname);
console.log('Source:', {user, repository, ref, dir});
} catch {
return updateStatus();
}
if (!navigator.onLine) {
updateStatus('⚠ You are offline.');
throw new Error('You are offline');
}
updateStatus(`Retrieving directory info \nRepo: ${user}/${repository}\nDirectory: /${dir}`);
const {private: repoIsPrivate} = await fetchRepoInfo(`${user}/${repository}`);
const repoListingConfig = {
user,
repository,
ref,
directory: decodeURIComponent(dir),
token: localStorage.token,
getFullData: true,
};
let files = await listContent.viaTreesApi(repoListingConfig);
if (files.length === 0 && files.truncated) {
updateStatus('Warning: It’s a large repo and this it take a long while just to download the list of files. You might want to use "git sparse checkout" instead.');
files = await listContent.viaContentsApi(repoListingConfig);
}
if (files.length === 0) {
updateStatus('No files to download');
return;
}
updateStatus(`Will download ${files.length} files`);
const controller = new AbortController();
const fetchPublicFile = async file => {
const response = await fetch(`https://raw.githubusercontent.com/${user}/${repository}/${ref}/${escapeFilepath(file.path)}`, {
signal: controller.signal,
});
if (!response.ok) {
throw new Error(`HTTP ${response.statusText} for ${file.path}`);
}
return response.blob();
};
const fetchPrivateFile = async file => {
const response = await fetch(file.url, {
headers: {
Authorization: `Bearer ${localStorage.token}`,
},
signal: controller.signal,
});
if (!response.ok) {
throw new Error(`HTTP ${response.statusText} for ${file.path}`);
}
const {content} = await response.json();
const decoder = await fetch(`data:application/octet-stream;base64,${content}`);
return decoder.blob();
};
let downloaded = 0;
const downloadFile = async file => {
const localDownload = () => repoIsPrivate ? fetchPrivateFile(file) : fetchPublicFile(file);
const onFailedAttempt = error => {
console.error(`Error downloading ${file.url}. Attempt ${error.attemptNumber}. ${error.retriesLeft} retries left.`);
};
const blob = await pRetry(localDownload, {onFailedAttempt});
downloaded++;
updateStatus(file.path);
const zip = await zipPromise;
zip.file(file.path.replace(dir + '/', ''), blob, {
binary: true,
});
};
if (repoIsPrivate) {
await waitForToken();
}
await pMap(files, downloadFile, {concurrency: 20}).catch(error => {
controller.abort();
if (!navigator.onLine) {
updateStatus('⚠ Could not download all files, network connection lost.');
} else if (error.message.startsWith('HTTP ')) {
updateStatus('⚠ Could not download all files.');
} else {
updateStatus('⚠ Some files were blocked from downloading, try to disable any ad blockers and refresh the page.');
}
throw error;
});
updateStatus(`Zipping ${downloaded} files`);
const zip = await zipPromise;
const zipBlob = await zip.generateAsync({
type: 'blob',
});
await saveFile(zipBlob, `${user} ${repository} ${ref} ${dir}.zip`.replace(/\//, '-'));
updateStatus(`Downloaded ${downloaded} files! Done!`);
}
// eslint-disable-next-line unicorn/prefer-top-level-await -- I like having an `init` function since there's a lot of code in this file
init();