Set content type in S3 when attaching via Paperclip 4?
Asked Answered
F

3

12

I'm trying to attach CSV files to a Rails3 model using paperclip 4.1.1, but I'm having trouble getting the content-type as reported by S3 to be text/csv (instead I am getting text/plain). When I subsequently download the file from S3, the extension is getting changed to match the content-type instead of preserving the original extension (so test.csv is downloaded as test.txt).

From what I can see, when you upload a file, the FileAdapter will cache the content-type on creation with whatever value was determined by the ContentTypeDetector (which calls file -b --mime filename). Unfortunately, CSV files return text/plain which makes sense, as how can you really distinguish this? Attempts to set the content-type with attachment.instance_write(:content_type, 'text/csv') only set the value in the model and do not affect what gets written to S3.

FileAdapter's content_type initialized here: https://github.com/thoughtbot/paperclip/blob/v4.0/lib/paperclip/io_adapters/file_adapter.rb#L14

Call which creates that io_adapter: https://github.com/thoughtbot/paperclip/blob/v4.0/lib/paperclip/attachment.rb#L98

I really have a generic upload here (so I can't hard-code the content type in the S3 headers definition in has_attached_file), and I don't really want the content-type spoofing protection. Any ideas/suggestions? I would prefer not to downgrade to 3.5 because it would mean just delaying the pain, but if that's the only way, I'll entertain it...

Footgear answered 17/4, 2014 at 4:40 Comment(3)
Are you using s3 or s3 through fog for your storage provider? You could pass a lambda into the s3_headers to determine if you should overwrite the content_type value, but the fog implementation doesn't have an equivalent :fog_headers option...Tripod
@Tripod If I use S3 through Fog, do I have any other ways to override the header?Dresser
@charinten hoping my answer below helps you.Tripod
T
14

If you are using fog then you can do something like this:

has_attached_file :report,
  fog_file: lambda { |attachment|
    {
      content_type: 'text/csv',
      content_disposition: "attachment; filename=#{attachment.original_filename}",
    }
  }

If you are using Amazon S3 as your storage provider, then something like this should work:

has_attached_file :report
  s3_headers: lambda { |attachment|
    { 
      'Content-Type' => 'text/csv',
      'Content-Disposition' => "attachment; filename=#{attachment.original_filename}",
    }
  }
Tripod answered 19/6, 2014 at 2:21 Comment(8)
In your setup, do you mean that we can only upload 'text/csv'? What about other format?Dresser
I think you have an extra comma at the end of the 'Content-Disposition' lines. The s3_headers section works fine if using s3 with fog.Nicolasanicolau
attachment instance here is the model itself, taking the example above - original_filename here is a method or could be an attribute in your model.Augustusaugy
Almost there... but the attachment is empty entity (before save) when sending to s3, so attachment.original_filename is always nil. How could I access the original filename before it's saved?Juggernaut
I have the exact same issue. Did you figure out a workaround?Martinmartina
does anyone know how to fix this? s3_headers lambda gets an attachment which has all the attachment attributes set to nil, the file name, size, content type etc everything is nil when s3_headers lambda gets executed. The lambda is only useful for static headers.Quintanilla
@Quintanilla others who have encountered empty attachment attributes have found success in adding custom setters to the model like this solution: https://mcmap.net/q/297881/-set-content-type-in-s3-when-attaching-via-paperclip-4Tripod
thanks @stereoscott, I've checked it already and also posted a version of my own solutionQuintanilla
C
1

Had this problem just recently and both the post process and the lambda don't work so did a work around. Same with others observation, the values of the attachment is empty when calling the s3 lambda headers.

  1. add this line to the model
attr_accessor :tmp_content_type, :tmp_file_name
  1. override the file assignment method so we could get the file info and store it for later use
 def file=(f)
  set_tmp_values(f.path)
  file.assign(f)
end

def set_tmp_values(file_path)
  self.tmp_file_name = File.basename(file_path)
  self.tmp_content_type = MIME::Types.type_for(file_path).first.content_type
end
  1. Use the temp vars
:s3_headers => lambda { |attachment|
  {
    'Content-Type' => attachment.tmp_content_type,
    "Content-Disposition" => "attachment; filename=\"# {attachment.tmp_file_name}\""
  }
}
Choreography answered 5/3, 2021 at 5:16 Comment(2)
which class' file= method is being overridden in this case?Quintanilla
figured it out, the file= method is due to the model having has_attached_file :file, so it is being overriden in the model itselfQuintanilla
Q
1

I'm using Paperclip 6.1.0 with Rails 5.2.3, Ruby 2.6.6, this answer uses the same idea proposed by James Euangel in an existing answer, but it's a bit more generic and uses monkey-patching.

I'll define attr_accessors on the model that uses Paperclip::Attachment for file attachment and use those to set values in the s3_headers options which get passed to has_attached_file macro.

# app/models/concerns/paperclip_attributes_maker.rb
# This concern defines attr_accessors for the paperclip attachment attributes of the ActiveRecord model

module PaperclipAttributesMaker
  extend ActiveSupport::Concern

  def self.included
    base.class_eval do
      base::PAPERCLIP_ATTACHMENT_ATTRS.each do |attachment_attr|
        attr_accessor "paperclip_#{attachment_attr}_content_type"
        attr_accessor "paperclip_#{attachment_attr}_filename"
      end
    end
  end
end

Now, we need to monkey-patch the attachment setter method defined in the Paperclip::HasAttachaedFile class:

# app/initializers/patch_paperclip_has_attached_file.rb
# it patches paperclip/has_attached_file.rb

module Paperclip
  class HasAttachedFile
    def define_setter
      name = @name
      @klass.send :define_method, "#{@name}=" do |file|
        content_type = (file.content_type || MIME::Types.type_for(file.path).first.content_type) rescue nil
        instance_variable_set("@paperclip_#{name}_content_type", content_type)
        instance_variable_set("@paperclip_#{name}_filename", (file.original_filename rescue nil))
        send(name).assign(file)
      end
    end
  end
end

Lastly, we declare the Paperclip attachments(by this I mean how many attachments we got in the ActiveRecord model):

class StudentAttachment < ApplicationRecord
  PAPERCLIP_ATTACHMENT_ATTRS = [:attachment1, :attachment2]
  
  include PaperclipAttributesMaker # should be included after definition of PAPERCLIP_ATTACHMENT_ATTRS
  
  #...other code

  has_attached_file :attachment1, { # other options here
    s3_headers: lambda { |attachment|
      {
        'Content-Type' => attachment.paperclip_attachment1_content_type,
        "Content-Disposition" => "attachment; filename=\"#{attachment.paperclip_attachment1_filename}\""}}
      }

  has_attached_file :attachment2, { # other options here
    s3_headers: lambda { |attachment|
      {
        'Content-Type' => attachment.paperclip_attachment2_content_type,
        "Content-Disposition" => "attachment; filename=\"#{attachment.paperclip_attachment2_filename}\""}}
      }
end
Quintanilla answered 19/9, 2023 at 13:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.