diff --git a/DOCUEMENTATION/FORDEV.md b/DOCUEMENTATION/FORDEV.md index 41031aa..0246897 100644 --- a/DOCUEMENTATION/FORDEV.md +++ b/DOCUEMENTATION/FORDEV.md @@ -24,14 +24,14 @@ Fixing os error 32 Stop-Process -Name "vyla-home", "node", "cargo" -Force -ErrorAction SilentlyContinue ``` -Fake a test? - -```powershell -$env:VYLA_TEST_UI="1"; npm run dev -``` - Fake an old local version: ```powershell '{"version":"0.0.1","sha256":null}' | Set-Content "$env:APPDATA\vyla-home\vyla-home-files\version.json" ``` + +Fake a test? + +```powershell +$env:VYLA_TEST_UI="1"; npm run dev +``` diff --git a/build-pipeline.js b/build-pipeline.js index f93e710..7042a28 100644 --- a/build-pipeline.js +++ b/build-pipeline.js @@ -174,7 +174,7 @@ function validatePayload(zipPath) { const requiredFiles = [ 'frontend/index.html' ]; - + const validationResults = { valid: true, errors: [], diff --git a/file-updater.js b/file-updater.js index 0dc2783..2c542dd 100644 --- a/file-updater.js +++ b/file-updater.js @@ -9,6 +9,9 @@ const SDK_NAME = '@vyla-entertainment/sdk'; const SDK_LATEST_URL = 'https://registry.npmjs.org/@vyla-entertainment%2Fsdk/latest'; const ALLOWED_ROOTS = ['frontend', 'player']; const MAX_TARBALL = 100 * 1024 * 1024; +const MAX_FILE = 50 * 1024 * 1024; +const LIVE_HOSTS = ['gitlab.com']; +const REPO_API = 'https://gitlab.com/api/v4/projects/vyla-entertainment%2F'; const verParts = (v) => String(v || '').replace(/^v/, '').split(/[.\-+]/).map((n) => parseInt(n, 10) || 0); const cmpVer = (a, b) => { @@ -22,6 +25,7 @@ const cmpVer = (a, b) => { const sha256Buf = (buf) => crypto.createHash('sha256').update(buf).digest('hex'); const sha256File = (p) => sha256Buf(fs.readFileSync(p)); +const gitBlobSha = (buf) => crypto.createHash('sha1').update(Buffer.concat([Buffer.from(`blob ${buf.length}\0`), buf])).digest('hex'); function fetchBuffer(url, { headers = {}, maxBytes, hosts, redirects = 0 }) { return new Promise((resolve, reject) => { @@ -114,7 +118,6 @@ class FileUpdater { this.appDataPath = appDataPath; this.updateClient = updateClient; this.onProgress = null; - this.pending = null; this.statePath = path.join(appDataPath, 'file-state.json'); } @@ -296,6 +299,99 @@ class FileUpdater { return changed ? Buffer.from(text, 'utf8') : buf; } + async repoTree(root) { + const out = []; + for (let page = 1; page <= 50; page++) { + const url = `${REPO_API}${root}/repository/tree?recursive=true&per_page=100&ref=main&page=${page}`; + const raw = await fetchBuffer(url, { headers: { Accept: 'application/json' }, maxBytes: 5 * 1024 * 1024, hosts: LIVE_HOSTS }); + const items = JSON.parse(raw.toString('utf8')); + if (!Array.isArray(items) || items.length === 0) break; + out.push(...items); + if (items.length < 100) break; + } + return out; + } + + fetchRepoFile(rel) { + const parts = rel.split('/'); + const root = parts.shift(); + const file = encodeURIComponent(parts.join('/')); + return fetchBuffer(`${REPO_API}${root}/repository/files/${file}/raw?ref=main`, { maxBytes: MAX_FILE, hosts: LIVE_HOSTS }); + } + + async checkRepos() { + const state = this.readState(); + const changes = []; + for (const root of ALLOWED_ROOTS) { + const tree = await this.repoTree(root); + for (const item of tree) { + if (item.type !== 'blob') continue; + const rel = safeRel(`${root}/${item.path}`); + if (!rel || !/^[0-9a-f]{40}$/.test(item.id)) continue; + const full = this.target(rel); + if (fs.existsSync(full) && (state.base[rel] === item.id || gitBlobSha(fs.readFileSync(full)) === item.id)) continue; + changes.push({ path: rel, sha256: item.id, size: 0 }); + } + } + return { seq: state.seq === null ? 0 : state.seq, changes, removed: [] }; + } + + async applyLive(info, replacements, onFile) { + const staged = []; + const total = info.changes.length; + let next = 0; + let finished = 0; + let failure = null; + const worker = async () => { + while (next < total && !failure) { + const c = info.changes[next++]; + try { + const data = await this.fetchRepoFile(c.path); + if (gitBlobSha(data) !== c.sha256) throw new Error(`Checksum mismatch: ${c.path}`); + staged.push({ c, data: this.applyReplacements(c.path, data, replacements) }); + finished++; + if (onFile) onFile(c.path, finished, total); + } catch (e) { + failure = e; + } + } + }; + await Promise.all(Array.from({ length: Math.min(8, total) }, worker)); + if (failure) throw failure; + + const done = []; + try { + for (const s of staged) { + const full = this.target(s.c.path); + const prev = fs.existsSync(full) ? fs.readFileSync(full) : null; + fs.mkdirSync(path.dirname(full), { recursive: true }); + fs.writeFileSync(`${full}.vyla-tmp`, s.data); + fs.renameSync(`${full}.vyla-tmp`, full); + done.push({ full, prev }); + } + for (const rel of info.removed) { + const full = this.target(rel); + if (!fs.existsSync(full)) continue; + const prev = fs.readFileSync(full); + fs.rmSync(full, { force: true }); + done.push({ full, prev }); + } + } catch (e) { + for (const d of done.reverse()) { + try { + if (d.prev === null) fs.rmSync(d.full, { force: true }); + else fs.writeFileSync(d.full, d.prev); + } catch (x) { } + } + throw e; + } + + const state = this.readState(); + for (const s of staged) state.base[s.c.path] = s.c.sha256; + for (const rel of info.removed) delete state.base[rel]; + this.writeState({ seq: info.seq, base: state.base }); + } + installedSdk() { const roots = [this.appDataPath, path.join(this.appDataPath, 'frontend'), path.join(this.appDataPath, 'player')]; const found = []; diff --git a/main.js b/main.js index 2493d97..7fa151e 100644 --- a/main.js +++ b/main.js @@ -242,7 +242,7 @@ async function start() { const versionNewer = isNewer(release.version, localVersion); const sameVerNewBuild = normalizeVer(release.version) === normalizeVer(localVersion) && !!release.sha256 && !!localSha && release.sha256 !== localSha; - const hasUpdate = versionNewer; + const hasUpdate = versionNewer || sameVerNewBuild; if (isFirstRun) { debugLog("First run with no local payload. Installing remote build..."); @@ -269,12 +269,14 @@ async function start() { patcher.bridgeCredentials(serviceManager.credentialManager); let ver = "1.0.0"; + let manSha = null; try { const man = JSON.parse(fs.readFileSync(path.join(__dirname, 'manifest.json'), 'utf8')); ver = man.version || ver; + manSha = man.sha256 || null; } catch (e) { } - patcher.saveCurrentVersion(ver); + patcher.saveCurrentVersion(ver, manSha); serviceManager.envConfig = getEnvConfig(); debugLog("Local payload extracted successfully."); } else { @@ -289,20 +291,21 @@ async function start() { sendToTauri('status', { message: 'Checking UI files and SDK...' }); const [filesCheck, sdkCheck] = await Promise.all([ - withTimeout(fileUpdater.checkFiles().catch((e) => { - debugLog(`File check failed: ${e.message}`); + withTimeout(fileUpdater.checkRepos().catch((e) => { + debugLog(`Live file check failed: ${e.message}`); return null; - }), 180000), + }), 60000), withTimeout(fileUpdater.checkSdk().catch((e) => { debugLog(`SDK check failed: ${e.message}`); return null; }), 12000) ]); + debugLog(`Repo check result: ${filesCheck ? `${filesCheck.changes.length} change(s)` : 'failed or timed out'}`); - if (filesCheck && filesCheck.changes.length > 0) { + if (filesCheck && (filesCheck.changes.length > 0 || filesCheck.removed.length > 0)) { sendToTauri('status', { message: `Updating ${filesCheck.changes.length} file(s)...` }); try { - await fileUpdater.applyFiles(filesCheck.changes, filesCheck.seq, getReplacementsConfig(), (file, done, total) => { + await fileUpdater.applyLive(filesCheck, getReplacementsConfig(), (file, done, total) => { sendToTauri('extract-progress', { file, done, total, percent: Math.round((done / total) * 100) }); }); sendToTauri('status', { message: `Updated ${filesCheck.changes.length} file(s).` }); diff --git a/package-lock.json b/package-lock.json index 4a8d8b0..10d4ebf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@tauri-apps/api": "2.12.1", "@tauri-apps/plugin-opener": "2.7.0", "@tauri-apps/plugin-process": "2.4.0", + "@tauri-apps/plugin-updater": "^2", "@tauri-apps/plugin-shell": "2.5.0", "@vyla-entertainment/sdk": "^1.20.0", "adm-zip": "^0.6.1", @@ -1535,4 +1536,4 @@ } } } -} +} \ No newline at end of file diff --git a/sign-ui.js b/sign-ui.js deleted file mode 100644 index bbbd011..0000000 --- a/sign-ui.js +++ /dev/null @@ -1,45 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const crypto = require('crypto'); - -const roots = { frontend: process.argv[2], player: process.argv[3] }; -const outDir = path.join(__dirname, 'ui-updates'); -const filesDir = path.join(outDir, 'files'); -const keyPath = process.env.VYLA_UI_KEY || path.join(__dirname, 'ui-private.pem'); - -fs.rmSync(outDir, { recursive: true, force: true }); -fs.mkdirSync(filesDir, { recursive: true }); -fs.writeFileSync(path.join(outDir, '.gitattributes'), '* -text\n'); - -const files = []; -const walk = (src, rel) => { - for (const name of fs.readdirSync(src)) { - if (name === 'node_modules' || name === '.git') continue; - const full = path.join(src, name); - const r = `${rel}/${name}`; - const st = fs.lstatSync(full); - if (st.isDirectory()) { - walk(full, r); - } else if (st.isFile()) { - const data = fs.readFileSync(full); - const dest = path.join(filesDir, ...r.split('/')); - fs.mkdirSync(path.dirname(dest), { recursive: true }); - fs.writeFileSync(dest, data); - files.push({ path: r, sha256: crypto.createHash('sha256').update(data).digest('hex'), size: data.length }); - } - } -}; - -for (const [root, src] of Object.entries(roots)) { - if (!src || !fs.existsSync(src)) { - console.error(`Missing source dir for ${root}`); - process.exit(1); - } - walk(path.resolve(src), root); -} - -const raw = Buffer.from(JSON.stringify({ seq: Math.floor(Date.now() / 1000), files })); -const sig = crypto.sign(null, raw, crypto.createPrivateKey(fs.readFileSync(keyPath))); -fs.writeFileSync(path.join(outDir, 'files-manifest.json'), raw); -fs.writeFileSync(path.join(outDir, 'files-manifest.sig'), sig.toString('base64')); -console.log(`${files.length} files signed`); \ No newline at end of file diff --git a/source.png b/source.png deleted file mode 100644 index 9fdbb82..0000000 Binary files a/source.png and /dev/null differ diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 455f5e0..f7efbb1 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -45,4 +45,4 @@ "../node_modules/**/*" ] } -} +} \ No newline at end of file