polish(desktop): rename userData folder to Backspace with first-launch migration
Electron derived userData from package.json's `@backspace/desktop` name, leaking the monorepo's pnpm scope into ~/Library/Application Support/. Now `app.setName` runs at module load before any userData consumer, and a one-shot migration atomically moves the historical folder to <appData>/Backspace, cleaning the empty @backspace/ parent. Conservative on conflict — never clobbers an existing populated target. EXDEV fallback to recursive copy. Smoke-recovery path flipped back to Backspace.
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
export type MigrationResult =
|
||||
| { kind: 'no-op'; reason: 'old-missing' | 'new-populated' | 'identical' }
|
||||
| { kind: 'migrated'; from: string; to: string }
|
||||
| { kind: 'failed'; error: Error };
|
||||
|
||||
export interface MigrationOptions {
|
||||
oldDir: string;
|
||||
newDir: string;
|
||||
oldParent: string;
|
||||
}
|
||||
|
||||
function dirExists(dir: string): boolean {
|
||||
try {
|
||||
return fs.statSync(dir).isDirectory();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isEmpty(dir: string): boolean {
|
||||
return fs.readdirSync(dir).length === 0;
|
||||
}
|
||||
|
||||
function copyDirRecursive(src: string, dest: string): void {
|
||||
fs.mkdirSync(dest, { recursive: true });
|
||||
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
||||
const s = path.join(src, entry.name);
|
||||
const d = path.join(dest, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
copyDirRecursive(s, d);
|
||||
} else if (entry.isSymbolicLink()) {
|
||||
fs.symlinkSync(fs.readlinkSync(s), d);
|
||||
} else {
|
||||
fs.copyFileSync(s, d);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function migrateUserData(opts: MigrationOptions): MigrationResult {
|
||||
const { oldDir, newDir, oldParent } = opts;
|
||||
|
||||
try {
|
||||
if (path.resolve(oldDir) === path.resolve(newDir)) {
|
||||
return { kind: 'no-op', reason: 'identical' };
|
||||
}
|
||||
if (!dirExists(oldDir)) {
|
||||
return { kind: 'no-op', reason: 'old-missing' };
|
||||
}
|
||||
if (dirExists(newDir)) {
|
||||
if (!isEmpty(newDir)) {
|
||||
return { kind: 'no-op', reason: 'new-populated' };
|
||||
}
|
||||
fs.rmdirSync(newDir);
|
||||
}
|
||||
|
||||
fs.mkdirSync(path.dirname(newDir), { recursive: true });
|
||||
|
||||
try {
|
||||
fs.renameSync(oldDir, newDir);
|
||||
} catch (err) {
|
||||
if ((err as NodeJS.ErrnoException).code !== 'EXDEV') throw err;
|
||||
copyDirRecursive(oldDir, newDir);
|
||||
fs.rmSync(oldDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
try {
|
||||
fs.rmdirSync(oldParent);
|
||||
} catch {
|
||||
// Parent has other children, or doesn't exist — both fine.
|
||||
}
|
||||
|
||||
return { kind: 'migrated', from: oldDir, to: newDir };
|
||||
} catch (err) {
|
||||
return { kind: 'failed', error: err instanceof Error ? err : new Error(String(err)) };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user