v1.0.0 — release avec auto-update Gitea
Ajout du systeme de mise a jour automatique : - UpdateManager (main) : verifie les tags Gitea, telecharge et applique les MAJ - UpdateBanner (renderer) : banniere UI avec progression et retry - IPC channels : check-for-updates, apply-update, update-available, update-progress, update-error - Desactivation asar pour permettre le remplacement des sources - version.json comme source de verite pour la version locale Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
410
src/main/UpdateManager.js
Normal file
410
src/main/UpdateManager.js
Normal file
@@ -0,0 +1,410 @@
|
||||
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 = 10000;
|
||||
|
||||
const UpdateManager = {
|
||||
obj_window: null,
|
||||
str_local_version: null,
|
||||
|
||||
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(() => {
|
||||
return null;
|
||||
});
|
||||
},
|
||||
|
||||
download_and_apply: (str_tag_name) => {
|
||||
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);
|
||||
app.relaunch();
|
||||
app.quit();
|
||||
})
|
||||
.catch((obj_err) => {
|
||||
UpdateManager._cleanup(str_temp_dir);
|
||||
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,
|
||||
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,
|
||||
headers: { "User-Agent": "MultiRenderBlender" },
|
||||
}, (obj_res) => {
|
||||
if (obj_res.statusCode === 301 || obj_res.statusCode === 302) {
|
||||
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) {
|
||||
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_cmd = "Expand-Archive";
|
||||
let list_args = [
|
||||
"-Path", str_zip_path,
|
||||
"-DestinationPath", str_dest_dir,
|
||||
"-Force",
|
||||
];
|
||||
execFile("powershell.exe", ["-Command", str_cmd + " " + list_args.join(" ")], (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()) {
|
||||
str_root = str_full;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!str_root) {
|
||||
reject(new Error("Dossier extrait introuvable"));
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(str_root);
|
||||
});
|
||||
},
|
||||
|
||||
_replace_app_files: (str_source_dir) => {
|
||||
let str_app_dir = path.join(__dirname, "..", "..");
|
||||
let LIST_TARGETS = ["main.js", "preload.js", "src", "version.json", "package.json"];
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
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)) {
|
||||
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;
|
||||
Reference in New Issue
Block a user