diff --git a/app/models/optimized_image.rb b/app/models/optimized_image.rb index b55622995..cbfb901ab 100644 --- a/app/models/optimized_image.rb +++ b/app/models/optimized_image.rb @@ -6,72 +6,74 @@ class OptimizedImage < ActiveRecord::Base def self.create_for(upload, width, height, opts={}) return unless width > 0 && height > 0 - # do we already have that thumbnail? - thumbnail = find_by(upload_id: upload.id, width: width, height: height) + DistributedMutex.synchronize("optimized_image_#{upload.id}_#{width}_#{height}") do + # do we already have that thumbnail? + thumbnail = find_by(upload_id: upload.id, width: width, height: height) - # make sure the previous thumbnail has not failed - if thumbnail && thumbnail.url.blank? - thumbnail.destroy - thumbnail = nil - end - - # return the previous thumbnail if any - return thumbnail unless thumbnail.nil? - - # create the thumbnail otherwise - external_copy = Discourse.store.download(upload) if Discourse.store.external? - original_path = if Discourse.store.external? - external_copy.try(:path) - else - Discourse.store.path_for(upload) - end - - if original_path.blank? - Rails.logger.error("Could not find file in the store located at url: #{upload.url}") - else - # create a temp file with the same extension as the original - extension = File.extname(original_path) - temp_file = Tempfile.new(["discourse-thumbnail", extension]) - temp_path = temp_file.path - - if extension =~ /\.svg$/i - FileUtils.cp(original_path, temp_path) - resized = true - else - resized = resize(original_path, temp_path, width, height, opts) + # make sure the previous thumbnail has not failed + if thumbnail && thumbnail.url.blank? + thumbnail.destroy + thumbnail = nil end - if resized - thumbnail = OptimizedImage.create!( - upload_id: upload.id, - sha1: Digest::SHA1.file(temp_path).hexdigest, - extension: extension, - width: width, - height: height, - url: "", - ) - # store the optimized image and update its url - url = Discourse.store.store_optimized_image(temp_file, thumbnail) - if url.present? - thumbnail.url = url - thumbnail.save + # return the previous thumbnail if any + return thumbnail unless thumbnail.nil? + + # create the thumbnail otherwise + external_copy = Discourse.store.download(upload) if Discourse.store.external? + original_path = if Discourse.store.external? + external_copy.try(:path) + else + Discourse.store.path_for(upload) + end + + if original_path.blank? + Rails.logger.error("Could not find file in the store located at url: #{upload.url}") + else + # create a temp file with the same extension as the original + extension = File.extname(original_path) + temp_file = Tempfile.new(["discourse-thumbnail", extension]) + temp_path = temp_file.path + + if extension =~ /\.svg$/i + FileUtils.cp(original_path, temp_path) + resized = true else - Rails.logger.error("Failed to store avatar #{size} for #{upload.url} from #{source}") + resized = resize(original_path, temp_path, width, height, opts) end - else - Rails.logger.error("Failed to create optimized image #{width}x#{height} for #{upload.url}") + + if resized + thumbnail = OptimizedImage.create!( + upload_id: upload.id, + sha1: Digest::SHA1.file(temp_path).hexdigest, + extension: extension, + width: width, + height: height, + url: "", + ) + # store the optimized image and update its url + url = Discourse.store.store_optimized_image(temp_file, thumbnail) + if url.present? + thumbnail.url = url + thumbnail.save + else + Rails.logger.error("Failed to store avatar #{size} for #{upload.url} from #{source}") + end + else + Rails.logger.error("Failed to create optimized image #{width}x#{height} for #{upload.url}") + end + + # close && remove temp file + temp_file.close! end - # close && remove temp file - temp_file.close! - end + # make sure we remove the cached copy from external stores + if Discourse.store.external? + external_copy.try(:close!) rescue nil + end - # make sure we remove the cached copy from external stores - if Discourse.store.external? - external_copy.try(:close!) rescue nil + thumbnail end - - thumbnail end def destroy diff --git a/app/models/upload.rb b/app/models/upload.rb index f9507aca8..d604dd0b8 100644 --- a/app/models/upload.rb +++ b/app/models/upload.rb @@ -52,43 +52,45 @@ class Upload < ActiveRecord::Base def self.create_for(user_id, file, filename, filesize, options = {}) sha1 = Digest::SHA1.file(file).hexdigest - # do we already have that upload? - upload = find_by(sha1: sha1) + DistributedMutex.synchronize("upload_#{sha1}") do + # 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 + # 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 = "" + upload.origin = options[:origin][0...1000] if options[:origin] + + # deal with width & height for images + upload = resize_image(filename, file, upload) if FileHelper.is_image?(filename) + + return upload unless upload.save + + # store the file and update its url + url = Discourse.store.store_upload(file, 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 + + # return the uploaded file + upload 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 = "" - upload.origin = options[:origin][0...1000] if options[:origin] - - # deal with width & height for images - upload = resize_image(filename, file, upload) if FileHelper.is_image?(filename) - - return upload unless upload.save - - # store the file and update its url - url = Discourse.store.store_upload(file, 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 - - # return the uploaded file - upload end def self.resize_image(filename, file, upload)