'use strict'; const path = require('path'); const fs = require('fs').promises; const { githubToken } = require('./github-token'); const { fetchToFile, downloadBodyToFile } = 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 fetch(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) { 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 fetch(listUrl, { headers: ghHeaders(token, true) }); const text = await res.text(); if (!res.ok) { let hint = ''; if (res.status === 404) hint = ' (wrong tag or private repo without token?)'; 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 fetch(assetURL, { headers: h, redirect: 'follow' }); await downloadBodyToFile(dl, destPath); } module.exports = { downloadGitHubRepoFile, downloadReleaseAsset, encodeRepoPath };