Files
redux-scraper/app/controllers/blob_entries_controller.rb

245 lines
6.9 KiB
Ruby

# typed: strict
class BlobEntriesController < ApplicationController
skip_before_action :authenticate_user!, only: [:show]
sig { void }
def show
thumb = params[:thumb]
if thumb.present? && !thumb_params(thumb)
raise ActionController::BadRequest.new("invalid thumbnail #{thumb}")
end
if thumb.present?
expires_dur = 1.week
else
expires_dur = 1.year
end
response.headers["Expires"] = expires_dur.from_now.httpdate
expires_in expires_dur, public: true
unless stale?(
last_modified: Time.at(0),
strong_etag: strong_etag_for_request,
)
return
end
sha256 = T.let(params[:sha256], String)
raise ActionController::BadRequest.new("no file specified") if sha256.blank?
if show_blob_file(sha256, thumb)
return
else
raise ActiveRecord::RecordNotFound
end
end
private
sig { params(sha256: String, thumb: T.nilable(String)).returns(T::Boolean) }
def show_blob_file(sha256, thumb)
if thumb
thumb_params = thumb_params(thumb)
if thumb_params.nil?
raise ActionController::BadRequest.new("invalid thumbnail: #{thumb}")
end
# if the requested format is gif, and the thumbnail type is content-container, we want to
# thumbnail the gif into another gif. Else, always thumbnail into a jpeg.
file_ext = "jpeg"
if params[:format] == "gif" && thumb == "content-container"
file_ext = "gif"
end
# content-container may be pre-thumbnailed, see if the file is on the disk
if thumb == "content-container" && file_ext == "jpeg"
thumbnail_path =
Domain::PostFile::Thumbnail.absolute_file_path(
sha256,
"content_container",
0,
)
send_file(thumbnail_path, type: "image/jpeg", disposition: "inline")
return true
end
width, height = thumb_params
filename = "thumb-#{sha256}-#{thumb}.#{file_ext}"
cache_key = "vips:#{filename}"
thumb_data =
Rack::MiniProfiler.step("vips: load from cache") do
Rails
.cache
.fetch(cache_key, expires_in: 1.day) do
blob_file = BlobFile.find_by(sha256: HexUtil.hex2bin(sha256))
if blob_file
content_type =
blob_file.content_type || "application/octet-stream"
if helpers.is_renderable_video_type?(content_type)
thumbnail_video_file(blob_file, width, height, file_ext)
elsif helpers.is_renderable_image_type?(content_type)
thumbnail_image_file(blob_file, width, height, file_ext)
end
end
end
end
if !thumb_data
Rails.cache.delete(cache_key)
return false
end
send_data(
thumb_data[0],
type: thumb_data[1],
disposition: "inline",
filename: filename,
)
else
blob_file = BlobFile.find_by(sha256: HexUtil.hex2bin(sha256))
return false if !blob_file
content_type = blob_file.content_type || "application/octet-stream"
send_file(
blob_file.absolute_file_path,
type: content_type,
disposition: "inline",
)
end
return true
end
sig do
params(
blob_file: BlobFile,
width: Integer,
height: Integer,
thumb: String,
).returns(T.nilable([String, String]))
end
def thumbnail_video_file(blob_file, width, height, thumb)
video_file = blob_file.absolute_file_path
temp_thumb_file = Tempfile.new(%w[video-thumb .png])
process_result =
system(
"ffmpegthumbnailer",
"-f", # overlay video strip indicator
"-i",
video_file,
"-o",
T.must(temp_thumb_file.path),
"-s",
"#{width}",
"-c",
"jpeg",
)
if !process_result
temp_thumb_file.unlink
return nil
end
thumb_data_tmp = File.read(T.must(temp_thumb_file.path), mode: "rb")
temp_thumb_file.unlink
[thumb_data_tmp, "image/jpeg"]
end
# Returns a tuple of the thumbnail data and the content type
sig do
params(
blob_file: BlobFile,
width: Integer,
height: Integer,
file_ext: String,
).returns(T.nilable([String, String]))
end
def thumbnail_image_file(blob_file, width, height, file_ext)
blob_file_path = blob_file.absolute_file_path
if file_ext == "gif"
VipsUtil.try_load_gif(
blob_file_path,
load_gif: -> do
Rack::MiniProfiler.step("vips: load gif") do
# Use libvips' gifload with n=-1 to load all frames
image = Vips::Image.gifload(blob_file_path, n: -1)
num_frames = image.get("n-pages")
image_width, image_height = image.width, (image.height / num_frames)
if width >= image_width && height >= image_height
logger.info(
"gif is already smaller than requested thumbnail size",
)
return File.binread(blob_file_path), "image/gif"
end
Rack::MiniProfiler.step("vips: thumbnail gif") do
image = image.thumbnail_image(width, height: height)
image_buffer =
image.gifsave_buffer(
dither: 1,
effort: 1,
interframe_maxerror: 16,
interpalette_maxerror: 10,
interlace: true,
)
[image_buffer, "image/gif"]
end
end
end,
on_load_failed: ->(detected_content_type) do
case detected_content_type
when %r{image/png}
thumbnail_image_file(blob_file, width, height, "png")
when %r{image/jpeg}, %r{image/jpg}
thumbnail_image_file(blob_file, width, height, "jpeg")
else
raise
end
end,
)
else
# Original static image thumbnailing logic
image_buffer =
Rack::MiniProfiler.step("vips: load image") do
T.unsafe(Vips::Image).thumbnail(
blob_file.absolute_file_path,
width,
height: height,
)
end
Rack::MiniProfiler.step("vips: thumbnail image") do
logger.info("rendering thumbnail as jpeg")
[
T.let(image_buffer.jpegsave_buffer(interlace: true, Q: 95), String),
"image/jpeg",
]
end
end
end
sig { params(thumb: String).returns(T.nilable([Integer, Integer])) }
def thumb_params(thumb)
case thumb
when "32-avatar"
[32, 32]
when "64-avatar"
[64, 64]
when "tiny"
[100, 100]
when "small"
[400, 300]
when "medium"
[800, 600]
when "content-container"
[768, 2048]
end
end
sig { returns(String) }
def strong_etag_for_request
[params[:sha256], params[:thumb], params[:format]].compact.join("-")
end
end