module Effective class Asset < ActiveRecord::Base self.table_name = EffectiveAssets.assets_table_name.to_s mount_uploader :data, EffectiveAssets.uploader belongs_to :user if defined?(User) # This is the user that uploaded the asset. # We are using belongs_to because it makes all the permissions 'just work', # in letting people create assets. # # But this doesn't exactly belong to a user. # Assets belong to acts_as_asset_box enabled classes through attachments. # # 1. The S3 uploader makes a post to s3_controller#create # 2. The asset is created by the current_user # 3. But it doesn't belong to anything yet. It might be attached to a user's asset_boxes, or a page, or a post through attachments # has_many :attachments, :dependent => :delete_all # structure do # title :string # extra :text # content_type :string # upload_file :string # The full url of the file, as originally uploaded # processed :boolean, :default => false # aws_acl :string, :default => 'public-read' # data :string # data_size :integer # height :integer # width :integer # versions_info :text # We store a hash of {:thumb => 34567, :medium => 3343434} data sizes # timestamps # end validates :content_type, presence: true validates :upload_file, presence: true validates :aws_acl, presence: true, inclusion: { in: [EffectiveAssets::AWS_PUBLIC, EffectiveAssets::AWS_PRIVATE] } validates :height, numericality: { allow_nil: true } validates :width, numericality: { allow_nil: true } serialize :versions_info, Hash serialize :extra, Hash before_validation :set_content_type before_save :update_asset_dimensions after_save :enqueue_delayed_job default_scope -> { order(:id) } scope :nonplaceholder, -> { where("#{EffectiveAssets.assets_table_name}.upload_file != ?", 'placeholder') } scope :images, -> { nonplaceholder().where("#{EffectiveAssets.assets_table_name}.content_type LIKE ?", '%image%') } scope :nonimages, -> { nonplaceholder().where("(#{EffectiveAssets.assets_table_name}.content_type NOT LIKE ?)", '%image%') } scope :videos, -> { nonplaceholder().where("#{EffectiveAssets.assets_table_name}.content_type LIKE ?", '%video%') } scope :audio, -> { nonplaceholder().where("#{EffectiveAssets.assets_table_name}.content_type LIKE ?", '%audio%') } scope :files, -> { nonplaceholder().where("(#{EffectiveAssets.assets_table_name}.content_type NOT LIKE ?) AND (#{EffectiveAssets.assets_table_name}.content_type NOT LIKE ?) AND (#{EffectiveAssets.assets_table_name}.content_type NOT LIKE ?)", '%image%', '%video%', '%audio%') } scope :today, -> { nonplaceholder().where("#{EffectiveAssets.assets_table_name}.created_at >= ?", Time.zone.today.beginning_of_day) } scope :this_week, -> { nonplaceholder().where("#{EffectiveAssets.assets_table_name}.created_at >= ?", Time.zone.today.beginning_of_week) } scope :this_month, -> { nonplaceholder().where("#{EffectiveAssets.assets_table_name}.created_at >= ?", Time.zone.today.beginning_of_month) } class << self def s3_base_path "https://#{EffectiveAssets.aws_bucket}.s3.amazonaws.com/" end def string_base_path "string://" end # Just call me with Asset.create_from_url('http://somewhere.com/somthing.jpg') def create_from_url(url, options = {}) opts = {:upload_file => url, :user_id => 1, :aws_acl => EffectiveAssets.aws_acl}.merge(options) attempts = 3 # Try to upload this string file 3 times begin asset = Asset.new(opts) if asset.save asset else puts asset.errors.inspect Rails.logger.info asset.errors.inspect false end rescue => e (attempts -= 1) >= 0 ? (sleep 2; retry) : false end end # This loads the raw contents of a string into a file and uploads that file to s3 # Expect to be passed something like # Asset.create_from_string('some binary stuff from a string', :filename => 'icon_close.png', :content_type => 'image/png') def create_from_string(str, options = {}) filename = options.delete(:filename) || "file-#{Time.now.strftime('%Y-%m-%d-%H-%M-%S')}.txt" filename = URI.escape(filename).gsub(/%\d\d|[^a-zA-Z0-9.-]/, '_') # Replace anything not A-Z, a-z, 0-9, . - opts = {:upload_file => "#{Asset.string_base_path}#{filename}", :user_id => 1, :aws_acl => EffectiveAssets.aws_acl}.merge(options) attempts = 3 # Try to upload this string file 3 times begin asset = Asset.new(opts) asset.data = AssetStringIO.new(filename, str) if asset.save asset else puts asset.errors.inspect Rails.logger.info asset.errors.inspect false end rescue => e (attempts -= 1) >= 0 ? (sleep 2; retry) : false end end end # End of Class methods def title self[:title].presence || file_name end def extra self[:extra] || {} end def url(version = nil, expire_in = nil) aws_acl == EffectiveAssets::AWS_PRIVATE ? authenticated_url(version, expire_in) : public_url(version) end def public_url(version = nil) if data.present? url = (version.present? ? data.send(version).file.try(:public_url) : data.file.try(:public_url)) url || '#' else "#{Asset.s3_base_path.chomp('/')}/#{EffectiveAssets.aws_path.chomp('/')}/#{id.to_i}/#{file_name}" end end def authenticated_url(version = nil, expire_in = 60.minutes) data.aws_authenticated_url_expiration = expire_in url = (version.present? ? data.send(version).file.try(:authenticated_url) : data.file.try(:authenticated_url)) url || '#' end def file_name upload_file.to_s.split('/').last rescue upload_file end def video? content_type.include? 'video' end def image? content_type.include? 'image' end def icon? content_type.include? 'image/x-icon' end def audio? content_type.include? 'audio' end def versions_info self[:versions_info] || {} end def to_s title end def process! begin Rails.logger.info "Processing ##{id}..." print "Processing ##{id}..." data.cache_stored_file! data.retrieve_from_cache!(data.cache_name) data.recreate_versions! save! Rails.logger.info "Successfully processed ##{id}" print "success"; puts '' true rescue => e Rails.logger.info "Error: #{id} -> #{e.to_s}" print "error: #{e.to_s}"; puts '' false end end alias_method :reprocess!, :process! protected def set_content_type if [nil, 'null', 'unknown', 'application/octet-stream', ''].include?(content_type) self.content_type = case File.extname(file_name).downcase.gsub(/\?.+/, '') when '.mp3' ; 'audio/mp3' when '.mp4' ; 'video/mp4' when '.mov' ; 'video/mov' when '.m2v' ; 'video/m2v' when '.m4v' ; 'video/m4v' when '.3gp' ; 'video/3gp' when '.3g2' ; 'video/3g2' when '.avi' ; 'video/avi' when '.jpg' ; 'image/jpg' when '.gif' ; 'image/gif' when '.png' ; 'image/png' when '.bmp' ; 'image/bmp' when '.ico' ; 'image/x-icon' when '.txt' ; 'text/plain' when '.doc' ; 'application/msword' when '.docx' ; 'application/msword' when '.xls' ; 'application/excel' when '.xlsx' ; 'application/excel' else ; 'unknown' end end end def update_asset_dimensions if data.present? && data_changed? && image? begin image = MiniMagick::Image.open(self.data.path) self.width = image[:width] self.height = image[:height] rescue => e end end true end def enqueue_delayed_job if !self.processed && self.upload_file.present? && self.upload_file != 'placeholder' Effective::DelayedJob.new.process_asset(self.id) end end end class AssetStringIO < StringIO attr_accessor :filepath def initialize(*args) super(*args[1..-1]) @filepath = args[0] end def original_filename File.basename(filepath) end end end