Files
go/patcher.js

187 lines
5.6 KiB
JavaScript

const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const AdmZip = require('adm-zip');
const CryptoUtils = require('./crypto-utils');
class Patcher {
constructor(appDataPath) {
this.appDataPath = appDataPath;
}
getCurrentVersion() {
const versionFile = path.join(this.appDataPath, 'version.json');
if (fs.existsSync(versionFile)) {
try {
return JSON.parse(fs.readFileSync(versionFile, 'utf8')).version || '0.0.0';
} catch (e) {
return '0.0.0';
}
}
return '0.0.0';
}
getCurrentSha() {
const versionFile = path.join(this.appDataPath, 'version.json');
if (fs.existsSync(versionFile)) {
try {
return JSON.parse(fs.readFileSync(versionFile, 'utf8')).sha256 || null;
} catch (e) {
return null;
}
}
return null;
}
bridgeCredentials(credentialManager) {
const encryptedPath = path.join(this.appDataPath, 'encrypted-credentials.json');
if (!fs.existsSync(encryptedPath)) {
return false;
}
const buildKey = CryptoUtils.deriveKey('vyla-home-build-key', 'vyla-home-payload-salt-v1');
const encryptedData = fs.readFileSync(encryptedPath, 'utf8');
const credentials = CryptoUtils.decryptObject(encryptedData, buildKey);
for (const [service, keys] of Object.entries(credentials)) {
credentialManager.setServiceCredentials(service, keys);
}
fs.unlinkSync(encryptedPath);
return true;
}
saveCurrentVersion(version, sha256 = null) {
const versionFile = path.join(this.appDataPath, 'version.json');
fs.mkdirSync(this.appDataPath, { recursive: true });
fs.writeFileSync(versionFile, JSON.stringify({ version, sha256 }, null, 2), 'utf8');
}
purgeOldCode() {
const dirs = ['frontend', 'player', 'stream-api', 'auth-proxy', 'config'];
for (const dir of dirs) {
const fullPath = path.join(this.appDataPath, dir);
if (fs.existsSync(fullPath)) {
try {
const trashPath = path.join(this.appDataPath, `.trash_${dir}_${Date.now()}`);
fs.renameSync(fullPath, trashPath);
fs.rmSync(trashPath, { recursive: true, force: true });
} catch (e) {
try {
fs.rmSync(fullPath, { recursive: true, force: true });
} catch (err2) { }
}
}
}
try {
const files = fs.readdirSync(this.appDataPath);
for (const file of files) {
if (file.startsWith('.trash_')) {
fs.rmSync(path.join(this.appDataPath, file), { recursive: true, force: true });
}
}
} catch (e) { }
}
extractZipPayload(zipPath) {
const AdmZip = require('adm-zip');
const zip = new AdmZip(zipPath);
const entries = zip.getEntries();
if (!entries || entries.length === 0) {
throw new Error('Downloaded archive is empty, invalid, or truncated.');
}
zip.extractAllTo(this.appDataPath, true);
const testPath = path.join(this.appDataPath, 'frontend', 'index.html');
if (!fs.existsSync(testPath)) {
const { execSync } = require('child_process');
if (process.platform === 'win32') {
execSync(`powershell.exe -NoProfile -NonInteractive -Command "Expand-Archive -Path '${zipPath}' -DestinationPath '${this.appDataPath}' -Force"`, { windowsHide: true });
} else {
execSync(`unzip -o "${zipPath}" -d "${this.appDataPath}"`);
}
}
}
verifyChecksum(zipPath, expectedSha256) {
if (!expectedSha256) return true;
const hash = crypto.createHash('sha256');
const data = fs.readFileSync(zipPath);
hash.update(data);
const calculated = hash.digest('hex');
return calculated === expectedSha256;
}
verifyExtraction() {
const requiredFiles = [
'frontend/index.html',
'player/server.js'
];
for (const file of requiredFiles) {
const fullPath = path.join(this.appDataPath, file);
if (!fs.existsSync(fullPath)) {
return false;
}
const stat = fs.statSync(fullPath);
if (stat.size === 0) {
return false;
}
try {
const sampleSize = Math.min(stat.size, 4096);
const fd = fs.openSync(fullPath, 'r');
const buffer = Buffer.alloc(sampleSize);
fs.readSync(fd, buffer, 0, sampleSize, 0);
fs.closeSync(fd);
if (buffer.every(byte => byte === 0)) {
return false;
}
} catch (e) {
return false;
}
}
return true;
}
processDirectory(dir, replacementList = []) {
const files = fs.readdirSync(dir);
for (const file of files) {
const fullPath = path.join(dir, file);
const stat = fs.statSync(fullPath);
if (stat.isDirectory()) {
if (file === 'node_modules' || file === '.git') continue;
this.processDirectory(fullPath, replacementList);
} else if (stat.isFile()) {
const ext = path.extname(fullPath).toLowerCase();
let content = fs.readFileSync(fullPath, 'utf8');
let modified = false;
for (const rep of replacementList) {
if (content.includes(rep.find)) {
content = content.split(rep.find).join(rep.replace);
modified = true;
}
}
if (modified) {
fs.writeFileSync(fullPath, content, 'utf8');
}
}
}
}
applyPatches(replacements) {
const targetDirs = ['frontend', 'player'];
for (const dirName of targetDirs) {
const dirPath = path.join(this.appDataPath, dirName);
if (!fs.existsSync(dirPath)) continue;
const replacementList = replacements[dirName] || [];
this.processDirectory(dirPath, replacementList);
}
}
}
module.exports = Patcher;