super-productivity/electron/simple-store.ts
Johannes Millan bbda6b1694 fix(electron): handle legacy simpleSettings directory causing EISDIR crash
In older app versions simpleSettings was stored as a directory. Reading
it as a file threw EISDIR which was caught/logged but saveSimpleStore()
would subsequently fail with an uncaught EISDIR when writing to the
same path.

- Catch EISDIR in loadSimpleStoreAll and rename legacy dir to .bak
- Catch EISDIR in saveSimpleStore, remove dir and retry write
- Replace module-level DATA_PATH with lazy getDataPath() to fix
  stale path when --user-data-dir or Snap overrides userData
- Remove debug console.log that leaked settings to stdout
- Replace console.error with electron-log for consistency

Closes #4791
2026-02-25 15:27:57 +01:00

57 lines
2.3 KiB
TypeScript

import { promises as fs } from 'fs';
import { app } from 'electron';
import * as path from 'path';
import { error, log } from 'electron-log/main';
// Lazy getter: userData path can be changed by start-app.ts (--user-data-dir, Snap)
// before any store function is called, so we must not cache it at import time.
const getDataPath = (): string => path.join(app.getPath('userData'), 'simpleSettings');
type SimpleStoreData = { [key: string]: unknown };
export const saveSimpleStore = async (dataKey = 'main', data: unknown): Promise<void> => {
const prevData = await loadSimpleStoreAll();
const dataPath = getDataPath();
const json = JSON.stringify({ ...prevData, [dataKey]: data });
try {
await fs.writeFile(dataPath, json, { encoding: 'utf8' });
} catch (e: unknown) {
const nodeErr = e as NodeJS.ErrnoException;
if (nodeErr.code === 'EISDIR') {
// In older app versions simpleSettings was stored as a directory.
// Remove the legacy directory so we can write the file.
log('simpleSettings is a directory, removing for file-based storage');
await fs.rm(dataPath, { recursive: true });
await fs.writeFile(dataPath, json, { encoding: 'utf8' });
} else {
error('Failed to save simple store:', e);
throw e;
}
}
};
export const loadSimpleStoreAll = async (): Promise<SimpleStoreData> => {
try {
const data = await fs.readFile(getDataPath(), { encoding: 'utf8' });
return JSON.parse(data);
} catch (e: unknown) {
const nodeErr = e as NodeJS.ErrnoException;
if (nodeErr.code === 'EISDIR') {
// In older app versions simpleSettings was stored as a directory.
// Reading it as a file threw EISDIR which was caught/logged but
// saveSimpleStore() would subsequently fail with an uncaught EISDIR
// when writing to the same path. Rename to .bak to unblock writes.
log('simpleSettings is a directory (legacy), renaming to .bak');
try {
await fs.rename(getDataPath(), getDataPath() + '.bak');
} catch (renameErr) {
error('Failed to rename legacy simpleSettings directory:', renameErr);
}
} else if (nodeErr.code !== 'ENOENT') {
// ENOENT is expected on first run — only log unexpected errors
error('Failed to load simple store:', e);
}
return {};
}
};