feat(media): content-hash dedup for uploads + media-library endpoint
Uploads previously wrote every file to disk with a fresh GUID name, so the
same image uploaded twice produced two identical files. Now:
- New MediaAsset table records each stored upload (SHA-256 hash, size, type,
url, kind, scope) + migration. Indexed on (CafeId, ContentHash).
- MediaStorageService computes the content hash on upload; if an identical file
already exists for that café it returns the existing URL instead of writing a
duplicate (covers images, videos, 3D models). Dedup lookup/record run via a
scoped DbContext (the service is a singleton) and never block an upload on
failure.
- GET /api/cafes/{cafeId}/media lists the café's library (newest first, optional
?kind=) so the UI can let users pick an existing file instead of re-uploading.
86 API tests pass.
This commit is contained in:
@@ -85,6 +85,9 @@ public class AppDbContext : DbContext
|
||||
// Idempotency keys for safe retry of offline-replayed writes.
|
||||
public DbSet<IdempotencyRecord> IdempotencyRecords => Set<IdempotencyRecord>();
|
||||
|
||||
// Uploaded files, recorded for content-hash de-duplication and a media library.
|
||||
public DbSet<MediaAsset> MediaAssets => Set<MediaAsset>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
@@ -113,6 +116,20 @@ public class AppDbContext : DbContext
|
||||
e.HasQueryFilter(x => x.DeletedAt == null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<MediaAsset>(e =>
|
||||
{
|
||||
e.HasKey(x => x.Id);
|
||||
// Dedup lookups: same content within a scope (café or platform).
|
||||
e.HasIndex(x => new { x.CafeId, x.ContentHash });
|
||||
e.Property(x => x.CafeId).HasMaxLength(64);
|
||||
e.Property(x => x.ContentHash).HasMaxLength(64).IsRequired();
|
||||
e.Property(x => x.ContentType).HasMaxLength(100).IsRequired();
|
||||
e.Property(x => x.Url).HasMaxLength(500).IsRequired();
|
||||
e.Property(x => x.Kind).HasMaxLength(40).IsRequired();
|
||||
e.Property(x => x.OriginalFileName).HasMaxLength(260);
|
||||
e.HasQueryFilter(x => x.DeletedAt == null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Cafe>(e =>
|
||||
{
|
||||
e.HasKey(x => x.Id);
|
||||
|
||||
Reference in New Issue
Block a user