require "tmpdir" class Upload < ApplicationRecord class Error < StandardError; end class FileValidator < ActiveModel::Validator def validate(record) validate_file_ext(record) validate_md5_uniqueness(record) validate_video_duration(record) validate_resolution(record) end def validate_file_ext(record) if record.file_ext == "bin" record.errors[:file_ext] << "is invalid (only JPEG, PNG, GIF, SWF, MP4, and WebM files are allowed" end end def validate_md5_uniqueness(record) if record.md5.nil? return end md5_post = Post.find_by_md5(record.md5) if md5_post.nil? return end if record.replaced_post && record.replaced_post == md5_post return end record.errors[:md5] << "duplicate: #{md5_post.id}" end def validate_resolution(record) resolution = record.image_width.to_i * record.image_height.to_i if resolution > Danbooru.config.max_image_resolution record.errors[:base] << "image resolution is too large (resolution: #{(resolution / 1_000_000.0).round(1)} megapixels (#{record.image_width}x#{record.image_height}); max: #{Danbooru.config.max_image_resolution / 1_000_000} megapixels)" elsif record.image_width > Danbooru.config.max_image_width record.errors[:image_width] << "is too large (width: #{record.image_width}; max width: #{Danbooru.config.max_image_width})" elsif record.image_height > Danbooru.config.max_image_height record.errors[:image_height] << "is too large (height: #{record.image_height}; max height: #{Danbooru.config.max_image_height})" end end def validate_video_duration(record) if record.is_video? && record.video.duration > 120 record.errors[:base] << "video must not be longer than 2 minutes" end end end attr_accessor :as_pending, :replaced_post, :file belongs_to :uploader, :class_name => "User" belongs_to :post, optional: true before_validation :initialize_attributes, on: :create before_validation :assign_rating_from_tags validate :uploader_is_not_limited, on: :create # validates :source, format: { with: /\Ahttps?/ }, if: ->(record) {record.file.blank?}, on: :create validates :rating, inclusion: { in: %w(q e s) }, allow_nil: true validates :md5, confirmation: true, if: ->(rec) { rec.md5_confirmation.present? } validates_with FileValidator, on: :file serialize :context, JSON after_destroy_commit :delete_files scope :preprocessed, -> { where(status: "preprocessed") } api_attributes including: [:uploader_name] def initialize_attributes self.uploader_id = CurrentUser.id self.uploader_ip_addr = CurrentUser.ip_addr self.server = Socket.gethostname end def self.prune!(date = 1.day.ago) where("created_at < ?", date).lock.destroy_all end module FileMethods def is_image? %w(jpg gif png).include?(file_ext) end def is_flash? %w(swf).include?(file_ext) end def is_video? %w(webm mp4).include?(file_ext) end def is_ugoira? %w(zip).include?(file_ext) end def delete_files # md5 is blank if the upload errored out before downloading the file. if is_completed? || md5.blank? || Upload.where(md5: md5).exists? || Post.where(md5: md5).exists? return end DanbooruLogger.info("Uploads: Deleting files for upload md5=#{md5}", upload: as_json) Danbooru.config.storage_manager.delete_file(nil, md5, file_ext, :original) Danbooru.config.storage_manager.delete_file(nil, md5, file_ext, :large) Danbooru.config.storage_manager.delete_file(nil, md5, file_ext, :preview) Danbooru.config.backup_storage_manager.delete_file(nil, md5, file_ext, :original) Danbooru.config.backup_storage_manager.delete_file(nil, md5, file_ext, :large) Danbooru.config.backup_storage_manager.delete_file(nil, md5, file_ext, :preview) end end module StatusMethods def is_pending? status == "pending" end def is_processing? status == "processing" end def is_completed? status == "completed" end def is_preprocessed? status == "preprocessed" end def is_preprocessing? status == "preprocessing" end def is_duplicate? status.match?(/duplicate: \d+/) end def is_errored? status.match?(/error:/) end def sanitized_status if is_errored? status.sub(/DETAIL:.+/m, "...") else status end end def duplicate_post_id @duplicate_post_id ||= status[/duplicate: (\d+)/, 1] end end module SourceMethods def source=(source) source = source.unicode_normalize(:nfc) # percent encode unicode characters in urls if source =~ %r!\Ahttps?://!i source = Addressable::URI.normalized_encode(source) rescue source end super(source) end def source_url return nil unless source =~ %r!\Ahttps?://!i Addressable::URI.heuristic_parse(source) rescue nil end end module UploaderMethods def uploader_name uploader.name end end module VideoMethods def video @video ||= FFMPEG::Movie.new(file.path) end end module SearchMethods def uploaded_by(user_id) where("uploader_id = ?", user_id) end def pending where(:status => "pending") end def search(params) q = super q = q.search_attributes(params, :uploader, :post, :source, :rating, :parent_id, :server, :md5, :server, :file_ext, :file_size, :image_width, :image_height, :referer_url) if params[:source_matches].present? q = q.where("uploads.source LIKE ? ESCAPE E'\\\\'", params[:source_matches].to_escaped_for_sql_like) end if params[:has_post].to_s.truthy? q = q.where.not(post_id: nil) elsif params[:has_post].to_s.falsy? q = q.where(post_id: nil) end if params[:status].present? q = q.where("uploads.status LIKE ? ESCAPE E'\\\\'", params[:status].to_escaped_for_sql_like) end if params[:backtrace].present? q = q.where("uploads.backtrace LIKE ? ESCAPE E'\\\\'", params[:backtrace].to_escaped_for_sql_like) end if params[:tag_string].present? q = q.where("uploads.tag_string LIKE ? ESCAPE E'\\\\'", params[:tag_string].to_escaped_for_sql_like) end q.apply_default_order(params) end end include FileMethods include StatusMethods include UploaderMethods include VideoMethods extend SearchMethods include SourceMethods def uploader_is_not_limited if !uploader.can_upload? errors.add(:uploader, uploader.upload_limited_reason) end end def assign_rating_from_tags if rating = Tag.has_metatag?(tag_string, :rating) self.rating = rating.downcase.first end end def presenter @presenter ||= UploadPresenter.new(self) end def upload_as_pending? as_pending.to_s.truthy? end end