@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.
@@ -0,0 +1,371 @@
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 } = 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 AutoUpdateRenderer {
17
+ constructor(reloadCallback) {
18
+ this.S3_BASE_URL = S3_BASE_URL;
19
+ this.currentVersion = this.getCurrentRendererVersion();
20
+ this.expectedHash = null;
21
+ this.updateInterval = null;
22
+ this.reloadCallback = reloadCallback;
23
+ }
24
+
25
+ /**
26
+ * Obtiene la versión actual del renderer desde package.json
27
+ */
28
+ getCurrentRendererVersion() {
29
+ try {
30
+ // Primero verificar en userData (actualizaciones)
31
+ const userDataRendererPath = path.join(userData.getUserDataDir(), 'renderer', 'package.json');
32
+ if (fs.existsSync(userDataRendererPath)) {
33
+ const packageJson = JSON.parse(fs.readFileSync(userDataRendererPath, 'utf8'));
34
+ return packageJson.version;
35
+ }
36
+
37
+ // Si no existe en userData, verificar en la aplicación (versión por defecto)
38
+ const appRendererPath = path.join(__dirname, 'renderer', 'package.json');
39
+ if (fs.existsSync(appRendererPath)) {
40
+ const packageJson = JSON.parse(fs.readFileSync(appRendererPath, 'utf8'));
41
+ return packageJson.version;
42
+ }
43
+
44
+ return '0.0.0'; // Versión por defecto si no existe
45
+ } catch (error) {
46
+ console.error('Error reading renderer version:', error);
47
+ return '0.0.0';
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Inicia el sistema de auto-actualización del renderer
53
+ */
54
+ start() {
55
+ // Primera verificación después de 10 segundos
56
+ setTimeout(() => {
57
+ this.checkForUpdates();
58
+ }, 10000);
59
+
60
+ // Configurar verificación periódica cada hora (3600000 ms)
61
+ this.updateInterval = setInterval(() => {
62
+ this.checkForUpdates();
63
+ }, 3600000);
64
+ }
65
+
66
+ /**
67
+ * Detiene el sistema de auto-actualización
68
+ */
69
+ stop() {
70
+ if (this.updateInterval) {
71
+ clearInterval(this.updateInterval);
72
+ this.updateInterval = null;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Verifica si hay actualizaciones disponibles del renderer
78
+ */
79
+ async checkForUpdates() {
80
+ try {
81
+ const versionUrl = `${this.S3_BASE_URL}/version_renderer.txt`;
82
+
83
+ const versionContent = await this.downloadTextFile(versionUrl);
84
+ if (!versionContent) {
85
+ console.error('Error downloading renderer version file');
86
+ return;
87
+ }
88
+
89
+ // Parsear contenido del archivo de versión
90
+ const versionParts = versionContent.trim().split(' ');
91
+ const newVersion = versionParts[0];
92
+ this.expectedHash = versionParts.length > 1 ? versionParts[1].trim() : null;
93
+
94
+ // Comparar versiones
95
+ if (this.compareVersions(this.currentVersion, newVersion) < 0) {
96
+ await this.downloadRenderer(newVersion);
97
+ }
98
+
99
+ } catch (error) {
100
+ console.error('Error checking for renderer updates:', error);
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Descarga un archivo desde una URL (para archivos de texto)
106
+ */
107
+ downloadTextFile(url) {
108
+ return new Promise((resolve, reject) => {
109
+ https.get(url, (response) => {
110
+ if (response.statusCode !== 200) {
111
+ console.error(`HTTP error ${response.statusCode} downloading text file from ${url}`);
112
+ reject(new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`));
113
+ return;
114
+ }
115
+
116
+ let data = '';
117
+ response.on('data', (chunk) => {
118
+ data += chunk;
119
+ });
120
+
121
+ response.on('end', () => {
122
+ resolve(data);
123
+ });
124
+
125
+ }).on('error', (error) => {
126
+ console.error(`Error downloading text file from ${url}:`, error.message);
127
+ reject(error);
128
+ });
129
+ });
130
+ }
131
+
132
+ /**
133
+ * Descarga un archivo binario desde una URL
134
+ */
135
+ downloadBinaryFile(url) {
136
+ return new Promise((resolve, reject) => {
137
+ https.get(url, (response) => {
138
+ if (response.statusCode !== 200) {
139
+ console.error(`HTTP error ${response.statusCode} downloading binary file from ${url}`);
140
+ reject(new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`));
141
+ return;
142
+ }
143
+
144
+ const contentLength = response.headers['content-length'];
145
+
146
+ const chunks = [];
147
+ let totalBytes = 0;
148
+ let lastLoggedPercent = -1;
149
+
150
+ response.on('data', (chunk) => {
151
+ chunks.push(chunk);
152
+ totalBytes += chunk.length;
153
+
154
+ // Log progress only at 10% intervals to avoid spam
155
+ if (contentLength) {
156
+ const percent = Math.floor((totalBytes / contentLength) * 100 / 10) * 10;
157
+ if (percent > lastLoggedPercent) {
158
+ lastLoggedPercent = percent;
159
+ }
160
+ }
161
+ });
162
+
163
+ response.on('end', () => {
164
+ const buffer = Buffer.concat(chunks);
165
+ resolve(buffer);
166
+ });
167
+
168
+ }).on('error', (error) => {
169
+ console.error(`Error downloading binary file from ${url}:`, error.message);
170
+ reject(error);
171
+ });
172
+ });
173
+ }
174
+
175
+ /**
176
+ * Compara dos versiones semánticas
177
+ */
178
+ compareVersions(version1, version2) {
179
+ const v1Parts = version1.split('.').map(part => parseInt(part) || 0);
180
+ const v2Parts = version2.split('.').map(part => parseInt(part) || 0);
181
+
182
+ for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) {
183
+ const v1 = i < v1Parts.length ? v1Parts[i] : 0;
184
+ const v2 = i < v2Parts.length ? v2Parts[i] : 0;
185
+
186
+ if (v1 > v2) return 1;
187
+ if (v1 < v2) return -1;
188
+ }
189
+ return 0;
190
+ }
191
+
192
+ /**
193
+ * Descarga el renderer con reintentos y verificación robusta
194
+ */
195
+ async downloadRenderer(version, maxRetries = 3) {
196
+ let retryCount = 0;
197
+
198
+ while (retryCount <= maxRetries) {
199
+ try {
200
+ const rendererUrl = `${this.S3_BASE_URL}/renderer-${version}.zip`;
201
+ const tempZipPath = path.join(app.getPath('temp'), `renderer-${version}.zip`);
202
+ const rendererDir = path.join(userData.getUserDataDir(), 'renderer');
203
+ const backupDir = path.join(app.getPath('temp'), `renderer-backup-${Date.now()}`);
204
+
205
+
206
+ const rendererBuffer = await this.downloadBinaryFile(rendererUrl);
207
+
208
+ // Verificar que el archivo se descargó completamente
209
+ if (!rendererBuffer || rendererBuffer.length === 0) {
210
+ throw new Error('Downloaded renderer is empty or incomplete');
211
+ }
212
+
213
+
214
+ // Guardar el zip temporalmente
215
+ fs.writeFileSync(tempZipPath, rendererBuffer);
216
+
217
+ // Verificar hash si se proporcionó expectedHash
218
+ if (this.expectedHash) {
219
+ const downloadedHash = this.calculateBufferHash(rendererBuffer);
220
+
221
+ // Comparación case-insensitive del hash
222
+ if (downloadedHash.toLowerCase() !== this.expectedHash.toLowerCase()) {
223
+ // Eliminar el archivo corrupto
224
+ fs.unlinkSync(tempZipPath);
225
+ throw new Error(`Renderer hash verification failed! Expected: ${this.expectedHash}, Got: ${downloadedHash}`);
226
+ }
227
+ }
228
+
229
+ // Crear backup del renderer actual
230
+ if (fs.existsSync(rendererDir)) {
231
+ fs.cpSync(rendererDir, backupDir, { recursive: true });
232
+ }
233
+
234
+ // Extraer el nuevo renderer
235
+ await this.extractRenderer(tempZipPath, rendererDir);
236
+
237
+ // Limpiar archivo temporal
238
+ fs.unlinkSync(tempZipPath);
239
+
240
+ // Actualizar versión actual
241
+ this.currentVersion = version;
242
+
243
+ // Notificar al main window para refrescar
244
+ this.notifyMainWindow();
245
+
246
+ return; // Éxito, salir del bucle
247
+
248
+ } catch (error) {
249
+ retryCount++;
250
+ console.error(`Error downloading renderer (attempt ${retryCount}/${maxRetries + 1}):`, error.message);
251
+
252
+ if (retryCount > maxRetries) {
253
+ console.error('All renderer download attempts failed. Giving up.');
254
+ // Restaurar backup si existe
255
+ await this.restoreBackupIfNeeded();
256
+ throw error;
257
+ }
258
+
259
+ // Esperar antes de reintentar (backoff exponencial)
260
+ const waitTime = Math.pow(2, retryCount) * 1000;
261
+ console.error(`Waiting ${waitTime}ms before retry...`);
262
+ await new Promise(resolve => setTimeout(resolve, waitTime));
263
+ }
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Extrae el renderer desde el archivo zip
269
+ */
270
+ async extractRenderer(zipPath, targetDir) {
271
+ return new Promise((resolve, reject) => {
272
+ // Asegurar que el directorio destino existe
273
+ if (!fs.existsSync(targetDir)) {
274
+ fs.mkdirSync(targetDir, { recursive: true });
275
+ }
276
+
277
+ const platform = process.platform;
278
+ let command;
279
+
280
+ if (platform === 'win32') {
281
+ // Windows - usar PowerShell
282
+ command = `powershell -Command "Expand-Archive -Path '${zipPath}' -DestinationPath '${targetDir}' -Force"`;
283
+ } else {
284
+ // macOS y Linux - usar unzip nativo
285
+ command = `unzip -o "${zipPath}" -d "${targetDir}"`;
286
+ }
287
+
288
+
289
+ exec(command, (error, stdout, stderr) => {
290
+ if (error) {
291
+ console.error('Error extracting renderer with system command:', error);
292
+ console.error('STDERR:', stderr);
293
+
294
+ // Fallback para macOS si unzip falla: usar ditto (comando nativo de macOS)
295
+ if (platform === 'darwin') {
296
+ const dittoCommand = `ditto -x -k "${zipPath}" "${targetDir}"`;
297
+
298
+ exec(dittoCommand, (dittoError, dittoStdout, dittoStderr) => {
299
+ if (dittoError) {
300
+ console.error('Ditto fallback also failed:', dittoError);
301
+ console.error('Ditto STDERR:', dittoStderr);
302
+ reject(new Error(`Failed to extract renderer on ${platform}. Both unzip and ditto commands failed.`));
303
+ } else {
304
+ resolve();
305
+ }
306
+ });
307
+ } else {
308
+ reject(new Error(`Failed to extract renderer on ${platform}`));
309
+ }
310
+ return;
311
+ }
312
+ resolve();
313
+ });
314
+ });
315
+ }
316
+
317
+ /**
318
+ * Restaura el backup si existe después de un error
319
+ */
320
+ async restoreBackupIfNeeded() {
321
+ try {
322
+ // Buscar el backup más reciente
323
+ const tempDir = app.getPath('temp');
324
+ const files = fs.readdirSync(tempDir);
325
+ const backupDirs = files.filter(f => f.startsWith('renderer-backup-') && fs.statSync(path.join(tempDir, f)).isDirectory());
326
+
327
+ if (backupDirs.length > 0) {
328
+ // Ordenar por timestamp (más reciente primero)
329
+ backupDirs.sort((a, b) => {
330
+ const aTime = parseInt(a.split('-').pop());
331
+ const bTime = parseInt(b.split('-').pop());
332
+ return bTime - aTime;
333
+ });
334
+
335
+ const latestBackup = path.join(tempDir, backupDirs[0]);
336
+ const rendererDir = path.join(userData.getUserDataDir(), 'renderer');
337
+
338
+
339
+ // Limpiar directorio actual
340
+ if (fs.existsSync(rendererDir)) {
341
+ fs.rmSync(rendererDir, { recursive: true, force: true });
342
+ }
343
+
344
+ // Restaurar backup
345
+ fs.cpSync(latestBackup, rendererDir, { recursive: true });
346
+ }
347
+ } catch (error) {
348
+ console.error('Error restoring backup:', error);
349
+ }
350
+ }
351
+
352
+ /**
353
+ * Calcula el hash SHA256 de un buffer
354
+ */
355
+ calculateBufferHash(buffer) {
356
+ const hash = crypto.createHash('sha256');
357
+ hash.update(buffer);
358
+ return hash.digest('hex');
359
+ }
360
+
361
+ /**
362
+ * Notifica al main window para que refresque el contenido
363
+ */
364
+ notifyMainWindow() {
365
+ if (this.reloadCallback) {
366
+ this.reloadCallback(this.currentVersion);
367
+ }
368
+ }
369
+ }
370
+
371
+ module.exports = AutoUpdateRenderer;