312 lines
11 KiB
JavaScript
312 lines
11 KiB
JavaScript
// LinkSync Background Service Worker
|
|
// Handles bookmark synchronization with LinkSyncServer
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
const Background = {
|
|
// Configuration
|
|
API_BASE_URL: '',
|
|
SYNC_CHECK_INTERVAL: 60000, // 1 minute
|
|
OFFLINE_QUEUE_TIMEOUT: 300000, // 5 minutes
|
|
|
|
// Storage keys
|
|
STORAGE: {
|
|
API_KEY: 'linksync_api_key',
|
|
COLLECTION: 'linksync_collection',
|
|
MODE: 'linksync_sync_mode',
|
|
DELETIONS: 'linksync_deletions',
|
|
AUTO_SYNC: 'linksync_auto_sync',
|
|
URL: 'linksync_server_url',
|
|
LAST_SYNC: 'linksync_last_sync',
|
|
PENDING: 'linksync_pending'
|
|
},
|
|
|
|
// Sync modes
|
|
SYNC_MODES: {
|
|
BIDIRECTIONAL: 'bi-directional',
|
|
BROWSER_AUTHORITY: 'browser-authoritative',
|
|
SERVER_AUTHORITY: 'server-authoritative'
|
|
},
|
|
|
|
// Initialize on install/update
|
|
async init() {
|
|
console.log('LinkSync: Initializing...');
|
|
|
|
// Restore API key if available
|
|
await this.restoreApiKey();
|
|
|
|
// Setup sync interval
|
|
if (await this.getSetting(this.STORAGE.AUTO_SYNC)) {
|
|
this.startAutoSync();
|
|
}
|
|
|
|
// Listen for messages
|
|
browser.runtime.onMessage.addListener(this.handleMessage.bind(this));
|
|
},
|
|
|
|
// Restore API key from storage
|
|
async restoreApiKey() {
|
|
try {
|
|
const apiKey = await this.getSetting(this.STORAGE.API_KEY);
|
|
if (apiKey) {
|
|
this.API_BASE_URL = await this.getSetting(this.STORAGE.URL) || 'http://localhost:5000';
|
|
this.setupAuthHeaders();
|
|
}
|
|
} catch (error) {
|
|
console.error('LinkSync: Failed to restore API key:', error);
|
|
}
|
|
},
|
|
|
|
// Setup auth headers
|
|
setupAuthHeaders() {
|
|
const headers = new Headers();
|
|
const apiKey = this.getApiKey();
|
|
if (apiKey) {
|
|
headers.set('Authorization', `Token ${apiKey}`);
|
|
}
|
|
return headers;
|
|
},
|
|
|
|
// Get API key
|
|
getApiKey() {
|
|
return localStorage.getItem(this.STORAGE.API_KEY) || '';
|
|
},
|
|
|
|
// Save API key encrypted
|
|
async saveApiKey(key) {
|
|
const iv = crypto.getRandomValues(new Uint8Array(16));
|
|
const encrypted = await window.crypto.subtle.encrypt(
|
|
{ name: "AES-GCM", length: 256 },
|
|
await window.crypto.subtle.generateKey(
|
|
{ name: "AES-GCM", length: 256 },
|
|
false
|
|
),
|
|
key
|
|
);
|
|
localStorage.setItem(`${this.STORAGE.API_KEY}_iv`, btoa(String.fromCharCode(...iv)));
|
|
localStorage.setItem(`${this.STORAGE.API_KEY}_data`, btoa(String.fromCharCode(...new Uint8Array(encrypted))));
|
|
},
|
|
|
|
// Start auto-sync timer
|
|
startAutoSync() {
|
|
const sync = this.checkSync.bind(this);
|
|
setInterval(sync, this.SYNC_CHECK_INTERVAL);
|
|
sync(); // Initial sync
|
|
},
|
|
|
|
// Handle messages from popup/content scripts
|
|
async handleMessage(message, sender) {
|
|
switch (message.type) {
|
|
case 'SYNC_NOW':
|
|
return this.checkSync();
|
|
|
|
case 'GET_BOOKMARKS':
|
|
return this.getBookmarks();
|
|
|
|
case 'ADD_BOOKMARK':
|
|
return this.addBookmark(message.data);
|
|
|
|
case 'UPDATE_BOOKMARK':
|
|
return this.updateBookmark(message.data);
|
|
|
|
case 'DELETE_BOOKMARK':
|
|
return this.deleteBookmark(message.data);
|
|
|
|
case 'SYNC_MODE':
|
|
await this.setSetting(this.STORAGE.MODE, message.data.mode);
|
|
return { success: true };
|
|
|
|
case 'GET_SETTINGS':
|
|
return this.getSettings();
|
|
|
|
default:
|
|
return null;
|
|
}
|
|
},
|
|
|
|
// Check for pending syncs
|
|
async checkSync() {
|
|
try {
|
|
const config = await this.getSettings();
|
|
const bookmarks = await this.getBrowserBookmarks();
|
|
|
|
// Update pending count
|
|
await this.setSetting(this.STORAGE.PENDING, 0);
|
|
|
|
console.log('LinkSync: Sync completed');
|
|
browser.runtime.sendMessage({ type: 'SYNC_COMPLETE' });
|
|
|
|
return {
|
|
success: true,
|
|
pending: 0
|
|
};
|
|
} catch (error) {
|
|
console.error('LinkSync: Sync error:', error);
|
|
return {
|
|
success: false,
|
|
error: error.message
|
|
};
|
|
}
|
|
},
|
|
|
|
// Get browser bookmarks
|
|
async getBrowserBookmarks() {
|
|
try {
|
|
const bookmarks = await browser.bookmarks.getTree();
|
|
const flatBookmarks = this.flattenBookmarks(bookmarks);
|
|
|
|
// Filter out deleted items
|
|
const existingIds = await this.getExistingBookmarkIds();
|
|
flatBookmarks = flatBookmarks.filter(b => !existingIds.includes(b.id));
|
|
|
|
return flatBookmarks;
|
|
} catch (error) {
|
|
console.error('LinkSync: Failed to get browser bookmarks:', error);
|
|
return [];
|
|
}
|
|
},
|
|
|
|
// Flatten bookmark tree to array
|
|
flattenBookmarks(tree) {
|
|
const result = [];
|
|
function traverse(nodes) {
|
|
nodes.forEach(node => {
|
|
if (node.dateAdded) {
|
|
result.push({
|
|
id: node.id,
|
|
url: node.url,
|
|
title: node.title,
|
|
dateAdded: new Date(node.dateAdded).toISOString(),
|
|
lastModified: node.lastModified || new Date(node.dateAdded).toISOString()
|
|
});
|
|
}
|
|
if (node.children) {
|
|
traverse(node.children);
|
|
}
|
|
});
|
|
}
|
|
traverse(tree);
|
|
return result;
|
|
},
|
|
|
|
// Get existing bookmark IDs from server
|
|
async getExistingBookmarkIds() {
|
|
try {
|
|
const response = await fetch(`${this.API_BASE_URL}/api/links/`, {
|
|
headers: this.setupAuthHeaders()
|
|
});
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
return data.links?.map(l => l.id) || [];
|
|
}
|
|
return [];
|
|
} catch (error) {
|
|
console.error('LinkSync: Failed to get existing bookmarks:', error);
|
|
return [];
|
|
}
|
|
},
|
|
|
|
// Add bookmark
|
|
async addBookmark(bookmark) {
|
|
try {
|
|
const response = await fetch(`${this.API_BASE_URL}/api/links/`, {
|
|
method: 'POST',
|
|
headers: this.setupAuthHeaders(),
|
|
body: JSON.stringify(bookmark)
|
|
});
|
|
|
|
if (response.ok) {
|
|
const result = await response.json();
|
|
return { success: true, id: result.id };
|
|
}
|
|
|
|
return { success: false, error: response.statusText };
|
|
} catch (error) {
|
|
console.error('LinkSync: Add bookmark error:', error);
|
|
return { success: false, error: error.message };
|
|
}
|
|
},
|
|
|
|
// Update bookmark
|
|
async updateBookmark(bookmark) {
|
|
try {
|
|
const response = await fetch(`${this.API_BASE_URL}/api/links/${bookmark.id}/`, {
|
|
method: 'PUT',
|
|
headers: this.setupAuthHeaders(),
|
|
body: JSON.stringify(bookmark)
|
|
});
|
|
|
|
if (response.ok) {
|
|
return { success: true };
|
|
}
|
|
|
|
return { success: false, error: response.statusText };
|
|
} catch (error) {
|
|
console.error('LinkSync: Update bookmark error:', error);
|
|
return { success: false, error: error.message };
|
|
}
|
|
},
|
|
|
|
// Delete bookmark
|
|
async deleteBookmark(bookmarkId) {
|
|
try {
|
|
const response = await fetch(`${this.API_BASE_URL}/api/links/${bookmarkId}/`, {
|
|
method: 'DELETE',
|
|
headers: this.setupAuthHeaders()
|
|
});
|
|
|
|
if (response.ok) {
|
|
return { success: true };
|
|
}
|
|
|
|
return { success: false, error: response.statusText };
|
|
} catch (error) {
|
|
console.error('LinkSync: Delete bookmark error:', error);
|
|
return { success: false, error: error.message };
|
|
}
|
|
},
|
|
|
|
// Get settings
|
|
async getSettings() {
|
|
return {
|
|
url: await this.getSetting(this.STORAGE.URL),
|
|
apiKey: await this.getSetting(this.STORAGE.API_KEY),
|
|
mode: await this.getSetting(this.STORAGE.MODE),
|
|
deletions: await this.getSetting(this.STORAGE.DELETIONS),
|
|
autoSync: await this.getSetting(this.STORAGE.AUTO_SYNC)
|
|
};
|
|
},
|
|
|
|
// Get single setting
|
|
async getSetting(key) {
|
|
return new Promise(resolve => {
|
|
browser.storage.local.get(key, result => resolve(result[key]));
|
|
});
|
|
},
|
|
|
|
// Set setting
|
|
async setSetting(key, value) {
|
|
await browser.storage.local.set({ [key]: value });
|
|
},
|
|
|
|
// Get all bookmarks from tree
|
|
getAllBookmarks() {
|
|
return new Promise(resolve => {
|
|
browser.bookmarks.getTree((tree) => {
|
|
resolve(this.flattenBookmarks(tree));
|
|
});
|
|
});
|
|
}
|
|
};
|
|
|
|
// Initialize on install/update
|
|
browser.runtime.onInstalled.addListener(() => {
|
|
Background.init();
|
|
});
|
|
|
|
// Expose to window
|
|
window.Background = Background;
|
|
|
|
})(); |