fix: repaired rebuild from filesystem

This commit is contained in:
2026-02-16 10:09:14 +01:00
parent d7286ef92f
commit bd964fb284
5 changed files with 93 additions and 42 deletions

View File

@@ -397,16 +397,21 @@ export class MetaEngine extends EventEmitter {
if (projectMetadataFileExists) {
await this.loadProjectMetadata();
// If project.json has a dataPath, sync it back to the database
if (this.projectMetadata?.dataPath !== undefined) {
// Keep dataPath authoritative in database (selected folder path on create/open).
// If project.json has a stale dataPath, update project.json from database.
const projectData = await this.fetchProjectFromDatabase();
if (projectData && projectData.dataPath !== this.projectMetadata.dataPath) {
const db = getDatabase().getLocal();
await db.update(projects)
.set({ dataPath: this.projectMetadata.dataPath || null })
.where(eq(projects.id, this.currentProjectId));
console.log(`[MetaEngine] Synced dataPath from project.json to database: ${this.projectMetadata.dataPath || '(default)'}`);
if (!projectData) {
throw new Error(`Project not found in database: ${this.currentProjectId}`);
}
const databaseDataPath = projectData.dataPath || undefined;
if (this.projectMetadata && this.projectMetadata.dataPath !== databaseDataPath) {
this.projectMetadata = {
...this.projectMetadata,
dataPath: databaseDataPath,
};
await this.saveProjectMetadata();
console.log(`[MetaEngine] Synced dataPath from database to project.json: ${databaseDataPath || '(default)'}`);
}
} else {
// No file exists, fetch project data from database and create file

View File

@@ -236,10 +236,7 @@ export function registerIpcHandlers(): void {
const dataDir = projectEngine.getDataDir(project.id, project.dataPath);
engine.setProjectContext(project.id, dataDir);
}
// Fire and forget - don't await, let it run in background
engine.rebuildDatabaseFromFiles().catch(err => {
console.error('Post rebuild failed:', err);
});
return engine.rebuildDatabaseFromFiles();
});
safeHandle('posts:search', async (_, query: string) => {
@@ -305,10 +302,7 @@ export function registerIpcHandlers(): void {
const dataDir = projectEngine.getDataDir(project.id, project.dataPath);
engine.setProjectContext(project.id, dataDir);
}
// Fire and forget - let it run as a background task
engine.reindexText().catch(err => {
console.error('Text reindex failed:', err);
});
return engine.reindexText();
});
// ============ Media Handlers ============
@@ -481,18 +475,12 @@ export function registerIpcHandlers(): void {
// This ensures all project data lives in the same location for backup
engine.setProjectContext(project.id, dataDir, dataDir);
}
// Fire and forget - don't await, let it run in background
engine.rebuildDatabaseFromFiles().catch(err => {
console.error('Media rebuild failed:', err);
});
return engine.rebuildDatabaseFromFiles();
});
safeHandle('media:reindexText', async () => {
const engine = getMediaEngine();
// Fire and forget - don't await, let it run in background
engine.reindexText().catch(err => {
console.error('Media text reindex failed:', err);
});
return engine.reindexText();
});
safeHandle('media:getThumbnail', async (_, id: string, size?: 'small' | 'medium' | 'large') => {

View File

@@ -242,20 +242,31 @@ const App: React.FC = () => {
);
unsubscribers.push(
window.electronAPI?.on('menu:rebuildDatabase', () => {
// Fire and forget - the handlers return immediately now
window.electronAPI?.posts.rebuildFromFiles();
window.electronAPI?.media.rebuildFromFiles();
// Also regenerate missing thumbnails after media rebuild
window.electronAPI?.media.regenerateMissingThumbnails();
window.electronAPI?.on('menu:rebuildDatabase', async () => {
try {
await Promise.all([
window.electronAPI?.posts.rebuildFromFiles(),
window.electronAPI?.media.rebuildFromFiles(),
]);
await window.electronAPI?.media.regenerateMissingThumbnails();
} catch (error) {
console.error('Database rebuild failed:', error);
showToast.error('Database rebuild failed');
}
}) || (() => {})
);
unsubscribers.push(
window.electronAPI?.on('menu:reindexText', () => {
// Fire and forget - runs as background tasks
window.electronAPI?.posts.reindexText();
window.electronAPI?.media.reindexText();
window.electronAPI?.on('menu:reindexText', async () => {
try {
await Promise.all([
window.electronAPI?.posts.reindexText(),
window.electronAPI?.media.reindexText(),
]);
} catch (error) {
console.error('Text reindex failed:', error);
showToast.error('Text reindex failed');
}
}) || (() => {})
);

View File

@@ -731,19 +731,19 @@ describe('MetaEngine', () => {
expect(metaDir).toContain('/custom/data/path');
});
it('should sync dataPath from project.json to database if different', async () => {
it('should sync dataPath from database to project.json if different', async () => {
const metaDir = metaEngine.getMetaDir();
mockFiles.set(normalizePath(`${metaDir}/project.json`), JSON.stringify({
name: 'Project',
dataPath: '/custom/path/from/file',
dataPath: '/old/path/from/file',
}));
// Database has different or missing dataPath
// Database has the currently selected (authoritative) path
mockProject = {
id: 'test-project',
name: 'Project',
description: null,
dataPath: null,
dataPath: '/new/path/from/database',
slug: 'project',
createdAt: new Date(),
updatedAt: new Date(),
@@ -752,8 +752,11 @@ describe('MetaEngine', () => {
await metaEngine.syncOnStartup();
// Should have synced (database update called)
expect(mockLocalDb.select).toHaveBeenCalled();
const savedProjectJson = mockFiles.get(normalizePath(`${metaDir}/project.json`));
expect(savedProjectJson).toBeDefined();
const parsed = JSON.parse(savedProjectJson!);
expect(parsed.dataPath).toBe('/new/path/from/database');
expect(mockLocalDb.update).not.toHaveBeenCalled();
});
});
});

View File

@@ -48,6 +48,7 @@ const mockPostEngine = {
isSlugAvailable: vi.fn(),
generateUniqueSlug: vi.fn(),
rebuildDatabaseFromFiles: vi.fn(),
reindexText: vi.fn(),
searchPosts: vi.fn(),
getPostsFiltered: vi.fn(),
getAvailableTags: vi.fn(),
@@ -629,6 +630,28 @@ describe('IPC Handlers', () => {
expect(result).toEqual(linkedPosts);
});
});
describe('posts:rebuildFromFiles', () => {
it('should propagate rebuild errors to the caller', async () => {
const rebuildError = new Error('rebuild failed');
mockPostEngine.rebuildDatabaseFromFiles.mockRejectedValue(rebuildError);
mockProjectEngine.getActiveProject.mockResolvedValue(null);
await expect(invokeHandler('posts:rebuildFromFiles')).rejects.toThrow('rebuild failed');
expect(mockPostEngine.rebuildDatabaseFromFiles).toHaveBeenCalled();
});
});
describe('posts:reindexText', () => {
it('should propagate reindex errors to the caller', async () => {
const reindexError = new Error('post reindex failed');
mockPostEngine.reindexText.mockRejectedValue(reindexError);
mockProjectEngine.getActiveProject.mockResolvedValue(null);
await expect(invokeHandler('posts:reindexText')).rejects.toThrow('post reindex failed');
expect(mockPostEngine.reindexText).toHaveBeenCalled();
});
});
});
// ============ Media Handlers ============
@@ -790,6 +813,27 @@ describe('IPC Handlers', () => {
expect(result).toEqual(thumbnailDataUrl);
});
});
describe('media:rebuildFromFiles', () => {
it('should propagate rebuild errors to the caller', async () => {
const rebuildError = new Error('media rebuild failed');
mockMediaEngine.rebuildDatabaseFromFiles.mockRejectedValue(rebuildError);
mockProjectEngine.getActiveProject.mockResolvedValue(null);
await expect(invokeHandler('media:rebuildFromFiles')).rejects.toThrow('media rebuild failed');
expect(mockMediaEngine.rebuildDatabaseFromFiles).toHaveBeenCalled();
});
});
describe('media:reindexText', () => {
it('should propagate reindex errors to the caller', async () => {
const reindexError = new Error('media reindex failed');
mockMediaEngine.reindexText.mockRejectedValue(reindexError);
await expect(invokeHandler('media:reindexText')).rejects.toThrow('media reindex failed');
expect(mockMediaEngine.reindexText).toHaveBeenCalled();
});
});
});
// ============ Meta Handlers ============