Files
redux-scraper/app/controllers/blob_entries_controller.rb
2025-02-25 19:59:41 +00:00

155 lines
4.0 KiB
Ruby

# typed: strict
class BlobEntriesController < ApplicationController
skip_before_action :authenticate_user!, only: [:show]
sig { void }
def show
thumb = params[:thumb]
raise("invalid thumb #{thumb}") if thumb.present? && !thumb_params(thumb)
expires_dur = 1.year
response.headers["Expires"] = expires_dur.from_now.httpdate
expires_in expires_dur, public: true
sha256 = params[:sha256]
etag = sha256
etag += "-#{thumb}" if thumb
return unless stale?(last_modified: Time.at(0), strong_etag: etag)
if show_blob_file(sha256, thumb)
return
elsif BlobFile.migrate_sha256!(sha256) && 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
filename = "thumb-#{thumb}-#{sha256}"
filename = T.must(filename[..File.extname(filename).length])
filename += ".jpeg"
cache_key = "vips:thumbnail:#{sha256}:#{thumb}"
thumb_params = thumb_params(thumb)
if thumb_params.nil?
raise ActionController::BadRequest.new("invalid thumbnail: #{thumb}")
end
width, height = thumb_params
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)
elsif helpers.is_renderable_image_type?(content_type)
thumbnail_image_file(blob_file, width, height)
end
end
end
end
if !thumb_data
Rails.cache.delete(cache_key)
return false
end
send_data(
thumb_data,
type: "image/jpg",
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).returns(
T.nilable(String),
)
end
def thumbnail_video_file(blob_file, width, height)
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
end
sig do
params(blob_file: BlobFile, width: Integer, height: Integer).returns(
T.nilable(String),
)
end
def thumbnail_image_file(blob_file, width, height)
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
image_buffer.jpegsave_buffer
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"
[2048, 2048]
end
end
end