fix(launcher): add missing gitea-release and patch-manifest to repo
These modules were required by main.js / auto-update.js / github.js but never committed, so packaged builds lacked them and crashed at startup. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
'use strict';
|
||||
|
||||
const { downloadBodyToFile } = require('./http-download');
|
||||
|
||||
function giteaApiBase(cfg) {
|
||||
const base = String(cfg.gitea.base_url || '').trim().replace(/\/+$/, '');
|
||||
return `${base}/api/v1`;
|
||||
}
|
||||
|
||||
function giteaToken(cfg) {
|
||||
const name = cfg.gitea && cfg.gitea.token_env;
|
||||
if (name && process.env[name]) return String(process.env[name]).trim();
|
||||
return String(process.env.GITEA_TOKEN || '').trim();
|
||||
}
|
||||
|
||||
function giteaHeaders(token, json = false) {
|
||||
const h = { 'User-Agent': 'Fractured-Launcher-Electron' };
|
||||
if (json) h.Accept = 'application/json';
|
||||
if (token) h.Authorization = `token ${token}`;
|
||||
return h;
|
||||
}
|
||||
|
||||
function useGiteaReleases(cfg) {
|
||||
const g = cfg.gitea;
|
||||
if (!g) return false;
|
||||
return !!(String(g.base_url || '').trim() && String(g.owner || '').trim() && String(g.repo || '').trim());
|
||||
}
|
||||
|
||||
async function downloadGiteaReleaseAsset(cfg, assetName, destPath) {
|
||||
const api = giteaApiBase(cfg);
|
||||
const { owner, repo } = cfg.gitea;
|
||||
const tag = (cfg.gitea.release_tag || 'latest').trim() || 'latest';
|
||||
const token = giteaToken(cfg);
|
||||
|
||||
let listUrl;
|
||||
if (tag.toLowerCase() === 'latest') {
|
||||
listUrl = `${api}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/latest`;
|
||||
} else {
|
||||
listUrl = `${api}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/tags/${encodeURIComponent(tag)}`;
|
||||
}
|
||||
|
||||
const res = await fetch(listUrl, { headers: giteaHeaders(token, true) });
|
||||
const text = await res.text();
|
||||
if (!res.ok) {
|
||||
let hint = '';
|
||||
if (res.status === 404) hint = ' (wrong tag / no release / check base_url owner repo)';
|
||||
if (res.status === 401 || res.status === 403) hint = ' (set GITEA_TOKEN or gitea.token_env)';
|
||||
throw new Error(`Gitea release ${res.status}${hint}: ${text.slice(0, 600)}`);
|
||||
}
|
||||
const rel = JSON.parse(text);
|
||||
const list = rel.attachments || rel.assets || [];
|
||||
let downloadUrl = '';
|
||||
for (const a of list) {
|
||||
if (a.name !== assetName) continue;
|
||||
downloadUrl = a.browser_download_url || a.download_url || '';
|
||||
break;
|
||||
}
|
||||
if (!downloadUrl) {
|
||||
const names = list.map((x) => x.name).filter(Boolean);
|
||||
throw new Error(`Gitea release asset "${assetName}" not found; attachments: ${names.join(', ') || '(none)'}`);
|
||||
}
|
||||
|
||||
const h = { Accept: 'application/octet-stream' };
|
||||
if (token) h.Authorization = `token ${token}`;
|
||||
const dl = await fetch(downloadUrl, { headers: h, redirect: 'follow' });
|
||||
await downloadBodyToFile(dl, destPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Base URL for electron-updater generic provider (expects latest.yml under this path).
|
||||
* Matches Gitea’s pattern: …/owner/repo/releases/download/{tag}/latest.yml
|
||||
*/
|
||||
async function getGiteaUpdaterFeedBase(cfg) {
|
||||
if (!useGiteaReleases(cfg)) return null;
|
||||
const api = giteaApiBase(cfg);
|
||||
const { owner, repo } = cfg.gitea;
|
||||
const tag = (cfg.gitea.release_tag || 'latest').trim() || 'latest';
|
||||
const token = giteaToken(cfg);
|
||||
let listUrl;
|
||||
if (tag.toLowerCase() === 'latest') {
|
||||
listUrl = `${api}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/latest`;
|
||||
} else {
|
||||
listUrl = `${api}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/tags/${encodeURIComponent(tag)}`;
|
||||
}
|
||||
const res = await fetch(listUrl, { headers: giteaHeaders(token, true) });
|
||||
if (!res.ok) return null;
|
||||
const rel = await res.json();
|
||||
const tagName = rel.tag_name;
|
||||
if (!tagName || typeof tagName !== 'string') return null;
|
||||
const root = String(cfg.gitea.base_url || '').trim().replace(/\/+$/, '');
|
||||
const url = `${root}/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/download/${encodeURIComponent(tagName)}/`;
|
||||
return { url, token };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
downloadGiteaReleaseAsset,
|
||||
giteaToken,
|
||||
useGiteaReleases,
|
||||
getGiteaUpdaterFeedBase,
|
||||
};
|
||||
@@ -0,0 +1,144 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const { createHash } = require('node:crypto');
|
||||
const { downloadReleaseAsset, downloadGitHubRepoFile } = require('./github');
|
||||
|
||||
async function sha256File(absPath) {
|
||||
const buf = await fs.readFile(absPath);
|
||||
return createHash('sha256').update(buf).digest('hex');
|
||||
}
|
||||
|
||||
function stateDir(gameDir) {
|
||||
return path.join(gameDir, '.fractured');
|
||||
}
|
||||
|
||||
function statePath(gameDir) {
|
||||
return path.join(stateDir(gameDir), 'patch-state.json');
|
||||
}
|
||||
|
||||
async function readPatchState(gameDir) {
|
||||
if (!gameDir) return null;
|
||||
try {
|
||||
const t = await fs.readFile(statePath(gameDir), 'utf8');
|
||||
return JSON.parse(t);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function writePatchState(gameDir, manifestVersion, fileShas) {
|
||||
const p = statePath(gameDir);
|
||||
await fs.mkdir(path.dirname(p), { recursive: true });
|
||||
const body = {
|
||||
client_build: manifestVersion,
|
||||
updated_at: new Date().toISOString(),
|
||||
files: fileShas,
|
||||
};
|
||||
const tmp = p + '.tmp';
|
||||
await fs.writeFile(tmp, JSON.stringify(body, null, 2), 'utf8');
|
||||
await fs.rename(tmp, p);
|
||||
}
|
||||
|
||||
function validateManifest(m) {
|
||||
if (!m || m.version == null || String(m.version).trim() === '') return false;
|
||||
if (!m.files || typeof m.files !== 'object') return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download and parse patch-manifest.json (or custom name). Returns null on any failure.
|
||||
*/
|
||||
async function loadManifest(cfg) {
|
||||
const pm = cfg.patch_manifest;
|
||||
if (!pm || !pm.enabled || !String(pm.source || '').trim()) return null;
|
||||
const tmp = path.join(os.tmpdir(), `fr-patch-manifest-${Date.now()}-${Math.random().toString(36).slice(2)}.json`);
|
||||
try {
|
||||
if (pm.from_release) {
|
||||
await downloadReleaseAsset(cfg, String(pm.source).trim(), tmp);
|
||||
} else {
|
||||
await downloadGitHubRepoFile(cfg, String(pm.source).trim(), tmp);
|
||||
}
|
||||
const raw = await fs.readFile(tmp, 'utf8');
|
||||
await fs.unlink(tmp).catch(() => {});
|
||||
return JSON.parse(raw);
|
||||
} catch {
|
||||
await fs.unlink(tmp).catch(() => {});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* True if every from_release file on disk matches manifest sha256.
|
||||
*/
|
||||
async function patchesMatchManifest(cfg, manifest, onStatus) {
|
||||
if (!validateManifest(manifest)) return false;
|
||||
const gameDir = cfg.game_dir;
|
||||
for (const entry of cfg.files || []) {
|
||||
if (!entry.from_release) continue;
|
||||
const spec = manifest.files[entry.source];
|
||||
if (!spec || !spec.sha256) return false;
|
||||
const parts = String(entry.dest).replace(/\\/g, '/').split('/').filter(Boolean);
|
||||
const destAbs = path.join(gameDir, ...parts);
|
||||
let disk;
|
||||
try {
|
||||
disk = await sha256File(destAbs);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
if (disk.toLowerCase() !== String(spec.sha256).trim().toLowerCase()) return false;
|
||||
}
|
||||
if (onStatus) {
|
||||
onStatus(`Client files already match build ${manifest.version} (nothing to download).`);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function verifyInstalledAgainstManifest(cfg, manifest) {
|
||||
if (!validateManifest(manifest)) return;
|
||||
for (const entry of cfg.files || []) {
|
||||
if (!entry.from_release) continue;
|
||||
const spec = manifest.files[entry.source];
|
||||
if (!spec || !spec.sha256) {
|
||||
throw new Error(
|
||||
`patch-manifest.json is missing a sha256 for "${entry.source}" — regenerate the manifest for this release.`
|
||||
);
|
||||
}
|
||||
const parts = String(entry.dest).replace(/\\/g, '/').split('/').filter(Boolean);
|
||||
const destAbs = path.join(cfg.game_dir, ...parts);
|
||||
const disk = await sha256File(destAbs);
|
||||
if (disk.toLowerCase() !== String(spec.sha256).trim().toLowerCase()) {
|
||||
throw new Error(
|
||||
`${entry.source}: checksum mismatch after install (expected ${spec.sha256.slice(0, 12)}…, got ${disk.slice(0, 12)}…). Try syncing again.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function recordPatchState(cfg, manifest) {
|
||||
if (!validateManifest(manifest)) return;
|
||||
const shas = {};
|
||||
for (const entry of cfg.files || []) {
|
||||
if (!entry.from_release) continue;
|
||||
const parts = String(entry.dest).replace(/\\/g, '/').split('/').filter(Boolean);
|
||||
const destAbs = path.join(cfg.game_dir, ...parts);
|
||||
try {
|
||||
shas[entry.source] = await sha256File(destAbs);
|
||||
} catch {
|
||||
/* skip */
|
||||
}
|
||||
}
|
||||
await writePatchState(cfg.game_dir, String(manifest.version), shas);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
loadManifest,
|
||||
validateManifest,
|
||||
patchesMatchManifest,
|
||||
verifyInstalledAgainstManifest,
|
||||
recordPatchState,
|
||||
readPatchState,
|
||||
statePath,
|
||||
};
|
||||
@@ -35,6 +35,8 @@
|
||||
"renderer.js",
|
||||
"styles.css",
|
||||
"default-launcher.json",
|
||||
"lib/gitea-release.js",
|
||||
"lib/patch-manifest.js",
|
||||
"lib/**/*"
|
||||
],
|
||||
"win": {
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Build patch-manifest.json for a release (same names as files[].source in launcher.json).
|
||||
*
|
||||
* Usage (from a folder containing the patch binaries):
|
||||
* node generate-patch-manifest.js v0.9.0-client patch-Z.MPQ Wow-patched.exe
|
||||
*
|
||||
* Prints JSON to stdout — redirect to file:
|
||||
* node generate-patch-manifest.js v0.9.0-client patch-Z.MPQ Wow-patched.exe > patch-manifest.json
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const version = process.argv[2];
|
||||
const names = process.argv.slice(3);
|
||||
if (!version || names.length === 0) {
|
||||
console.error('Usage: generate-patch-manifest.js <version-label> <file1> [file2 ...]');
|
||||
console.error(' Example: generate-patch-manifest.js v0.9.0-client patch-Z.MPQ Wow-patched.exe');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const out = { version, files: {} };
|
||||
for (const f of names) {
|
||||
const base = path.basename(f);
|
||||
const buf = fs.readFileSync(f);
|
||||
const sha256 = crypto.createHash('sha256').update(buf).digest('hex');
|
||||
out.files[base] = { sha256 };
|
||||
}
|
||||
process.stdout.write(`${JSON.stringify(out, null, 2)}\n`);
|
||||
Reference in New Issue
Block a user