diff --git a/PhotoCleaner/ImportTask.cs b/PhotoCleaner/ImportTask.cs index 185bb56..1ab706a 100644 --- a/PhotoCleaner/ImportTask.cs +++ b/PhotoCleaner/ImportTask.cs @@ -146,11 +146,12 @@ private async Task ImportFileAsync( string? sha256 = null; string? sha1 = null; + FileRecord? cached = null; if (database is not null || skipDatabase is not null || trashDatabase is not null) { // ResolveHashesAsync returns cached hashes when size and mtime still match disk. // The cache is keyed by source path because import inserts source paths. - FileRecord? cached = database is null + cached = database is null ? null : await database.GetByPathAsync(file, cancellationToken).ConfigureAwait(false); Log.Debug("Hashing '{FilePath}'", file); @@ -341,20 +342,45 @@ await MediaUtilities // Lookups are by source content hash. if (database is not null && sha256 is not null && sha1 is not null) { - Log.Debug("Inserting source '{SourcePath}' with SHA-256 '{Sha256}'", file, sha256); - await database - .InsertAsync( - new FileRecord( + if (cached is null) + { + Log.Debug( + "Inserting source '{SourcePath}' with SHA-256 '{Sha256}'", + file, + sha256 + ); + await database + .InsertAsync( + new FileRecord( + file, + sha256, + sha1, + sourceInfo.Length, + sourceInfo.LastWriteTimeUtc.Ticks, + false + ), + cancellationToken + ) + .ConfigureAwait(false); + } + else + { + Log.Debug( + "Updating source '{SourcePath}' with SHA-256 '{Sha256}'", + file, + sha256 + ); + await database + .UpdateHashesAsync( file, sha256, sha1, sourceInfo.Length, sourceInfo.LastWriteTimeUtc.Ticks, - false - ), - cancellationToken - ) - .ConfigureAwait(false); + cancellationToken + ) + .ConfigureAwait(false); + } } return ImportResult.Imported; diff --git a/PhotoCleanerTests/ImportTaskTests.cs b/PhotoCleanerTests/ImportTaskTests.cs index 7b70921..a40608e 100644 --- a/PhotoCleanerTests/ImportTaskTests.cs +++ b/PhotoCleanerTests/ImportTaskTests.cs @@ -924,6 +924,83 @@ public async Task ExecuteAsync_ImportTwice_SecondRunSkipsAndCreatesNoDuplicate() } } + [Fact] + public async Task ExecuteAsync_ReplacedSource_UpdatesRecordAndNextRunSkips() + { + string srcDir = TempDir(); + string outDir = TempDir(); + string dbPath = TempDb(); + try + { + string jpg = Path.Combine(srcDir, "photo.jpg"); + File.Copy(fixture.SourceFile(TempDirectoryFixture.SmallJpegFile), jpg); + await SetExifDateAsync(jpg, "2024:06:15 10:00:00"); + + await using Database db = new(dbPath); + await db.InitializeAsync(TestContext.Current.CancellationToken); + ImportTask task = new( + CreateOptions(outDir), + database: db, + skipDatabase: null, + trashDatabase: null, + new() + ); + + (int organized1, _, int skipped1, _, _, _, int failed1, _) = await task.ExecuteAsync( + [jpg], + new DirectoryInfo(srcDir), + TestContext.Current.CancellationToken + ); + organized1.Should().Be(1); + skipped1.Should().Be(0); + failed1.Should().Be(0); + + DateTime firstMtime = File.GetLastWriteTimeUtc(jpg); + await SetExifDateAsync(jpg, "2024:07:15 10:00:00"); + File.SetLastWriteTimeUtc(jpg, firstMtime.AddSeconds(2)); + FileInfo replacedInfo = new(jpg); + (string replacedSha256, string replacedSha1) = await Database.ComputeHashesAsync( + jpg, + cancellationToken: TestContext.Current.CancellationToken + ); + + (int organized2, _, int skipped2, _, _, _, int failed2, _) = await task.ExecuteAsync( + [jpg], + new DirectoryInfo(srcDir), + TestContext.Current.CancellationToken + ); + organized2.Should().Be(1); + skipped2.Should().Be(0); + failed2.Should().Be(0); + + FileRecord? row = await db.GetByPathAsync( + jpg, + cancellationToken: TestContext.Current.CancellationToken + ); + row.Should().NotBeNull(); + row.Sha256.Should().Be(replacedSha256); + row.Sha1.Should().Be(replacedSha1); + row.FileSize.Should().Be(replacedInfo.Length); + row.MtimeTicks.Should().Be(replacedInfo.LastWriteTimeUtc.Ticks); + + (int organized3, _, int skipped3, _, _, _, int failed3, _) = await task.ExecuteAsync( + [jpg], + new DirectoryInfo(srcDir), + TestContext.Current.CancellationToken + ); + organized3.Should().Be(0); + skipped3.Should().Be(1); + failed3.Should().Be(0); + File.Exists(Path.Combine(outDir, "2024-07", "photo_1.jpg")).Should().BeFalse(); + } + finally + { + Directory.Delete(srcDir, recursive: true); + Directory.Delete(outDir, recursive: true); + File.Delete(dbPath); + } + } + // -- Helpers -------------------------------------------------------------- private static async Task GetXmpSubjectAsync(string filePath)