fix(backend): отдавать контрольную сумму оригинала в ответе на загрузку
Ответ на загрузку и публикуемое событие теперь несут хеш сохранённого объекта, чтобы клиент синхронизации мог сверить принятые байты со своим файлом и только после этого удалять прежнюю версию элемента.
This commit is contained in:
@@ -2,7 +2,9 @@ package app
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"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)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -216,6 +216,9 @@ func (s *Service) Upload(ctx context.Context, in UploadInput) (*models.Photo, er
|
|||||||
Height: height,
|
Height: height,
|
||||||
Checksum: hex.EncodeToString(hasher.Sum(nil)),
|
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,
|
// The metadata layer is the ledger of record: the row is written first,
|
||||||
// then the bytes are stored, then the object is activated.
|
// then the bytes are stored, then the object is activated.
|
||||||
|
|||||||
Reference in New Issue
Block a user