GetOrAddFolder was a check-then-insert with no unique constraint behind it, so two callers racing the same folder could both miss the lookup and both insert. Enforce identity in the schema and make the loser adopt the winner. - LibraryFolder gains a SHA-256 PathHash (the MediaFile.Path/PathHash precedent): Path is MySQL longtext, which cannot be indexed without a prefix length and collates case-insensitively, so the unique index is on (LibraryPathId, PathHash) instead. - GetOrAddFolder and SetEtag catch a classified unique violation via the existing TvContext.IsUniqueConstraintViolation seam (#308) and re-read. - Dual-provider migration audits and collapses pre-existing duplicates (repointing MediaFile, ParentId and ImageFolderDuration) before creating the index; legacy rows keep a null hash and heal on the next scan. - Tests: deterministic cross-connection race, 8x10 barrier stress with an insert-attempt vacuity guard, classifier-inversion negative control, and a real-migration dedupe test. Refs #488 #308 fix #491
26 lines
1.1 KiB
C#
26 lines
1.1 KiB
C#
namespace ErsatzTV.Core.Domain;
|
|
|
|
public class LibraryFolder
|
|
{
|
|
public int Id { get; set; }
|
|
public string Path { get; set; }
|
|
|
|
/// <summary>
|
|
/// SHA-256 hex of <see cref="Path" /> (<see cref="ErsatzTV.Core.PathUtils.GetPathHash" />), the
|
|
/// indexable stand-in for the unbounded <see cref="Path" /> column that backs the unique
|
|
/// <c>(LibraryPathId, PathHash)</c> constraint — the same shape as <c>MediaFile.PathHash</c>.
|
|
/// Nullable: rows created before ersatztv#491 carry <c>null</c> until a scan heals them, and a
|
|
/// unique index treats nulls as distinct so those legacy rows never collide.
|
|
/// </summary>
|
|
public string PathHash { get; set; }
|
|
|
|
public int LibraryPathId { get; set; }
|
|
public LibraryPath LibraryPath { get; set; }
|
|
public int? ParentId { get; set; }
|
|
public LibraryFolder Parent { get; set; }
|
|
public ICollection<LibraryFolder> Children { get; set; }
|
|
public ICollection<MediaFile> MediaFiles { get; set; }
|
|
public ImageFolderDuration ImageFolderDuration { get; set; }
|
|
public string Etag { get; set; }
|
|
}
|