Files
danbooru/app/logical/upload_service/utils.rb
evazion dc6575dc76 uploads: fix corrupted image detection.
* Fix corrupted image detection. We were shelling out to vips and trying
  to grep for error messages, but the error message for jpeg files changed.
  Now we load the file in ruby vips, which raises an error on failure.

* Don't attempt to redownload corrupted images. If a download completes
  without any errors yet the downloaded file is corrupt, then something is
  wrong at the source and redownloading is unlikely to help. Let the
  upload fail and the user retry if necessary.

* Validate that all uploads are uncorrupted, including files uploaded
  from a computer, not just files uploaded from a source.
2020-04-13 15:30:17 -05:00

205 lines
6.8 KiB
Ruby

class UploadService
module Utils
module_function
def file_header_to_file_ext(file)
case File.read(file.path, 16)
when /^\xff\xd8/n
"jpg"
when /^GIF87a/, /^GIF89a/
"gif"
when /^\x89PNG\r\n\x1a\n/n
"png"
when /^CWS/, /^FWS/, /^ZWS/
"swf"
when /^\x1a\x45\xdf\xa3/n
"webm"
when /^....ftyp(?:isom|3gp5|mp42|MSNV|avc1)/
"mp4"
when /^PK\x03\x04/
"zip"
else
"bin"
end
end
def corrupt?(filename)
image = Vips::Image.new_from_file(filename, fail: true)
image.stats
false
rescue Vips::Error
true
end
def calculate_ugoira_dimensions(source_path)
folder = Zip::File.new(source_path)
Tempfile.open("ugoira-dim-") do |tempfile|
folder.first.extract(tempfile.path) { true }
image_size = ImageSpec.new(tempfile.path)
return [image_size.width, image_size.height]
end
end
def calculate_dimensions(upload, file)
if upload.is_video?
video = FFMPEG::Movie.new(file.path)
yield(video.width, video.height)
elsif upload.is_ugoira?
w, h = calculate_ugoira_dimensions(file.path)
yield(w, h)
elsif upload.is_image? || upload.is_flash?
image_size = ImageSpec.new(file.path)
yield(image_size.width, image_size.height)
elsif upload.file_ext == "bin"
yield(0, 0)
else
raise ArgumentError, "unhandled file type (#{upload.file_ext})" # should not happen
end
end
def distribute_files(file, record, type, original_post_id: nil)
# need to do this for hybrid storage manager
post = Post.new
post.id = original_post_id if original_post_id.present?
post.md5 = record.md5
post.file_ext = record.file_ext
[Danbooru.config.storage_manager, Danbooru.config.backup_storage_manager].each do |sm|
sm.store_file(file, post, type)
end
end
def is_downloadable?(source)
source =~ /^https?:\/\//
end
def generate_resizes(file, upload)
if upload.is_video?
video = FFMPEG::Movie.new(file.path)
crop_file = generate_video_crop_for(video, Danbooru.config.small_image_width)
preview_file = generate_video_preview_for(video, Danbooru.config.small_image_width, Danbooru.config.small_image_width)
elsif upload.is_ugoira?
preview_file = PixivUgoiraConverter.generate_preview(file)
crop_file = PixivUgoiraConverter.generate_crop(file)
sample_file = PixivUgoiraConverter.generate_webm(file, upload.context["ugoira"]["frame_data"])
elsif upload.is_image?
preview_file = DanbooruImageResizer.resize(file, Danbooru.config.small_image_width, Danbooru.config.small_image_width, 85)
crop_file = DanbooruImageResizer.crop(file, Danbooru.config.small_image_width, Danbooru.config.small_image_width, 85)
if upload.image_width > Danbooru.config.large_image_width
sample_file = DanbooruImageResizer.resize(file, Danbooru.config.large_image_width, upload.image_height, 90)
end
end
[preview_file, crop_file, sample_file]
end
def generate_video_crop_for(video, width)
vp = Tempfile.new(["video-preview", ".jpg"], binmode: true)
video.screenshot(vp.path, :seek_time => 0, :resolution => "#{video.width}x#{video.height}")
crop = DanbooruImageResizer.crop(vp, width, width, 85)
vp.close
return crop
end
def generate_video_preview_for(video, width, height)
dimension_ratio = video.width.to_f / video.height
if dimension_ratio > 1
height = (width / dimension_ratio).to_i
else
width = (height * dimension_ratio).to_i
end
output_file = Tempfile.new(["video-preview", ".jpg"], binmode: true)
video.screenshot(output_file.path, :seek_time => 0, :resolution => "#{width}x#{height}")
output_file
end
def process_file(upload, file, original_post_id: nil)
upload.file = file
upload.file_ext = Utils.file_header_to_file_ext(file)
upload.file_size = file.size
upload.md5 = Digest::MD5.file(file.path).hexdigest
Utils.calculate_dimensions(upload, file) do |width, height|
upload.image_width = width
upload.image_height = height
end
upload.validate!(:file)
upload.tag_string = "#{upload.tag_string} #{Utils.automatic_tags(upload, file)}"
preview_file, crop_file, sample_file = Utils.generate_resizes(file, upload)
begin
Utils.distribute_files(file, upload, :original, original_post_id: original_post_id)
Utils.distribute_files(sample_file, upload, :large, original_post_id: original_post_id) if sample_file.present?
Utils.distribute_files(preview_file, upload, :preview, original_post_id: original_post_id) if preview_file.present?
Utils.distribute_files(crop_file, upload, :crop, original_post_id: original_post_id) if crop_file.present?
ensure
preview_file.try(:close!)
crop_file.try(:close!)
sample_file.try(:close!)
end
end
# these methods are only really used during upload processing even
# though logically they belong on upload. post can rely on the
# automatic tag that's added.
def is_animated_gif?(upload, file)
return false if upload.file_ext != "gif"
# Check whether the gif has multiple frames by trying to load the second frame.
result = Vips::Image.gifload(file.path, page: 1) rescue $ERROR_INFO
if result.is_a?(Vips::Image)
true
elsif result.is_a?(Vips::Error) && result.message =~ /too few frames in GIF file/
false
else
raise result
end
end
def is_animated_png?(upload, file)
upload.file_ext == "png" && APNGInspector.new(file.path).inspect!.animated?
end
def is_video_with_audio?(upload, file)
return false if !upload.is_video? # avoid ffprobe'ing the file if it's not a video (issue #3826)
video = FFMPEG::Movie.new(file.path)
video.audio_channels.present?
end
def automatic_tags(upload, file)
tags = []
tags << "video_with_sound" if is_video_with_audio?(upload, file)
tags << "animated_gif" if is_animated_gif?(upload, file)
tags << "animated_png" if is_animated_png?(upload, file)
tags.join(" ")
end
def get_file_for_upload(upload, file: nil)
return file if file.present?
raise "No file or source URL provided" if upload.source_url.blank?
download = Downloads::File.new(upload.source_url, upload.referer_url)
file, strategy = download.download!
if download.data[:ugoira_frame_data].present?
upload.context = {
"ugoira" => {
"frame_data" => download.data[:ugoira_frame_data],
"content_type" => "image/jpeg"
}
}
end
return file
end
end
end