Fixes bug described in d3e4ac7c17 (commitcomment-39049351)
When dealing with searches, there are several variables we have to keep
in mind:
* Whether tag aliases should be applied.
* Whether search terms should be sorted.
* Whether the rating:s and -status:deleted metatags should be added by
safe mode and the hide deleted posts setting.
Which of these things we need to do depends on the context:
* We want to apply aliases when actually doing the search, calculating
the count, looking up the wiki excerpt, recording missed/popular
searches in Reportbooru, and calculating related tags for the sidebar,
but not when displaying the raw search as typed by the user (for
example, in the page title or in the tag search box).
* We want to sort the search when calculating cache keys for fast_count
or related tags, and when recording missed/popular searches, but not
in the page title or when displaying the raw search.
* We want to add rating:s and -status:deleted when performing the
search, calculating the count, or recording missed/popular searches,
but not when calculating related tags for the sidebar, or when
displaying the page title or raw search.
Here we introduce normalized_query and try to use it in contexts where
query normalization is necessary. When to use the normalized query
versus the raw unnormalized query is still subtle and prone to error.
261 lines
7.0 KiB
Ruby
261 lines
7.0 KiB
Ruby
require "tmpdir"
|
|
|
|
class Upload < ApplicationRecord
|
|
class Error < StandardError; end
|
|
|
|
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 == "bin"
|
|
record.errors[:file_ext] << "is invalid (only JPEG, PNG, GIF, SWF, MP4, and WebM files are allowed"
|
|
end
|
|
end
|
|
|
|
def validate_integrity(record)
|
|
if record.file_ext.in?(["jpg", "gif", "png"]) && UploadService::Utils.corrupt?(record.file.path)
|
|
record.errors[:file] << "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[: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
|
|
# 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 :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!(date = 1.day.ago)
|
|
where("created_at < ?", date).lock.destroy_all
|
|
end
|
|
|
|
def self.visible(user)
|
|
if user.is_admin?
|
|
all
|
|
elsif user.is_member?
|
|
where(uploader: user)
|
|
else
|
|
none
|
|
end
|
|
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 VideoMethods
|
|
def video
|
|
@video ||= FFMPEG::Movie.new(file.path)
|
|
end
|
|
end
|
|
|
|
module SearchMethods
|
|
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_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
|
|
end
|
|
|
|
include FileMethods
|
|
include StatusMethods
|
|
include VideoMethods
|
|
extend SearchMethods
|
|
include SourceMethods
|
|
|
|
def assign_rating_from_tags
|
|
if rating = PostQueryBuilder.new(tag_string).find_metatag(: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
|
|
|
|
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
|