bingo: fix photo rebuild on UTF-8 tagged S3 bodies
aws-sdk returns object bodies from a plain StringIO, so S3.get handed back a UTF-8 tagged string and Photo.sniff's binary prefix comparison raised Encoding::CompatibilityError: "Rebuild resized photos" died on the first photo with a 500. URL imports had the same flaw via a UTF-8 read buffer. Treat image data as binary at the source and in sniff, and let one failing photo no longer abort the batch (errors are reported in the admin flash and logged). Assisted-by: Claude:opus-5
This commit is contained in:
parent
201a70b07b
commit
61bb1393fe
4 changed files with 33 additions and 4 deletions
|
|
@ -138,7 +138,10 @@ module Bingo
|
|||
|
||||
module_function
|
||||
|
||||
# Input may arrive tagged UTF-8 (aws-sdk bodies are plain StringIOs), and
|
||||
# comparing that with binary literals raises, so look at raw bytes.
|
||||
def sniff(bytes)
|
||||
bytes = bytes.b
|
||||
return 'jpg' if bytes.start_with?("\xFF\xD8\xFF".b)
|
||||
return 'png' if bytes.start_with?("\x89PNG\r\n\x1A\n".b)
|
||||
return 'webp' if bytes[0, 4] == 'RIFF' && bytes[8, 4] == 'WEBP'
|
||||
|
|
@ -175,6 +178,7 @@ module Bingo
|
|||
end
|
||||
|
||||
def build_variants(key, bytes)
|
||||
bytes = bytes.b
|
||||
ext = sniff(bytes) or raise Error, 'not an image'
|
||||
WIDTHS.each { |w| S3.put(variant_key(key, w), resize(bytes, ext, w), content_type: TYPES['webp']) }
|
||||
true
|
||||
|
|
@ -198,6 +202,8 @@ module Bingo
|
|||
out.b
|
||||
rescue Errno::ENOENT
|
||||
raise Error, 'ImageMagick (convert) not installed'
|
||||
rescue Errno::EPIPE, IOError => e # convert exited before reading all input
|
||||
raise Error, "convert: #{e.class}"
|
||||
end
|
||||
|
||||
# Photos are served byte-for-byte, so drop EXIF/XMP/IPTC/text metadata:
|
||||
|
|
@ -276,7 +282,7 @@ module Bingo
|
|||
raise Error, 'too many redirects' if redirects.zero?
|
||||
return fetch(URI.join(uri, res['location']).to_s, redirects: redirects - 1)
|
||||
when Net::HTTPSuccess
|
||||
body = +''
|
||||
body = +''.b
|
||||
res.read_body do |chunk|
|
||||
body << chunk
|
||||
raise Error, 'image too large (max 8 MB)' if body.bytesize > MAX_BYTES
|
||||
|
|
|
|||
|
|
@ -162,8 +162,14 @@ module BingoAdmin
|
|||
# Regenerate resized variants from the originals already in the bucket.
|
||||
app.post '/admin/bingo/photos/rebuild' do
|
||||
dcs = Bingo::Datacenter.exclude(photo_key: nil).all
|
||||
dcs.each { |dc| Bingo::Photo.rebuild(dc) }
|
||||
failed = dcs.reject(&:photo_variants).map(&:slug)
|
||||
errors = {}
|
||||
dcs.each do |dc|
|
||||
Bingo::Photo.rebuild(dc)
|
||||
rescue StandardError => e # one bad object must not abort the batch
|
||||
errors[dc.slug] = "#{e.class}: #{e.message.lines.first&.strip}"
|
||||
$logger.error("bingo: rebuild #{dc.slug}: #{e.class}: #{e.message}")
|
||||
end
|
||||
failed = dcs.reject(&:photo_variants).map { |dc| errors[dc.slug] ? "#{dc.slug} (#{errors[dc.slug]})" : dc.slug }
|
||||
session[:bingo_flash] = "Rebuilt #{dcs.size - failed.size}/#{dcs.size}.#{" Failed: #{failed.join(', ')}" if failed.any?}"
|
||||
redirect '/admin/bingo'
|
||||
end
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ module S3
|
|||
end
|
||||
|
||||
def get(key)
|
||||
client.get_object(bucket: bucket, key: key).body.read
|
||||
client.get_object(bucket: bucket, key: key).body.read.b # body is a UTF-8 StringIO
|
||||
end
|
||||
|
||||
def exists?(key)
|
||||
|
|
|
|||
|
|
@ -170,6 +170,23 @@ class BingoTest < Minitest::Test
|
|||
assert_equal '480x240', dims
|
||||
end
|
||||
|
||||
def test_sniff_accepts_utf8_tagged_bytes
|
||||
assert_equal 'jpg', Bingo::Photo.sniff(JPEG.dup.force_encoding('UTF-8'))
|
||||
end
|
||||
|
||||
def test_rebuild_from_utf8_s3_body
|
||||
skip 'ImageMagick not available' unless system(Bingo::Photo::CONVERT, '-version', out: File::NULL, err: File::NULL)
|
||||
png, = Open3.capture2(Bingo::Photo::CONVERT, '-size', '800x600', 'gradient:red-blue', 'png:-', binmode: true)
|
||||
Aws.config[:s3] = { stub_responses: { get_object: { body: png.dup.force_encoding('UTF-8') } } }
|
||||
S3.instance_variable_set(:@client, nil)
|
||||
@dcs[0].update(photo_key: 'bingo/dc-1-0123abcd.png')
|
||||
Bingo::Photo.rebuild(@dcs[0])
|
||||
assert @dcs[0].refresh.photo_variants
|
||||
ensure
|
||||
Aws.config[:s3] = { stub_responses: true }
|
||||
S3.instance_variable_set(:@client, nil)
|
||||
end
|
||||
|
||||
def test_photo_rejects_non_images
|
||||
assert_raises(Bingo::Photo::Error) { Bingo::Photo.store('dc-1', '<html>') }
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue