| 1 | # Redmine - project management software
 | 
  
    | 2 | # Copyright (C) 2006-2017  Jean-Philippe Lang
 | 
  
    | 3 | #
 | 
  
    | 4 | # This program is free software; you can redistribute it and/or
 | 
  
    | 5 | # modify it under the terms of the GNU General Public License
 | 
  
    | 6 | # as published by the Free Software Foundation; either version 2
 | 
  
    | 7 | # of the License, or (at your option) any later version.
 | 
  
    | 8 | #
 | 
  
    | 9 | # This program is distributed in the hope that it will be useful,
 | 
  
    | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
  
    | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
  
    | 12 | # GNU General Public License for more details.
 | 
  
    | 13 | #
 | 
  
    | 14 | # You should have received a copy of the GNU General Public License
 | 
  
    | 15 | # along with this program; if not, write to the Free Software
 | 
  
    | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 | 
  
    | 17 | 
 | 
  
    | 18 | require "digest"
 | 
  
    | 19 | require "fileutils"
 | 
  
    | 20 | require "mimemagic/overlay"
 | 
  
    | 21 | 
 | 
  
    | 22 | class Attachment < ActiveRecord::Base
 | 
  
    | 23 |   include Redmine::SafeAttributes
 | 
  
    | 24 |   belongs_to :container, :polymorphic => true
 | 
  
    | 25 |   belongs_to :author, :class_name => "User"
 | 
  
    | 26 | 
 | 
  
    | 27 |   validates_presence_of :filename, :author
 | 
  
    | 28 |   validates_length_of :filename, :maximum => 255
 | 
  
    | 29 |   validates_length_of :disk_filename, :maximum => 255
 | 
  
    | 30 |   validates_length_of :description, :maximum => 255
 | 
  
    | 31 |   validate :validate_max_file_size, :validate_file_extension, :validate_file_content
 | 
  
    | 32 | 
 | 
  
    | 33 | 
 | 
  
    | 34 |   acts_as_event :title => :filename,
 | 
  
    | 35 |                 :url => Proc.new {|o| {:controller => 'attachments', :action => 'show', :id => o.id, :filename => o.filename}}
 | 
  
    | 36 | 
 | 
  
    | 37 |   acts_as_activity_provider :type => 'files',
 | 
  
    | 38 |                             :permission => :view_files,
 | 
  
    | 39 |                             :author_key => :author_id,
 | 
  
    | 40 |                             :scope => select("#{Attachment.table_name}.*").
 | 
  
    | 41 |                                       joins("LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " +
 | 
  
    | 42 |                                             "LEFT JOIN #{Project.table_name} ON #{Version.table_name}.project_id = #{Project.table_name}.id OR ( #{Attachment.table_name}.container_type='Project' AND #{Attachment.table_name}.container_id = #{Project.table_name}.id )")
 | 
  
    | 43 | 
 | 
  
    | 44 |   acts_as_activity_provider :type => 'documents',
 | 
  
    | 45 |                             :permission => :view_documents,
 | 
  
    | 46 |                             :author_key => :author_id,
 | 
  
    | 47 |                             :scope => select("#{Attachment.table_name}.*").
 | 
  
    | 48 |                                       joins("LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " +
 | 
  
    | 49 |                                             "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id")
 | 
  
    | 50 | 
 | 
  
    | 51 |   cattr_accessor :storage_path
 | 
  
    | 52 |   @@storage_path = Redmine::Configuration['attachments_storage_path'] || File.join(Rails.root, "files")
 | 
  
    | 53 | 
 | 
  
    | 54 |   cattr_accessor :thumbnails_storage_path
 | 
  
    | 55 |   @@thumbnails_storage_path = File.join(Rails.root, "tmp", "thumbnails")
 | 
  
    | 56 | 
 | 
  
    | 57 |   before_create :files_to_final_location
 | 
  
    | 58 |   after_rollback :delete_from_disk, :on => :create
 | 
  
    | 59 |   after_commit :delete_from_disk, :on => :destroy
 | 
  
    | 60 |   after_commit :reuse_existing_file_if_possible, :on => :create
 | 
  
    | 61 | 
 | 
  
    | 62 |   safe_attributes 'filename', 'content_type', 'description'
 | 
  
    | 63 | 
 | 
  
    | 64 |   # Returns an unsaved copy of the attachment
 | 
  
    | 65 |   def copy(attributes=nil)
 | 
  
    | 66 |     copy = self.class.new
 | 
  
    | 67 |     copy.attributes = self.attributes.dup.except("id", "downloads")
 | 
  
    | 68 |     copy.attributes = attributes if attributes
 | 
  
    | 69 |     copy
 | 
  
    | 70 |   end
 | 
  
    | 71 | 
 | 
  
    | 72 |   def validate_max_file_size
 | 
  
    | 73 |     if @temp_file && self.filesize > Setting.attachment_max_size.to_i.kilobytes
 | 
  
    | 74 |       errors.add(:base, l(:error_attachment_too_big, :max_size => Setting.attachment_max_size.to_i.kilobytes))
 | 
  
    | 75 |     end
 | 
  
    | 76 |   end
 | 
  
    | 77 | 
 | 
  
    | 78 |   def validate_file_extension
 | 
  
    | 79 |     if @temp_file
 | 
  
    | 80 |       extension = File.extname(filename)
 | 
  
    | 81 | 	logger.info( "RAJESH1")
 | 
  
    | 82 |       unless self.class.valid_extension?(extension) 
 | 
  
    | 83 |         errors.add(:base, l(:error_attachment_extension_not_allowed, :extension => extension))
 | 
  
    | 84 |       end
 | 
  
    | 85 |     end
 | 
  
    | 86 |   end
 | 
  
    | 87 | ###################
 | 
  
    | 88 |  def validate_file_content
 | 
  
    | 89 |     if @temp_file
 | 
  
    | 90 | 
 | 
  
    | 91 |       mimemagic_content_type  = MimeMagic.by_magic(@temp_file)
 | 
  
    | 92 |       content_type = Redmine::MimeType.of(filename)
 | 
  
    | 93 |       logger.error ("RAJESH234")
 | 
  
    | 94 |       if mimemagic_content_type != content_type
 | 
  
    | 95 |         errors.add(:base, l(:error_attachment_mime_not_allowed, :content_type=> mimemagic_content_type))
 | 
  
    | 96 |       else
 | 
  
    | 97 |         #puts "I can't guess the number"
 | 
  
    | 98 |       end
 | 
  
    | 99 |     end
 | 
  
    | 100 |   end
 | 
  
    | 101 | ###################
 | 
  
    | 102 | 
 | 
  
    | 103 |   def file=(incoming_file)
 | 
  
    | 104 |     unless incoming_file.nil?
 | 
  
    | 105 |       @temp_file = incoming_file
 | 
  
    | 106 |         if @temp_file.respond_to?(:original_filename)
 | 
  
    | 107 |           self.filename = @temp_file.original_filename
 | 
  
    | 108 |           self.filename.force_encoding("UTF-8")
 | 
  
    | 109 |         end
 | 
  
    | 110 |         if @temp_file.respond_to?(:content_type)
 | 
  
    | 111 |           self.content_type = @temp_file.content_type.to_s.chomp
 | 
  
    | 112 |         end
 | 
  
    | 113 |         self.filesize = @temp_file.size
 | 
  
    | 114 |     end
 | 
  
    | 115 |   end
 | 
  
    | 116 | 
 | 
  
    | 117 |   def file
 | 
  
    | 118 |     nil
 | 
  
    | 119 |   end
 | 
  
    | 120 | 
 | 
  
    | 121 |   def filename=(arg)
 | 
  
    | 122 |     write_attribute :filename, sanitize_filename(arg.to_s)
 | 
  
    | 123 |     filename
 | 
  
    | 124 |   end
 | 
  
    | 125 | 
 | 
  
    | 126 |   # Copies the temporary file to its final location
 | 
  
    | 127 |   # and computes its MD5 hash
 | 
  
    | 128 |   def files_to_final_location
 | 
  
    | 129 |     if @temp_file
 | 
  
    | 130 |       self.disk_directory = target_directory
 | 
  
    | 131 |       self.disk_filename = Attachment.disk_filename(filename, disk_directory)
 | 
  
    | 132 |       logger.info("Saving attachment '#{self.diskfile}' (#{@temp_file.size} bytes)") if logger
 | 
  
    | 133 |       path = File.dirname(diskfile)
 | 
  
    | 134 |       unless File.directory?(path)
 | 
  
    | 135 |         FileUtils.mkdir_p(path)
 | 
  
    | 136 |       end
 | 
  
    | 137 |       sha = Digest::SHA256.new
 | 
  
    | 138 |       File.open(diskfile, "wb") do |f|
 | 
  
    | 139 |         if @temp_file.respond_to?(:read)
 | 
  
    | 140 |           buffer = ""
 | 
  
    | 141 |           while (buffer = @temp_file.read(8192))
 | 
  
    | 142 |             f.write(buffer)
 | 
  
    | 143 |             sha.update(buffer)
 | 
  
    | 144 |           end
 | 
  
    | 145 |         else
 | 
  
    | 146 |           f.write(@temp_file)
 | 
  
    | 147 |           sha.update(@temp_file)
 | 
  
    | 148 |         end
 | 
  
    | 149 |       end
 | 
  
    | 150 |       self.digest = sha.hexdigest
 | 
  
    | 151 |     end
 | 
  
    | 152 |     @temp_file = nil
 | 
  
    | 153 | 
 | 
  
    | 154 |     if content_type.blank? && filename.present?
 | 
  
    | 155 |       self.content_type = Redmine::MimeType.of(filename)
 | 
  
    | 156 |     end
 | 
  
    | 157 |     # Don't save the content type if it's longer than the authorized length
 | 
  
    | 158 |     if self.content_type && self.content_type.length > 255
 | 
  
    | 159 |       self.content_type = nil
 | 
  
    | 160 |     end
 | 
  
    | 161 |   end
 | 
  
    | 162 | 
 | 
  
    | 163 |   # Deletes the file from the file system if it's not referenced by other attachments
 | 
  
    | 164 |   def delete_from_disk
 | 
  
    | 165 |     if Attachment.where("disk_filename = ? AND id <> ?", disk_filename, id).empty?
 | 
  
    | 166 |       delete_from_disk!
 | 
  
    | 167 |     end
 | 
  
    | 168 |   end
 | 
  
    | 169 | 
 | 
  
    | 170 |   # Returns file's location on disk
 | 
  
    | 171 |   def diskfile
 | 
  
    | 172 |     File.join(self.class.storage_path, disk_directory.to_s, disk_filename.to_s)
 | 
  
    | 173 |   end
 | 
  
    | 174 | 
 | 
  
    | 175 |   def title
 | 
  
    | 176 |     title = filename.dup
 | 
  
    | 177 |     if description.present?
 | 
  
    | 178 |       title << " (#{description})"
 | 
  
    | 179 |     end
 | 
  
    | 180 |     title
 | 
  
    | 181 |   end
 | 
  
    | 182 | 
 | 
  
    | 183 |   def increment_download
 | 
  
    | 184 |     increment!(:downloads)
 | 
  
    | 185 |   end
 | 
  
    | 186 | 
 | 
  
    | 187 |   def project
 | 
  
    | 188 |     container.try(:project)
 | 
  
    | 189 |   end
 | 
  
    | 190 | 
 | 
  
    | 191 |   def visible?(user=User.current)
 | 
  
    | 192 |     if container_id
 | 
  
    | 193 |       container && container.attachments_visible?(user)
 | 
  
    | 194 |     else
 | 
  
    | 195 |       author == user
 | 
  
    | 196 |     end
 | 
  
    | 197 |   end
 | 
  
    | 198 | 
 | 
  
    | 199 |   def editable?(user=User.current)
 | 
  
    | 200 |     if container_id
 | 
  
    | 201 |       container && container.attachments_editable?(user)
 | 
  
    | 202 |     else
 | 
  
    | 203 |       author == user
 | 
  
    | 204 |     end
 | 
  
    | 205 |   end
 | 
  
    | 206 | 
 | 
  
    | 207 |   def deletable?(user=User.current)
 | 
  
    | 208 |     if container_id
 | 
  
    | 209 |       container && container.attachments_deletable?(user)
 | 
  
    | 210 |     else
 | 
  
    | 211 |       author == user
 | 
  
    | 212 |     end
 | 
  
    | 213 |   end
 | 
  
    | 214 | 
 | 
  
    | 215 |   def image?
 | 
  
    | 216 |     !!(self.filename =~ /\.(bmp|gif|jpg|jpe|jpeg|png)$/i)
 | 
  
    | 217 |   end
 | 
  
    | 218 | 
 | 
  
    | 219 |   def thumbnailable?
 | 
  
    | 220 |     image?
 | 
  
    | 221 |   end
 | 
  
    | 222 | 
 | 
  
    | 223 |   # Returns the full path the attachment thumbnail, or nil
 | 
  
    | 224 |   # if the thumbnail cannot be generated.
 | 
  
    | 225 |   def thumbnail(options={})
 | 
  
    | 226 |     if thumbnailable? && readable?
 | 
  
    | 227 |       size = options[:size].to_i
 | 
  
    | 228 |       if size > 0
 | 
  
    | 229 |         # Limit the number of thumbnails per image
 | 
  
    | 230 |         size = (size / 50) * 50
 | 
  
    | 231 |         # Maximum thumbnail size
 | 
  
    | 232 |         size = 800 if size > 800
 | 
  
    | 233 |       else
 | 
  
    | 234 |         size = Setting.thumbnails_size.to_i
 | 
  
    | 235 |       end
 | 
  
    | 236 |       size = 100 unless size > 0
 | 
  
    | 237 |       target = File.join(self.class.thumbnails_storage_path, "#{id}_#{digest}_#{size}.thumb")
 | 
  
    | 238 | 
 | 
  
    | 239 |       begin
 | 
  
    | 240 |         Redmine::Thumbnail.generate(self.diskfile, target, size)
 | 
  
    | 241 |       rescue => e
 | 
  
    | 242 |         logger.error "An error occured while generating thumbnail for #{disk_filename} to #{target}\nException was: #{e.message}" if logger
 | 
  
    | 243 |         return nil
 | 
  
    | 244 |       end
 | 
  
    | 245 |     end
 | 
  
    | 246 |   end
 | 
  
    | 247 | 
 | 
  
    | 248 |   # Deletes all thumbnails
 | 
  
    | 249 |   def self.clear_thumbnails
 | 
  
    | 250 |     Dir.glob(File.join(thumbnails_storage_path, "*.thumb")).each do |file|
 | 
  
    | 251 |       File.delete file
 | 
  
    | 252 |     end
 | 
  
    | 253 |   end
 | 
  
    | 254 | 
 | 
  
    | 255 |   def is_text?
 | 
  
    | 256 |     Redmine::MimeType.is_type?('text', filename) || Redmine::SyntaxHighlighting.filename_supported?(filename)
 | 
  
    | 257 |   end
 | 
  
    | 258 | 
 | 
  
    | 259 |   def is_image?
 | 
  
    | 260 |     Redmine::MimeType.is_type?('image', filename)
 | 
  
    | 261 |   end
 | 
  
    | 262 | 
 | 
  
    | 263 |   def is_diff?
 | 
  
    | 264 |     self.filename =~ /\.(patch|diff)$/i
 | 
  
    | 265 |   end
 | 
  
    | 266 | 
 | 
  
    | 267 |   def is_pdf?
 | 
  
    | 268 |     #Redmine::MimeType.of(filename) == "application/pdf"
 | 
  
    | 269 |      Redmine::MimeType.of(filename) == "application/pdf" && MimeMagic.by_magic(File.open(filename)).type == 'application/pdf'
 | 
  
    | 270 |   end
 | 
  
    | 271 | 
 | 
  
    | 272 |   def is_video?
 | 
  
    | 273 |     Redmine::MimeType.is_type?('video', filename)
 | 
  
    | 274 |   end
 | 
  
    | 275 | 
 | 
  
    | 276 |   def is_audio?
 | 
  
    | 277 |     Redmine::MimeType.is_type?('audio', filename)
 | 
  
    | 278 |   end
 | 
  
    | 279 | 
 | 
  
    | 280 |   def previewable?
 | 
  
    | 281 |     is_text? || is_image? || is_video? || is_audio?
 | 
  
    | 282 |   end
 | 
  
    | 283 | 
 | 
  
    | 284 |   # Returns true if the file is readable
 | 
  
    | 285 |   def readable?
 | 
  
    | 286 |     disk_filename.present? && File.readable?(diskfile)
 | 
  
    | 287 |   end
 | 
  
    | 288 | 
 | 
  
    | 289 |   # Returns the attachment token
 | 
  
    | 290 |   def token
 | 
  
    | 291 |     "#{id}.#{digest}"
 | 
  
    | 292 |   end
 | 
  
    | 293 | 
 | 
  
    | 294 |   # Finds an attachment that matches the given token and that has no container
 | 
  
    | 295 |   def self.find_by_token(token)
 | 
  
    | 296 |     if token.to_s =~ /^(\d+)\.([0-9a-f]+)$/
 | 
  
    | 297 |       attachment_id, attachment_digest = $1, $2
 | 
  
    | 298 |       attachment = Attachment.find_by(:id => attachment_id, :digest => attachment_digest)
 | 
  
    | 299 |       if attachment && attachment.container.nil?
 | 
  
    | 300 |         attachment
 | 
  
    | 301 |       end
 | 
  
    | 302 |     end
 | 
  
    | 303 |   end
 | 
  
    | 304 | 
 | 
  
    | 305 |   # Bulk attaches a set of files to an object
 | 
  
    | 306 |   #
 | 
  
    | 307 |   # Returns a Hash of the results:
 | 
  
    | 308 |   # :files => array of the attached files
 | 
  
    | 309 |   # :unsaved => array of the files that could not be attached
 | 
  
    | 310 |   def self.attach_files(obj, attachments)
 | 
  
    | 311 |     result = obj.save_attachments(attachments, User.current)
 | 
  
    | 312 |     obj.attach_saved_attachments
 | 
  
    | 313 |     result
 | 
  
    | 314 |   end
 | 
  
    | 315 | 
 | 
  
    | 316 |   # Updates the filename and description of a set of attachments
 | 
  
    | 317 |   # with the given hash of attributes. Returns true if all
 | 
  
    | 318 |   # attachments were updated.
 | 
  
    | 319 |   #
 | 
  
    | 320 |   # Example:
 | 
  
    | 321 |   #   Attachment.update_attachments(attachments, {
 | 
  
    | 322 |   #     4 => {:filename => 'foo'},
 | 
  
    | 323 |   #     7 => {:filename => 'bar', :description => 'file description'}
 | 
  
    | 324 |   #   })
 | 
  
    | 325 |   #
 | 
  
    | 326 |   def self.update_attachments(attachments, params)
 | 
  
    | 327 |     params = params.transform_keys {|key| key.to_i}
 | 
  
    | 328 | 
 | 
  
    | 329 |     saved = true
 | 
  
    | 330 |     transaction do
 | 
  
    | 331 |       attachments.each do |attachment|
 | 
  
    | 332 |         if p = params[attachment.id]
 | 
  
    | 333 |           attachment.filename = p[:filename] if p.key?(:filename)
 | 
  
    | 334 |           attachment.description = p[:description] if p.key?(:description)
 | 
  
    | 335 |           saved &&= attachment.save
 | 
  
    | 336 |         end
 | 
  
    | 337 |       end
 | 
  
    | 338 |       unless saved
 | 
  
    | 339 |         raise ActiveRecord::Rollback
 | 
  
    | 340 |       end
 | 
  
    | 341 |     end
 | 
  
    | 342 |     saved
 | 
  
    | 343 |   end
 | 
  
    | 344 | 
 | 
  
    | 345 |   def self.latest_attach(attachments, filename)
 | 
  
    | 346 |     attachments.sort_by(&:created_on).reverse.detect do |att|
 | 
  
    | 347 |       filename.casecmp(att.filename) == 0
 | 
  
    | 348 |     end
 | 
  
    | 349 |   end
 | 
  
    | 350 | 
 | 
  
    | 351 |   def self.prune(age=1.day)
 | 
  
    | 352 |     Attachment.where("created_on < ? AND (container_type IS NULL OR container_type = '')", Time.now - age).destroy_all
 | 
  
    | 353 |   end
 | 
  
    | 354 | 
 | 
  
    | 355 |   # Moves an existing attachment to its target directory
 | 
  
    | 356 |   def move_to_target_directory!
 | 
  
    | 357 |     return unless !new_record? & readable?
 | 
  
    | 358 | 
 | 
  
    | 359 |     src = diskfile
 | 
  
    | 360 |     self.disk_directory = target_directory
 | 
  
    | 361 |     dest = diskfile
 | 
  
    | 362 | 
 | 
  
    | 363 |     return if src == dest
 | 
  
    | 364 | 
 | 
  
    | 365 |     if !FileUtils.mkdir_p(File.dirname(dest))
 | 
  
    | 366 |       logger.error "Could not create directory #{File.dirname(dest)}" if logger
 | 
  
    | 367 |       return
 | 
  
    | 368 |     end
 | 
  
    | 369 | 
 | 
  
    | 370 |     if !FileUtils.mv(src, dest)
 | 
  
    | 371 |       logger.error "Could not move attachment from #{src} to #{dest}" if logger
 | 
  
    | 372 |       return
 | 
  
    | 373 |     end
 | 
  
    | 374 | 
 | 
  
    | 375 |     update_column :disk_directory, disk_directory
 | 
  
    | 376 |   end
 | 
  
    | 377 | 
 | 
  
    | 378 |   # Moves existing attachments that are stored at the root of the files
 | 
  
    | 379 |   # directory (ie. created before Redmine 2.3) to their target subdirectories
 | 
  
    | 380 |   def self.move_from_root_to_target_directory
 | 
  
    | 381 |     Attachment.where("disk_directory IS NULL OR disk_directory = ''").find_each do |attachment|
 | 
  
    | 382 |       attachment.move_to_target_directory!
 | 
  
    | 383 |     end
 | 
  
    | 384 |   end
 | 
  
    | 385 | 
 | 
  
    | 386 |   # Updates digests to SHA256 for all attachments that have a MD5 digest
 | 
  
    | 387 |   # (ie. created before Redmine 3.4)
 | 
  
    | 388 |   def self.update_digests_to_sha256
 | 
  
    | 389 |     Attachment.where("length(digest) < 64").find_each do |attachment|
 | 
  
    | 390 |       attachment.update_digest_to_sha256!
 | 
  
    | 391 |     end
 | 
  
    | 392 |   end
 | 
  
    | 393 | 
 | 
  
    | 394 |   # Updates attachment digest to SHA256
 | 
  
    | 395 |   def update_digest_to_sha256!
 | 
  
    | 396 |     if readable?
 | 
  
    | 397 |       sha = Digest::SHA256.new
 | 
  
    | 398 |       File.open(diskfile, 'rb') do |f|
 | 
  
    | 399 |         while buffer = f.read(8192)
 | 
  
    | 400 |           sha.update(buffer)
 | 
  
    | 401 |         end
 | 
  
    | 402 |       end
 | 
  
    | 403 |       update_column :digest, sha.hexdigest
 | 
  
    | 404 |     end
 | 
  
    | 405 |   end
 | 
  
    | 406 | 
 | 
  
    | 407 |   # Returns true if the extension is allowed regarding allowed/denied
 | 
  
    | 408 |   # extensions defined in application settings, otherwise false
 | 
  
    | 409 |   def self.valid_extension?(extension)
 | 
  
    | 410 |     denied, allowed = [:attachment_extensions_denied, :attachment_extensions_allowed].map do |setting|
 | 
  
    | 411 |       Setting.send(setting)
 | 
  
    | 412 |     end
 | 
  
    | 413 |     if denied.present? && extension_in?(extension, denied)
 | 
  
    | 414 |       return false
 | 
  
    | 415 |     end
 | 
  
    | 416 |     if allowed.present? && !extension_in?(extension, allowed)
 | 
  
    | 417 |       return false
 | 
  
    | 418 |     end
 | 
  
    | 419 |     true
 | 
  
    | 420 |   end
 | 
  
    | 421 | 
 | 
  
    | 422 |   # Returns true if extension belongs to extensions list.
 | 
  
    | 423 |   def self.extension_in?(extension, extensions)
 | 
  
    | 424 |     extension = extension.downcase.sub(/\A\.+/, '')
 | 
  
    | 425 | 
 | 
  
    | 426 |     unless extensions.is_a?(Array)
 | 
  
    | 427 |       extensions = extensions.to_s.split(",").map(&:strip)
 | 
  
    | 428 |     end
 | 
  
    | 429 |     extensions = extensions.map {|s| s.downcase.sub(/\A\.+/, '')}.reject(&:blank?)
 | 
  
    | 430 |     extensions.include?(extension)
 | 
  
    | 431 |   end
 | 
  
    | 432 | 
 | 
  
    | 433 |   # Returns true if attachment's extension belongs to extensions list.
 | 
  
    | 434 |   def extension_in?(extensions)
 | 
  
    | 435 |     self.class.extension_in?(File.extname(filename), extensions)
 | 
  
    | 436 |   end
 | 
  
    | 437 | 
 | 
  
    | 438 |   # returns either MD5 or SHA256 depending on the way self.digest was computed
 | 
  
    | 439 |   def digest_type
 | 
  
    | 440 |     digest.size < 64 ? "MD5" : "SHA256" if digest.present?
 | 
  
    | 441 |   end
 | 
  
    | 442 | 
 | 
  
    | 443 |   private
 | 
  
    | 444 | 
 | 
  
    | 445 |   def reuse_existing_file_if_possible
 | 
  
    | 446 |     original_diskfile = nil
 | 
  
    | 447 | 
 | 
  
    | 448 |     reused = with_lock do
 | 
  
    | 449 |       if existing = Attachment
 | 
  
    | 450 |                       .where(digest: self.digest, filesize: self.filesize)
 | 
  
    | 451 |                       .where('id <> ? and disk_filename <> ?',
 | 
  
    | 452 |                              self.id, self.disk_filename)
 | 
  
    | 453 |                       .first
 | 
  
    | 454 |         existing.with_lock do
 | 
  
    | 455 | 
 | 
  
    | 456 |           original_diskfile = self.diskfile
 | 
  
    | 457 |           existing_diskfile = existing.diskfile
 | 
  
    | 458 | 
 | 
  
    | 459 |           if File.readable?(original_diskfile) &&
 | 
  
    | 460 |             File.readable?(existing_diskfile) &&
 | 
  
    | 461 |             FileUtils.identical?(original_diskfile, existing_diskfile)
 | 
  
    | 462 | 
 | 
  
    | 463 |             self.update_columns disk_directory: existing.disk_directory,
 | 
  
    | 464 |                                 disk_filename: existing.disk_filename
 | 
  
    | 465 |           end
 | 
  
    | 466 |         end
 | 
  
    | 467 |       end
 | 
  
    | 468 |     end
 | 
  
    | 469 |     if reused
 | 
  
    | 470 |       File.delete(original_diskfile)
 | 
  
    | 471 |     end
 | 
  
    | 472 |   rescue ActiveRecord::StatementInvalid, ActiveRecord::RecordNotFound
 | 
  
    | 473 |     # Catch and ignore lock errors. It is not critical if deduplication does
 | 
  
    | 474 |     # not happen, therefore we do not retry.
 | 
  
    | 475 |     # with_lock throws ActiveRecord::RecordNotFound if the record isnt there
 | 
  
    | 476 |     # anymore, thats why this is caught and ignored as well.
 | 
  
    | 477 |   end
 | 
  
    | 478 | 
 | 
  
    | 479 | 
 | 
  
    | 480 |   # Physically deletes the file from the file system
 | 
  
    | 481 |   def delete_from_disk!
 | 
  
    | 482 |     if disk_filename.present? && File.exist?(diskfile)
 | 
  
    | 483 |       File.delete(diskfile)
 | 
  
    | 484 |     end
 | 
  
    | 485 |   end
 | 
  
    | 486 | 
 | 
  
    | 487 |   def sanitize_filename(value)
 | 
  
    | 488 |     # get only the filename, not the whole path
 | 
  
    | 489 |     just_filename = value.gsub(/\A.*(\\|\/)/m, '')
 | 
  
    | 490 | 
 | 
  
    | 491 |     # Finally, replace invalid characters with underscore
 | 
  
    | 492 |     just_filename.gsub(/[\/\?\%\*\:\|\"\'<>\n\r]+/, '_')
 | 
  
    | 493 |   end
 | 
  
    | 494 | 
 | 
  
    | 495 |   # Returns the subdirectory in which the attachment will be saved
 | 
  
    | 496 |   def target_directory
 | 
  
    | 497 |     time = created_on || DateTime.now
 | 
  
    | 498 |     time.strftime("%Y/%m")
 | 
  
    | 499 |   end
 | 
  
    | 500 | 
 | 
  
    | 501 |   # Returns an ASCII or hashed filename that do not
 | 
  
    | 502 |   # exists yet in the given subdirectory
 | 
  
    | 503 |   def self.disk_filename(filename, directory=nil)
 | 
  
    | 504 |     timestamp = DateTime.now.strftime("%y%m%d%H%M%S")
 | 
  
    | 505 |     ascii = ''
 | 
  
    | 506 |     if filename =~ %r{^[a-zA-Z0-9_\.\-]*$} && filename.length <= 50
 | 
  
    | 507 |       ascii = filename
 | 
  
    | 508 |     else
 | 
  
    | 509 |       ascii = Digest::MD5.hexdigest(filename)
 | 
  
    | 510 |       # keep the extension if any
 | 
  
    | 511 |       ascii << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
 | 
  
    | 512 |     end
 | 
  
    | 513 |     while File.exist?(File.join(storage_path, directory.to_s, "#{timestamp}_#{ascii}"))
 | 
  
    | 514 |       timestamp.succ!
 | 
  
    | 515 |     end
 | 
  
    | 516 |     "#{timestamp}_#{ascii}"
 | 
  
    | 517 |   end
 | 
  
    | 518 | end
 |