bugfix hash dup
This commit is contained in:
@@ -63,6 +63,10 @@ export class DuplicateIndex {
|
|||||||
private byHash: Map<string, Set<string>> = new Map();
|
private byHash: Map<string, Set<string>> = new Map();
|
||||||
private byTitle: Map<string, Set<string>> = new Map();
|
private byTitle: Map<string, Set<string>> = new Map();
|
||||||
|
|
||||||
|
// Track paths by midhash256 (for hash duplicates from events)
|
||||||
|
// This detects duplicates directly from the event stream without Redis lookup
|
||||||
|
private pathsByMidHash: Map<string, Set<string>> = new Map();
|
||||||
|
|
||||||
// File info storage: hashId -> FileInfo
|
// File info storage: hashId -> FileInfo
|
||||||
private fileInfo: Map<string, FileInfo> = new Map();
|
private fileInfo: Map<string, FileInfo> = new Map();
|
||||||
|
|
||||||
@@ -73,6 +77,34 @@ export class DuplicateIndex {
|
|||||||
logger.info('DuplicateIndex initialized');
|
logger.info('DuplicateIndex initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a path to the midhash tracking (for hash duplicates from events)
|
||||||
|
*/
|
||||||
|
addPath(midhash256: string, path: string): void {
|
||||||
|
const pathSet = this.pathsByMidHash.get(midhash256) ?? new Set();
|
||||||
|
pathSet.add(path);
|
||||||
|
this.pathsByMidHash.set(midhash256, pathSet);
|
||||||
|
this.lastUpdated = new Date();
|
||||||
|
|
||||||
|
if (pathSet.size > 1) {
|
||||||
|
logger.info(`Hash duplicate detected: ${midhash256} has ${pathSet.size} paths`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a path from the midhash tracking
|
||||||
|
*/
|
||||||
|
removePath(midhash256: string, path: string): void {
|
||||||
|
const pathSet = this.pathsByMidHash.get(midhash256);
|
||||||
|
if (pathSet) {
|
||||||
|
pathSet.delete(path);
|
||||||
|
if (pathSet.size === 0) {
|
||||||
|
this.pathsByMidHash.delete(midhash256);
|
||||||
|
}
|
||||||
|
this.lastUpdated = new Date();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add or update a file in the index
|
* Add or update a file in the index
|
||||||
*/
|
*/
|
||||||
@@ -194,13 +226,57 @@ export class DuplicateIndex {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all hash-based duplicates (files with same SHA-256)
|
* Get path-based duplicates (files with same midhash256 from events)
|
||||||
|
* These are detected directly from the event stream without Redis lookup
|
||||||
|
*/
|
||||||
|
getPathDuplicates(): DuplicateGroup[] {
|
||||||
|
const groups: DuplicateGroup[] = [];
|
||||||
|
|
||||||
|
for (const [midhash256, paths] of this.pathsByMidHash) {
|
||||||
|
if (paths.size > 1) {
|
||||||
|
// Create FileInfo objects with just the path (minimal info from events)
|
||||||
|
const files: FileInfo[] = Array.from(paths).map(path => ({
|
||||||
|
hashId: midhash256,
|
||||||
|
filePath: path,
|
||||||
|
}));
|
||||||
|
groups.push({
|
||||||
|
key: midhash256,
|
||||||
|
files: files.sort((a, b) => a.filePath.localeCompare(b.filePath)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by number of duplicates (descending)
|
||||||
|
return groups.sort((a, b) => b.files.length - a.files.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all hash-based duplicates (combines path-based midhash256 and SHA-256 duplicates)
|
||||||
|
* Path duplicates are detected from events, SHA-256 duplicates from fullhash plugin
|
||||||
*/
|
*/
|
||||||
getHashDuplicates(): DuplicateGroup[] {
|
getHashDuplicates(): DuplicateGroup[] {
|
||||||
const groups: DuplicateGroup[] = [];
|
const groups: DuplicateGroup[] = [];
|
||||||
|
const seenKeys = new Set<string>();
|
||||||
|
|
||||||
|
// First, add path-based duplicates (from events, using midhash256)
|
||||||
|
for (const [midhash256, paths] of this.pathsByMidHash) {
|
||||||
|
if (paths.size > 1) {
|
||||||
|
seenKeys.add(midhash256);
|
||||||
|
const files: FileInfo[] = Array.from(paths).map(path => ({
|
||||||
|
hashId: midhash256,
|
||||||
|
filePath: path,
|
||||||
|
}));
|
||||||
|
groups.push({
|
||||||
|
key: midhash256,
|
||||||
|
files: files.sort((a, b) => a.filePath.localeCompare(b.filePath)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then, add SHA-256 duplicates (from fullhash plugin)
|
||||||
|
// Only add if not already covered by path duplicates
|
||||||
for (const [hash, hashIds] of this.byHash) {
|
for (const [hash, hashIds] of this.byHash) {
|
||||||
if (hashIds.size > 1) {
|
if (hashIds.size > 1 && !seenKeys.has(hash)) {
|
||||||
const files = this.getFileInfos(hashIds);
|
const files = this.getFileInfos(hashIds);
|
||||||
groups.push({
|
groups.push({
|
||||||
key: hash,
|
key: hash,
|
||||||
@@ -253,13 +329,20 @@ export class DuplicateIndex {
|
|||||||
getStats(): DuplicateStats {
|
getStats(): DuplicateStats {
|
||||||
const hashDuplicates = this.getHashDuplicates();
|
const hashDuplicates = this.getHashDuplicates();
|
||||||
const titleDuplicates = this.getTitleDuplicates();
|
const titleDuplicates = this.getTitleDuplicates();
|
||||||
|
const pathDuplicates = this.getPathDuplicates();
|
||||||
|
|
||||||
|
// Count unique paths tracked in pathsByMidHash
|
||||||
|
let pathsTracked = 0;
|
||||||
|
for (const paths of this.pathsByMidHash.values()) {
|
||||||
|
pathsTracked += paths.size;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hashGroupCount: hashDuplicates.length,
|
hashGroupCount: hashDuplicates.length,
|
||||||
hashFileCount: hashDuplicates.reduce((sum, g) => sum + g.files.length, 0),
|
hashFileCount: hashDuplicates.reduce((sum, g) => sum + g.files.length, 0),
|
||||||
titleGroupCount: titleDuplicates.length,
|
titleGroupCount: titleDuplicates.length,
|
||||||
titleFileCount: titleDuplicates.reduce((sum, g) => sum + g.files.length, 0),
|
titleFileCount: titleDuplicates.reduce((sum, g) => sum + g.files.length, 0),
|
||||||
totalFilesTracked: this.fileInfo.size,
|
totalFilesTracked: Math.max(this.fileInfo.size, pathsTracked),
|
||||||
lastUpdated: this.lastUpdated.toISOString(),
|
lastUpdated: this.lastUpdated.toISOString(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -282,6 +365,7 @@ export class DuplicateIndex {
|
|||||||
clear(): void {
|
clear(): void {
|
||||||
this.byHash.clear();
|
this.byHash.clear();
|
||||||
this.byTitle.clear();
|
this.byTitle.clear();
|
||||||
|
this.pathsByMidHash.clear();
|
||||||
this.fileInfo.clear();
|
this.fileInfo.clear();
|
||||||
this.lastUpdated = new Date();
|
this.lastUpdated = new Date();
|
||||||
logger.info('DuplicateIndex cleared');
|
logger.info('DuplicateIndex cleared');
|
||||||
|
|||||||
@@ -53,22 +53,42 @@ async function handleStreamMessage(
|
|||||||
// Direct file events from meta-core
|
// Direct file events from meta-core
|
||||||
case 'add':
|
case 'add':
|
||||||
case 'change': {
|
case 'change': {
|
||||||
// File added or changed - look up by path and update index
|
// File added or changed - track by midhash256 for duplicate detection
|
||||||
// Note: We need to wait for meta-sort to process the file first
|
if (message.midhash256 && message.path) {
|
||||||
// For now, just log - the duplicate index is rebuilt on startup
|
duplicateIndex.addPath(message.midhash256, message.path);
|
||||||
logger.debug(`File ${message.type}: ${message.path}`);
|
logger.debug(`File ${message.type}: ${message.path} (midhash: ${message.midhash256.substring(0, 12)}...)`);
|
||||||
|
|
||||||
|
// Groundwork for title duplicates - try to get metadata if already processed
|
||||||
|
const metadata = await redisClient.getFileByHashId(message.midhash256);
|
||||||
|
if (metadata) {
|
||||||
|
duplicateIndex.addFile(message.midhash256, metadata);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.debug(`File ${message.type}: ${message.path} (no midhash256)`);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'delete': {
|
case 'delete': {
|
||||||
// File deleted - would need to find by path and remove
|
// File deleted - remove from path tracking
|
||||||
logger.debug(`File deleted: ${message.path}`);
|
if (message.midhash256 && message.path) {
|
||||||
|
duplicateIndex.removePath(message.midhash256, message.path);
|
||||||
|
logger.debug(`File deleted: ${message.path} (midhash: ${message.midhash256.substring(0, 12)}...)`);
|
||||||
|
} else {
|
||||||
|
logger.debug(`File deleted: ${message.path} (no midhash256)`);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'rename': {
|
case 'rename': {
|
||||||
// File renamed - treated as delete + add
|
// File renamed - update path tracking
|
||||||
logger.debug(`File renamed: ${message.oldPath} -> ${message.path}`);
|
if (message.midhash256 && message.oldPath && message.path) {
|
||||||
|
duplicateIndex.removePath(message.midhash256, message.oldPath);
|
||||||
|
duplicateIndex.addPath(message.midhash256, message.path);
|
||||||
|
logger.debug(`File renamed: ${message.oldPath} -> ${message.path} (midhash: ${message.midhash256.substring(0, 12)}...)`);
|
||||||
|
} else {
|
||||||
|
logger.debug(`File renamed: ${message.oldPath} -> ${message.path} (no midhash256)`);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+660
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Vendored
+13
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>meta-dup - Duplicate Detection</title>
|
||||||
|
<script type="module" crossorigin src="/assets/index-GT6exvk4.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>␍
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user