Initial commit: LinkSyncServer and LinkSyncExtension projects with complete documentation, models, API endpoints, tests, and extension implementation
This commit is contained in:
312
LinkSyncExtension/background.js
Normal file
312
LinkSyncExtension/background.js
Normal file
@@ -0,0 +1,312 @@
|
||||
// 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;
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user