@@ -3,15 +3,11 @@ import {join} from 'node:path'
33import fs from 'node:fs'
44import * as unzipper from 'unzipper'
55import http from 'node:http'
6- import currentVer from './workers/update.json'
76import { inspect } from "node:util"
8- import { app as electronApp } from 'electron'
9- // 定义 update.json 的数据结构
10- interface UpdateInfo {
11- ver : string
12- filename : string
13- }
7+ import os from 'node:os'
8+
149const MAX_REDIRECTS = 5 // 防止无限重定向
10+ const tempUpdatePath = join ( os . tmpdir ( ) , `conet-update-${ Date . now ( ) } ` )
1511/**
1612 * 辅助函数:下载文件并流式解压到指定路径
1713 * @param downloadUrl 文件的URL
@@ -98,10 +94,103 @@ const getRandomNode = (nodes: nodes_info[]): nodes_info => {
9894 return nodes [ randomIndex ]
9995}
10096
97+ /**
98+ * 验证解压后的文件夹内容是否符合预期的文件结构。
99+ * @param folderPath 要检查的文件夹根路径
100+ * @returns 如果验证通过则返回 true,否则返回 false
101+ */
102+ const validateUpdateContents = async ( folderPath : string , ver : string ) : Promise < boolean > => {
103+ try {
104+ // 1. 检查根目录下的 index.html
105+ const hasIndexHtml = fs . existsSync ( join ( folderPath , 'index.html' ) )
106+ if ( ! hasIndexHtml ) {
107+ logger ( '🔴 验证失败: 未找到 index.html' )
108+ return false ;
109+ }
110+
111+ // 2. 检查 static/js 目录下的 main.*.js
112+ const jsFolderPath = join ( folderPath , 'static' , 'js' )
113+ if ( ! fs . existsSync ( jsFolderPath ) ) {
114+ logger ( '🔴 验证失败: 未找到目录 static/js' )
115+ return false ;
116+ }
117+ const jsFiles = fs . readdirSync ( jsFolderPath ) ;
118+ const hasMainJs = jsFiles . some ( file => / ^ m a i n \. .+ \. j s $ / . test ( file ) )
119+ if ( ! hasMainJs ) {
120+ logger ( '🔴 验证失败: 在 static/js/ 中未找到 main.*.js 文件' )
121+ return false ;
122+ }
123+
124+ // 3. 检查 static/css 目录下的 main.*.css
125+ const cssFolderPath = join ( folderPath , 'static' , 'css' )
126+ if ( ! fs . existsSync ( cssFolderPath ) ) {
127+ logger ( '🔴 验证失败: 未找到目录 static/css' )
128+ return false ;
129+ }
130+ const cssFiles = fs . readdirSync ( cssFolderPath )
131+ const hasMainCss = cssFiles . some ( file => / ^ m a i n \. .+ \. c s s $ / . test ( file ) )
132+ if ( ! hasMainCss ) {
133+ logger ( '🔴 验证失败: 在 static/css/ 中未找到 main.*.css 文件' )
134+ return false
135+ }
136+
137+ await readUpdateInfo ( folderPath , ver )
138+
139+ logger ( '✅ 更新内容验证通过,文件结构正确!' )
140+ return true
141+ } catch ( error ) {
142+ logger ( '🔴 验证过程中发生错误:' , error )
143+ return false
144+ }
145+ }
146+
147+ /**
148+ * Reads and parses the update.json file from a specified folder.
149+ *
150+ * @param staticFolder The path to the directory containing update.json.
151+ * @returns A Promise that resolves to the UpdateInfo object, or null if an error occurs.
152+ */
153+ export const readUpdateInfo = async ( staticFolder : string , ver : string ) : Promise < UpdateInfo | null > => new Promise ( executor => {
154+ // Construct the full path to the update.json file
155+ const filePath = join ( staticFolder , 'update.json' )
156+
157+
158+ // Read the file's content asynchronously
159+ fs . readFile ( filePath , 'utf8' , ( err , fileContent ) => {
160+ // Parse the JSON string into an object and cast it to the UpdateInfo type
161+ if ( err ) {
162+ logger ( `readUpdateInfo get ${ filePath } Error!` )
163+
164+ if ( ! ver ) {
165+ return executor ( null )
166+ }
167+
168+ const data : UpdateInfo = {
169+ ver,
170+ filename : `${ ver } .zip`
171+ }
172+ fs . writeFileSync ( filePath , JSON . stringify ( data ) , 'utf8' )
173+ return executor ( data )
174+ }
175+
176+ try {
177+ const updateData : UpdateInfo = JSON . parse ( fileContent )
178+ logger ( `readUpdateInfo success` , inspect ( updateData , false , 3 , true ) )
179+ return executor ( updateData )
180+ } catch ( ex ) {
181+ logger ( `readUpdateInfo JSON.parse(fileContent) ${ fileContent } Error!` )
182+ return executor ( null )
183+ }
184+
185+ } )
186+
187+ } )
188+
189+
101190/**
102191 * 主更新函数
103192 */
104- export const runUpdater = async ( nodes : nodes_info [ ] ) => {
193+ export const runUpdater = async ( nodes : nodes_info [ ] , currentVer : UpdateInfo , reactFolder : string ) => {
105194
106195
107196 logger ( '🚀 开始执行动态节点更新程序...' )
@@ -138,24 +227,61 @@ export const runUpdater = async (nodes: nodes_info[] ) => {
138227 const downloadUrl = `${ baseApiUrl } ${ updateInfo . filename } `
139228 // 关键改动 2: 不再使用 __dirname,而是使用 userData 目录
140229 // 这确保了我们将文件解压到 Electron 应用的可写区域
141- const userDataPath = electronApp . getPath ( 'userData' ) ;
142- const extractPath = join ( userDataPath , 'workers' ) ;
230+ const userDataPath = reactFolder
231+ const extractPath = join ( userDataPath , 'workers' )
232+ // 定义最终的工作目录和临时的更新目录
233+ const finalWorkersPath = join ( reactFolder , 'workers' ) ;
234+ // 1. 解压到临时目录
235+ fs . mkdirSync ( tempUpdatePath , { recursive : true } )
143236
144- logger ( `⏳ 正在从 ${ downloadUrl } 下载并解压...` ) ;
145- logger ( `将解压到可写目录: ${ extractPath } ` ) ; // 日志信息更新,更准确
146237
238+ logger ( `⏳ 正在从 ${ downloadUrl } 下载并解压...` ) ;
239+
240+ logger ( `创建临时更新目录: ${ tempUpdatePath } ` )
147241 // 确保目标目录存在
148242 // 这个逻辑在这里依然有效,它会创建 userData/workers 目录(如果尚不存在)
149243 if ( ! fs . existsSync ( extractPath ) ) {
150- fs . mkdirSync ( extractPath , { recursive : true } ) ;
244+ fs . mkdirSync ( extractPath , { recursive : true } )
151245 }
152246
153- await downloadAndUnzip ( downloadUrl , extractPath )
154- logger ( `🎉 成功下载并解压文件到 ${ extractPath } ` )
247+ await downloadAndUnzip ( downloadUrl , tempUpdatePath )
248+ logger ( `🎉 成功下载并解压文件到 ${ tempUpdatePath } ` )
249+
250+ // 2. 验证内容
251+ if ( ! ( await validateUpdateContents ( tempUpdatePath , currentVer . ver ) ) ) {
252+ throw new Error ( '下载的内容无效或不完整,已终止更新。' )
253+ }
254+
255+
256+ // 3. 原子化替换
257+ logger ( '准备替换工作目录...' )
258+ const backupPath = `${ finalWorkersPath } _old_${ Date . now ( ) } `
259+
260+ // a. 如果旧目录存在,将其重命名为备份(这是一个原子操作)
261+ if ( fs . existsSync ( finalWorkersPath ) ) {
262+ fs . renameSync ( finalWorkersPath , backupPath )
263+ logger ( `旧目录已备份到: ${ backupPath } ` )
264+ }
265+
266+ // b. 将新的、已验证的临时目录重命名为工作目录(这是另一个原子操作)
267+ fs . renameSync ( tempUpdatePath , finalWorkersPath ) ;
268+ logger ( `✅ 更新成功!工作目录已指向新版本: ${ finalWorkersPath } ` )
269+
270+ // c. 如果存在备份,则删除备份
271+ if ( fs . existsSync ( backupPath ) ) {
272+ fs . rmSync ( backupPath , { recursive : true , force : true } )
273+ logger ( `旧的备份目录已清理。` )
274+ }
155275
156276 } catch ( error ) {
157277 console . error ( '❌ 更新过程中发生错误:' , error instanceof Error ? error . message : error )
158- }
278+ } finally {
279+ // 4. 清理:无论成功或失败,最后都尝试删除临时目录
280+ if ( fs . existsSync ( tempUpdatePath ) ) {
281+ fs . rmSync ( tempUpdatePath , { recursive : true , force : true } )
282+ logger ( `临时更新目录已清理。` )
283+ }
284+ }
159285}
160286// 执行更新程序
161287
@@ -166,18 +292,18 @@ export const runUpdater = async (nodes: nodes_info[] ) => {
166292 * @returns 如果 newVer 比 oldVer 新,则返回 true;否则返回 false。
167293 */
168294function isNewerVersion ( oldVer : string , newVer : string ) : boolean {
169- const oldParts = oldVer . split ( '.' ) . map ( Number ) ;
170- const newParts = newVer . split ( '.' ) . map ( Number ) ;
295+ const oldParts = oldVer . split ( '.' ) . map ( Number )
296+ const newParts = newVer . split ( '.' ) . map ( Number )
171297
172298 for ( let i = 0 ; i < oldParts . length ; i ++ ) {
173299 if ( newParts [ i ] > oldParts [ i ] ) {
174- return true ;
300+ return true
175301 }
176302 if ( newParts [ i ] < oldParts [ i ] ) {
177- return false ;
303+ return false
178304 }
179305 }
180306
181- return false ; // 如果版本号完全相同,则不是更新的版本
307+ return false // 如果版本号完全相同,则不是更新的版本
182308}
183309
0 commit comments