|
|
|
require 'active_support/core_ext/numeric/bytes' |
|
require 'mini_magick' |
|
require 'value_object' |
|
|
|
class AvatarValidator |
|
class UploadedFile |
|
def initialize(source) |
|
@data = get_data_from source |
|
end |
|
|
|
def size |
|
data.size |
|
end |
|
|
|
def to_image |
|
MiniMagick::Image.read data |
|
end |
|
|
|
private |
|
|
|
attr_reader :data |
|
|
|
# `source` is either a filename, a data blob, or (for testing) a (possibly |
|
# mock) instance of UploadedFile. If we pass a data blob to File#exists?, |
|
# it will raise an ArgumentError. This makes it fairly easy to figure out |
|
# which is which. |
|
def get_data_from(source) |
|
return source.send(:data) if source.respond_to?(:data, true) |
|
# Not an instance; keep trying |
|
_ = File.exists? source |
|
# If `source` is *not* a filename, we wouldn't get here |
|
File.open(source) { |f| f.read } |
|
rescue ArgumentError |
|
# source is (presumably) image data |
|
source |
|
end |
|
end # class AvatarValidator::UploadedFile |
|
|
|
def initialize(uploaded_file, config = nil) |
|
@uploaded_file = UploadedFile.new uploaded_file |
|
@config = config || AvatarValidator.default_config |
|
end |
|
|
|
def validate |
|
return @valid unless @valid.nil? |
|
verify_file_size |
|
validate_image_format |
|
verify_allowed_format |
|
verify_image_dimensions |
|
@valid = true |
|
rescue RuntimeError => e |
|
error_message_for YAML.load(e.message).first |
|
end |
|
|
|
def validate_as_json |
|
result = validate |
|
return({ success: true }) if result == true |
|
{ success: false, message: result } |
|
end |
|
|
|
def self.default_config |
|
Class.new(ValueObject::Base) do |
|
has_fields :allowed_formats, :max_file_size, :max_height, :max_width |
|
end.new max_file_size: 1.megabyte, |
|
allowed_formats: %w(PNG JPEG GIF), |
|
max_width: 5000, |
|
max_height: 5000 |
|
end |
|
|
|
private |
|
|
|
attr_reader :config, :uploaded_file |
|
|
|
def error_message_for(error_data) |
|
matches = { |
|
invalid_file_size: 'Picture size must be no larger than %d bytes', |
|
invalid_image_format: 'Picture must be in a valid image format', |
|
disallowed_format: 'Picture must be an image of type %s', |
|
invalid_height: 'Picture height must not exceed %d pixels', |
|
invalid_width: 'Picture height must not exceed %d pixels' |
|
} |
|
format matches[error_data.keys.first], matches[error_data.values.first] |
|
end |
|
|
|
def fail_with(error_key, data_value) |
|
error_item = {} |
|
error_item[error_key] = data_value |
|
fail YAML.dump([error_item]) |
|
end |
|
|
|
def image |
|
@image ||= uploaded_file.to_image |
|
end |
|
|
|
def verify_file_size |
|
return self unless uploaded_file.size > config.max_file_size |
|
fail_with :invalid_file_size, uploaded_file.size |
|
end |
|
|
|
def validate_image_format |
|
image.validate! |
|
self |
|
rescue MiniMagick::Error => e |
|
fail_with :invalid_image_format, e.message |
|
end |
|
|
|
def verify_allowed_format |
|
return self if config.allowed_formats.include? image.type |
|
message = config.allowed_format.join ' or ' |
|
fail_with :disallowed_format, message |
|
end |
|
|
|
def verify_image_dimensions |
|
fail_with :invalid_width, config.max_width if image.width > config.max_width |
|
invalid_height = image.height > config.max_height |
|
fail_with :invalid_height, config.max_height if invalid_height |
|
self |
|
end |
|
end |