diff --git a/main.js b/main.js index 5d1a43a..160bc85 100644 --- a/main.js +++ b/main.js @@ -4,6 +4,7 @@ const fs = require("fs"); const CameraParser = require("./src/main/CameraParser.js"); const QueueManager = require("./src/main/QueueManager.js"); const ConfigManager = require("./src/main/ConfigManager.js"); +const UpdateManager = require("./src/main/UpdateManager.js"); let obj_main_window = null; let obj_queue_manager = null; @@ -25,6 +26,11 @@ const create_window = () => { obj_main_window.loadFile(path.join(__dirname, "src", "renderer", "index.html")); obj_queue_manager = new QueueManager(obj_main_window); + + UpdateManager.init(obj_main_window); + obj_main_window.webContents.on("did-finish-load", () => { + UpdateManager.check_for_updates(); + }); }; // ── App lifecycle ────────────────────────────────────────────── @@ -153,6 +159,14 @@ ipcMain.handle("read-image", (event, str_image_path) => { return fn_try_read(0); }); +ipcMain.handle("check-for-updates", () => { + return UpdateManager.check_for_updates(); +}); + +ipcMain.handle("apply-update", (event, str_tag_name) => { + return UpdateManager.download_and_apply(str_tag_name); +}); + ipcMain.handle("select-output-folder", () => { return dialog.showOpenDialog(obj_main_window, { title: "Selectionner le dossier de sortie", diff --git a/package-lock.json b/package-lock.json index 0a0c901..6c24f43 100644 --- a/package-lock.json +++ b/package-lock.json @@ -851,7 +851,6 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -1018,6 +1017,7 @@ "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "archiver-utils": "^2.1.0", "async": "^3.2.4", @@ -1037,6 +1037,7 @@ "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "glob": "^7.1.4", "graceful-fs": "^4.2.0", @@ -1059,6 +1060,7 @@ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -1074,7 +1076,8 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/archiver-utils/node_modules/string_decoder": { "version": "1.1.1", @@ -1082,6 +1085,7 @@ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "safe-buffer": "~5.1.0" } @@ -1710,6 +1714,7 @@ "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "buffer-crc32": "^0.2.13", "crc32-stream": "^4.0.2", @@ -1834,6 +1839,7 @@ "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "crc32": "bin/crc32.njs" }, @@ -1847,6 +1853,7 @@ "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "crc-32": "^1.2.0", "readable-stream": "^3.4.0" @@ -2061,7 +2068,6 @@ "integrity": "sha512-NoXo6Liy2heSklTI5OIZbCgXC1RzrDQsZkeEwXhdOro3FT1VBOvbubvscdPnjVuQ4AMwwv61oaH96AbiYg9EnQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "app-builder-lib": "25.1.8", "builder-util": "25.1.7", @@ -2257,6 +2263,7 @@ "integrity": "sha512-2ntkJ+9+0GFP6nAISiMabKt6eqBB0kX1QqHNWFWAXgi0VULKGisM46luRFpIBiU3u/TDmhZMM8tzvo2Abn3ayg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "app-builder-lib": "25.1.8", "archiver": "^5.3.1", @@ -2270,6 +2277,7 @@ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -2285,6 +2293,7 @@ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "universalify": "^2.0.0" }, @@ -2298,6 +2307,7 @@ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 10.0.0" } @@ -2675,7 +2685,8 @@ "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/fs-extra": { "version": "8.1.0", @@ -3269,7 +3280,8 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/isbinaryfile": { "version": "5.0.7", @@ -3406,6 +3418,7 @@ "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "readable-stream": "^2.0.5" }, @@ -3419,6 +3432,7 @@ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -3434,7 +3448,8 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/lazystream/node_modules/string_decoder": { "version": "1.1.1", @@ -3442,6 +3457,7 @@ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "safe-buffer": "~5.1.0" } @@ -3458,35 +3474,40 @@ "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/lodash.difference": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/lodash.flatten": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/lodash.union": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/log-symbols": { "version": "4.1.0", @@ -3959,6 +3980,7 @@ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -4206,7 +4228,8 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/progress": { "version": "2.0.3", @@ -4307,6 +4330,7 @@ "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "minimatch": "^5.1.0" } @@ -4316,7 +4340,8 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/readdir-glob/node_modules/brace-expansion": { "version": "2.0.2", @@ -4324,6 +4349,7 @@ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "balanced-match": "^1.0.0" } @@ -4334,6 +4360,7 @@ "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "brace-expansion": "^2.0.1" }, @@ -4836,6 +4863,7 @@ "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", @@ -5213,6 +5241,7 @@ "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "archiver-utils": "^3.0.4", "compress-commons": "^4.1.2", @@ -5228,6 +5257,7 @@ "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "glob": "^7.2.3", "graceful-fs": "^4.2.0", diff --git a/package.json b/package.json index 9d1bc49..70ff36a 100644 --- a/package.json +++ b/package.json @@ -16,10 +16,12 @@ "build": { "appId": "com.multirender.blender", "productName": "Multi Render Blender", + "asar": false, "files": [ "main.js", "preload.js", - "src/**/*" + "src/**/*", + "version.json" ], "extraResources": [ { diff --git a/preload.js b/preload.js index 988630f..346f5e6 100644 --- a/preload.js +++ b/preload.js @@ -66,4 +66,30 @@ contextBridge.exposeInMainWorld("api", { fn_callback(str_message); }); }, + + check_for_updates: () => { + return ipcRenderer.invoke("check-for-updates"); + }, + + apply_update: (str_tag_name) => { + return ipcRenderer.invoke("apply-update", str_tag_name); + }, + + on_update_available: (fn_callback) => { + ipcRenderer.on("update-available", (event, obj_data) => { + fn_callback(obj_data); + }); + }, + + on_update_progress: (fn_callback) => { + ipcRenderer.on("update-progress", (event, obj_data) => { + fn_callback(obj_data); + }); + }, + + on_update_error: (fn_callback) => { + ipcRenderer.on("update-error", (event, obj_data) => { + fn_callback(obj_data); + }); + }, }); diff --git a/src/main/CameraParser.js b/src/main/CameraParser.js index bc4f07c..eafc4b9 100644 --- a/src/main/CameraParser.js +++ b/src/main/CameraParser.js @@ -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) { diff --git a/src/main/QueueManager.js b/src/main/QueueManager.js index fd9167e..25c7b03 100644 --- a/src/main/QueueManager.js +++ b/src/main/QueueManager.js @@ -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 { diff --git a/src/main/UpdateManager.js b/src/main/UpdateManager.js new file mode 100644 index 0000000..31e7fc7 --- /dev/null +++ b/src/main/UpdateManager.js @@ -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; diff --git a/src/renderer/index.html b/src/renderer/index.html index eb35d68..ee6b093 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -67,17 +67,33 @@