Files
multi_render_blender/src/main/UpdateManager.js
sorlinv 68815645a4 fix: auto-update corrige — tag, robustesse UpdateManager, .env securise
- Supprime .env du tracking git (token expose) + ajout .gitignore
- UpdateManager : timeout download 60s, validation root par version.json,
  erreur si fichiers requis manquants, drain response sur redirect,
  gestion redirects 3xx, protection double-clic, logging erreurs
- UpdateBanner : log erreur dans le .catch au lieu de silencieux
- release.sh : tag cree localement avant push pour eviter desync

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 10:28:55 +01:00

449 lines
16 KiB
JavaScript

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;