Can someone help me understand why images are not direct uploading to s3?

I’m following the docs for uploading multiple files and direct S3 uploads, but they’re not working as expected, and I do not understand why. Files are submitted in the params request as files[], but they should be uploading to S3 upon selection. Instead, the only submission that occurs is the POST request that happens when I click submit to save everything else to my database. Why is this not working? Thank you for taking the time to look at this!

Processing by ItemsController#create as HTML

Parameters: {"utf8"=>"✓", "authenticity_token"=>"[FILTERED]", "files"=>[#<ActionDispatch::Http::UploadedFile:0x00007f9cbc59a050 u/tempfile=#<Tempfile:/var/folders/hj/g_90jx_n7s58m73qlf9h0c4w0000gn/T/RackMultipart20200924-5853-1pb0sdf.jpg>, u/original_filename="in.jpg", u/content_type="image/jpeg", u/headers="Content-Disposition: form-data; name=\"files[]\"; filename=\"in.jpg\"\r\nContent-Type: image/jpeg\r\n">], "item"=>{"title"=>"dip"}, "commit"=>"Submit"}

Item Create (10.1ms) INSERT INTO "items" ("created_at", "updated_at", "title", "user_id") VALUES ($1, $2, $3, $4) RETURNING "id" [["created_at", "2020-09-24 22:04:41.333195"], ["updated_at", "2020-09-24 22:04:41.333195"], ["title", "dip"], ["user_id", 1]]

My initializer:

require "shrine"
require "shrine/storage/file_system"
require "shrine/storage/s3"

s3 = { 
 bucket: ENV['S3_BUCKET_NAME'],
 region: ENV['AWS_REGION'],
 access_key_id: ENV['AWS_ACCESS_KEY_ID'],
 secret_access_key: ENV['AWS_SECRET_ACCESS_KEY']
}
Shrine.storages = { 
 cache: Shrine::Storage::S3.new(prefix: "cache", **s3), 
 store: Shrine::Storage::S3.new(prefix: "store", **s3)
}

Shrine.plugin :activerecord
Shrine.plugin :cached_attachment_data
Shrine.plugin :restore_cached_data
Shrine.plugin :validation
Shrine.plugin :validation_helpers
Shrine.plugin :determine_mime_type, analyzer: :marcel
Shrine.plugin :remove_invalid

My uploader:

class ImagesUploader < Shrine
 require "image_processing/mini_magick"
 plugin :pretty_location
 plugin :derivatives
	
 Shrine.plugin :presign_endpoint, presign_options: -> (request) {
  filename = request.params["filename"]
  type     = request.params["type"]

  {
   content_disposition:    ContentDisposition.inline(filename),
   content_type:           type,
   content_length_range:   0..(5*1024*1024),
  }
 }

 Attacher.validate do
  validate_min_size		10.kilobytes	
  validate_max_size		5.megabytes, message: 'must be smaller than 5MB'
  validate_mime_type 	%w[image/jpeg image/png]
  validate_extension 	%w[jpg jpeg png]
 end

 Attacher.derivatives do |original|
  magick = ImageProcessing::MiniMagick.source(original)
  {
   large: magick.resize_to_limit!(1000, 1000),
   small: magick.resize_and_pad!(225, 220)
  }
 end
end

A polymorphic photos model:

create_table "photos", force: :cascade do |t|
 t.string "imageable_type"
 t.bigint "imageable_id"
 t.text "image_data"
 t.datetime "created_at", null: false
 t.datetime "updated_at", null: false
 t.index ["imageable_type", "imageable_id"], name: "index_photos_on_imageable_type_and_imageable_id"
end

Models:

class Item < ApplicationRecord
 has_many :photos, as: :imageable, dependent: :destroy
end

class Photo < ApplicationRecord
 include ImagesUploader::Attachment(:image)
 belongs_to :imageable, polymorphic: true
 validates_presence_of :image
end

A view:

<%= form_for @item, html: { enctype: "multipart/form-data" } do |f| %>
 <%= f.fields_for :photos do |i| %>
  <%= i.label :image %>
  <%= i.hidden_field :image, value: i.object.cached_photos_data, class: "upload-data" %>
  <%= i.file_field :image, class: "upload-file" %>
 <% end %>
 <%= file_field_tag "files[]", multiple: true %>
 
 <%= f.text_field :title %>
      
 <%= f.submit "Submit" %>    
<% end %>

My items controller:

class ItemsController < ApplicationController
 def create
  @item = current_user.owned_items.create(item_params)
  @item.save
 end

 private
 def item_params
  params.require(:item).permit(:title, photos_attributes: { image: [] })
 end
end

My route and presigns controller action for authorization:

get 'presign/s3/params', to: 'presigns#image'


class PresignsController < InheritedResources::Base
 before_action :authenticate_user!

 def image
  set_rack_response ImagesUploader.presign_response(:cache, request.env)
 end

 private
 def set_rack_response((status, headers, body))
  self.status = status
  self.headers.merge!(headers)
  self.response_body = body
 end
end

To confirm that this works, I visit presign/s3/params?filename=nature&type=jpg and get JSON that shows a file key and my AWS credentials.

And then all the webpacker code for fileUpload.js, except I changed this line…

.use(AwsS3, {
 companionUrl: '/',
})

…to the endpoint my app uses for presigning:

.use(AwsS3, {
 companionUrl: 'presign/s3/params',
})

This is because Uppy doesn’t know about your files[] file input field. Uppy won’t activate automatically, you have to tell it.

In your case, you probably want to mount Uppy’s FileInput component, and then upload via that (and hide the files[] field).

You’ll also likely need to change companionUrl to /presign, as Uppy automatically adds the /s3/params bit. Just follow the official Rails demo app which shows a working implementation of multiple uploads directly to S3.

1 Like

Aha, thank you for the note about the companionUrl!:smiling_face_with_three_hearts: Two things:

  1. The other file_field doesn’t show for some reason, so if I hide the files[] one, I get no file_field at all. I was actually wondering why this doc suggests two fields, but the one on the github wiki for presign endpoints suggests just the one?

  2. When you say I might need to mount Uppy’s FileInput component, do you mean to put this in javascript/packs/application.js? I am doing this.

    import fileUpload from 'fileUpload'
    document.addEventListener('turbolinks:load', () => {
     document.querySelectorAll('.upload-file').forEach(fileInput => {
      fileUpload(fileInput)
     })
    })
    

Edit: ok, :rofl:I see that part of the issue is the code that runs on turbolinks:load mounts FileInput on each class named upload-file. However, this part of my form is invisible for some reason, No wonder it’s not working! Posting to StackOverflow to figure out why that field isn’t rendering :thinking::shushing_face: