Skip to content

Commit 328fdd9

Browse files
committed
Add Google Drive sync with bidirectional support and auto-save
Integrate Google Drive API for session backup and restore: - GoogleDriveService class with OAuth2 via chrome.identity API - Bidirectional sync: upload sessions to Drive and restore from Drive - Auto-save option that syncs after each annotation change (debounced) - Drive UI section in popup with connect/disconnect, auto-save toggle, manual save, and session list panel for loading from Drive - 19 unit tests for GoogleDriveService covering auth, CRUD, and token refresh Note: Requires Google Cloud Console setup with OAuth client ID in manifest.json https://claude.ai/code/session_011YPMfBufueyjL8qmHamKaC
1 parent 7b0fe6e commit 328fdd9

11 files changed

Lines changed: 1260 additions & 2 deletions

background.js

Lines changed: 170 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,73 @@ import { Question } from './src/Annotation.js';
77
import { ExportSessionCSV } from './src/ExportSessionCSV.js';
88
import { JSonSessionService } from './src/JSonSessionService.js';
99
import { getSystemInfo } from './src/browserInfo.js';
10+
import { GoogleDriveService } from './src/GoogleDriveService.js';
1011

1112
let session = new Session();
1213

14+
// --- Google Drive state ---
15+
const driveService = new GoogleDriveService();
16+
let driveAutoSave = false;
17+
let driveFileId = null;
18+
let driveSyncStatus = 'idle'; // idle | syncing | synced | error
19+
let syncTimeout = null;
20+
21+
async function loadDriveSettings() {
22+
const data = await chrome.storage.local.get(['driveAutoSave', 'driveFileId']);
23+
driveAutoSave = data.driveAutoSave || false;
24+
driveFileId = data.driveFileId || null;
25+
26+
// Try to restore token silently if auto-save was on
27+
if (driveAutoSave) {
28+
try {
29+
await driveService.getToken();
30+
} catch (e) {
31+
driveAutoSave = false;
32+
await chrome.storage.local.set({ driveAutoSave: false });
33+
}
34+
}
35+
}
36+
37+
async function saveDriveSettings() {
38+
await chrome.storage.local.set({ driveAutoSave, driveFileId });
39+
}
40+
41+
function scheduleDriveSync() {
42+
if (!driveAutoSave || !driveService.isAuthenticated()) return;
43+
if (syncTimeout) clearTimeout(syncTimeout);
44+
syncTimeout = setTimeout(() => syncToDrive(), 1500);
45+
}
46+
47+
async function syncToDrive() {
48+
if (!driveService.isAuthenticated()) return;
49+
if (session.getAnnotations().length === 0) return;
50+
51+
driveSyncStatus = 'syncing';
52+
try {
53+
const jsonService = new JSonSessionService();
54+
const sessionJson = jsonService.getJSon(session);
55+
const fileName = getDriveFileName();
56+
57+
const result = await driveService.uploadSession(sessionJson, fileName, driveFileId);
58+
driveFileId = result.id;
59+
driveSyncStatus = 'synced';
60+
await saveDriveSettings();
61+
} catch (error) {
62+
console.error('Drive sync failed:', error);
63+
driveSyncStatus = 'error';
64+
}
65+
}
66+
67+
function getDriveFileName() {
68+
const date = new Date(session.getStartDateTime());
69+
const startDateTime = date.getFullYear() +
70+
('0' + (date.getMonth() + 1)).slice(-2) +
71+
('0' + date.getDate()).slice(-2) + '_' +
72+
('0' + date.getHours()).slice(-2) +
73+
('0' + date.getMinutes()).slice(-2);
74+
return `ExploratorySession_${startDateTime}.json`;
75+
}
76+
1377
// Función para guardar la sesión en el storage
1478
async function saveSession() {
1579
try {
@@ -78,6 +142,9 @@ async function saveSession() {
78142
throw error;
79143
}
80144
}
145+
146+
// Trigger Drive auto-sync after local save
147+
scheduleDriveSync();
81148
}
82149

83150
// Función para cargar la sesión desde el storage
@@ -113,8 +180,9 @@ async function loadSession() {
113180
}
114181
}
115182

116-
// Cargar la sesión al iniciar
183+
// Cargar la sesión y configuración de Drive al iniciar
117184
loadSession();
185+
loadDriveSettings();
118186

119187
// Helper function for notifications of processing errors (before addAnnotation is called)
120188
function notifyProcessingError(annotationType, descriptionName, errorMessage = "") {
@@ -328,6 +396,105 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
328396
}))
329397
});
330398
break;
399+
400+
// --- Google Drive handlers ---
401+
case "driveConnect":
402+
driveService.authenticate()
403+
.then(() => {
404+
sendResponse({ status: "ok" });
405+
})
406+
.catch(error => {
407+
sendResponse({ status: "error", error: error.message });
408+
});
409+
isAsync = true;
410+
break;
411+
412+
case "driveDisconnect":
413+
driveService.disconnect()
414+
.then(async () => {
415+
driveAutoSave = false;
416+
driveFileId = null;
417+
driveSyncStatus = 'idle';
418+
await saveDriveSettings();
419+
sendResponse({ status: "ok" });
420+
})
421+
.catch(error => {
422+
sendResponse({ status: "error", error: error.message });
423+
});
424+
isAsync = true;
425+
break;
426+
427+
case "driveGetStatus":
428+
sendResponse({
429+
connected: driveService.isAuthenticated(),
430+
autoSave: driveAutoSave,
431+
syncStatus: driveSyncStatus,
432+
fileId: driveFileId
433+
});
434+
break;
435+
436+
case "driveSetAutoSave":
437+
driveAutoSave = request.enabled;
438+
saveDriveSettings().then(() => {
439+
sendResponse({ status: "ok", autoSave: driveAutoSave });
440+
});
441+
isAsync = true;
442+
break;
443+
444+
case "driveSaveNow":
445+
syncToDrive()
446+
.then(() => {
447+
sendResponse({ status: "ok", syncStatus: driveSyncStatus, fileId: driveFileId });
448+
})
449+
.catch(error => {
450+
sendResponse({ status: "error", error: error.message, syncStatus: driveSyncStatus });
451+
});
452+
isAsync = true;
453+
break;
454+
455+
case "driveListSessions":
456+
driveService.listSessions()
457+
.then(files => {
458+
sendResponse({ status: "ok", files });
459+
})
460+
.catch(error => {
461+
sendResponse({ status: "error", error: error.message });
462+
});
463+
isAsync = true;
464+
break;
465+
466+
case "driveLoadSession":
467+
driveService.downloadSession(request.fileId)
468+
.then(jsonData => {
469+
if (importSessionJSon(jsonData)) {
470+
driveFileId = request.fileId;
471+
return saveDriveSettings().then(() => saveSession()).then(() => {
472+
sendResponse({ status: "ok" });
473+
});
474+
} else {
475+
sendResponse({ status: "error", error: "Invalid session data" });
476+
}
477+
})
478+
.catch(error => {
479+
sendResponse({ status: "error", error: error.message });
480+
});
481+
isAsync = true;
482+
break;
483+
484+
case "driveDeleteSession":
485+
driveService.deleteSession(request.fileId)
486+
.then(() => {
487+
if (driveFileId === request.fileId) {
488+
driveFileId = null;
489+
saveDriveSettings();
490+
}
491+
sendResponse({ status: "ok" });
492+
})
493+
.catch(error => {
494+
sendResponse({ status: "error", error: error.message });
495+
});
496+
isAsync = true;
497+
break;
331498
}
332499
return isAsync; // Return true only if sendResponse is used asynchronously in any of the handled cases.
333500
});
@@ -511,6 +678,8 @@ async function startSession() {
511678

512679
async function clearSession() {
513680
session.clearAnnotations();
681+
driveFileId = null;
682+
await saveDriveSettings();
514683
await saveSession();
515684
}
516685

0 commit comments

Comments
 (0)