class Upload < ApplicationRecord class Error < StandardError; end MAX_VIDEO_DURATION = 140 class FileValidator < ActiveModel::Validator def validate(record) validate_file_ext(record) validate_integrity(record) validate_md5_uniqueness(record) validate_video_duration(record) validate_resolution(record) end def validate_file_ext(record) if record.file_ext.in?(["bin", "swf"]) record.errors.add(:file_ext, "is invalid (only JPEG, PNG, GIF, MP4, and WebM files are allowed") end end def validate_integrity(record) if record.media_file.is_corrupt? record.errors.add(:file, "is corrupted") 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.add(: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.add(: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.add(: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.add(: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.uploader.is_admin? && record.media_file.is_video? && record.media_file.duration > MAX_VIDEO_DURATION record.errors.add(:base, "video must not be longer than #{MAX_VIDEO_DURATION.seconds.inspect}") 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 # 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 :pending, -> { where(status: "pending") } scope :preprocessed, -> { where(status: "preprocessed") } scope :completed, -> { where(status: "completed") } scope :uploaded_by, ->(user_id) { where(uploader_id: user_id) } def initialize_attributes self.uploader_id = CurrentUser.id self.uploader_ip_addr = CurrentUser.ip_addr self.server = Socket.gethostname end def self.prune! completed.where("created_at < ?", 1.hour.ago).lock.destroy_all preprocessed.where("created_at < ?", 1.day.ago).lock.destroy_all where("created_at < ?", 3.days.ago).lock.destroy_all end def self.visible(user) if user.is_admin? all elsif user.is_anonymous? completed else completed.or(where(uploader: user)) end end concerning :FileMethods do def media_file @media_file ||= MediaFile.open(file, frame_data: context.to_h.dig("ugoira", "frame_data")) 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}") 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 concerning :StatusMethods do 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 concerning :SourceMethods do 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 def self.search(params) q = search_attributes(params, :id, :created_at, :updated_at, :source, :rating, :parent_id, :server, :md5, :server, :file_ext, :file_size, :image_width, :image_height, :referer_url, :uploader, :post) if params[:source_matches].present? q = q.where_like(:source, params[:source_matches]) 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_like(:status, params[:status]) end if params[:backtrace].present? q = q.where_like(:backtrace, params[:backtrace]) end if params[:tag_string].present? q = q.where_like(:tag_string, params[:tag_string]) end q.apply_default_order(params) end def assign_rating_from_tags if rating = PostQueryBuilder.new(tag_string).find_metatag(:rating) self.rating = rating.downcase.first end end def upload_as_pending? as_pending.to_s.truthy? end def has_commentary? artist_commentary_title.present? || artist_commentary_desc.present? || translated_commentary_title.present? || translated_commentary_desc.present? end def self.available_includes [:uploader, :post] end end