Skip to content

Instantly share code, notes, and snippets.

@vbalazs
Created January 29, 2017 15:08
Show Gist options
  • Save vbalazs/5af0bae67f0c3a9ca799da7b6c8be33b to your computer and use it in GitHub Desktop.
Save vbalazs/5af0bae67f0c3a9ca799da7b6c8be33b to your computer and use it in GitHub Desktop.
Grape validator to check file existence, size and content type on a given temporary bucket. This is useful when your frontend app only has access to a S3 bucket for temp files (with auto expiry policies for example). It uploads images from javascript and just passes the file id to the Grape API backend where we copy the image to the correct plac…
module Validators
# Provides fast image validation on the temp S3 bucket based on the file's metadata
# It checks for Content Type and file size
class S3TempImage < Grape::Validations::Base
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10 MB
attr_reader :attr_name, :file_name, :file_metadata
def validate_param!(attr_name, params)
@attr_name = attr_name
@file_name = params[attr_name]
# noop on false or blank
return if @option == false || file_name.blank?
fail_with('must be an uploaded image') unless file_exists?
fail_with('must be an image') unless valid_image_type?
fail_with('must be under 10 MB') unless valid_image_size?
end
def s3_client
@s3_client ||= Aws::S3::Client.new
end
private
def fail_with(message)
raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)],
message: message
end
def bucket_name
ENV.fetch('AWS_TEMP_BUCKET_NAME')
end
def file_exists?
@file_metadata = s3_client.head_object({ bucket: bucket_name, key: file_name })
rescue Aws::S3::Errors::NotFound => _
false
end
def valid_image_type?
file_metadata.content_type.start_with?('image/')
end
def valid_image_size?
file_metadata.content_length.between?(100, MAX_FILE_SIZE)
end
end
end
# rubocop:disable Rails/HttpPositionalArguments
require 'spec_helper'
require 'api/admin/validators/s3_temp_image'
describe Validators::S3TempImage do
module ValidationsSpec
module S3TempImageValidatorSpec
class API < Grape::API
default_format :json
params do
optional :photo, s3_temp_image: false
end
get '/noop_on_false'
params do
optional :photo, s3_temp_image: true
end
get '/allows_blank'
params do
optional :photo, s3_temp_image: true
end
get '/disallow_non_image_file'
end
end
end
def app
ValidationsSpec::S3TempImageValidatorSpec::API
end
context 'when it is disabled' do
it 'should not validate the param' do
get '/noop_on_false', photo: 'xyz'
expect(last_response.status).to eq(200)
end
end
context 'when param is empty' do
it 'should pass it, can mean delete' do
get '/allows_blank', photo: ''
expect(last_response.status).to eq(200)
end
end
context 'when param is non-empty' do
let(:s3_client) { Aws::S3::Client.new(stub_responses: true) }
before { allow_any_instance_of(described_class).to receive(:s3_client).and_return(s3_client) }
it 'should fail on non-existent files' do
s3_client.stub_responses(:head_object, 'NotFound')
get '/disallow_non_image_file', photo: '404.jpg'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('{"error":"photo must be an uploaded image"}')
end
it 'should fail on non-image files' do
s3_client.stub_responses(:head_object, { content_type: 'text/html' })
get '/disallow_non_image_file', photo: 'invalid_image.html.jpg'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('{"error":"photo must be an image"}')
end
it 'should fail on too big images' do
s3_client.stub_responses(:head_object,
{
content_type: 'image/jpeg',
content_length: 12_000_000 # ~12MB
})
get '/disallow_non_image_file', photo: 'too_big.jpg'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq('{"error":"photo must be under 10 MB"}')
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment