From 97f8f4e7681c5cb0b30e542bea5d26b7e8c4a725 Mon Sep 17 00:00:00 2001 From: Iu Date: Tue, 20 Oct 2026 20:32:20 +0300 Subject: [PATCH] =?UTF-8?q?fix(backend):=20=D0=BE=D1=82=D0=B4=D0=B0=D0=B2?= =?UTF-8?q?=D0=B0=D1=82=D1=8C=20=D0=BA=D0=BE=D0=BD=D1=82=D1=80=D0=BE=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D1=83=D1=8E=20=D1=81=D1=83=D0=BC=D0=BC=D1=83=20?= =?UTF-8?q?=D0=BE=D1=80=D0=B8=D0=B3=D0=B8=D0=BD=D0=B0=D0=BB=D0=B0=20=D0=B2?= =?UTF-8?q?=20=D0=BE=D1=82=D0=B2=D0=B5=D1=82=D0=B5=20=D0=BD=D0=B0=20=D0=B7?= =?UTF-8?q?=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ответ на загрузку и публикуемое событие теперь несут хеш сохранённого объекта, чтобы клиент синхронизации мог сверить принятые байты со своим файлом и только после этого удалять прежнюю версию элемента. --- backend/internal/app/photo_checksum_test.go | 35 +++++++++++++++++++++ backend/internal/photos/service.go | 3 ++ 2 files changed, 38 insertions(+) diff --git a/backend/internal/app/photo_checksum_test.go b/backend/internal/app/photo_checksum_test.go index 8b8bfb6..d9ef67a 100644 --- a/backend/internal/app/photo_checksum_test.go +++ b/backend/internal/app/photo_checksum_test.go @@ -2,7 +2,9 @@ package app import ( "context" + "crypto/sha256" "encoding/json" + "fmt" "net/http" "testing" @@ -70,3 +72,36 @@ func TestPhotoResponsesExposeOriginalChecksum(t *testing.T) { t.Fatalf("list original_checksum = %+v, want one photo with sha256-photo-a", list.Photos) } } + +// The upload response is photo data too: it must carry the stored original's +// checksum, which a synchronization client uses to confirm what the server +// actually accepted before it drops an earlier version of the same element. +func TestUploadResponseExposesOriginalChecksum(t *testing.T) { + ctx := context.Background() + r, token, uid, repos := newAvatarServer(t) + + album := &models.Album{ID: auth.NewID(), OwnerID: uid, Title: "A"} + if err := repos.Albums.Create(ctx, album); err != nil { + t.Fatalf("create album: %v", err) + } + if err := repos.Members.Add(ctx, album.ID, uid, "editor", nil); err != nil { + t.Fatalf("add owner member: %v", err) + } + + img := makeTestPNG(t) + body, contentType := multipartFile(t, "file", "c.png", img) + rec := doAuthed(r, http.MethodPost, "/api/albums/"+album.ID+"/photos", body, contentType, token) + if rec.Code != http.StatusCreated { + t.Fatalf("upload = %d body=%s", rec.Code, rec.Body.String()) + } + var out struct { + Photos []models.Photo `json:"photos"` + } + if err := json.Unmarshal(rec.Body.Bytes(), &out); err != nil { + t.Fatalf("decode upload: %v", err) + } + want := fmt.Sprintf("%x", sha256.Sum256(img)) + if len(out.Photos) != 1 || out.Photos[0].OriginalChecksum != want { + t.Fatalf("upload original_checksum = %+v, want %q", out.Photos, want) + } +} diff --git a/backend/internal/photos/service.go b/backend/internal/photos/service.go index 35a24b7..87986ed 100644 --- a/backend/internal/photos/service.go +++ b/backend/internal/photos/service.go @@ -216,6 +216,9 @@ func (s *Service) Upload(ctx context.Context, in UploadInput) (*models.Photo, er Height: height, Checksum: hex.EncodeToString(hasher.Sum(nil)), } + // The response and the published event carry the checksum the object ledger + // holds, so a client can match its local file against the stored bytes. + p.OriginalChecksum = obj.Checksum // The metadata layer is the ledger of record: the row is written first, // then the bytes are stored, then the object is activated.