2013-11-06 02:04:47 +08:00
require " digest/sha1 "
2014-04-15 04:55:57 +08:00
require_dependency " file_helper "
2015-06-12 18:02:36 +08:00
require_dependency " url_helper "
require_dependency " db_helper "
2014-04-15 04:55:57 +08:00
require_dependency " validators/upload_validator "
2015-06-12 18:02:36 +08:00
require_dependency " file_store/local_store "
2017-08-22 23:46:15 +08:00
require_dependency " base62 "
2013-02-06 03:16:51 +08:00
class Upload < ActiveRecord :: Base
2019-02-21 10:13:37 +08:00
include ActionView :: Helpers :: NumberHelper
2019-04-09 04:37:35 +08:00
include HasUrl
2019-02-21 10:13:37 +08:00
2018-09-10 10:10:39 +08:00
SHA1_LENGTH = 40
2019-01-02 15:29:17 +08:00
SEEDED_ID_THRESHOLD = 0
2019-04-09 04:37:35 +08:00
URL_REGEX || = / ( \/ original \/ \ dX[ \/ \ . \ w]* \/ ([a-zA-Z0-9]+)[ \ . \ w]*) /
2018-09-10 10:10:39 +08:00
2013-02-06 03:16:51 +08:00
belongs_to :user
2013-11-06 02:04:47 +08:00
has_many :post_uploads , dependent : :destroy
2013-06-14 05:44:24 +08:00
has_many :posts , through : :post_uploads
2013-06-13 07:43:50 +08:00
2013-06-21 15:34:02 +08:00
has_many :optimized_images , dependent : :destroy
2018-09-20 13:33:10 +08:00
has_many :user_uploads , dependent : :destroy
2013-06-16 16:39:48 +08:00
2017-06-13 04:41:29 +08:00
attr_accessor :for_group_message
2017-05-10 05:20:28 +08:00
attr_accessor :for_theme
2017-06-13 04:41:29 +08:00
attr_accessor :for_private_message
2018-04-19 19:30:31 +08:00
attr_accessor :for_export
2018-11-14 15:03:02 +08:00
attr_accessor :for_site_setting
2016-03-01 05:39:24 +08:00
2013-02-06 03:16:51 +08:00
validates_presence_of :filesize
validates_presence_of :original_filename
2014-04-15 04:55:57 +08:00
validates_with :: Validators :: UploadValidator
2018-08-31 12:46:22 +08:00
after_destroy do
User . where ( uploaded_avatar_id : self . id ) . update_all ( uploaded_avatar_id : nil )
UserAvatar . where ( gravatar_upload_id : self . id ) . update_all ( gravatar_upload_id : nil )
UserAvatar . where ( custom_upload_id : self . id ) . update_all ( custom_upload_id : nil )
end
2019-01-02 15:29:17 +08:00
scope :by_users , - > { where ( " uploads.id > ? " , SEEDED_ID_THRESHOLD ) }
2018-11-14 15:03:02 +08:00
def to_s
self . url
end
2018-08-28 10:48:43 +08:00
def thumbnail ( width = self . thumbnail_width , height = self . thumbnail_height )
2014-05-06 21:41:59 +08:00
optimized_images . find_by ( width : width , height : height )
2013-06-17 07:00:25 +08:00
end
2013-11-06 02:04:47 +08:00
def has_thumbnail? ( width , height )
2013-09-27 16:55:50 +08:00
thumbnail ( width , height ) . present?
2013-06-17 07:00:25 +08:00
end
2018-12-15 05:50:28 +08:00
def create_thumbnail! ( width , height , opts = nil )
2013-06-17 07:00:25 +08:00
return unless SiteSetting . create_thumbnails?
2018-12-15 05:50:28 +08:00
opts || = { }
opts [ :allow_animation ] = SiteSetting . allow_animated_thumbnails
2015-09-21 04:01:03 +08:00
2018-08-17 12:00:27 +08:00
if get_optimized_image ( width , height , opts )
2015-06-27 07:26:16 +08:00
save ( validate : false )
2013-09-27 16:55:50 +08:00
end
2013-06-17 07:00:25 +08:00
end
2018-08-17 12:00:27 +08:00
# this method attempts to correct old incorrect extensions
def get_optimized_image ( width , height , opts )
if ( ! extension || extension . length == 0 )
fix_image_extension
end
opts = opts . merge ( raise_on_error : true )
begin
OptimizedImage . create_for ( self , width , height , opts )
2018-12-15 06:44:38 +08:00
rescue = > ex
Rails . logger . info ex if Rails . env . development?
2018-08-17 12:00:27 +08:00
opts = opts . merge ( raise_on_error : false )
if fix_image_extension
OptimizedImage . create_for ( self , width , height , opts )
else
nil
end
end
end
def fix_image_extension
return false if extension == " unknown "
begin
# this is relatively cheap once cached
original_path = Discourse . store . path_for ( self )
if original_path . blank?
external_copy = Discourse . store . download ( self ) rescue nil
original_path = external_copy . try ( :path )
end
image_info = FastImage . new ( original_path ) rescue nil
new_extension = image_info & . type & . to_s || " unknown "
if new_extension != self . extension
self . update_columns ( extension : new_extension )
true
end
rescue
self . update_columns ( extension : " unknown " )
true
end
end
2013-06-21 15:34:02 +08:00
def destroy
2013-06-20 03:51:41 +08:00
Upload . transaction do
2013-08-14 04:08:29 +08:00
Discourse . store . remove_upload ( self )
2013-06-20 03:51:41 +08:00
super
end
end
2017-08-22 23:46:15 +08:00
def short_url
" upload:// #{ Base62 . encode ( sha1 . hex ) } . #{ extension } "
end
2018-08-28 10:48:43 +08:00
def local?
! ( url =~ / ^(https?:)? \/ \/ / )
end
def fix_dimensions!
2018-09-10 10:22:45 +08:00
return if ! FileHelper . is_supported_image? ( " image. #{ extension } " )
2018-08-28 10:48:43 +08:00
path =
if local?
Discourse . store . path_for ( self )
else
Discourse . store . download ( self ) . path
end
2018-12-03 23:19:49 +08:00
begin
2018-12-26 23:17:08 +08:00
w , h = FastImage . new ( path , raise_on_failure : true ) . size
self . width = w || 0
self . height = h || 0
self . thumbnail_width , self . thumbnail_height = ImageSizer . resize ( w , h )
self . update_columns (
width : width ,
height : height ,
thumbnail_width : thumbnail_width ,
thumbnail_height : thumbnail_height
)
2018-12-03 23:19:49 +08:00
rescue = > e
Discourse . warn_exception ( e , message : " Error getting image dimensions " )
end
2018-08-28 10:48:43 +08:00
nil
end
# on demand image size calculation, this allows us to null out image sizes
# and still handle as needed
def get_dimension ( key )
if v = read_attribute ( key )
return v
end
fix_dimensions!
read_attribute ( key )
end
def width
get_dimension ( :width )
end
def height
get_dimension ( :height )
end
def thumbnail_width
get_dimension ( :thumbnail_width )
end
def thumbnail_height
get_dimension ( :thumbnail_height )
end
2017-08-22 23:46:15 +08:00
def self . sha1_from_short_url ( url )
if url =~ / (upload: \/ \/ )?([a-zA-Z0-9]+)( \ ..*)? /
sha1 = Base62 . decode ( $2 ) . to_s ( 16 )
2017-08-23 23:08:18 +08:00
2018-09-10 10:10:39 +08:00
if sha1 . length > SHA1_LENGTH
2017-08-23 23:08:18 +08:00
nil
else
2018-09-10 10:10:39 +08:00
sha1 . rjust ( SHA1_LENGTH , '0' )
2017-08-23 23:08:18 +08:00
end
2017-08-22 23:46:15 +08:00
end
end
2016-09-02 14:50:13 +08:00
def self . generate_digest ( path )
Digest :: SHA1 . file ( path ) . hexdigest
end
2019-02-21 10:13:37 +08:00
def human_filesize
number_to_human_size ( self . filesize )
end
2019-04-03 12:37:50 +08:00
def rebake_posts_on_old_scheme
self . posts . where ( " cooked LIKE '%/_optimized/%' " ) . find_each ( & :rebake! )
end
2019-04-24 11:56:48 +08:00
def self . migrate_to_new_scheme ( limit : nil )
2015-06-12 18:02:36 +08:00
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
2019-04-24 11:56:48 +08:00
scope = Upload . by_users . where ( " url NOT LIKE '%/original/_X/%' AND url LIKE '%/uploads/ #{ RailsMultisite :: ConnectionManagement . current_db } %' " ) . order ( id : :desc )
2019-03-14 12:38:16 +08:00
2018-03-22 10:56:06 +08:00
scope = scope . limit ( limit ) if limit
2019-04-24 13:59:23 +08:00
if scope . count == 0
SiteSetting . migrate_to_new_scheme = false
return problems
end
2019-04-24 11:56:48 +08:00
remap_scope = nil
2016-09-02 10:55:11 +08:00
scope . each do | upload |
2015-06-12 18:02:36 +08:00
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
2019-03-28 15:58:42 +08:00
begin
retries || = 0
file = FileHelper . download (
url ,
max_file_size : max_file_size_kb ,
tmp_file_name : " discourse " ,
follow_redirect : true
)
rescue OpenURI :: HTTPError
retry if ( retires += 1 ) < 1
next
end
2015-06-12 18:02:36 +08:00
path = file . path
else
path = local_store . path_for ( upload )
end
# compute SHA if missing
if upload . sha1 . blank?
2016-09-02 14:50:13 +08:00
upload . sha1 = Upload . generate_digest ( path )
2015-06-12 18:02:36 +08:00
end
2019-03-26 15:07:50 +08:00
2015-06-12 18:02:36 +08:00
# store to new location & update the filesize
File . open ( path ) do | f |
2016-09-02 11:58:56 +08:00
upload . url = Discourse . store . store_upload ( f , upload )
upload . filesize = f . size
2019-03-26 14:28:39 +08:00
upload . save! ( validate : false )
2015-06-12 18:02:36 +08:00
end
# remap the URLs
DbHelper . remap ( UrlHelper . absolute ( previous_url ) , upload . url ) unless external
2019-04-24 11:56:48 +08:00
DbHelper . remap (
previous_url ,
upload . url ,
excluded_tables : %w{
posts
post_search_data
}
)
remap_scope || = begin
2019-04-24 12:20:53 +08:00
Post . with_deleted
. where ( " raw ~ '/uploads/default/ \\ d+/' " )
. select ( :raw , :cooked )
. all
2019-04-24 11:56:48 +08:00
end
remap_scope . each do | post |
post . raw . gsub! ( previous_url , upload . url )
2019-04-24 12:20:53 +08:00
post . cooked . gsub! ( previous_url , upload . url )
2019-04-24 11:56:48 +08:00
post . save! if post . changed?
end
2019-04-03 07:38:57 +08:00
upload . optimized_images . find_each ( & :destroy! )
upload . rebake_posts_on_old_scheme
2015-06-12 18:02:36 +08:00
# remove the old file (when local)
unless external
2018-03-28 16:20:08 +08:00
FileUtils . rm ( path , force : true )
2015-06-12 18:02:36 +08:00
end
rescue = > e
problems << { upload : upload , ex : e }
ensure
2018-03-28 16:20:08 +08:00
file & . unlink
file & . close
2015-06-12 18:02:36 +08:00
end
end
end
problems
end
2013-02-06 03:16:51 +08:00
end
2013-05-24 10:48:32 +08:00
# == Schema Information
#
# Table name: uploads
#
# id :integer not null, primary key
# user_id :integer not null
2019-01-12 03:29:56 +08:00
# original_filename :string not null
2013-05-24 10:48:32 +08:00
# filesize :integer not null
# width :integer
# height :integer
2019-01-12 03:29:56 +08:00
# url :string not null
2014-08-27 13:19:25 +08:00
# created_at :datetime not null
# updated_at :datetime not null
2013-06-18 04:16:14 +08:00
# sha1 :string(40)
2013-12-05 14:40:35 +08:00
# origin :string(1000)
2014-11-20 11:53:15 +08:00
# retain_hours :integer
2017-08-16 22:38:11 +08:00
# extension :string(10)
2018-09-20 10:40:51 +08:00
# thumbnail_width :integer
# thumbnail_height :integer
2019-01-04 14:16:22 +08:00
# etag :string
2013-05-24 10:48:32 +08:00
#
# Indexes
#
2019-01-12 01:19:23 +08:00
# index_uploads_on_etag (etag)
2017-10-06 11:13:01 +08:00
# index_uploads_on_extension (lower((extension)::text))
2013-10-04 11:28:49 +08: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 10:48:32 +08:00
#