diff --git a/lagoon/attach/thumb.go b/lagoon/attach/thumb.go index 33d5057..93fac1c 100644 --- a/lagoon/attach/thumb.go +++ b/lagoon/attach/thumb.go @@ -89,7 +89,7 @@ func defaultResizeImage(src image.Image, w, h int, mode string) image.Image { var resizeImage = defaultResizeImage -func encodeImage(w io.Writer, img image.Image, ext string) error { +func defaultEncodeImage(w io.Writer, img image.Image, ext string) error { format := imaging.JPEG switch strings.ToLower(ext) { case "png": @@ -100,6 +100,8 @@ func encodeImage(w io.Writer, img image.Image, ext string) error { return imaging.Encode(w, img, format) } +var encodeImage = defaultEncodeImage + // Thumb returns the public URL of a lazily generated thumbnail. The second // call for the same dimensions hits the existing blob and does not resize. func (f *File) Thumb(ctx context.Context, bucket *blob.Bucket, w, h int, mode string) (string, error) { @@ -164,10 +166,11 @@ func (f *File) Thumb(ctx context.Context, bucket *blob.Bucket, w, h int, mode st } encErr := encodeImage(wr, resized, ext) closeErr = wr.Close() - if encErr != nil { - return "", encErr - } - if closeErr != nil { + if encErr != nil || closeErr != nil { + _ = deleteKey(ctx, bucket, thumbKey) + if encErr != nil { + return "", encErr + } return "", closeErr } return publicURL(thumbKey), nil diff --git a/lagoon/attach/thumb_test.go b/lagoon/attach/thumb_test.go index ba28e44..49cbe35 100644 --- a/lagoon/attach/thumb_test.go +++ b/lagoon/attach/thumb_test.go @@ -2,6 +2,7 @@ package attach import ( "bytes" + "errors" "image" "image/color" "image/jpeg" @@ -129,6 +130,46 @@ func TestFileThumbResizesOnce(t *testing.T) { } } +func TestFileThumbEncodeFailureDoesNotCache(t *testing.T) { + ctx := t.Context() + bucket := memblob.OpenBucket(nil) + t.Cleanup(func() { _ = bucket.Close() }) + + f := &File{ID: 7, DiskName: "abc123xyz.jpg"} + if err := bucket.WriteAll(ctx, BlobKey(f.DiskName), testJPEG(t), &blob.WriterOptions{ContentType: "image/jpeg"}); err != nil { + t.Fatal(err) + } + + orig := encodeImage + encodeImage = func(w io.Writer, img image.Image, ext string) error { + _, _ = w.Write([]byte("partial")) + return errors.New("encode boom") + } + t.Cleanup(func() { encodeImage = orig }) + + if _, err := f.Thumb(ctx, bucket, 200, 200, "crop"); err == nil { + t.Fatal("encode failure must surface") + } + thumbKey := PartitionDirectory(f.DiskName) + ThumbFilename(f.ID, 200, 200, 0, 0, "crop", "jpg") + exists, err := bucket.Exists(ctx, thumbKey) + if err != nil { + t.Fatal(err) + } + if exists { + t.Fatal("failed encode must not leave a cached thumb blob") + } + + encodeImage = orig + url, err := f.Thumb(ctx, bucket, 200, 200, "crop") + if err != nil { + t.Fatalf("retry after failed encode: %v", err) + } + want := "/storage/uploads/abc/123/xyz/thumb_7_200_200_0_0_crop.jpg" + if url != want { + t.Fatalf("retry url = %q, want %q", url, want) + } +} + func testJPEG(t *testing.T) []byte { t.Helper() img := image.NewRGBA(image.Rect(0, 0, 8, 8))