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:
soroush.asadi
2026-06-02 22:16:11 +03:30
parent eb165db182
commit 97a9481627
7 changed files with 3686 additions and 7 deletions
@@ -1308,6 +1308,55 @@ namespace Meezi.Infrastructure.Data.Migrations
b.ToTable("LeaveRequests");
});
modelBuilder.Entity("Meezi.Core.Entities.MediaAsset", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<string>("CafeId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("ContentHash")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("ContentType")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Kind")
.IsRequired()
.HasMaxLength(40)
.HasColumnType("character varying(40)");
b.Property<string>("OriginalFileName")
.HasMaxLength(260)
.HasColumnType("character varying(260)");
b.Property<long>("SizeBytes")
.HasColumnType("bigint");
b.Property<string>("Url")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("character varying(500)");
b.HasKey("Id");
b.HasIndex("CafeId", "ContentHash");
b.ToTable("MediaAssets");
});
modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b =>
{
b.Property<string>("Id")