'use strict'; const path = require('path'); const fs = require('fs').promises; const { githubToken } = require('./github-token'); const { downloadGiteaReleaseAsset, useGiteaReleases, listGiteaReleaseAttachmentNames } = 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 fetchGitHubReleaseJson(cfg) { 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)}`); } return JSON.parse(text); } async function listReleaseAttachmentNames(cfg) { if (useGiteaReleases(cfg)) { return listGiteaReleaseAttachmentNames(cfg); } const rel = await fetchGitHubReleaseJson(cfg); const assets = rel.assets || []; return assets.map((a) => a.name).filter(Boolean); } async function downloadReleaseAsset(cfg, assetName, destPath) { if (useGiteaReleases(cfg)) { return downloadGiteaReleaseAsset(cfg, assetName, destPath); } const token = githubToken(cfg); const rel = await fetchGitHubReleaseJson(cfg); 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, fetchGitHubReleaseJson, listReleaseAttachmentNames, };