276 líneas
6.8 KiB
JavaScript
276 líneas
6.8 KiB
JavaScript
"use strict";
|
|
|
|
// Módulo de base de datos para WebExtensions
|
|
// Usa sql.js (SQLite compilado a JavaScript/WebAssembly)
|
|
|
|
const DB = {
|
|
db: null,
|
|
SQL: null,
|
|
|
|
/**
|
|
* Inicializa sql.js (debe ser llamado primero)
|
|
*/
|
|
async initSQL() {
|
|
if (this.SQL) return this.SQL;
|
|
|
|
// Cargar sql.js desde archivos locales de la extensión
|
|
try {
|
|
const browserAPI = typeof browser !== "undefined" ? browser : chrome;
|
|
|
|
// Intentar cargar sql.js si no está ya cargado
|
|
if (typeof window.initSqlJs === "undefined") {
|
|
const sqlJsUrl = browserAPI.runtime.getURL("content/sql-wasm.js");
|
|
await this.loadScript(sqlJsUrl);
|
|
}
|
|
|
|
this.SQL = await initSqlJs({
|
|
locateFile: file => browserAPI.runtime.getURL(`content/${file}`)
|
|
});
|
|
|
|
return this.SQL;
|
|
} catch (e) {
|
|
console.error("Failed to initialize sql.js:", e);
|
|
throw new Error("Could not load SQL engine");
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Cargar script dinámicamente
|
|
*/
|
|
loadScript(src) {
|
|
return new Promise((resolve, reject) => {
|
|
const script = document.createElement("script");
|
|
script.src = src;
|
|
script.onload = resolve;
|
|
script.onerror = reject;
|
|
document.head.appendChild(script);
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Abre o crea la base de datos
|
|
* @param {string} dbname - Nombre del archivo de base de datos
|
|
* @param {Document} document - Documento HTML
|
|
* @returns {boolean} - true si se abrió correctamente
|
|
*/
|
|
async openDB(dbname, document) {
|
|
try {
|
|
await this.initSQL();
|
|
|
|
// Intentar cargar la base de datos desde el storage de la extensión
|
|
const dbData = await this.loadDBFromStorage(dbname);
|
|
|
|
if (dbData) {
|
|
// Cargar base de datos existente
|
|
this.db = new this.SQL.Database(new Uint8Array(dbData));
|
|
console.log("Database loaded from storage");
|
|
} else {
|
|
// Intentar descargar la base de datos empaquetada
|
|
await this.downloadAndStoreDB(dbname, document);
|
|
}
|
|
|
|
return true;
|
|
} catch (e) {
|
|
console.error("Error opening database:", e);
|
|
this.showMessage(document,
|
|
"Error loading database. Please check the console for details.",
|
|
"db-warning"
|
|
);
|
|
return false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Carga la base de datos desde el storage local de la extensión
|
|
*/
|
|
async loadDBFromStorage(dbname) {
|
|
try {
|
|
const browserAPI = typeof browser !== "undefined" ? browser : chrome;
|
|
const result = await browserAPI.storage.local.get(dbname);
|
|
return result[dbname];
|
|
} catch (e) {
|
|
console.warn("Could not load DB from storage:", e);
|
|
return null;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Descarga y almacena la base de datos
|
|
*/
|
|
async downloadAndStoreDB(dbname, document) {
|
|
this.showMessage(document,
|
|
"Loading database for the first time. Please wait...",
|
|
"db-warning ok"
|
|
);
|
|
|
|
try {
|
|
// Intentar cargar desde la extensión
|
|
const browserAPI = typeof browser !== "undefined" ? browser : chrome;
|
|
const dbUrl = browserAPI.runtime.getURL(`content/db/${dbname}`);
|
|
|
|
const response = await fetch(dbUrl);
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to fetch database: ${response.status}`);
|
|
}
|
|
|
|
const arrayBuffer = await response.arrayBuffer();
|
|
this.db = new this.SQL.Database(new Uint8Array(arrayBuffer));
|
|
|
|
// Guardar en storage para uso futuro
|
|
await this.saveDBToStorage(dbname, arrayBuffer);
|
|
|
|
this.showMessage(document,
|
|
"Database loaded successfully!",
|
|
"db-warning ok"
|
|
);
|
|
|
|
console.log("Database downloaded and stored");
|
|
} catch (e) {
|
|
console.error("Error downloading database:", e);
|
|
this.showMessage(document,
|
|
"Failed to load database. Please reinstall the extension.",
|
|
"db-warning bad"
|
|
);
|
|
throw e;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Guarda la base de datos en el storage local
|
|
*/
|
|
async saveDBToStorage(dbname, arrayBuffer) {
|
|
try {
|
|
const browserAPI = typeof browser !== "undefined" ? browser : chrome;
|
|
|
|
// Convertir ArrayBuffer a Array para storage
|
|
const uint8Array = new Uint8Array(arrayBuffer);
|
|
const array = Array.from(uint8Array);
|
|
|
|
// Chrome tiene límite de storage, considerar usar chunks para DBs grandes
|
|
// Por ahora, intentar guardar directamente
|
|
await browserAPI.storage.local.set({ [dbname]: array });
|
|
|
|
console.log("Database saved to storage");
|
|
} catch (e) {
|
|
console.warn("Could not save DB to storage (may be too large):", e);
|
|
// No es crítico si falla, se recargará la próxima vez
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Muestra un mensaje en la página
|
|
*/
|
|
showMessage(document, msg, style) {
|
|
const div = document.createElement("div");
|
|
div.className = style;
|
|
div.appendChild(document.createTextNode(msg));
|
|
const page = document.getElementById("page");
|
|
if (page) {
|
|
// Limpiar solo contenido dinámico, mantener cabecera
|
|
Array.from(page.children).forEach(child => {
|
|
if (!child.classList.contains('amo-header')) child.remove();
|
|
});
|
|
page.appendChild(div);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Cierra la base de datos
|
|
*/
|
|
closeDB() {
|
|
if (this.db) {
|
|
this.db.close();
|
|
this.db = null;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Ejecuta una consulta SQL
|
|
*/
|
|
execute(sql, params = []) {
|
|
if (!this.db) {
|
|
throw new Error("Database not opened");
|
|
}
|
|
return this.db.exec(sql, params);
|
|
},
|
|
|
|
/**
|
|
* Ejecuta una consulta y retorna la primera fila
|
|
*/
|
|
executeRow(sql, params = []) {
|
|
const results = this.execute(sql, params);
|
|
if (results.length > 0 && results[0].values.length > 0) {
|
|
return results[0].values[0];
|
|
}
|
|
return null;
|
|
},
|
|
|
|
/**
|
|
* Prepara una statement (compatible con API antigua)
|
|
*/
|
|
createStatement(sql) {
|
|
if (!this.db) {
|
|
throw new Error("Database not opened");
|
|
}
|
|
|
|
// Crear un objeto que emule la API de mozIStorageStatement
|
|
const stmt = this.db.prepare(sql);
|
|
let currentRow = null;
|
|
const columnNames = stmt.getColumnNames();
|
|
const paramsObj = {};
|
|
let paramsBound = false;
|
|
|
|
return {
|
|
params: paramsObj,
|
|
executeStep() {
|
|
// Bind parameters only once before first execution
|
|
if (!paramsBound && Object.keys(paramsObj).length > 0) {
|
|
// Convert params object to sql.js format (add ':' prefix to keys)
|
|
const boundParams = {};
|
|
for (const key in paramsObj) {
|
|
boundParams[':' + key] = paramsObj[key];
|
|
}
|
|
stmt.bind(boundParams);
|
|
paramsBound = true;
|
|
}
|
|
|
|
const hasRow = stmt.step();
|
|
if (hasRow) {
|
|
const values = stmt.get();
|
|
currentRow = {};
|
|
columnNames.forEach((name, index) => {
|
|
currentRow[name] = values[index];
|
|
});
|
|
}
|
|
return hasRow;
|
|
},
|
|
reset() {
|
|
stmt.reset();
|
|
currentRow = null;
|
|
paramsBound = false;
|
|
},
|
|
finalize() {
|
|
stmt.free();
|
|
currentRow = null;
|
|
},
|
|
get row() {
|
|
return currentRow || {};
|
|
},
|
|
getString(index) {
|
|
return currentRow ? Object.values(currentRow)[index] : null;
|
|
},
|
|
getInt32(index) {
|
|
return currentRow ? Object.values(currentRow)[index] : 0;
|
|
},
|
|
getDouble(index) {
|
|
return currentRow ? Object.values(currentRow)[index] : 0.0;
|
|
}
|
|
};
|
|
}
|
|
};
|
|
|
|
// Hacer disponible globalmente para compatibilidad
|
|
if (typeof window !== "undefined") {
|
|
window.DB = DB;
|
|
}
|