247 lines
6.9 KiB
Ruby
247 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,
|
|
)
|
|
if File.exist?(thumbnail_path)
|
|
send_file(thumbnail_path, type: "image/jpeg", disposition: "inline")
|
|
return true
|
|
end
|
|
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
|