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:
@@ -1,12 +1,14 @@
|
||||
const { spawn } = require("child_process");
|
||||
const PathResolver = require("./PathResolver.js");
|
||||
|
||||
const STR_CAMERA_MARKER = "CAMERAS_JSON:";
|
||||
const STR_SCENE_MARKER = "SCENE_JSON:";
|
||||
|
||||
const STR_PYTHON_EXPR = [
|
||||
"import bpy, json, sys;",
|
||||
"s=bpy.context.scene;",
|
||||
"cams=[o.name for o in bpy.data.objects if o.type=='CAMERA'];",
|
||||
"sys.stdout.write('CAMERAS_JSON:' + json.dumps(cams) + '\\n');",
|
||||
"info={'list_cameras':cams,'obj_scene':{'nb_resolution_x':s.render.resolution_x,'nb_resolution_y':s.render.resolution_y,'nb_frame_start':s.frame_start,'nb_frame_end':s.frame_end,'nb_frame_step':s.frame_step}};",
|
||||
"sys.stdout.write('SCENE_JSON:' + json.dumps(info) + '\\n');",
|
||||
"sys.stdout.flush()",
|
||||
].join("");
|
||||
|
||||
@@ -35,14 +37,14 @@ const CameraParser = {
|
||||
return;
|
||||
}
|
||||
|
||||
let list_cameras = CameraParser._parse_camera_output(str_stdout);
|
||||
if (list_cameras === null) {
|
||||
let obj_result = CameraParser._parse_scene_output(str_stdout);
|
||||
if (obj_result === null) {
|
||||
console.error("[CameraParser] Stdout Blender:\n" + str_stdout.substring(str_stdout.length - 2000));
|
||||
reject(new Error("Impossible de parser les cameras. Verifiez que le fichier .blend contient des cameras."));
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(list_cameras);
|
||||
resolve(obj_result);
|
||||
});
|
||||
|
||||
obj_process.on("error", (obj_err) => {
|
||||
@@ -51,12 +53,12 @@ const CameraParser = {
|
||||
});
|
||||
},
|
||||
|
||||
_parse_camera_output: (str_stdout) => {
|
||||
_parse_scene_output: (str_stdout) => {
|
||||
let list_lines = str_stdout.split("\n");
|
||||
for (let str_line of list_lines) {
|
||||
let nb_index = str_line.indexOf(STR_CAMERA_MARKER);
|
||||
let nb_index = str_line.indexOf(STR_SCENE_MARKER);
|
||||
if (nb_index !== -1) {
|
||||
let str_json = str_line.substring(nb_index + STR_CAMERA_MARKER.length).trim();
|
||||
let str_json = str_line.substring(nb_index + STR_SCENE_MARKER.length).trim();
|
||||
try {
|
||||
return JSON.parse(str_json);
|
||||
} catch (obj_err) {
|
||||
|
||||
@@ -66,6 +66,8 @@ class QueueManager {
|
||||
let str_mode = obj_config.str_render_mode;
|
||||
let str_base_output = obj_config.str_output_path;
|
||||
let str_output_mode = obj_config.str_output_mode || "subfolder";
|
||||
let str_frame_prefix = obj_config.str_frame_prefix !== undefined ? obj_config.str_frame_prefix : "f_";
|
||||
let nb_frame_padding = obj_config.nb_frame_padding || 5;
|
||||
let list_cameras = obj_config.list_cameras;
|
||||
|
||||
let list_enabled = [];
|
||||
@@ -77,8 +79,9 @@ class QueueManager {
|
||||
|
||||
if (str_mode === "camera_by_camera") {
|
||||
for (let obj_cam of list_enabled) {
|
||||
for (let nb_frame = obj_cam.nb_frame_start; nb_frame <= obj_cam.nb_frame_end; nb_frame++) {
|
||||
list_queue.push(this._create_queue_item(str_blend_path, str_base_output, str_output_mode, obj_cam, nb_frame));
|
||||
let nb_step = obj_cam.nb_frame_step || 1;
|
||||
for (let nb_frame = obj_cam.nb_frame_start; nb_frame <= obj_cam.nb_frame_end; nb_frame += nb_step) {
|
||||
list_queue.push(this._create_queue_item(str_blend_path, str_base_output, str_output_mode, str_frame_prefix, nb_frame_padding, obj_cam, nb_frame));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -95,8 +98,9 @@ class QueueManager {
|
||||
|
||||
for (let nb_frame = nb_min_frame; nb_frame <= nb_max_frame; nb_frame++) {
|
||||
for (let obj_cam of list_enabled) {
|
||||
if (nb_frame >= obj_cam.nb_frame_start && nb_frame <= obj_cam.nb_frame_end) {
|
||||
list_queue.push(this._create_queue_item(str_blend_path, str_base_output, str_output_mode, obj_cam, nb_frame));
|
||||
let nb_cam_step = obj_cam.nb_frame_step || 1;
|
||||
if (nb_frame >= obj_cam.nb_frame_start && nb_frame <= obj_cam.nb_frame_end && (nb_frame - obj_cam.nb_frame_start) % nb_cam_step === 0) {
|
||||
list_queue.push(this._create_queue_item(str_blend_path, str_base_output, str_output_mode, str_frame_prefix, nb_frame_padding, obj_cam, nb_frame));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -105,8 +109,9 @@ class QueueManager {
|
||||
return list_queue;
|
||||
}
|
||||
|
||||
_create_queue_item(str_blend_path, str_base_output, str_output_mode, obj_cam, nb_frame) {
|
||||
let str_padded_frame = String(nb_frame).padStart(5, "0");
|
||||
_create_queue_item(str_blend_path, str_base_output, str_output_mode, str_frame_prefix, nb_frame_padding, obj_cam, nb_frame) {
|
||||
let str_padded_frame = String(nb_frame).padStart(nb_frame_padding, "0");
|
||||
let str_hash_pattern = "#".repeat(nb_frame_padding);
|
||||
let str_ext = obj_cam.str_format.toLowerCase();
|
||||
if (str_ext === "open_exr") {
|
||||
str_ext = "exr";
|
||||
@@ -120,14 +125,16 @@ class QueueManager {
|
||||
let str_expected_file = "";
|
||||
|
||||
if (str_output_mode === "prefix") {
|
||||
// Flat: /sortie/Camera.001_frame_#####
|
||||
str_output_path = path.join(str_base_output, obj_cam.str_name + "_frame_#####");
|
||||
str_expected_file = path.join(str_base_output, obj_cam.str_name + "_frame_" + str_padded_frame + "." + str_ext);
|
||||
} else {
|
||||
// Subfolder: /sortie/Camera.001/frame_#####
|
||||
str_output_path = path.join(str_base_output, obj_cam.str_name + "_" + str_frame_prefix + str_hash_pattern);
|
||||
str_expected_file = path.join(str_base_output, obj_cam.str_name + "_" + str_frame_prefix + str_padded_frame + "." + str_ext);
|
||||
} else if (str_output_mode === "both") {
|
||||
let str_output_dir = path.join(str_base_output, obj_cam.str_name);
|
||||
str_output_path = path.join(str_output_dir, "frame_#####");
|
||||
str_expected_file = path.join(str_output_dir, "frame_" + str_padded_frame + "." + str_ext);
|
||||
str_output_path = path.join(str_output_dir, obj_cam.str_name + "_" + str_frame_prefix + str_hash_pattern);
|
||||
str_expected_file = path.join(str_output_dir, obj_cam.str_name + "_" + str_frame_prefix + str_padded_frame + "." + str_ext);
|
||||
} else {
|
||||
let str_output_dir = path.join(str_base_output, obj_cam.str_name);
|
||||
str_output_path = path.join(str_output_dir, str_frame_prefix + str_hash_pattern);
|
||||
str_expected_file = path.join(str_output_dir, str_frame_prefix + str_padded_frame + "." + str_ext);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
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