fix: repaired rebuild from filesystem
This commit is contained in:
@@ -397,16 +397,21 @@ export class MetaEngine extends EventEmitter {
|
|||||||
if (projectMetadataFileExists) {
|
if (projectMetadataFileExists) {
|
||||||
await this.loadProjectMetadata();
|
await this.loadProjectMetadata();
|
||||||
|
|
||||||
// If project.json has a dataPath, sync it back to the database
|
// Keep dataPath authoritative in database (selected folder path on create/open).
|
||||||
if (this.projectMetadata?.dataPath !== undefined) {
|
// If project.json has a stale dataPath, update project.json from database.
|
||||||
const projectData = await this.fetchProjectFromDatabase();
|
const projectData = await this.fetchProjectFromDatabase();
|
||||||
if (projectData && projectData.dataPath !== this.projectMetadata.dataPath) {
|
if (!projectData) {
|
||||||
const db = getDatabase().getLocal();
|
throw new Error(`Project not found in database: ${this.currentProjectId}`);
|
||||||
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)'}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
} else {
|
||||||
// No file exists, fetch project data from database and create file
|
// No file exists, fetch project data from database and create file
|
||||||
|
|||||||
@@ -236,10 +236,7 @@ export function registerIpcHandlers(): void {
|
|||||||
const dataDir = projectEngine.getDataDir(project.id, project.dataPath);
|
const dataDir = projectEngine.getDataDir(project.id, project.dataPath);
|
||||||
engine.setProjectContext(project.id, dataDir);
|
engine.setProjectContext(project.id, dataDir);
|
||||||
}
|
}
|
||||||
// Fire and forget - don't await, let it run in background
|
return engine.rebuildDatabaseFromFiles();
|
||||||
engine.rebuildDatabaseFromFiles().catch(err => {
|
|
||||||
console.error('Post rebuild failed:', err);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
safeHandle('posts:search', async (_, query: string) => {
|
safeHandle('posts:search', async (_, query: string) => {
|
||||||
@@ -305,10 +302,7 @@ export function registerIpcHandlers(): void {
|
|||||||
const dataDir = projectEngine.getDataDir(project.id, project.dataPath);
|
const dataDir = projectEngine.getDataDir(project.id, project.dataPath);
|
||||||
engine.setProjectContext(project.id, dataDir);
|
engine.setProjectContext(project.id, dataDir);
|
||||||
}
|
}
|
||||||
// Fire and forget - let it run as a background task
|
return engine.reindexText();
|
||||||
engine.reindexText().catch(err => {
|
|
||||||
console.error('Text reindex failed:', err);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============ Media Handlers ============
|
// ============ Media Handlers ============
|
||||||
@@ -481,18 +475,12 @@ export function registerIpcHandlers(): void {
|
|||||||
// This ensures all project data lives in the same location for backup
|
// This ensures all project data lives in the same location for backup
|
||||||
engine.setProjectContext(project.id, dataDir, dataDir);
|
engine.setProjectContext(project.id, dataDir, dataDir);
|
||||||
}
|
}
|
||||||
// Fire and forget - don't await, let it run in background
|
return engine.rebuildDatabaseFromFiles();
|
||||||
engine.rebuildDatabaseFromFiles().catch(err => {
|
|
||||||
console.error('Media rebuild failed:', err);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
safeHandle('media:reindexText', async () => {
|
safeHandle('media:reindexText', async () => {
|
||||||
const engine = getMediaEngine();
|
const engine = getMediaEngine();
|
||||||
// Fire and forget - don't await, let it run in background
|
return engine.reindexText();
|
||||||
engine.reindexText().catch(err => {
|
|
||||||
console.error('Media text reindex failed:', err);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
safeHandle('media:getThumbnail', async (_, id: string, size?: 'small' | 'medium' | 'large') => {
|
safeHandle('media:getThumbnail', async (_, id: string, size?: 'small' | 'medium' | 'large') => {
|
||||||
|
|||||||
@@ -242,20 +242,31 @@ const App: React.FC = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
unsubscribers.push(
|
unsubscribers.push(
|
||||||
window.electronAPI?.on('menu:rebuildDatabase', () => {
|
window.electronAPI?.on('menu:rebuildDatabase', async () => {
|
||||||
// Fire and forget - the handlers return immediately now
|
try {
|
||||||
window.electronAPI?.posts.rebuildFromFiles();
|
await Promise.all([
|
||||||
window.electronAPI?.media.rebuildFromFiles();
|
window.electronAPI?.posts.rebuildFromFiles(),
|
||||||
// Also regenerate missing thumbnails after media rebuild
|
window.electronAPI?.media.rebuildFromFiles(),
|
||||||
window.electronAPI?.media.regenerateMissingThumbnails();
|
]);
|
||||||
|
await window.electronAPI?.media.regenerateMissingThumbnails();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Database rebuild failed:', error);
|
||||||
|
showToast.error('Database rebuild failed');
|
||||||
|
}
|
||||||
}) || (() => {})
|
}) || (() => {})
|
||||||
);
|
);
|
||||||
|
|
||||||
unsubscribers.push(
|
unsubscribers.push(
|
||||||
window.electronAPI?.on('menu:reindexText', () => {
|
window.electronAPI?.on('menu:reindexText', async () => {
|
||||||
// Fire and forget - runs as background tasks
|
try {
|
||||||
window.electronAPI?.posts.reindexText();
|
await Promise.all([
|
||||||
window.electronAPI?.media.reindexText();
|
window.electronAPI?.posts.reindexText(),
|
||||||
|
window.electronAPI?.media.reindexText(),
|
||||||
|
]);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Text reindex failed:', error);
|
||||||
|
showToast.error('Text reindex failed');
|
||||||
|
}
|
||||||
}) || (() => {})
|
}) || (() => {})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -731,19 +731,19 @@ describe('MetaEngine', () => {
|
|||||||
expect(metaDir).toContain('/custom/data/path');
|
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();
|
const metaDir = metaEngine.getMetaDir();
|
||||||
mockFiles.set(normalizePath(`${metaDir}/project.json`), JSON.stringify({
|
mockFiles.set(normalizePath(`${metaDir}/project.json`), JSON.stringify({
|
||||||
name: 'Project',
|
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 = {
|
mockProject = {
|
||||||
id: 'test-project',
|
id: 'test-project',
|
||||||
name: 'Project',
|
name: 'Project',
|
||||||
description: null,
|
description: null,
|
||||||
dataPath: null,
|
dataPath: '/new/path/from/database',
|
||||||
slug: 'project',
|
slug: 'project',
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
@@ -752,8 +752,11 @@ describe('MetaEngine', () => {
|
|||||||
|
|
||||||
await metaEngine.syncOnStartup();
|
await metaEngine.syncOnStartup();
|
||||||
|
|
||||||
// Should have synced (database update called)
|
const savedProjectJson = mockFiles.get(normalizePath(`${metaDir}/project.json`));
|
||||||
expect(mockLocalDb.select).toHaveBeenCalled();
|
expect(savedProjectJson).toBeDefined();
|
||||||
|
const parsed = JSON.parse(savedProjectJson!);
|
||||||
|
expect(parsed.dataPath).toBe('/new/path/from/database');
|
||||||
|
expect(mockLocalDb.update).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ const mockPostEngine = {
|
|||||||
isSlugAvailable: vi.fn(),
|
isSlugAvailable: vi.fn(),
|
||||||
generateUniqueSlug: vi.fn(),
|
generateUniqueSlug: vi.fn(),
|
||||||
rebuildDatabaseFromFiles: vi.fn(),
|
rebuildDatabaseFromFiles: vi.fn(),
|
||||||
|
reindexText: vi.fn(),
|
||||||
searchPosts: vi.fn(),
|
searchPosts: vi.fn(),
|
||||||
getPostsFiltered: vi.fn(),
|
getPostsFiltered: vi.fn(),
|
||||||
getAvailableTags: vi.fn(),
|
getAvailableTags: vi.fn(),
|
||||||
@@ -629,6 +630,28 @@ describe('IPC Handlers', () => {
|
|||||||
expect(result).toEqual(linkedPosts);
|
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 ============
|
// ============ Media Handlers ============
|
||||||
@@ -790,6 +813,27 @@ describe('IPC Handlers', () => {
|
|||||||
expect(result).toEqual(thumbnailDataUrl);
|
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 ============
|
// ============ Meta Handlers ============
|
||||||
|
|||||||
Reference in New Issue
Block a user