@glassnote/client 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/autoupdate.js ADDED
@@ -0,0 +1,473 @@
1
+ const { app } = require('electron');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const https = require('https');
5
+ const crypto = require('crypto');
6
+ const { exec, spawn } = require('child_process');
7
+ const userData = require('./userData');
8
+
9
+ // El renderer se baja siempre del bucket de PRODUCCIÓN. Antes esto lo decidía un campo
10
+ // `stage` en package.json que elegía entre este bucket y `glassnotedev`: ese ambiente ya
11
+ // no existe, y el campo solo servía para publicar apuntando a un bucket muerto sin que se
12
+ // notara hasta que alguien viera un renderer viejo. Para probar contra otro origen está
13
+ // GLASSNOTE_S3_BASE_URL, que es explícito y no viaja en el paquete publicado.
14
+ const S3_BASE_URL = process.env.GLASSNOTE_S3_BASE_URL || 'https://glassnote.s3.us-east-1.amazonaws.com';
15
+
16
+ class AutoUpdate {
17
+ constructor() {
18
+ this.S3_BASE_URL = S3_BASE_URL;
19
+ this.currentVersion = app.getVersion();
20
+ this.expectedHash = null;
21
+ this.updateInterval = null;
22
+ }
23
+
24
+ /**
25
+ * Inicia el sistema de auto-actualización
26
+ */
27
+ start() {
28
+ // Primera verificación después de 5 segundos
29
+ setTimeout(() => {
30
+ this.checkForUpdates();
31
+ }, 5000);
32
+
33
+ // Verificación periódica cada hora (3600000 ms = 1 hora)
34
+ this.updateInterval = setInterval(() => {
35
+ this.checkForUpdates();
36
+ }, 60 * 60 * 3000); // 3 hora en milisegundos
37
+ }
38
+
39
+ /**
40
+ * Detiene el sistema de auto-actualización
41
+ */
42
+ stop() {
43
+ if (this.updateInterval) {
44
+ clearInterval(this.updateInterval);
45
+ this.updateInterval = null;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Verifica si hay actualizaciones disponibles
51
+ */
52
+ async checkForUpdates() {
53
+ try {
54
+ const desiredVersion = userData.get('desiredVersion') || 'lastest';
55
+
56
+ if (!desiredVersion || desiredVersion === '') {
57
+ return;
58
+ }
59
+
60
+ const versionUrl = this.getVersionUrl(desiredVersion);
61
+ const versionContent = await this.downloadTextFile(versionUrl);
62
+ if (!versionContent) {
63
+ console.error('Error downloading version file from:', versionUrl);
64
+ return;
65
+ }
66
+
67
+ // Parsear contenido del archivo de versión
68
+ const versionParts = versionContent.trim().split(' ');
69
+ const newVersion = versionParts[0];
70
+ this.expectedHash = versionParts.length > 1 ? versionParts[1].trim() : null;
71
+
72
+ // Log informativo de versiones
73
+
74
+ const isLatestDesired = desiredVersion === 'lastest';
75
+
76
+ if (isLatestDesired) {
77
+ // Para "lastest" - instalar si current < remote
78
+ const versionComparison = this.compareVersions(this.currentVersion, newVersion);
79
+ if (versionComparison < 0) {
80
+ await this.downloadInstaller(newVersion);
81
+ }
82
+ } else {
83
+ // Para versión específica - instalar si current != desired
84
+ if (this.currentVersion !== desiredVersion) {
85
+ await this.downloadInstaller(desiredVersion);
86
+ }
87
+ }
88
+
89
+ } catch (error) {
90
+ console.error('Error checking for updates:', error);
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Obtiene la URL del archivo de versión
96
+ */
97
+ getVersionUrl(desiredVersion) {
98
+ const platform = process.platform;
99
+ let platformName = 'windows';
100
+
101
+ if (platform === 'darwin') platformName = 'macos';
102
+ if (platform === 'linux') platformName = 'linux';
103
+
104
+ const versionPrefix = desiredVersion === 'lastest' ? '' : `${desiredVersion}_`;
105
+ return `${this.S3_BASE_URL}/version_${versionPrefix}${platformName}.txt`;
106
+ }
107
+
108
+ /**
109
+ * Descarga un archivo desde una URL (para archivos de texto)
110
+ */
111
+ downloadTextFile(url) {
112
+ return new Promise((resolve, reject) => {
113
+ https.get(url, (response) => {
114
+ if (response.statusCode !== 200) {
115
+ console.error(`HTTP error ${response.statusCode} downloading text file from ${url}`);
116
+ reject(new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`));
117
+ return;
118
+ }
119
+
120
+ let data = '';
121
+ response.on('data', (chunk) => {
122
+ data += chunk;
123
+ });
124
+
125
+ response.on('end', () => {
126
+ resolve(data);
127
+ });
128
+
129
+ }).on('error', (error) => {
130
+ console.error(`Error downloading text file from ${url}:`, error.message);
131
+ reject(error);
132
+ });
133
+ });
134
+ }
135
+
136
+ /**
137
+ * Descarga un archivo binario desde una URL
138
+ */
139
+ downloadBinaryFile(url) {
140
+ return new Promise((resolve, reject) => {
141
+ https.get(url, (response) => {
142
+ if (response.statusCode !== 200) {
143
+ console.error(`HTTP error ${response.statusCode} downloading binary file from ${url}`);
144
+ reject(new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`));
145
+ return;
146
+ }
147
+
148
+ const chunks = [];
149
+ let totalBytes = 0;
150
+
151
+ response.on('data', (chunk) => {
152
+ chunks.push(chunk);
153
+ totalBytes += chunk.length;
154
+ });
155
+
156
+ response.on('end', () => {
157
+ const buffer = Buffer.concat(chunks);
158
+ resolve(buffer);
159
+ });
160
+
161
+ }).on('error', (error) => {
162
+ console.error(`Error downloading binary file from ${url}:`, error.message);
163
+ reject(error);
164
+ });
165
+ });
166
+ }
167
+
168
+ /**
169
+ * Compara dos versiones semánticas
170
+ */
171
+ compareVersions(version1, version2) {
172
+ const v1Parts = version1.split('.').map(part => parseInt(part) || 0);
173
+ const v2Parts = version2.split('.').map(part => parseInt(part) || 0);
174
+
175
+ for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) {
176
+ const v1 = i < v1Parts.length ? v1Parts[i] : 0;
177
+ const v2 = i < v2Parts.length ? v2Parts[i] : 0;
178
+
179
+ if (v1 > v2) return 1;
180
+ if (v1 < v2) return -1;
181
+ }
182
+ return 0;
183
+ }
184
+
185
+ /**
186
+ * Obtiene la extensión del instalador según la plataforma
187
+ */
188
+ getInstallerExtension() {
189
+ switch (process.platform) {
190
+ case 'darwin': return '.dmg';
191
+ case 'linux': return '.AppImage';
192
+ default: return '.exe';
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Obtiene el nombre del archivo del instalador según la plataforma
198
+ */
199
+ getInstallerFilename() {
200
+ return process.platform === 'linux' ? 'glassnote' : 'glassnote-installer';
201
+ }
202
+
203
+ /**
204
+ * Obtiene la ruta local del instalador
205
+ */
206
+ getInstallerPath(version) {
207
+ const filename = `${this.getInstallerFilename()}-${version}${this.getInstallerExtension()}`;
208
+ return path.join(userData.getUserDataDir(), filename);
209
+ }
210
+
211
+ /**
212
+ * Descarga el instalador con reintentos y verificación robusta
213
+ */
214
+ async downloadInstaller(version, maxRetries = 3) {
215
+ let retryCount = 0;
216
+
217
+ while (retryCount <= maxRetries) {
218
+ try {
219
+ const installerPath = this.getInstallerPath(version);
220
+ const installerUrl = `${this.S3_BASE_URL}/${this.getInstallerFilename()}-${version}${this.getInstallerExtension()}`;
221
+
222
+ // Verificar si el instalador ya existe y tiene el hash correcto
223
+ if (fs.existsSync(installerPath)) {
224
+ if (this.expectedHash) {
225
+ const fileHash = await this.calculateFileHash(installerPath);
226
+
227
+ // Comparación case-insensitive del hash
228
+ if (fileHash.toLowerCase() === this.expectedHash.toLowerCase()) {
229
+ this.runInstaller(version);
230
+ return;
231
+ }
232
+ // Eliminar archivo corrupto antes de reintentar
233
+ fs.unlinkSync(installerPath);
234
+ } else {
235
+ fs.unlinkSync(installerPath);
236
+ }
237
+ }
238
+
239
+ const installerBuffer = await this.downloadBinaryFile(installerUrl);
240
+
241
+ // Verificar que el archivo se descargó completamente
242
+ if (!installerBuffer || installerBuffer.length === 0) {
243
+ throw new Error('Downloaded installer is empty or incomplete');
244
+ }
245
+
246
+ // Guardar el instalador
247
+ fs.writeFileSync(installerPath, installerBuffer);
248
+
249
+ // Verificar hash inmediatamente después de guardar
250
+ if (this.expectedHash) {
251
+ const downloadedHash = await this.calculateBufferHash(installerBuffer);
252
+
253
+ // Comparación case-insensitive del hash
254
+ if (downloadedHash.toLowerCase() !== this.expectedHash.toLowerCase()) {
255
+ // Eliminar el archivo corrupto
256
+ fs.unlinkSync(installerPath);
257
+ throw new Error(`Installer hash verification failed! Expected: ${this.expectedHash}, Got: ${downloadedHash}`);
258
+ }
259
+ }
260
+
261
+ this.runInstaller(version);
262
+ return; // Éxito, salir del bucle
263
+
264
+ } catch (error) {
265
+ retryCount++;
266
+ console.error(`Error downloading installer (attempt ${retryCount}/${maxRetries + 1}):`, error.message);
267
+
268
+ if (retryCount > maxRetries) {
269
+ console.error('All download attempts failed. Giving up.');
270
+ throw error;
271
+ }
272
+
273
+ // Esperar antes de reintentar (backoff exponencial)
274
+ const waitTime = Math.pow(2, retryCount) * 1000;
275
+ await new Promise(resolve => setTimeout(resolve, waitTime));
276
+ }
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Calcula el hash SHA256 de un archivo
282
+ */
283
+ calculateFileHash(filePath) {
284
+ return new Promise((resolve, reject) => {
285
+ const hash = crypto.createHash('sha256');
286
+ const stream = fs.createReadStream(filePath);
287
+
288
+ stream.on('data', (data) => {
289
+ hash.update(data);
290
+ });
291
+
292
+ stream.on('end', () => {
293
+ resolve(hash.digest('hex'));
294
+ });
295
+
296
+ stream.on('error', (error) => {
297
+ reject(error);
298
+ });
299
+ });
300
+ }
301
+
302
+ /**
303
+ * Calcula el hash SHA256 de un buffer
304
+ */
305
+ calculateBufferHash(buffer) {
306
+ const hash = crypto.createHash('sha256');
307
+ hash.update(buffer);
308
+ return hash.digest('hex');
309
+ }
310
+
311
+ /**
312
+ * Ejecuta el instalador
313
+ */
314
+ runInstaller(version) {
315
+ const installerPath = this.getInstallerPath(version);
316
+
317
+ // Verificar hash si se proporcionó expectedHash
318
+ if (this.expectedHash) {
319
+ this.calculateFileHash(installerPath)
320
+ .then((fileHash) => {
321
+ // Comparación case-insensitive del hash
322
+ if (fileHash.toLowerCase() !== this.expectedHash.toLowerCase()) {
323
+ console.error('Installer hash verification failed!');
324
+ console.error('Expected:', this.expectedHash);
325
+ console.error('Actual:', fileHash);
326
+ return;
327
+ }
328
+ this.executeInstaller(installerPath);
329
+ })
330
+ .catch((error) => {
331
+ console.error('Error verifying installer hash:', error);
332
+ });
333
+ } else {
334
+ this.executeInstaller(installerPath);
335
+ }
336
+ }
337
+
338
+ /**
339
+ * Ejecuta el instalador según la plataforma
340
+ */
341
+ executeInstaller(installerPath) {
342
+ try {
343
+ if (process.platform === 'darwin') {
344
+ // macOS - manejo especial para DMG
345
+ this.executeMacInstaller(installerPath);
346
+ } else {
347
+ // Windows y Linux - ejecución normal
348
+ const isLinux = process.platform === 'linux';
349
+
350
+ // Ejecutar el instalador de forma asíncrona en un proceso independiente
351
+ let command;
352
+ if (process.platform === 'win32') {
353
+ // Windows: usar start para ejecutar en proceso independiente
354
+ command = `start "" "${installerPath}"`;
355
+ } else if (isLinux) {
356
+ // Linux: ejecutar instalador en background
357
+ command = `chmod +x "${installerPath}" && "${installerPath}" &`;
358
+ }
359
+
360
+
361
+ // Ejecutar el instalador en un proceso independiente
362
+ exec(command, (error) => {
363
+ if (error) {
364
+ console.error('Error executing installer:', error);
365
+ }
366
+ // El instalador se está ejecutando independientemente,
367
+ // ahora podemos cerrar la aplicación limpiamente
368
+ app.quit();
369
+ });
370
+ }
371
+ } catch (error) {
372
+ console.error('Error executing installer:', error);
373
+ // En caso de error, asegurar cierre de la aplicación
374
+ app.quit();
375
+ }
376
+ }
377
+
378
+ /**
379
+ * Maneja la instalación en macOS (DMG)
380
+ */
381
+ executeMacInstaller(installerPath) {
382
+ // Crear script de instalación automática para macOS
383
+ const installScript = this.createMacInstallScript(installerPath);
384
+ const scriptPath = path.join(app.getPath('temp'), 'glassnote_install.sh');
385
+
386
+ try {
387
+ // Guardar el script
388
+ fs.writeFileSync(scriptPath, installScript, { mode: 0o755 });
389
+
390
+ // Ejecutar el script (que cerrará la app y hará la instalación)
391
+ exec(`"${scriptPath}"`, (error) => {
392
+ if (error) {
393
+ console.error('Error executing install script:', error);
394
+ return;
395
+ }
396
+ // El script se encarga de cerrar la aplicación
397
+ });
398
+ } catch (error) {
399
+ console.error('Error creating install script:', error);
400
+ }
401
+ }
402
+
403
+ /**
404
+ * Crea un script de instalación para macOS
405
+ */
406
+ createMacInstallScript(installerPath) {
407
+ const appName = 'glassnote.app';
408
+ const tempMountPoint = '/Volumes/GlassNoteInstaller';
409
+ const applicationsDir = '/Applications';
410
+
411
+ return `#!/bin/bash
412
+
413
+ # Script de instalación automática para GlassNote macOS
414
+
415
+ echo " Iniciando instalación automática de GlassNote..."
416
+
417
+ # Cerrar la aplicación si está ejecutándose
418
+ echo " Cerrando GlassNote si está en ejecución..."
419
+ pkill -f "GlassNote" || true
420
+ sleep 2
421
+
422
+ # Asegurarse de que la aplicación esté completamente cerrada
423
+ pkill -9 -f "GlassNote" || true
424
+ sleep 1
425
+
426
+ # Desmontar punto de montaje si existe
427
+ echo " Desmontando imagen previa si existe..."
428
+ hdiutil detach "${tempMountPoint}" -force 2>/dev/null || true
429
+
430
+ # Montar el DMG
431
+ echo " Montando imagen de instalación..."
432
+ hdiutil attach "${installerPath}" -nobrowse -mountpoint "${tempMountPoint}"
433
+
434
+ # Verificar que se montó correctamente
435
+ if [ ! -d "${tempMountPoint}/${appName}" ]; then
436
+ echo " Error: No se pudo encontrar ${appName} en la imagen DMG"
437
+ hdiutil detach "${tempMountPoint}" -force
438
+ exit 1
439
+ fi
440
+
441
+ # Eliminar versión anterior si existe
442
+ echo "️ Eliminando versión anterior si existe..."
443
+ rm -rf "${applicationsDir}/${appName}"
444
+
445
+ # Copiar la nueva aplicación
446
+ echo " Copiando nueva versión a Applications..."
447
+ cp -R "${tempMountPoint}/${appName}" "${applicationsDir}/"
448
+
449
+ # Ajustar permisos
450
+ echo " Ajustando permisos..."
451
+ chmod -R 755 "${applicationsDir}/${appName}"
452
+
453
+ # Desmontar la imagen
454
+ echo " Desmontando imagen..."
455
+ hdiutil detach "${tempMountPoint}" -force
456
+
457
+ # Limpiar archivo de instalación temporal
458
+ echo " Limpiando archivos temporales..."
459
+ rm -f "${installerPath}"
460
+
461
+ echo " Instalación completada exitosamente!"
462
+ echo " GlassNote ha sido actualizado a la última versión"
463
+
464
+ # Abrir la aplicación (opcional)
465
+ sleep 2
466
+ open -a "${applicationsDir}/${appName}"
467
+
468
+ exit 0
469
+ `;
470
+ }
471
+ }
472
+
473
+ module.exports = AutoUpdate;