@ -72,130 +72,186 @@ try {
}
}
// Database path resolution utilities
// Database path resolution utilities
const getAppDataPath = ( ) : string = > {
const getAppDataPath = async ( ) : Promise < string > = > {
try {
// Read config file directly
const configPath = path . join ( __dirname , '..' , 'capacitor.config.json' ) ;
const configContent = await fs . promises . readFile ( configPath , 'utf-8' ) ;
const config = JSON . parse ( configContent ) ;
const linuxPath = config ? . plugins ? . CapacitorSQLite ? . electronLinuxLocation ;
if ( linuxPath ) {
// Expand ~ to home directory
const expandedPath = linuxPath . replace ( /^~/ , process . env . HOME || '' ) ;
logger . info ( "[Electron] Using configured database path:" , expandedPath ) ;
return expandedPath ;
}
// Fallback to app.getPath if config path is not available
const userDataPath = app . getPath ( 'userData' ) ;
logger . info ( "[Electron] Using fallback user data path:" , userDataPath ) ;
return userDataPath ;
} catch ( error ) {
logger . error ( "[Electron] Error getting app data path:" , error ) ;
// Fallback to app.getPath if anything fails
const userDataPath = app . getPath ( 'userData' ) ;
const userDataPath = app . getPath ( 'userData' ) ;
logger . info ( "[Electron] User data path:" , userDataPath ) ;
logger . info ( "[Electron] Using fallback us er data path after error :" , userDataPath ) ;
return userDataPath ;
return userDataPath ;
}
} ;
} ;
const validateAndNormalizePath = ( filePath : string ) : string = > {
const validateAndNormalizePath = async ( filePath : string ) : Promise < string > = > {
try {
// Resolve any relative paths
// Resolve any relative paths and normalize
const resolvedPath = path . resolve ( filePath ) ;
const normalizedPath = path . resolve ( filePath ) ;
logger . info ( "[Electron] Normalized database path:" , normalizedPath ) ;
// Verify the path is absolute
// Ensure it's an absolute path
if ( ! path . isAbsolute ( normalizedPath ) ) {
if ( ! path . isAbsolute ( resolv edPath) ) {
throw new Error ( ` Database path must be absolute: ${ normalizedPath } ` ) ;
throw new Error ( ` Database path must be absolute: ${ resolv edPath} ` ) ;
}
}
// Verify the path is within the user data directory
// Ensure it's within the app data directory
const userDataPath = getAppDataPath ( ) ;
const appDataPath = await getAppDataPath ( ) ;
if ( ! normalizedPath . startsWith ( userDataPath ) ) {
if ( ! resolv edPath. startsWith ( app DataPath) ) {
throw new Error ( ` Database path must be within user data directory: ${ normalizedPath } ` ) ;
throw new Error ( ` Database path must be within app data directory: ${ resolv edPath } ` ) ;
}
}
// Normalize the path
const normalizedPath = path . normalize ( resolvedPath ) ;
logger . info ( "[Electron] Validated database path:" , {
original : filePath ,
resolved : resolvedPath ,
normalized : normalizedPath ,
appDataPath ,
isAbsolute : path.isAbsolute ( normalizedPath ) ,
isWithinAppData : normalizedPath.startsWith ( appDataPath )
} ) ;
return normalizedPath ;
return normalizedPath ;
} catch ( error ) {
logger . error ( "[Electron] Path validation failed:" , error ) ;
throw error ;
}
} ;
} ;
const ensureDirectoryExists = ( dirPath : string ) : void = > {
const ensureDirectoryExists = async ( dirPath : string ) : Promise < void > = > {
try {
try {
const normalizedDir = validateAndNormalizePath ( dirPath ) ;
// Normalize the path first
if ( ! fs . existsSync ( normalizedDir ) ) {
const normalizedPath = path . normalize ( dirPath ) ;
fs . mkdirSync ( normalizedDir , { recursive : true , mode : 0o755 } ) ;
logger . info ( "[Electron] Created directory:" , normalizedDir ) ;
// Check if directory exists
if ( ! fs . existsSync ( normalizedPath ) ) {
logger . info ( "[Electron] Creating database directory:" , normalizedPath ) ;
await fs . promises . mkdir ( normalizedPath , { recursive : true } ) ;
}
}
// Verify directory permissions
// Verify directory permissions
const stats = fs . statSync ( normalizedDir ) ;
try {
if ( ! stats . isDirectory ( ) ) {
await fs . promises . access ( normalizedPath , fs . constants . R_OK | fs . constants . W_OK ) ;
throw new Error ( ` Path exists but is not a directory: ${ normalizedDir } ` ) ;
logger . info ( "[Electron] Database directory permissions verified:" , normalizedPath ) ;
} catch ( error ) {
logger . error ( "[Electron] Database directory permission error:" , error ) ;
throw new Error ( ` Database directory not accessible: ${ normalizedPath } ` ) ;
}
}
// Check if directory is writable
// Test write permissions
const testFile = path . join ( normalizedPath , '.write-test' ) ;
try {
try {
const testFile = path . join ( normalizedDir , '.write-test' ) ;
await fs . promises . writeFile ( testFile , 'test' ) ;
fs . writeFileSync ( testFile , 'test' ) ;
await fs . promises . unlink ( testFile ) ;
fs . unlinkSync ( testFile ) ;
logger . info ( "[Electron] Database directory write test passed:" , normalizedPath ) ;
} catch ( err ) {
} catch ( error ) {
throw new Error ( ` Directory not writable: ${ normalizedDir } ` ) ;
logger . error ( "[Electron] Database directory write test failed:" , error ) ;
throw new Error ( ` Database directory not writable: ${ normalizedPath } ` ) ;
}
}
} catch ( error ) {
} catch ( error ) {
logger . error ( "[Electron] Directory setup failed :" , error ) ;
logger . error ( "[Electron] Failed to ensure database directory exists :" , error ) ;
throw error ;
throw error ;
}
}
} ;
} ;
// Database path logic
let dbPath : string ;
let dbDir : string ;
// Initialize database paths
// Initialize database paths
const initializeDatabasePaths = ( ) : void = > {
let dbPath : string | undefined ;
try {
let dbDir : string | undefined ;
const basePath = getAppDataPath ( ) ;
let dbPathInitialized = false ;
dbDir = path . join ( basePath , 'timesafari' ) ;
let dbPathInitializationPromise : Promise < void > | null = null ;
dbPath = path . join ( dbDir , 'timesafari.db' ) ;
// Validate and normalize paths
const initializeDatabasePaths = async ( ) : Promise < void > = > {
dbDir = validateAndNormalizePath ( dbDir ) ;
// Prevent multiple simultaneous initializations
dbPath = validateAndNormalizePath ( dbPath ) ;
if ( dbPathInitializationPromise ) {
return dbPathInitializationPromise ;
}
// Ensure directory exists and is writable
if ( dbPathInitialized ) {
ensureDirectoryExists ( dbDir ) ;
return ;
}
dbPathInitializationPromise = ( async ( ) = > {
try {
// Get the base directory from config
dbDir = await getAppDataPath ( ) ;
logger . info ( "[Electron] Database directory:" , dbDir ) ;
logger . info ( "[Electron] Database directory:" , dbDir ) ;
logger . info ( "[Electron] Database file path:" , dbPath ) ;
// Verify database file permissions if it exists
// Ensure the directory exists and is writable
await ensureDirectoryExists ( dbDir ) ;
// Construct the database path
dbPath = await validateAndNormalizePath ( path . join ( dbDir , 'timesafari.db' ) ) ;
logger . info ( "[Electron] Database path initialized:" , dbPath ) ;
// Verify the database file if it exists
if ( fs . existsSync ( dbPath ) ) {
if ( fs . existsSync ( dbPath ) ) {
try {
try {
// Ensure the database file is writable
await fs . promises . access ( dbPath , fs . constants . R_OK | fs . constants . W_OK ) ;
fs . accessSync ( dbPath , fs . constants . R_OK | fs . constants . W_OK ) ;
logger . info ( "[Electron] Existing database file permissions verified:" , dbPath ) ;
// Try to open the file in write mode to verify permissions
} catch ( error ) {
const fd = fs . openSync ( dbPath , 'r+' ) ;
logger . error ( "[Electron] Database file permission error:" , error ) ;
fs . closeSync ( fd ) ;
throw new Error ( ` Database file not accessible: ${ dbPath } ` ) ;
logger . info ( "[Electron] Database file exists and is writable" ) ;
} catch ( err ) {
logger . error ( "[Electron] Database file exists but is not writable:" , err ) ;
// Try to fix permissions
try {
fs . chmodSync ( dbPath , 0 o644 ) ;
logger . info ( "[Electron] Fixed database file permissions" ) ;
} catch ( chmodErr ) {
logger . error ( "[Electron] Failed to fix database permissions:" , chmodErr ) ;
throw new Error ( ` Database file not writable: ${ dbPath } ` ) ;
}
}
}
}
}
dbPathInitialized = true ;
} catch ( error ) {
} catch ( error ) {
logger . error ( "[Electron] Failed to initialize database paths:" , error ) ;
logger . error ( "[Electron] Failed to initialize database paths:" , error ) ;
throw error ;
throw error ;
} finally {
dbPathInitializationPromise = null ;
}
}
} ;
} ) ( ) ;
// Initialize paths when app is ready
return dbPathInitializationPromise ;
app . whenReady ( ) . then ( ( ) = > {
} ;
initializeDatabasePaths ( ) ;
} ) ;
// Initialize SQLite plugin
// Initialize SQLite plugin
let sqlitePlugin : any = null ;
let sqlitePlugin : any = null ;
let sqliteInitialized = false ;
let sqliteInitializationPromise : Promise < void > | null = null ;
async function initializeSQLite() {
async function initializeSQLite() {
// Prevent multiple simultaneous initializations
if ( sqliteInitializationPromise ) {
return sqliteInitializationPromise ;
}
if ( sqliteInitialized ) {
return ;
}
sqliteInitializationPromise = ( async ( ) = > {
try {
try {
logger . info ( "[Electron] Initializing SQLite plugin..." ) ;
logger . info ( "[Electron] Initializing SQLite plugin..." ) ;
sqlitePlugin = new CapacitorSQLite ( ) ;
sqlitePlugin = new CapacitorSQLite ( ) ;
// Initialize database paths first
await initializeDatabasePaths ( ) ;
if ( ! dbPath ) {
throw new Error ( "Database path not initialized" ) ;
}
// Test the plugin
// Test the plugin
const echoResult = await sqlitePlugin . echo ( { value : "test" } ) ;
const echoResult = await sqlitePlugin . echo ( { value : "test" } ) ;
logger . info ( "[Electron] SQLite plugin echo test:" , echoResult ) ;
logger . info ( "[Electron] SQLite plugin echo test:" , echoResult ) ;
// Initialize database connection using absolute dbPath
// Initialize database connection using validated dbPath
const connectionOptions = {
const connectionOptions = {
database : dbPath , // This is now guaranteed to be a valid absolute path
database : dbPath ,
version : 1 ,
version : 1 ,
readOnly : false ,
readOnly : false ,
encryption : "no-encryption" ,
encryption : "no-encryption" ,
@ -219,57 +275,26 @@ async function initializeSQLite() {
// Wait a moment for the connection to be fully established
// Wait a moment for the connection to be fully established
await new Promise ( resolve = > setTimeout ( resolve , 100 ) ) ;
await new Promise ( resolve = > setTimeout ( resolve , 100 ) ) ;
// Verify the connection is working
try {
try {
// Verify connection is not read-only
const result = await db . query ( "PRAGMA journal_mode;" ) ;
const result = await db . query ( { statement : "PRAGMA journal_mode;" } ) ;
logger . info ( "[Electron] Database connection verified:" , result ) ;
logger . info ( "[Electron] Database journal mode:" , result ) ;
} catch ( error ) {
logger . error ( "[Electron] Database connection verification failed:" , error ) ;
if ( result ? . values ? . [ 0 ] ? . journal_mode === "off" ) {
throw error ;
logger . warn ( "[Electron] Database opened in read-only mode, attempting to fix..." ) ;
// Try to close and reopen with explicit permissions
await db . closeConnection ( ) ;
const newDb = await sqlitePlugin . createConnection ( {
. . . connectionOptions ,
mode : "rwc" ,
readOnly : false
} ) ;
if ( ! newDb || typeof newDb !== 'object' ) {
throw new Error ( ` Failed to create new database connection - invalid response. Path used: ${ dbPath } ` ) ;
}
logger . info ( "[Electron] Reopened database connection" ) ;
return newDb ;
}
} catch ( queryError ) {
logger . error ( "[Electron] Error verifying database connection:" , queryError ) ;
// If we can't query, try to close and reopen
try {
await db . closeConnection ( ) ;
} catch ( closeError ) {
logger . warn ( "[Electron] Error closing failed connection:" , closeError ) ;
}
// Try one more time with basic options
const retryDb = await sqlitePlugin . createConnection ( {
database : dbPath ,
version : 1 ,
readOnly : false
} ) ;
if ( ! retryDb || typeof retryDb !== 'object' ) {
throw new Error ( ` Failed to create database connection after retry. Path used: ${ dbPath } ` ) ;
}
return retryDb ;
}
}
sqliteInitialized = true ;
logger . info ( "[Electron] SQLite plugin initialized successfully" ) ;
logger . info ( "[Electron] SQLite plugin initialized successfully" ) ;
return db ;
} catch ( error ) {
} catch ( error ) {
logger . error ( "[Electron] Failed to initialize SQLite plugin:" , error ) ;
logger . error ( "[Electron] Failed to initialize SQLite plugin:" , error ) ;
throw error ;
throw error ;
} finally {
sqliteInitializationPromise = null ;
}
}
} ) ( ) ;
return sqliteInitializationPromise ;
}
}
// Initialize app when ready
// Initialize app when ready
@ -311,7 +336,7 @@ app.whenReady().then(async () => {
} ) ;
} ) ;
// Check if running in dev mode
// Check if running in dev mode
const isDev = process . argv . includes ( "--inspect" ) ;
// const isDev = process.argv.includes("--inspect");
function createWindow ( ) : BrowserWindow {
function createWindow ( ) : BrowserWindow {
// Resolve preload path based on environment
// Resolve preload path based on environment
@ -364,11 +389,11 @@ function createWindow(): BrowserWindow {
} ) ;
} ) ;
// Handle window errors
// Handle window errors
mainWindow . webContents . on ( 'render-process-gone' , ( event , details ) = > {
mainWindow . webContents . on ( 'render-process-gone' , ( _ event, details ) = > {
logger . error ( "[Electron] Render process gone:" , details ) ;
logger . error ( "[Electron] Render process gone:" , details ) ;
} ) ;
} ) ;
mainWindow . webContents . on ( 'did-fail-load' , ( event , errorCode , errorDescription ) = > {
mainWindow . webContents . on ( 'did-fail-load' , ( _ event, errorCode , errorDescription ) = > {
logger . error ( "[Electron] Page failed to load:" , errorCode , errorDescription ) ;
logger . error ( "[Electron] Page failed to load:" , errorCode , errorDescription ) ;
logger . error ( "[Electron] Failed URL:" , mainWindow . webContents . getURL ( ) ) ;
logger . error ( "[Electron] Failed URL:" , mainWindow . webContents . getURL ( ) ) ;
} ) ;
} ) ;
@ -391,7 +416,7 @@ function createWindow(): BrowserWindow {
logger . info ( "[Electron] Using file URL:" , fileUrl ) ;
logger . info ( "[Electron] Using file URL:" , fileUrl ) ;
// Load the index.html with retry logic
// Load the index.html with retry logic
const loadIndexHtml = async ( retryCount = 0 ) = > {
const loadIndexHtml = async ( retryCount = 0 ) : Promise < void > = > {
try {
try {
if ( mainWindow . isDestroyed ( ) ) {
if ( mainWindow . isDestroyed ( ) ) {
logger . error ( "[Electron] Window was destroyed before loading index.html" ) ;
logger . error ( "[Electron] Window was destroyed before loading index.html" ) ;
@ -453,7 +478,7 @@ function createWindow(): BrowserWindow {
} ;
} ;
// Start loading the index.html
// Start loading the index.html
loadIndexHtml ( ) . catch ( error = > {
loadIndexHtml ( ) . catch ( ( error : unknown ) = > {
logger . error ( "[Electron] Fatal error loading index.html:" , error ) ;
logger . error ( "[Electron] Fatal error loading index.html:" , error ) ;
} ) ;
} ) ;
@ -502,6 +527,13 @@ ipcMain.handle("sqlite-echo", async (_event, value) => {
ipcMain . handle ( "sqlite-create-connection" , async ( _event , options ) = > {
ipcMain . handle ( "sqlite-create-connection" , async ( _event , options ) = > {
try {
try {
// Ensure database is initialized
await initializeSQLite ( ) ;
if ( ! dbPath ) {
throw new Error ( "Database path not initialized" ) ;
}
// Override any provided database path with our resolved path
// Override any provided database path with our resolved path
const connectionOptions = {
const connectionOptions = {
. . . options ,
. . . options ,
@ -531,19 +563,7 @@ ipcMain.handle("sqlite-create-connection", async (_event, options) => {
}
}
} catch ( queryError ) {
} catch ( queryError ) {
logger . error ( "[Electron] Error verifying connection:" , queryError ) ;
logger . error ( "[Electron] Error verifying connection:" , queryError ) ;
// If verification fails, try a simpler connection
throw queryError ;
await result . closeConnection ( ) . catch ( ( ) = > { } ) ;
const retryResult = await sqlitePlugin . createConnection ( {
database : dbPath ,
version : 1 ,
readOnly : false
} ) ;
if ( ! retryResult || typeof retryResult !== 'object' ) {
throw new Error ( "Failed to create database connection after retry" ) ;
}
return retryResult ;
}
}
logger . info ( "[Electron] Database connection created successfully" ) ;
logger . info ( "[Electron] Database connection created successfully" ) ;