@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/INSTALACION.md +258 -0
- package/README.md +228 -0
- package/autoStart.js +608 -0
- package/autoupdate.js +473 -0
- package/autoupdate_renderer.js +371 -0
- package/bin/glassnote.js +523 -0
- package/images/demo01.png +0 -0
- package/images/dmg-background.png +0 -0
- package/images/dmg-background.svg +37628 -0
- package/images/icon-16.png +0 -0
- package/images/icon-24.png +0 -0
- package/images/icon-512.png +0 -0
- package/images/icon.ico +0 -0
- package/images/icon.png +0 -0
- package/images/splash.svg +61 -0
- package/localserver.js +297 -0
- package/logUtilities.js +128 -0
- package/main.js +745 -0
- package/npmMode.js +139 -0
- package/npmUpdate.js +152 -0
- package/package.json +141 -0
- package/preload.js +148 -0
- package/userData.js +492 -0
|
@@ -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;
|