fix: rebuild database for media also rebuilds post-to-media linkage
This commit is contained in:
@@ -6,7 +6,7 @@ import * as crypto from 'crypto';
|
|||||||
import { eq, and, gte, lte, lt, desc } from 'drizzle-orm';
|
import { eq, and, gte, lte, lt, desc } from 'drizzle-orm';
|
||||||
import { app } from 'electron';
|
import { app } from 'electron';
|
||||||
import { getDatabase } from '../database';
|
import { getDatabase } from '../database';
|
||||||
import { media, Media, NewMedia } from '../database/schema';
|
import { media, Media, NewMedia, postMedia } from '../database/schema';
|
||||||
|
|
||||||
// Thumbnail sizes
|
// Thumbnail sizes
|
||||||
const THUMBNAIL_SIZES = {
|
const THUMBNAIL_SIZES = {
|
||||||
@@ -769,6 +769,10 @@ export class MediaEngine extends EventEmitter {
|
|||||||
console.log(`Deleted ${existingMedia.length} existing media record(s) for project ${this.currentProjectId}`);
|
console.log(`Deleted ${existingMedia.length} existing media record(s) for project ${this.currentProjectId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Also delete all post-media links for the current project
|
||||||
|
await db.delete(postMedia).where(eq(postMedia.projectId, this.currentProjectId));
|
||||||
|
console.log(`Deleted post-media links for project ${this.currentProjectId}`);
|
||||||
|
|
||||||
onProgress(5, 'Scanning media directory...');
|
onProgress(5, 'Scanning media directory...');
|
||||||
|
|
||||||
// Recursively find all .meta files in the media directory tree
|
// Recursively find all .meta files in the media directory tree
|
||||||
@@ -834,6 +838,20 @@ export class MediaEngine extends EventEmitter {
|
|||||||
checksum,
|
checksum,
|
||||||
tags: JSON.stringify(metadata.tags),
|
tags: JSON.stringify(metadata.tags),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Insert post-media links based on linkedPostIds from sidecar
|
||||||
|
const linkedPostIds = metadata.linkedPostIds || [];
|
||||||
|
for (let j = 0; j < linkedPostIds.length; j++) {
|
||||||
|
const postId = linkedPostIds[j];
|
||||||
|
await db.insert(postMedia).values({
|
||||||
|
id: uuidv4(),
|
||||||
|
projectId: this.currentProjectId,
|
||||||
|
postId,
|
||||||
|
mediaId: metadata.id,
|
||||||
|
sortOrder: j,
|
||||||
|
createdAt: new Date(),
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Media file not found for sidecar: ${sidecarPath}`, error);
|
console.error(`Media file not found for sidecar: ${sidecarPath}`, error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,8 +27,14 @@ import { MediaEngine, MediaData } from '../../src/main/engine/MediaEngine';
|
|||||||
|
|
||||||
// Create mock data stores
|
// Create mock data stores
|
||||||
const mockMedia = new Map<string, any>();
|
const mockMedia = new Map<string, any>();
|
||||||
|
const mockPostMedia = new Map<string, any>();
|
||||||
const mockFiles = new Map<string, Buffer | string>();
|
const mockFiles = new Map<string, Buffer | string>();
|
||||||
|
|
||||||
|
// Track database operations for testing
|
||||||
|
let mediaDeleteCalled = false;
|
||||||
|
let postMediaDeleteCalled = false;
|
||||||
|
let postMediaInserts: any[] = [];
|
||||||
|
|
||||||
// Create chainable mock for Drizzle ORM
|
// Create chainable mock for Drizzle ORM
|
||||||
function createSelectChain() {
|
function createSelectChain() {
|
||||||
return {
|
return {
|
||||||
@@ -47,9 +53,15 @@ function createSelectChain() {
|
|||||||
function createDrizzleMock() {
|
function createDrizzleMock() {
|
||||||
return {
|
return {
|
||||||
select: vi.fn(() => createSelectChain()),
|
select: vi.fn(() => createSelectChain()),
|
||||||
insert: vi.fn(() => ({
|
insert: vi.fn((table: any) => ({
|
||||||
values: vi.fn((data: any) => {
|
values: vi.fn((data: any) => {
|
||||||
if (data && data.id) {
|
// Check if this is an insert to postMedia table by looking at inserted data structure
|
||||||
|
if (data && data.postId && data.mediaId && !data.filename) {
|
||||||
|
// This is a postMedia insert
|
||||||
|
mockPostMedia.set(data.id, data);
|
||||||
|
postMediaInserts.push(data);
|
||||||
|
} else if (data && data.id) {
|
||||||
|
// This is a media insert
|
||||||
mockMedia.set(data.id, data);
|
mockMedia.set(data.id, data);
|
||||||
}
|
}
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
@@ -60,8 +72,20 @@ function createDrizzleMock() {
|
|||||||
where: vi.fn(() => Promise.resolve()),
|
where: vi.fn(() => Promise.resolve()),
|
||||||
})),
|
})),
|
||||||
})),
|
})),
|
||||||
delete: vi.fn(() => ({
|
delete: vi.fn((table: any) => ({
|
||||||
where: vi.fn(() => Promise.resolve()),
|
where: vi.fn((condition: any) => {
|
||||||
|
// Track which table is being deleted from
|
||||||
|
// We detect by the condition - if it involves projectId and no filename, it's likely postMedia
|
||||||
|
// This is a simplified heuristic for testing
|
||||||
|
if (table && table.postId !== undefined) {
|
||||||
|
postMediaDeleteCalled = true;
|
||||||
|
mockPostMedia.clear();
|
||||||
|
} else {
|
||||||
|
mediaDeleteCalled = true;
|
||||||
|
mockMedia.clear();
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
}),
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -135,7 +159,11 @@ describe('MediaEngine', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
mockMedia.clear();
|
mockMedia.clear();
|
||||||
|
mockPostMedia.clear();
|
||||||
mockFiles.clear();
|
mockFiles.clear();
|
||||||
|
mediaDeleteCalled = false;
|
||||||
|
postMediaDeleteCalled = false;
|
||||||
|
postMediaInserts = [];
|
||||||
resetMockCounters();
|
resetMockCounters();
|
||||||
|
|
||||||
// Reset the mock implementations
|
// Reset the mock implementations
|
||||||
@@ -519,4 +547,137 @@ describe('MediaEngine', () => {
|
|||||||
expect(decemberPath).toMatch(/[/\\]2024[/\\]12[/\\]/);
|
expect(decemberPath).toMatch(/[/\\]2024[/\\]12[/\\]/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('rebuildDatabaseFromFiles', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mediaEngine.setProjectContext('test-project');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete post-media links for the project during rebuild', async () => {
|
||||||
|
const fs = await import('fs/promises');
|
||||||
|
|
||||||
|
// Mock readdir to return media with sidecar
|
||||||
|
vi.mocked(fs.readdir).mockImplementation(async (dir: string, options?: any) => {
|
||||||
|
if (typeof dir === 'string' && dir.includes('media')) {
|
||||||
|
return [
|
||||||
|
{ name: 'media-1.jpg', isFile: () => true, isDirectory: () => false },
|
||||||
|
{ name: 'media-1.jpg.meta', isFile: () => true, isDirectory: () => false },
|
||||||
|
] as any;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set up sidecar file with linkedPostIds
|
||||||
|
const sidecarContent = `id: media-1
|
||||||
|
originalName: test-image.jpg
|
||||||
|
mimeType: image/jpeg
|
||||||
|
size: 1024
|
||||||
|
createdAt: 2024-01-15T10:00:00.000Z
|
||||||
|
updatedAt: 2024-01-15T10:00:00.000Z
|
||||||
|
linkedPostIds: ["post-1", "post-2"]`;
|
||||||
|
mockFiles.set('/mock/userData/projects/test-project/media/media-1.jpg.meta', sidecarContent);
|
||||||
|
mockFiles.set('/mock/userData/projects/test-project/media/media-1.jpg', Buffer.from('image-data'));
|
||||||
|
|
||||||
|
await mediaEngine.rebuildDatabaseFromFiles();
|
||||||
|
|
||||||
|
expect(postMediaDeleteCalled).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should insert post-media links based on linkedPostIds from sidecar files', async () => {
|
||||||
|
const fs = await import('fs/promises');
|
||||||
|
|
||||||
|
// Mock readdir to simulate directory traversal
|
||||||
|
vi.mocked(fs.readdir).mockImplementation(async (dir: string, options?: any) => {
|
||||||
|
if (typeof dir === 'string' && dir.includes('media')) {
|
||||||
|
return [
|
||||||
|
{ name: 'media-1.jpg', isFile: () => true, isDirectory: () => false },
|
||||||
|
{ name: 'media-1.jpg.meta', isFile: () => true, isDirectory: () => false },
|
||||||
|
] as any;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set up sidecar file with linkedPostIds
|
||||||
|
const sidecarContent = `id: media-1
|
||||||
|
originalName: test-image.jpg
|
||||||
|
mimeType: image/jpeg
|
||||||
|
size: 1024
|
||||||
|
createdAt: 2024-01-15T10:00:00.000Z
|
||||||
|
updatedAt: 2024-01-15T10:00:00.000Z
|
||||||
|
linkedPostIds: ["post-1", "post-2"]`;
|
||||||
|
mockFiles.set('/mock/userData/projects/test-project/media/media-1.jpg.meta', sidecarContent);
|
||||||
|
mockFiles.set('/mock/userData/projects/test-project/media/media-1.jpg', Buffer.from('image-data'));
|
||||||
|
|
||||||
|
await mediaEngine.rebuildDatabaseFromFiles();
|
||||||
|
|
||||||
|
// Should have inserted 2 post-media links
|
||||||
|
expect(postMediaInserts).toHaveLength(2);
|
||||||
|
expect(postMediaInserts[0].postId).toBe('post-1');
|
||||||
|
expect(postMediaInserts[0].mediaId).toBe('media-1');
|
||||||
|
expect(postMediaInserts[1].postId).toBe('post-2');
|
||||||
|
expect(postMediaInserts[1].mediaId).toBe('media-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle media without linkedPostIds', async () => {
|
||||||
|
const fs = await import('fs/promises');
|
||||||
|
|
||||||
|
vi.mocked(fs.readdir).mockImplementation(async (dir: string, options?: any) => {
|
||||||
|
if (typeof dir === 'string' && dir.includes('media')) {
|
||||||
|
return [
|
||||||
|
{ name: 'media-1.jpg', isFile: () => true, isDirectory: () => false },
|
||||||
|
{ name: 'media-1.jpg.meta', isFile: () => true, isDirectory: () => false },
|
||||||
|
] as any;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sidecar without linkedPostIds
|
||||||
|
const sidecarContent = `id: media-1
|
||||||
|
originalName: test-image.jpg
|
||||||
|
mimeType: image/jpeg
|
||||||
|
size: 1024
|
||||||
|
createdAt: 2024-01-15T10:00:00.000Z
|
||||||
|
updatedAt: 2024-01-15T10:00:00.000Z`;
|
||||||
|
mockFiles.set('/mock/userData/projects/test-project/media/media-1.jpg.meta', sidecarContent);
|
||||||
|
mockFiles.set('/mock/userData/projects/test-project/media/media-1.jpg', Buffer.from('image-data'));
|
||||||
|
|
||||||
|
await mediaEngine.rebuildDatabaseFromFiles();
|
||||||
|
|
||||||
|
// Should not insert any post-media links
|
||||||
|
expect(postMediaInserts).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set correct sortOrder for post-media links', async () => {
|
||||||
|
const fs = await import('fs/promises');
|
||||||
|
|
||||||
|
vi.mocked(fs.readdir).mockImplementation(async (dir: string, options?: any) => {
|
||||||
|
if (typeof dir === 'string' && dir.includes('media')) {
|
||||||
|
return [
|
||||||
|
{ name: 'media-1.jpg', isFile: () => true, isDirectory: () => false },
|
||||||
|
{ name: 'media-1.jpg.meta', isFile: () => true, isDirectory: () => false },
|
||||||
|
] as any;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sidecar with multiple linked posts
|
||||||
|
const sidecarContent = `id: media-1
|
||||||
|
originalName: test-image.jpg
|
||||||
|
mimeType: image/jpeg
|
||||||
|
size: 1024
|
||||||
|
createdAt: 2024-01-15T10:00:00.000Z
|
||||||
|
updatedAt: 2024-01-15T10:00:00.000Z
|
||||||
|
linkedPostIds: ["post-a", "post-b", "post-c"]`;
|
||||||
|
mockFiles.set('/mock/userData/projects/test-project/media/media-1.jpg.meta', sidecarContent);
|
||||||
|
mockFiles.set('/mock/userData/projects/test-project/media/media-1.jpg', Buffer.from('image-data'));
|
||||||
|
|
||||||
|
await mediaEngine.rebuildDatabaseFromFiles();
|
||||||
|
|
||||||
|
// Verify sortOrder is set correctly (0, 1, 2)
|
||||||
|
expect(postMediaInserts).toHaveLength(3);
|
||||||
|
expect(postMediaInserts[0].sortOrder).toBe(0);
|
||||||
|
expect(postMediaInserts[1].sortOrder).toBe(1);
|
||||||
|
expect(postMediaInserts[2].sortOrder).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user