Facing very wired issue while using Paperclip (With s3 storage) and Apartment gem.
Apartment gem is being used for multi-tenancy with Postgres DB. ( i.e Separate Schema for every tenant )
For Storage, I would like to create a separate folder for every tenant.
class Media < ApplicationRecord
belongs_to :user
has_attached_file :file,
:storage => :s3,
:s3_credentials => {
:access_key_id => "ACCESSKEY",
:secret_access_key => "SECRETE KEY"
},
:path => "#{Apartment::Tenant.current}/:class/:attachment/:id_partition/:style/:filename"
end
Above is my media folder, which belongs to User.
class User < ApplicationRecord
has_attached_file :avatar,
:storage => :s3,
:s3_credentials => {
:access_key_id => "ACCESSKEY",
:secret_access_key => "SECRETE KEY"
},
:path => "#{Apartment::Tenant.current}/:class/:attachment/:id_partition/:style/:filename"
end
Apartment.rb file looks like below
require 'apartment/elevators/subdomain'
require 'rescued_apartment_middleware'
Apartment.configure do |config|
config.excluded_models = %w{ Account }
config.tenant_names = lambda { Account.pluck :subdomain }
config.use_schemas = true
end
Rails.application.config.middleware.insert_before Warden::Manager, RescuedApartmentElevator
Apartment::Elevators::Subdomain.excluded_subdomains = ['www','admin']
rescued_apartment_elevator.rb file looks like below
class RescuedApartmentElevator < Apartment::Elevators::Subdomain
def call(env)
begin
super
rescue Apartment::TenantNotFound
env[:apartment_tenant_not_found] = true # to be later referenced in your ApplicationController
#app.call(env) # the middleware call method should return this, but it was probably short-circuited by the raise
end
end
end
In application_controller.rb I have handled the code something like below
class ApplicationController < ActionController::Base
before_action :load_schema, :authenticate_user!, :set_mailer_host
before_action :configure_permitted_parameters, if: :devise_controller?
private
def load_schema
Apartment::Tenant.switch!
return if request.subdomain == "www"
if request.subdomain == ""
redirect_to root_url(subdomain: 'www')
else
if current_account
Apartment::Tenant.switch!(current_account.subdomain)
else
if request.env[:apartment_tenant_not_found]
redirect_to root_url(subdomain: 'www'), notice: 'Account you are looking for do not exists'
end
end
end
end
end
I start my server, go to one of the tenant e.g http://apple.lvh.me:3000 (apple is tenant name here)
1. If I go to edit user profile and upload the file for user profile,
Apartment::Tenant.current is returning "public" in model and hence
the file is getting uploaded in public folder instead of "apple"
folder.
I am using devise gem (latest version) below is how my user_controller.rb looks like
class UsersController < ApplicationController
before_action : set_user, only: [:edit,:update]
def update
if #user.update(user_params)
if #user.pending_reconfirmation?
redirect_to edit_user_path(current_user), notice: "Your profile is successfully updated! But if you changed the email address, then please confirm it before!"
else
redirect_to edit_user_path(current_user), notice: "Your profile is successfully updated!"
end
else
render "edit"
end
end
end
Problem 2
**When I go to media and add some media, they would go properly to "apple" folder on s3 bucket. I would sign out, go to different tanent i.e http://google.lvh.me:3000/ (google is tenant name here). If I go to media listing, URL will continue to point to Apartment::Tenant.current -> apple only. This results in wrong URL for google tenant and images wont display.
If I shut down and restart the server, then rails will start returning google for Apartment::Tenant.current method and hence URL pointing correctly to correct folder. **
To Summerise, below are my problems.
Using Apartment::Tenant.current in model for paperclip custom path -
is it right way? Is there some other method that could return me
current tanent so I can use it for creating folder on s3 bucket.
Have I configured the middleware correctly in my apartment.rb file?
Is there different way of doing this?
After posting to StackOverflow, I was able to figureout the solution. Posting it here in case someone else stumbleupon this in future.
My Mistake was tryign to use #{Apartment::Tenant.current} into path directly. This is wrong. paperclip support Interpolates, I should be using that in order to generate dynamic paths.
so below are the changes required.
has_attached_file :file,
:storage => :s3,
:s3_credentials => {
:access_key_id => "AccessKey",
:secret_access_key => "SecreteKey"
},
:path => ":tenant/:class/:attachment/:id_partition/:style/:filename"
Notice the :tenant being introduced in the path.
Add following line into paperclip.rb
Paperclip.interpolates :tenant do |attachment, style|
Apartment::Tenant.current
end
This is it. Hope it helps someone!
Related
I am trying to write a method in my "team" model but current_user is showing this error
undefined local variable or method `current_user' for #
def set_default_url
if current_user.id == self.user_id
"/assets/default_black_:style_logo.jpg"
else
"/assets/default_:style_logo.jpg"
end
end
The method current_user is working fine for other models and controllers . I am calling this method like this .
has_attached_file :logo, :styles => {
:medium => "200x200#",
:thumb => "100x100#",
:small => "50x50#"
},
:default_url => :set_default_url
I am using rails 3.2 , ruby 1.9.3 and devise 3.1 . It seems to be simple task but I don't understand where the fault lies . I will be really thankful if someone helps me out here .
current_user is not available in any model, to access current_user in model do this
In your application controller
before_filter :set_current_user
def set_current_user
Team.current_user = current_user
end
in your Team model add this line
cattr_accessor :current_user
Congrats, now current_user is available in every model, to get the current user just use the following line everywhere
Team.current_user
NOTE: Restart the server after adding the lines mentioned above!
Now in your question you can use it like
def set_default_url
if Team.current_user.id == self.user_id
"/assets/default_black_:style_logo.jpg"
else
"/assets/default_:style_logo.jpg"
end
end
Hope this helps!
If you are using it only once than while calling this method pass current_user as argument like
has_attached_file :logo, :styles => {
:medium => "200x200#",
:thumb => "100x100#",
:small => "50x50#"
},
:default_url => :set_default_url(current_user)
and in model
def set_default_url(current_user)
if current_user.id == self.user_id
"/assets/default_black_:style_logo.jpg"
else
"/assets/default_:style_logo.jpg"
end
end
If you dont want above step then follow these
Go to User Model
def self.current_user
Thread.current[:user]
end
def self.current_user=(user)
Thread.current[:user] = user
end
Then go to application controller
before_filter :set_current_user
def set_current_user
User.current_user = current_user
end
Now we can easily fetch current_user in any model not only in Team
just give as User.current_user so in your code
def set_default_url
if User.current_user.id == self.user_id
"/assets/default_black_:style_logo.jpg"
else
"/assets/default_:style_logo.jpg"
end
end
So make use of it.
Hope it solves your issue in a good way. Free to use in any model
User.current_user to fetch the current user
User.current_user= to assign the current user.
Thanks
I have 2 Carrierwave uploaders - ItemUploader and ImageUploader, and am using fog.
I can upload files to S3 just fine, but doing a destroy doesn't remove them from S3.
This is my destroy action:
def destroy
#item = Item.find(params[:id])
#item.destroy
respond_to do |format|
format.html { redirect_to items_url }
format.json { head :no_content }
end
end
When I do item.destroy, it deletes the record from my db but it doesn't remove the file from S3 and it doesn't remove the folders.
This is a brand new S3 bucket, with vanilla settings. Also a brand new Carrierwave install.
FYI: I have tried adding #item.remove_item! and #item.remove_image! to the destroy action of the controller but that hasn't done the trick either.
Edit 1
So it seems that what happens is that it deletes 1 of the attachments.
The model has this:
class Item < ActiveRecord::Base
# image :string(255)
# link :string(255)
mount_uploader :link, ItemUploader
mount_uploader :image, ImageUploader
end
So, when I delete an object in my console, it removes the object associated with ItemUploader and not the image related via the ImageUploader.
Why would it delete 1 and not the other?
It seems there is something wrong with my console - because once I delete the object via the web UI it deletes all the related objects in S3.
But if I do it via the console, it doesn't work.
I will be opening another SO question for that particular issue.
It deletes but take little time specially if you are referring file(image) from cdn.
Uses aws sdk - https://github.com/amazonwebservices/aws-sdk-for-ruby
You can build a facade to manage your objects on 3, for example:
require 'aws-sdk'
class Facades::AmazonFacade
attr_reader :s3
#
# Connection to Amazon S3
#
def initialize
#s3 = AWS::S3.new(
:access_key_id => config['access_key_id'],
:secret_access_key => config['secret_access_key']
)
#bucket = #s3.buckets[self.config['bucket']]
end
def config
##config ||= YAML::load(File.open("#{Rails.root}/config/amazon_s3.yml" ))[Rails.env]
end
####
def policy(bucket, options = {})
# Base 64 policy
end
def signature(bucket, options = {})
# Base64 signature
end
#
# Find object and get public urls
#
def url_link(obj, expires)
#bucket.objects[obj].url_for(:read, :secure => true, :expires => 10*60).to_s
end
def object_exists_on_amazon?(obj)
#bucket.objects[obj].exists?
end
def object_size(obj)
unless Rails.env.test?
#bucket.objects[obj].content_length
end
end
def object_upload_date(obj)
#bucket.objects[obj].last_modified
end
#
# create, delete objects
#
def store_object_on_amazon(obj, file, access)
#bucket.objects[obj].write(file, :acl => access)
end
def delete_object_on_amazon(obj)
#bucket.objects[obj].delete(:force => true)
end
end
I'm trying to upload an attachment using REST API on my server through a PUT request. I can do this by putting the binary file in the request body but I'd also like to save this file as an attachment to a model which uses paperclip to save attachments.
Here's my current involved class definitions:
class Cl < ActiveRecord::Base
after_update :save_tses
validates_associated :tses
has_many :tses
...truncated...
def save_tses
tses.each do |ts|
ts.save(false)
end
end
end
class Ts < ActiveRecord::Base
has_attached_file :tsa, :styles => { :thumb => {:geometry => "100x141>", :format => :jpg} },
:path => ":rails_root/public/system/:attachment/:id/:style/:friendly_filename",
:url => "/system/:attachment/:id/:style/:friendly_filename"
belongs_to :cl
def friendly_filename
"#{self.tsa_file_name.gsub( /[^a-zA-Z0-9_\.]/, '_')}"
end
end
I can save the attachments just fine using the file upload on the html page. I'd like to do this on a controller that receives the file as binary data through a PUT request.
Any suggestions?
Also you can you use -
https://github.com/jwagener/httmultiparty
Got it,
# controller.rb
def add_ts
# params[:id]
# params[:tsa]
#cl = Cl.find(params[:id])
ts = #cl.tses.build(:name => "#{#cl.name}_#{Time.now.to_i}")
ts.tsa = params[:tsa]
if ts.save
render :json => {:status => "OK"}
else
render :json => {:status => "ERROR"}
end
end
# Test
curl -F "tsa=#file.pdf" "http://host/cl/474/add_ts"
=> {"status":"OK"}
I am working with Rails 3 and Paperclip to attach uploaded files to several object types using a polymorphic association. I have created an Asset model and an inherited Image model (will be adding others, like Video and Documents later) as follows:
# app/models/asset.rb
class Asset < ActiveRecord::Base
# Nothing here yet
end
# app/models/image.rb
class Image < Asset
belongs_to :assetable, :polymorphic => true
has_attached_file :file, {
:styles => {
:small => { :geometry => '23x23#', :format => 'png' },
:medium => { :geometry => '100x100#', :format => 'png' } }
}.merge(PAPERCLIP_STORAGE_OPTIONS).merge(PAPERCLIP_STORAGE_OPTIONS_ASSET_IMAGE) # Variables sent in environments to direct uploads to filesystem storage in development.rb and S3 in production.rb
validates_attachment_presence :file
validates_attachment_size :file, :less_than => 5.megabytes
end
I then have another object type, Unit, which I am attaching multiple images to as follows:
# app/models/unit.rb
class Unit < ActiveRecord::Base
# ...
has_many :images, :as => :assetable, :dependent => :destroy
accepts_nested_attributes_for :images
end
# app/controllers/units_controller.rb
class UnitsController < ApplicationController
# ...
def new
#unit = current_user.units.new
# ...
#unit.images.build
end
def create
#unit = current_user.units.new(params[:unit])
# ...
respond_to do |format|
if #unit.save
format.html { redirect_to(#unit, :notice => 'Unit creation successful!') }
else
format.html { render :action => "new" }
end
end
end
def show
#unit = current_user.units.find(params[:id])
#unit_images = #unit.images
# ...
end
def edit
#unit = current_user.units.find(params[:id])
# ...
#unit.images.build
end
def update
#unit = current_user.units.find(params[:id], :readonly => false)
respond_to do |format|
if #unit.update_attributes(params[:unit])
format.html { redirect_to(#unit, :notice => 'Unit was successfully updated.') }
else
format.html { render :action => "edit" }
end
end
end
def destroy
#unit = current_user.units.find(params[:id])
#unit.destroy
respond_to do |format|
format.html { redirect_to(units_url) }
end
end
end
# app/views/units/_form.html.haml
.field # Display already uploaded images
= f.fields_for :images do |assets|
- unless assets.object.new_record?
= link_to(image_tag(assets.object.file.url(:medium)), assets.object.file.url(:original))
.field # Display field to add new image
= f.fields_for :images do |assets|
- if assets.object.new_record?
= assets.label :images, "Image File"
= assets.file_field :file, :class => 'uploadify'
Using these settings I am able to upload images one at at time, per display of the form.
The issues start when I try to integrate Uploadify to add multi file uploading/previewing. I have satisfied all the Uploadify dependancies, but in order to save the images associated with the Unit model I need to somehow include a reverence to the unit_id so that the polymorphic association can be made properly. Below is my current Uploadify code:
%script
$(document).ready(function() {
$('.uploadify').uploadify({
uploader : '/uploadify/uploadify.swf',
cancelImg : '/uploadify/cancel.png',
auto : true,
multi : true,
script : '#{units_path}',
scriptData : {
"#{key = Rails.application.config.session_options[:key]}" : "#{cookies[key]}",
"#{request_forgery_protection_token}" : "#{form_authenticity_token}",
}
});
});
So while I can easily upload with Paperclip along, Uploadify will not work. Any help would be much appreciated. Thank you in advance.
UPDATE:
After doing more research I ran across this comment to a similar issue: Rails3, S3, Paperclip Attachment as it's own model?. Any thoughts on whether or not that would work in this situation? Is there an easy way of determining the unit.id from the /new method and passing it to the Uploadify-created Asset?
We had solved a very similar problem once by saving the model right away when loading the form in a draft state (using state machine). Like this the model is available when you're trying to attach the files you're uploading and once you're submitting the rest of the form, you're basically just updating the model which changes it's state to e.g. published. It's a little work to update the controllers etc., but it did the trick.
I had a quick question. Is it possible to save a file without actually uploading it through a form?
For example, let's say I'm looking at attachments from emails, and I want to save them using a paperclip. How do I do this? Do I manually have to call a save_file(or something similar) somewhere?
Any help would be much appreciated!
I have a rake task that loads images (client logos) from a directory directly onto parperclip. You can probably adapt it to your needs.
This is my simplified Client model:
class Client < ActiveRecord::Base
LOGO_STYLES = {
:original => ['1024x768>', :jpg],
:medium => ['256x192#', :jpg],
:small => ['128x96#', :jpg]
}
has_attached_file :logo,
:styles => Client::LOGO_STYLES,
:url => "/clients/logo/:id.jpg?style=:style"
attr_protected :logo_file_name, :logo_content_type, :logo_size
Then on my rake task I do this:
# the logos are in a folder with path logos_dir
Dir.glob(File.join(logos_dir,'*')).each do |logo_path|
if File.basename(logo_path)[0]!= '.' and !File.directory? logo_path
client_code = File.basename(logo_path, '.*') #filename without extension
client = Client.find_by_code(client_code) #you could use the ids, too
raise "could not find client for client_code #{client_code}" if client.nil?
File.open(logo_path) do |f|
client.logo = f # just assign the logo attribute to a file
client.save
end #file gets closed automatically here
end
end
Regards!
The file saved in Paperclip doesn't have to be uploaded directly through a form.
I'm using Paperclip in a project to save files from URLs from webcrawler results. I'm not sure how you'd get email attachments (are they on the local file system of the server? Is your app an email app like GMail?) but as long as you can get a file stream (via something like open(URI.parse(crawl_result)) in my case...) you can attach that file to your model field that's marked has_attached_file.
This blog post about Easy Upload via URL with Paperclip helped me figure this out.
Since it now appears the original blog post is no longer available - here's the gist of it pulled from wayback machine:
This example shows a Photo model that has an Image attachment.
The technique we're using requires adding a *_remote_url (string) column for your attachment, which is used to store the original URL. So, in this case, we need to add a column named image_remote_url the photos table.
# db/migrate/20081210200032_add_image_remote_url_to_photos.rb
class AddImageRemoteUrlToPhotos < ActiveRecord::Migration
def self.up
add_column :photos, :image_remote_url, :string
end
def self.down
remove_column :photos, :image_remote_url
end
end
Nothing special is required for the controller...
# app/controllers/photos_controller.rb
class PhotosController < ApplicationController
def create
#photo = Photo.new(params[:photo])
if #photo.save
redirect_to photos_path
else
render :action => 'new'
end
end
end
In the form, we add a text_field called :image_url, so people can upload a file or provide a URL...
# app/views/photos/new.html.erb
<%= error_messages_for :photo %>
<% form_for :photo, :html => { :multipart => true } do |f| %>
Upload a photo: <%= f.file_field :image %><br>
...or provide a URL: <%= f.text_field :image_url %><br>
<%= f.submit 'Submit' %>
<% end %>
The meaty stuff is in the Photo model. We need to require open-uri, add an attr_accessor :image_url, and do the normal has_attached_file stuff. Then, we add a before_validation callback to download the file in the image_url attribute (if provided) and save the original URL as image_remote_url. Finally, we do a validates_presence_of :image_remote_url, which allows us to rescue from the many exceptions that can be raised when attempting to download the file.
# app/models/photo.rb
require 'open-uri'
class Photo < ActiveRecord::Base
attr_accessor :image_url
has_attached_file :image # etc...
before_validation :download_remote_image, :if => :image_url_provided?
validates_presence_of :image_remote_url, :if => :image_url_provided?, :message => 'is invalid or inaccessible'
private
def image_url_provided?
!self.image_url.blank?
end
def download_remote_image
self.image = do_download_remote_image
self.image_remote_url = image_url
end
def do_download_remote_image
io = open(URI.parse(image_url))
def io.original_filename; base_uri.path.split('/').last; end
io.original_filename.blank? ? nil : io
rescue # catch url errors with validations instead of exceptions (Errno::ENOENT, OpenURI::HTTPError, etc...)
end
end
Everything will work as normal, including the creation of thumbnails, etc. Plus, since we're doing all of the hard stuff in the model, "uploading" a file via URL works from within script/console as well:
$ script/console
Loading development environment (Rails 2.2.2)
>> Photo.new(:image_url => 'http://www.google.com/intl/en_ALL/images/logo.gif')
=> #<Photo image_file_name: "logo.gif", image_remote_url: "http://www.google.com/intl/en_ALL/images/logo.gif">