Last active
April 17, 2019 12:22
-
-
Save omnilord/4f308d4a1d0b9df02293dcaa8ee4d605 to your computer and use it in GitHub Desktop.
ActiveStorage is a great new feature introduced in Rails 5.2. I had a need for uploading images for profiles, and rather than go the route of including a gem that duplicated functionality (in this case, multiple gems, paperclip, fog, etc. to upload, process, and transport to storage), I wanted to use the internal solution. It was a GREAT choice,…
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# | |
# A similar file will be created in ${RAILS_ROOT}/db/migrate/ when you run | |
# > rails active_storage:install && rails db:migrate | |
# | |
# This migration comes from active_storage (originally 20170806125915) | |
class CreateActiveStorageTables < ActiveRecord::Migration[5.2] | |
def change | |
create_table :active_storage_blobs do |t| | |
t.string :key, null: false | |
t.string :filename, null: false | |
t.string :content_type | |
t.text :metadata | |
t.bigint :byte_size, null: false | |
t.string :checksum, null: false | |
t.datetime :created_at, null: false | |
t.index [ :key ], unique: true | |
end | |
create_table :active_storage_attachments do |t| | |
t.string :name, null: false | |
t.references :record, null: false, polymorphic: true, index: false | |
t.references :blob, null: false | |
t.datetime :created_at, null: false | |
t.index [ :record_type, :record_id, :name, :blob_id ], name: "index_active_storage_attachments_uniqueness", unique: true | |
end | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!-- | |
file location: ${RAILS_ROOT}/app/views/profiles/_form.html.erb | |
--> | |
<%# This view expects you are using the usual form partial render that comes default with Rails scaffold %> | |
<%# Since I am copying this from my real project, you will notice I have some bootstrap sprinkled in %> | |
<div class="form-group"> | |
<%= form_with(model: profile, url: submit_path, id: 'profile-form', local: true) do |form| %> | |
<div class="row"> | |
<div class="col-md-2 field"> | |
<div id="profile-container" class="profile-avatar"> | |
<%= form.file_field(:avatar, multiple: false, class: 'img-upload-input hidden', direct_upload: true) %> | |
<%= form.check_box(:delete_avatar, class: 'hidden', include_hidden: false) %> | |
<div class="thumbnail-container"> | |
<% if profile.avatar.attached? %> | |
<%= image_tag(profile.thumbnail, id: 'avatar-preview', class: 'avatar present', alt: t(:avatar), size: '100x100') %> | |
<button type="button" id="remove-avatar" class="btn btn-xs btn-danger avatar-btn pull-right" title="<%= t(:remove) %>"> | |
<i class="fa fa-trash"></i> Delete | |
</button> | |
<% else %> | |
<%= image_tag('click_to_add.png', id: 'avatar-preview', class: 'avatar', alt: t(:click_to_add), size: '100x100') %> | |
<% end %> | |
</div> | |
</div> | |
</div> | |
<div class="col-md-10 field"> | |
<%= form.label :name %> | |
<%= form.text_field :name, id: :profile_name, class: 'form-control' %> | |
</div> | |
</div> | |
<div class="row"> | |
<div class="col-md-12 field"> | |
<%= form.label :description %> | |
<%= form.text_area :description, id: :profile_description, class: 'form-control markdown-cntl' %> | |
</div> | |
</div> | |
<%= end %> | |
</div> | |
<script> | |
/* | |
// | |
// this JavaScript probably doesn't work 100% out of the box since it is a snippet from inside a larger block | |
// | |
// uses jQuery and Jcrop | |
// | |
*/ | |
$(function () { | |
var thumbnail_crop = {}, | |
default_css = { | |
height: '100px', | |
width: '100px', | |
marginLeft: '0px', | |
marginTop: '0px' | |
}, | |
avatar_url = '<%= profile.avatar_attached? ? url_for(profile.avatar) : '' %>', | |
thumbnail_url = '<%= profile.avatar_attached? ? url_for(profile.thumbnail) : '' %>', | |
add_avatar_image_path = '<%= image_path('click_to_add.png') %>', | |
$avatar_thumb = $('#avatar-preview'), | |
$avatar_input = $('#profile_avatar').on('change', function (ev) { | |
var vertical_ratio = 100 / thumbnail_crop.h, | |
horizonal_ratio = 100 / thumbnail_crop.w, | |
$captureImage, reader = new FileReader(); | |
/* | |
Do whatever you need to to capture a crop of the image for the thumbnail | |
coordinates and dimensions. I popup a dialog containing Jcrop and capture | |
the crop values with a save button. Just assume that on clicking the save | |
button, thumbnail_crop is populated with [h]eight, [w]idth, x, y values. | |
For the sake of simplicity, $captureImage is the jQuery references to the | |
img tag containing the image in Jcrop. I omitted everything else. | |
*/ | |
if (this.files && this.files[0]) { | |
reader.onload = function (ev) { | |
$captureImage.attr('src', ev.target.result); | |
}; | |
reader.readAsDataURL(input.files[0]); | |
$avatar_delete_checkbox.prop('checked', false); // Saving a new image will delete the old one implicitly | |
$avatar_thumb.attr(src, $captureImage.attr('src')).css({ | |
height: Math.round(vertical_ratio * $captureImage.prop('naturalHeight')) + 'px', | |
width: Math.round(horizonal_ratio * $captureImage.prop('naturalWidth')) + 'px', | |
marginTop: '-' + Math.round(vertical_ratio * thumb.y) + 'px', | |
marginLeft: '-' + Math.round(horizonal_ratio * thumb.x) + 'px' | |
}); | |
} | |
}); | |
$avatar_delete_checkbox = $('#profile_delete_avatar'), | |
$remove_btn = $('#remove-avatar'),on('click', function () { | |
$avatar_input.val(null); | |
if ($avatar_thumb.hasClass('present')) { | |
$avatar_delete_checkbox.prop('checked', true); | |
} | |
$avatar_thumb.attr(src, add_avatar_image_path).css(default_css); | |
}); | |
$('#profile-form').on('submit', function (ev) { | |
if (thumbnail_crop && thumbnail_crop.h && thumbnail_crop.w) { | |
$(this).append($('<input />').attr({ type: 'hidden', name: 'thumbnail', value: JSON.stringify(thumbnail_crop) })); | |
} | |
return true; | |
}); | |
}); | |
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# | |
# file location: ${RAILS_ROOT}/config/environments/development.rb | |
# | |
Rails.application.configure do | |
# Settings specified here will take precedence over those in config/application.rb. | |
... | |
# Store uploaded files on the local file system (see config/storage.yml for options) | |
config.active_storage.service = :local # <= EXAMPLE CODE | |
... | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# | |
# file location: ${RAILS_ROOT}/config/environments/production.rb | |
# | |
Rails.application.configure do | |
# Settings specified here will take precedence over those in config/application.rb. | |
... | |
# Store uploaded files on the local file system (see config/storage.yml for options) | |
config.active_storage.service = :amazon # <= EXAMPLE CODE | |
... | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# | |
# file location: ${RAILS_ROOT}/app/models/profile.rb | |
# | |
class Profile < ApplicationRecord | |
... | |
has_one_attached :avatar | |
attr_accessor :delete_avatar | |
before_save :destroy_avatar!, if: -> { delete_avatar == '1' } | |
... | |
def thumbnail(size = '100x100') | |
if avatar.attached? | |
# I store thumbnail as a json in PostgreSQL. YMMV depending on your persistence layer options. | |
if thumbnail.is_a? Hash | |
dimensions = "#{thumbnail['h']}x#{thumbnail['w']}" | |
coord = "#{thumbnail['x']}+#{thumbnail['y']}" | |
avatar.variant(crop: "#{dimensions}+#{coord}", resize: size).processed | |
else | |
avatar.variant(resize: size).processed | |
end | |
end | |
end | |
... | |
private | |
# My destroy avatar function has the option to tell it do it now, or later, depending on your expected system load | |
def destroy_avatar!(now = true) | |
if avatar.attached? | |
now ? avatar.purge : avatar.purge_later | |
end | |
end | |
def clean_texts! | |
name = sanitize(name) | |
description = sanitize(description, tags: %w[div span img i p], attributes: %w[style class src alt]) | |
if avatar.attached? | |
fn = "#{avatar.filename.base.parameterize}.#{avatar.filename.extension}" | |
avatar.filename = ActiveStorage::Filename.new(fn) | |
end | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# | |
# file location: ${RAILS_ROOT}/app/controllers/profiles_controller.rb | |
# | |
class ProfilesController < ApplicationController | |
# You're usual RESTful endpoint functions here: index, view, new, create, end, update, destroy | |
... | |
# Never trust parameters from the scary internet, only allow the white list through. | |
def profile_params | |
permitted = params.require(:profile).permit(:name, :description, # :whatever_fields_you_use, | |
:avatar, :delete_avatar) # <= EXAMPLE CODE | |
if params['thumbnail'].present? | |
thumb_data = JSON.parse(params['thumbnail'], symbolize_names: true) | |
# Make sure we only have integers for the expected 4 values. | |
if thumb_data[:h].present? && thumb_data[:w].present? && thumb_data[:x].present? && thumb_data[:y].present? | |
permitted[:thumbnail] = { | |
h: thumb_data[:h].to_i, | |
w: thumb_data[:w].to_i, | |
x: thumb_data[:x].to_i, | |
y: thumb_data[:y].to_i | |
} | |
end | |
end | |
permitted.permit! | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!-- | |
file location: ${RAILS_ROOT}/assets/views/profiles/show.html.erb | |
--> | |
<!-- put something like this anywhere you want to show the full-size avatar image --> | |
<%= profile.avatar.attached? ? | |
image_tag(profile.avatar, class: 'media-object avatar thumbnail', alt: t(:avatar)) : | |
image_tag('coming_soon.png', class: 'media-object thumbnail', alt: t(:coming_soon)) | |
%> | |
<!-- put this anywhere you want to show the thumbnail of the image --> | |
<%= profile.avatar.attached? ? | |
image_tag(profile.thumbnail, class: 'media-object avatar thumbnail', alt: t(:avatar), size: '100x100') : | |
image_tag('coming_soon.png', class: 'media-object thumbnail', alt: t(:coming_soon), size: '100x100') | |
%> | |
<!-- I also have this beautiful popup with the thumbnail in my show: --> | |
<script> | |
$(function () { | |
var body_overflow = $('body').css('overflow') || 'visible', | |
$avatar = $('img.avatar.thumbnail'); | |
$avatar.on('click', function () { | |
var $closePreview = $('<span class="overlay-closer"><i class="fa fa-2x fa-window-close white"></i></span>'), | |
$previewImage = $('<div class="preview-image"></div>') | |
.css('background-image', 'url(<%= url_for(profile.avatar) %>)'), | |
$onionskin = $('<div class="avatar-preview-onionskin"></div>').append([ | |
$previewImage, | |
$closePreview | |
]), | |
esc_keypress = function (ev) { | |
if (ev.keyCode == 27) { | |
$closePreview.trigger('click'); | |
} | |
}; | |
$closePreview.on('click', function () { | |
$(document).off('keyup', esc_keypress); | |
$onionskin.remove(); | |
$onionskin = undefined; | |
$closePreview = undefined; | |
$previewImage = undefined; | |
$('body').css('overflow', body_overflow); | |
}); | |
$(document).on('keyup', esc_keypress); | |
$('body').css('overflow', 'hidden').prepend($onionskin); | |
}); | |
}); | |
</script> | |
<% end %> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# | |
# file location: ${RAILS_ROOT}/config/storage.yml | |
# | |
test: | |
service: Disk | |
root: <%= Rails.root.join("tmp/storage") %> | |
local: | |
service: Disk | |
root: <%= Rails.root.join("storage") %> | |
# Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) | |
# <= EXAMPLE CODE => | |
amazon: | |
service: S3 | |
access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> | |
secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> | |
region: <%= ENV.fetch('ACTIVESTORAGE_AWS_S3_REGION') { 'us-west-2' } %> | |
bucket: <%= ENV.fetch('ACTIVESTORAGE_AWS_S3_BUCKET') %> | |
# <= END EXAMPLE CODE => | |
# Remember not to checkin your GCS keyfile to a repository | |
# google: | |
# service: GCS | |
# project: your_project | |
# keyfile: <%= Rails.root.join("path/to/gcs.keyfile") %> | |
# bucket: your_own_bucket | |
# Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) | |
# microsoft: | |
# service: AzureStorage | |
# path: your_azure_storage_path | |
# storage_account_name: your_account_name | |
# storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> | |
# container: your_container_name | |
# mirror: | |
# service: Mirror | |
# primary: local | |
# mirrors: [ amazon, google, microsoft ] |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment