c1f7eaa153
- fetchOrThrow wraps global fetch with TLS/DNS/refused hints + URL (sanitized). - Use in gitea-release, github paths; fetchToFile already benefits. - README checklist for sync Wow.exe fetch failed; version 1.0.5. Co-authored-by: Cursor <cursoragent@cursor.com>
122 lines
4.2 KiB
JavaScript
122 lines
4.2 KiB
JavaScript
'use strict';
|
|
|
|
const path = require('path');
|
|
const fs = require('fs').promises;
|
|
const { githubToken } = require('./github-token');
|
|
const { downloadGiteaReleaseAsset, useGiteaReleases } = require('./gitea-release');
|
|
const { fetchToFile, downloadBodyToFile, fetchOrThrow } = require('./http-download');
|
|
|
|
function encodeRepoPath(repoPath) {
|
|
let p = String(repoPath || '').replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
|
|
if (!p) return '';
|
|
return p.split('/').map((seg) => encodeURIComponent(seg)).join('/');
|
|
}
|
|
|
|
function ghHeaders(token, json = false) {
|
|
const h = {
|
|
'User-Agent': 'Fractured-Launcher-Electron',
|
|
'X-GitHub-Api-Version': '2022-11-28',
|
|
};
|
|
if (json) h.Accept = 'application/vnd.github+json';
|
|
if (token) h.Authorization = `Bearer ${token}`;
|
|
return h;
|
|
}
|
|
|
|
async function downloadGitHubRepoFile(cfg, repoPath, destPath) {
|
|
const token = githubToken(cfg);
|
|
const enc = encodeRepoPath(repoPath);
|
|
const ref = cfg.github.ref || 'main';
|
|
const { owner, repo } = cfg.github;
|
|
|
|
if (!token) {
|
|
const url = `https://raw.githubusercontent.com/${owner}/${repo}/${ref}/${enc}`;
|
|
await fetchToFile(url, {}, destPath);
|
|
return;
|
|
}
|
|
|
|
const apiUrl = `https://api.github.com/repos/${owner}/${repo}/contents/${enc}?ref=${encodeURIComponent(ref)}`;
|
|
const res = await fetchOrThrow(apiUrl, { headers: ghHeaders(token, true) });
|
|
const body = await res.text();
|
|
if (!res.ok) {
|
|
throw new Error(`GitHub contents API ${res.status}: ${body.slice(0, 800)}`);
|
|
}
|
|
const meta = JSON.parse(body);
|
|
if (meta.type && meta.type !== 'file') {
|
|
throw new Error(`not a file: ${repoPath}`);
|
|
}
|
|
if (meta.download_url) {
|
|
const h = { Accept: 'application/octet-stream' };
|
|
if (token) {
|
|
h.Authorization = `Bearer ${token}`;
|
|
h['X-GitHub-Api-Version'] = '2022-11-28';
|
|
}
|
|
await fetchToFile(meta.download_url, h, destPath);
|
|
return;
|
|
}
|
|
if (meta.content && meta.encoding === 'base64') {
|
|
const buf = Buffer.from(String(meta.content).replace(/\n/g, ''), 'base64');
|
|
if (!buf.length) throw new Error('empty base64 content');
|
|
await fs.mkdir(path.dirname(destPath), { recursive: true });
|
|
const tmp = destPath + '.downloading';
|
|
await fs.writeFile(tmp, buf);
|
|
await fs.rename(tmp, destPath);
|
|
return;
|
|
}
|
|
throw new Error(`unexpected GitHub response for ${repoPath}`);
|
|
}
|
|
|
|
async function downloadReleaseAsset(cfg, assetName, destPath) {
|
|
if (useGiteaReleases(cfg)) {
|
|
return downloadGiteaReleaseAsset(cfg, assetName, destPath);
|
|
}
|
|
const token = githubToken(cfg);
|
|
const tag = (cfg.github.release_tag || 'latest').trim() || 'latest';
|
|
const { owner, repo } = cfg.github;
|
|
let listUrl;
|
|
if (tag.toLowerCase() === 'latest') {
|
|
listUrl = `https://api.github.com/repos/${owner}/${repo}/releases/latest`;
|
|
} else {
|
|
listUrl = `https://api.github.com/repos/${owner}/${repo}/releases/tags/${encodeURIComponent(tag)}`;
|
|
}
|
|
const res = await fetchOrThrow(listUrl, { headers: ghHeaders(token, true) });
|
|
const text = await res.text();
|
|
if (!res.ok) {
|
|
let hint = '';
|
|
if (res.status === 404) {
|
|
hint =
|
|
' (wrong tag, private repo without token, or releases live on Gitea — set gitea.base_url, gitea.owner, gitea.repo in launcher.json)';
|
|
}
|
|
if (res.status === 401 || res.status === 403) hint = ' (set GITHUB_TOKEN or token_env PAT)';
|
|
throw new Error(`releases list ${res.status}${hint}: ${text.slice(0, 600)}`);
|
|
}
|
|
const rel = JSON.parse(text);
|
|
const assets = rel.assets || [];
|
|
let assetURL = '';
|
|
for (const a of assets) {
|
|
if (a.name !== assetName) continue;
|
|
if (token && a.url) {
|
|
assetURL = a.url;
|
|
break;
|
|
}
|
|
if (a.browser_download_url) {
|
|
assetURL = a.browser_download_url;
|
|
break;
|
|
}
|
|
assetURL = a.url;
|
|
break;
|
|
}
|
|
if (!assetURL) {
|
|
const names = assets.map((x) => x.name);
|
|
throw new Error(`release asset "${assetName}" not found; attachments: ${names.join(', ')}`);
|
|
}
|
|
const h = { Accept: 'application/octet-stream' };
|
|
if (token) {
|
|
h.Authorization = `Bearer ${token}`;
|
|
h['X-GitHub-Api-Version'] = '2022-11-28';
|
|
}
|
|
const dl = await fetchOrThrow(assetURL, { headers: h, redirect: 'follow' });
|
|
await downloadBodyToFile(dl, destPath);
|
|
}
|
|
|
|
module.exports = { downloadGitHubRepoFile, downloadReleaseAsset, encodeRepoPath };
|