const https = require("https"); const http = require("http"); const fs = require("fs"); const path = require("path"); const { app } = require("electron"); const { execFile } = require("child_process"); const GITEA_HOST = "git.sorlinv.fr"; const REPO_PATH = "/api/v1/repos/sorlinv/multi_render_blender/tags"; const ARCHIVE_URL_BASE = "https://git.sorlinv.fr/sorlinv/multi_render_blender/archive/"; const NB_TIMEOUT_API = 10000; const NB_TIMEOUT_DOWNLOAD = 60000; const LIST_REQUIRED_FILES = ["main.js", "version.json"]; const LIST_TARGETS = ["main.js", "preload.js", "src", "version.json", "package.json"]; const UpdateManager = { obj_window: null, str_local_version: null, is_updating: false, init: (obj_window) => { UpdateManager.obj_window = obj_window; UpdateManager.str_local_version = UpdateManager._read_local_version(); }, check_for_updates: () => { if (!UpdateManager.str_local_version) { return Promise.resolve(null); } return UpdateManager._fetch_tags() .then((list_tags) => { if (!list_tags || list_tags.length === 0) { return null; } let str_latest = null; let str_latest_tag = null; for (let obj_tag of list_tags) { let str_name = obj_tag.name || ""; let str_ver = str_name.replace(/^v/, ""); if (!UpdateManager._is_valid_semver(str_ver)) { continue; } if (!str_latest || UpdateManager._compare_versions(str_ver, str_latest) > 0) { str_latest = str_ver; str_latest_tag = str_name; } } if (!str_latest) { return null; } if (UpdateManager._compare_versions(str_latest, UpdateManager.str_local_version) > 0) { UpdateManager._send_event("update-available", { str_version: str_latest, str_tag_name: str_latest_tag, }); return { str_version: str_latest, str_tag_name: str_latest_tag }; } return null; }) .catch((obj_err) => { console.error("UpdateManager: check_for_updates echoue :", obj_err.message); return null; }); }, download_and_apply: (str_tag_name) => { if (UpdateManager.is_updating) { return Promise.reject(new Error("Mise a jour deja en cours")); } UpdateManager.is_updating = true; let str_temp_dir = path.join(app.getPath("temp"), "mrb_update_" + Date.now()); let str_zip_path = path.join(str_temp_dir, "update.zip"); UpdateManager._send_event("update-progress", { str_step: "downloading", nb_percent: 0, }); return UpdateManager._ensure_dir(str_temp_dir) .then(() => { return UpdateManager._download_zip(str_tag_name, str_zip_path); }) .then(() => { UpdateManager._send_event("update-progress", { str_step: "extracting", nb_percent: 0, }); return UpdateManager._extract_zip(str_zip_path, str_temp_dir); }) .then(() => { UpdateManager._send_event("update-progress", { str_step: "installing", nb_percent: 0, }); return UpdateManager._find_extracted_root(str_temp_dir); }) .then((str_extracted_root) => { return UpdateManager._replace_app_files(str_extracted_root); }) .then(() => { UpdateManager._send_event("update-progress", { str_step: "restarting", nb_percent: 100, }); UpdateManager._cleanup(str_temp_dir); UpdateManager.is_updating = false; app.relaunch(); app.quit(); }) .catch((obj_err) => { UpdateManager.is_updating = false; UpdateManager._cleanup(str_temp_dir); console.error("UpdateManager: download_and_apply echoue :", obj_err.message); UpdateManager._send_event("update-error", { str_message: obj_err.message || "Erreur inconnue", str_tag_name: str_tag_name, }); throw obj_err; }); }, // ── Private helpers ────────────────────────────────────── _read_local_version: () => { let str_version_path = path.join(__dirname, "..", "..", "version.json"); try { let str_content = fs.readFileSync(str_version_path, "utf8"); let obj_data = JSON.parse(str_content); return obj_data.str_version || null; } catch (obj_err) { console.error("UpdateManager: impossible de lire version.json :", obj_err.message); return null; } }, _is_valid_semver: (str_ver) => { return /^\d+\.\d+\.\d+$/.test(str_ver); }, _compare_versions: (str_a, str_b) => { let list_a = str_a.split(".").map(Number); let list_b = str_b.split(".").map(Number); for (let i = 0; i < 3; i++) { if (list_a[i] > list_b[i]) { return 1; } if (list_a[i] < list_b[i]) { return -1; } } return 0; }, _fetch_tags: () => { return new Promise((resolve, reject) => { let obj_options = { hostname: GITEA_HOST, path: REPO_PATH, method: "GET", timeout: NB_TIMEOUT_API, headers: { "Accept": "application/json", "User-Agent": "MultiRenderBlender", }, }; let obj_req = https.request(obj_options, (obj_res) => { let str_data = ""; obj_res.on("data", (chunk) => { str_data += chunk; }); obj_res.on("end", () => { if (obj_res.statusCode !== 200) { reject(new Error("Gitea API status " + obj_res.statusCode)); return; } try { let list_tags = JSON.parse(str_data); resolve(list_tags); } catch (obj_err) { reject(new Error("Reponse JSON invalide")); } }); }); obj_req.on("timeout", () => { obj_req.destroy(); reject(new Error("Timeout")); }); obj_req.on("error", (obj_err) => { reject(obj_err); }); obj_req.end(); }); }, _download_zip: (str_tag_name, str_zip_path) => { let str_url = ARCHIVE_URL_BASE + str_tag_name + ".zip"; let fn_download = (str_download_url, nb_redirects) => { if (nb_redirects > 5) { return Promise.reject(new Error("Trop de redirections")); } return new Promise((resolve, reject) => { let obj_parsed = new URL(str_download_url); let obj_module = obj_parsed.protocol === "https:" ? https : http; let obj_req = obj_module.get(str_download_url, { timeout: NB_TIMEOUT_DOWNLOAD, headers: { "User-Agent": "MultiRenderBlender" }, }, (obj_res) => { if (obj_res.statusCode >= 300 && obj_res.statusCode < 400) { obj_res.resume(); let str_redirect = obj_res.headers.location; if (!str_redirect) { reject(new Error("Redirection sans header Location")); return; } fn_download(str_redirect, nb_redirects + 1) .then(resolve) .catch(reject); return; } if (obj_res.statusCode !== 200) { obj_res.resume(); reject(new Error("Telechargement echoue : HTTP " + obj_res.statusCode)); return; } let nb_total = parseInt(obj_res.headers["content-length"], 10) || 0; let nb_downloaded = 0; let obj_file = fs.createWriteStream(str_zip_path); obj_res.on("data", (chunk) => { nb_downloaded += chunk.length; if (nb_total > 0) { let nb_percent = Math.round((nb_downloaded / nb_total) * 100); UpdateManager._send_event("update-progress", { str_step: "downloading", nb_percent: nb_percent, }); } }); obj_res.pipe(obj_file); obj_file.on("finish", () => { obj_file.close(() => { resolve(); }); }); obj_file.on("error", (obj_err) => { fs.unlink(str_zip_path, () => {}); reject(obj_err); }); }); obj_req.on("timeout", () => { obj_req.destroy(); reject(new Error("Timeout telechargement")); }); obj_req.on("error", (obj_err) => { reject(obj_err); }); }); }; return fn_download(str_url, 0); }, _extract_zip: (str_zip_path, str_dest_dir) => { return new Promise((resolve, reject) => { if (process.platform === "win32") { let str_script = "Expand-Archive -Path '" + str_zip_path + "' -DestinationPath '" + str_dest_dir + "' -Force"; execFile("powershell.exe", ["-NoProfile", "-Command", str_script], (obj_err) => { if (obj_err) { reject(new Error("Extraction echouee : " + obj_err.message)); return; } resolve(); }); } else { execFile("unzip", ["-o", str_zip_path, "-d", str_dest_dir], (obj_err) => { if (obj_err) { reject(new Error("Extraction echouee : " + obj_err.message)); return; } resolve(); }); } }); }, _find_extracted_root: (str_temp_dir) => { return new Promise((resolve, reject) => { let list_entries = fs.readdirSync(str_temp_dir); let str_root = null; for (let str_entry of list_entries) { if (str_entry === "update.zip") { continue; } let str_full = path.join(str_temp_dir, str_entry); if (!fs.statSync(str_full).isDirectory()) { continue; } let str_version_check = path.join(str_full, "version.json"); if (fs.existsSync(str_version_check)) { str_root = str_full; break; } let list_sub = fs.readdirSync(str_full); for (let str_sub of list_sub) { let str_sub_full = path.join(str_full, str_sub); if (fs.statSync(str_sub_full).isDirectory()) { let str_nested_check = path.join(str_sub_full, "version.json"); if (fs.existsSync(str_nested_check)) { str_root = str_sub_full; break; } } } if (str_root) { break; } } if (!str_root) { reject(new Error("Dossier extrait introuvable (version.json absent)")); return; } resolve(str_root); }); }, _replace_app_files: (str_source_dir) => { let str_app_dir = path.join(__dirname, "..", ".."); return new Promise((resolve, reject) => { try { for (let str_required of LIST_REQUIRED_FILES) { let str_check = path.join(str_source_dir, str_required); if (!fs.existsSync(str_check)) { reject(new Error("Fichier requis manquant dans la mise a jour : " + str_required)); return; } } for (let str_target of LIST_TARGETS) { let str_src = path.join(str_source_dir, str_target); let str_dest = path.join(str_app_dir, str_target); if (!fs.existsSync(str_src)) { console.log("UpdateManager: cible absente dans la source, ignoree : " + str_target); continue; } if (fs.existsSync(str_dest)) { let obj_stat = fs.statSync(str_dest); if (obj_stat.isDirectory()) { UpdateManager._rm_recursive(str_dest); } else { fs.unlinkSync(str_dest); } } UpdateManager._copy_recursive(str_src, str_dest); } resolve(); } catch (obj_err) { reject(new Error("Remplacement fichiers echoue : " + obj_err.message)); } }); }, _copy_recursive: (str_src, str_dest) => { let obj_stat = fs.statSync(str_src); if (obj_stat.isDirectory()) { if (!fs.existsSync(str_dest)) { fs.mkdirSync(str_dest, { recursive: true }); } let list_entries = fs.readdirSync(str_src); for (let str_entry of list_entries) { UpdateManager._copy_recursive( path.join(str_src, str_entry), path.join(str_dest, str_entry) ); } } else { fs.copyFileSync(str_src, str_dest); } }, _rm_recursive: (str_path) => { fs.rmSync(str_path, { recursive: true, force: true }); }, _ensure_dir: (str_dir) => { return new Promise((resolve, reject) => { try { fs.mkdirSync(str_dir, { recursive: true }); resolve(); } catch (obj_err) { reject(obj_err); } }); }, _cleanup: (str_dir) => { try { if (fs.existsSync(str_dir)) { UpdateManager._rm_recursive(str_dir); } } catch (obj_err) { console.error("UpdateManager: nettoyage echoue :", obj_err.message); } }, _send_event: (str_channel, obj_data) => { if (UpdateManager.obj_window && !UpdateManager.obj_window.isDestroyed()) { UpdateManager.obj_window.webContents.send(str_channel, obj_data); } }, }; module.exports = UpdateManager;