2013-11-05 19:04:47 +01:00
require " digest/sha1 "
2014-04-14 22:55:57 +02:00
require_dependency " image_sizer "
require_dependency " file_helper "
2015-06-12 12:02:36 +02:00
require_dependency " url_helper "
require_dependency " db_helper "
2014-04-14 22:55:57 +02:00
require_dependency " validators/upload_validator "
2015-06-12 12:02:36 +02:00
require_dependency " file_store/local_store "
2013-02-05 14:16:51 -05:00
class Upload < ActiveRecord :: Base
belongs_to :user
2013-11-05 19:04:47 +01:00
has_many :post_uploads , dependent : :destroy
2013-06-13 23:44:24 +02:00
has_many :posts , through : :post_uploads
2013-06-13 01:43:50 +02:00
2013-06-21 09:34:02 +02:00
has_many :optimized_images , dependent : :destroy
2013-06-16 10:39:48 +02:00
2013-02-05 14:16:51 -05:00
validates_presence_of :filesize
validates_presence_of :original_filename
2014-04-14 22:55:57 +02:00
validates_with :: Validators :: UploadValidator
2013-11-05 19:04:47 +01:00
def thumbnail ( width = self . width , height = self . height )
2014-05-06 14:41:59 +01:00
optimized_images . find_by ( width : width , height : height )
2013-06-17 01:00:25 +02:00
end
2013-11-05 19:04:47 +01:00
def has_thumbnail? ( width , height )
2013-09-27 10:55:50 +02:00
thumbnail ( width , height ) . present?
2013-06-17 01:00:25 +02:00
end
2015-05-28 01:48:07 +02:00
def create_thumbnail! ( width , height )
2013-06-17 01:00:25 +02:00
return unless SiteSetting . create_thumbnails?
2015-05-28 01:48:07 +02:00
thumbnail = OptimizedImage . create_for ( self , width , height , allow_animation : SiteSetting . allow_animated_thumbnails )
2013-09-27 10:55:50 +02:00
if thumbnail
optimized_images << thumbnail
self . width = width
self . height = height
2015-06-27 01:26:16 +02:00
save ( validate : false )
2013-09-27 10:55:50 +02:00
end
2013-06-17 01:00:25 +02:00
end
2013-06-21 09:34:02 +02:00
def destroy
2013-06-19 21:51:41 +02:00
Upload . transaction do
2013-08-13 22:08:29 +02:00
Discourse . store . remove_upload ( self )
2013-06-19 21:51:41 +02:00
super
end
end
2013-08-13 22:08:29 +02:00
def extension
File . extname ( original_filename )
end
2015-07-15 17:15:43 +02:00
# list of image types that will be cropped
CROPPED_IMAGE_TYPES || = [ " avatar " , " profile_background " , " card_background " ]
2014-04-15 17:15:47 +02:00
# options
# - content_type
# - origin
2015-07-15 17:15:43 +02:00
# - image_type
2014-04-15 17:15:47 +02:00
def self . create_for ( user_id , file , filename , filesize , options = { } )
2015-07-15 17:15:43 +02:00
DistributedMutex . synchronize ( " upload_ #{ user_id } _ #{ filename } " ) do
# do some work on images
if FileHelper . is_image? ( filename )
if filename =~ / \ .svg$ /i
svg = Nokogiri :: XML ( file ) . at_css ( " svg " )
w = svg [ " width " ] . to_i
h = svg [ " height " ] . to_i
else
2015-07-22 17:10:42 +02:00
# fix orientation first (but not for GIFs)
fix_image_orientation ( file . path ) unless filename =~ / \ .GIF$ /i
2015-07-15 17:15:43 +02:00
# retrieve image info
image_info = FastImage . new ( file ) rescue nil
w , h = * ( image_info . try ( :size ) || [ 0 , 0 ] )
end
# default size
width , height = ImageSizer . resize ( w , h )
# make sure we're at the beginning of the file (both FastImage and Nokogiri move the pointer)
file . rewind
# crop images depending on their type
if CROPPED_IMAGE_TYPES . include? ( options [ :image_type ] )
2015-07-22 17:10:42 +02:00
allow_animation = SiteSetting . allow_animated_thumbnails
2015-07-15 17:15:43 +02:00
max_pixel_ratio = Discourse :: PIXEL_RATIOS . max
case options [ :image_type ]
when " avatar "
allow_animation = SiteSetting . allow_animated_avatars
width = height = Discourse . avatar_sizes . max
when " profile_background "
max_width = 850 * max_pixel_ratio
width , height = ImageSizer . resize ( w , h , max_width : max_width , max_height : max_width )
when " card_background "
max_width = 590 * max_pixel_ratio
width , height = ImageSizer . resize ( w , h , max_width : max_width , max_height : max_width )
end
OptimizedImage . resize ( file . path , file . path , width , height , allow_animation : allow_animation )
end
# optimize image
ImageOptim . new . optimize_image! ( file . path ) rescue nil
end
# compute the sha of the file
sha1 = Digest :: SHA1 . file ( file ) . hexdigest
2015-02-03 18:44:18 +01:00
2015-05-12 16:45:33 +02:00
# do we already have that upload?
upload = find_by ( sha1 : sha1 )
# make sure the previous upload has not failed
if upload && upload . url . blank?
upload . destroy
upload = nil
end
# return the previous upload if any
return upload unless upload . nil?
# create the upload otherwise
upload = Upload . new
upload . user_id = user_id
upload . original_filename = filename
upload . filesize = filesize
upload . sha1 = sha1
upload . url = " "
2015-07-15 17:15:43 +02:00
upload . width = width
upload . height = height
2015-05-12 16:45:33 +02:00
upload . origin = options [ :origin ] [ 0 ... 1000 ] if options [ :origin ]
2015-07-15 17:15:43 +02:00
if FileHelper . is_image? ( filename ) && ( upload . width == 0 || upload . height == 0 )
upload . errors . add ( :base , I18n . t ( " upload.images.size_not_found " ) )
2015-05-29 15:57:24 +02:00
end
2015-05-12 16:45:33 +02:00
return upload unless upload . save
# store the file and update its url
2015-05-29 15:57:24 +02:00
File . open ( file . path ) do | f |
url = Discourse . store . store_upload ( f , upload , options [ :content_type ] )
if url . present?
upload . url = url
upload . save
else
upload . errors . add ( :url , I18n . t ( " upload.store_failure " , { upload_id : upload . id , user_id : user_id } ) )
end
2015-05-12 16:45:33 +02:00
end
upload
2013-06-15 10:33:57 +02:00
end
2013-04-07 17:52:46 +02:00
end
2013-07-08 01:39:08 +02:00
def self . get_from_url ( url )
2014-07-18 17:54:18 +02:00
return if url . blank?
2013-07-22 00:37:23 +02:00
# we store relative urls, so we need to remove any host/cdn
2015-05-26 11:47:33 +02:00
url = url . sub ( / ^ #{ Discourse . asset_host } /i , " " ) if Discourse . asset_host . present?
# when using s3, we need to replace with the absolute base url
url = url . sub ( / ^ #{ SiteSetting . s3_cdn_url } /i , Discourse . store . absolute_base_url ) if SiteSetting . s3_cdn_url . present?
2015-06-01 20:08:41 +02:00
Upload . find_by ( url : url )
2013-07-08 01:39:08 +02:00
end
2014-07-09 23:59:57 +02:00
def self . fix_image_orientation ( path )
2015-02-02 01:27:52 -08:00
` convert #{ path } -auto-orient #{ path } `
2014-07-09 23:59:57 +02:00
end
2015-06-12 12:02:36 +02:00
def self . migrate_to_new_scheme ( limit = 50 )
problems = [ ]
if SiteSetting . migrate_to_new_scheme
max_file_size_kb = [ SiteSetting . max_image_size_kb , SiteSetting . max_attachment_size_kb ] . max . kilobytes
local_store = FileStore :: LocalStore . new
Upload . where ( " url NOT LIKE '%/original/_X/%' " )
. limit ( limit )
. order ( id : :desc )
. each do | upload |
begin
# keep track of the url
previous_url = upload . url . dup
# where is the file currently stored?
external = previous_url =~ / ^ \/ \/ /
# download if external
if external
url = SiteSetting . scheme + " : " + previous_url
file = FileHelper . download ( url , max_file_size_kb , " discourse " , true ) rescue nil
path = file . path
else
path = local_store . path_for ( upload )
end
# compute SHA if missing
if upload . sha1 . blank?
upload . sha1 = Digest :: SHA1 . file ( path ) . hexdigest
end
# optimize if image
if FileHelper . is_image? ( File . basename ( path ) )
ImageOptim . new . optimize_image! ( path )
end
# store to new location & update the filesize
File . open ( path ) do | f |
upload . url = Discourse . store . store_upload ( f , upload )
upload . filesize = f . size
upload . save
end
# remap the URLs
DbHelper . remap ( UrlHelper . absolute ( previous_url ) , upload . url ) unless external
DbHelper . remap ( previous_url , upload . url )
# remove the old file (when local)
unless external
FileUtils . rm ( path , force : true ) rescue nil
end
rescue = > e
problems << { upload : upload , ex : e }
ensure
file . try ( :unlink ) rescue nil
file . try ( :close ) rescue nil
end
end
end
problems
end
2013-02-05 14:16:51 -05:00
end
2013-05-24 12:48:32 +10:00
# == Schema Information
#
# Table name: uploads
#
# id :integer not null, primary key
# user_id :integer not null
# original_filename :string(255) not null
# filesize :integer not null
# width :integer
# height :integer
# url :string(255) not null
2014-08-27 15:19:25 +10:00
# created_at :datetime not null
# updated_at :datetime not null
2013-06-17 22:16:14 +02:00
# sha1 :string(40)
2013-12-05 17:40:35 +11:00
# origin :string(1000)
2014-11-20 14:53:15 +11:00
# retain_hours :integer
2013-05-24 12:48:32 +10:00
#
# Indexes
#
2013-10-04 13:28:49 +10:00
# index_uploads_on_id_and_url (id,url)
# index_uploads_on_sha1 (sha1) UNIQUE
# index_uploads_on_url (url)
# index_uploads_on_user_id (user_id)
2013-05-24 12:48:32 +10:00
#