Class: Attachment
- Inherits:
-
ActiveRecord::Base
- Object
- ActiveRecord::Base
- Attachment
- Includes:
- Redmine::SafeAttributes
- Defined in:
- app/models/attachment.rb
Overview
Constant Summary collapse
- @@storage_path =
Redmine::Configuration['attachments_storage_path'] || File.join(Rails.root, "files")
- @@thumbnails_storage_path =
File.join(Rails.root, "tmp", "thumbnails")
Class Method Summary collapse
-
.attach_files(obj, attachments) ⇒ Object
Bulk attaches a set of files to an object.
-
.clear_thumbnails ⇒ Object
Deletes all thumbnails.
-
.extension_in?(extension, extensions) ⇒ Boolean
Returns true if extension belongs to extensions list.
-
.find_by_token(token) ⇒ Object
Finds an attachment that matches the given token and that has no container.
- .latest_attach(attachments, filename) ⇒ Object
-
.move_from_root_to_target_directory ⇒ Object
Moves existing attachments that are stored at the root of the files directory (ie. created before Redmine 2.3) to their target subdirectories.
- .prune(age = 1.day) ⇒ Object
-
.update_attachments(attachments, params) ⇒ Object
Updates the filename and description of a set of attachments with the given hash of attributes.
-
.update_digests_to_sha256 ⇒ Object
Updates digests to SHA256 for all attachments that have a MD5 digest (ie. created before Redmine 3.4).
-
.valid_extension?(extension) ⇒ Boolean
Returns true if the extension is allowed regarding allowed/denied extensions defined in application settings, otherwise false.
Instance Method Summary collapse
-
#copy(attributes = nil) ⇒ Object
Returns an unsaved copy of the attachment.
- #deletable?(user = User.current) ⇒ Boolean
-
#delete_from_disk ⇒ Object
Deletes the file from the file system if it's not referenced by other attachments.
-
#digest_type ⇒ Object
returns either MD5 or SHA256 depending on the way self.digest was computed.
-
#diskfile ⇒ Object
Returns file's location on disk.
- #editable?(user = User.current) ⇒ Boolean
-
#extension_in?(extensions) ⇒ Boolean
Returns true if attachment's extension belongs to extensions list.
- #file ⇒ Object
- #file=(incoming_file) ⇒ Object
- #filename=(arg) ⇒ Object
-
#files_to_final_location ⇒ Object
Copies the temporary file to its final location and computes its MD5 hash.
- #image? ⇒ Boolean
- #increment_download ⇒ Object
- #is_audio? ⇒ Boolean
- #is_diff? ⇒ Boolean
- #is_image? ⇒ Boolean
- #is_pdf? ⇒ Boolean
- #is_text? ⇒ Boolean
- #is_video? ⇒ Boolean
-
#move_to_target_directory! ⇒ Object
Moves an existing attachment to its target directory.
- #previewable? ⇒ Boolean
- #project ⇒ Object
-
#readable? ⇒ Boolean
Returns true if the file is readable.
-
#thumbnail(options = {}) ⇒ Object
Returns the full path the attachment thumbnail, or nil if the thumbnail cannot be generated.
- #thumbnailable? ⇒ Boolean
- #title ⇒ Object
-
#token ⇒ Object
Returns the attachment token.
-
#update_digest_to_sha256! ⇒ Object
Updates attachment digest to SHA256.
- #validate_file_extension ⇒ Object
- #validate_max_file_size ⇒ Object
- #visible?(user = User.current) ⇒ Boolean
Methods included from Redmine::SafeAttributes
#delete_unsafe_attributes, #safe_attribute?, #safe_attribute_names, #safe_attributes=
Class Method Details
.attach_files(obj, attachments) ⇒ Object
Bulk attaches a set of files to an object
Returns a Hash of the results: :files => array of the attached files :unsaved => array of the files that could not be attached
291 292 293 294 295 |
# File 'app/models/attachment.rb', line 291 def self.attach_files(obj, ) result = obj.(, User.current) obj. result end |
.clear_thumbnails ⇒ Object
Deletes all thumbnails
231 232 233 234 235 |
# File 'app/models/attachment.rb', line 231 def self.clear_thumbnails Dir.glob(File.join(thumbnails_storage_path, "*.thumb")).each do |file| File.delete file end end |
.extension_in?(extension, extensions) ⇒ Boolean
Returns true if extension belongs to extensions list.
404 405 406 407 408 409 410 411 412 |
# File 'app/models/attachment.rb', line 404 def self.extension_in?(extension, extensions) extension = extension.downcase.sub(/\A\.+/, '') unless extensions.is_a?(Array) extensions = extensions.to_s.split(",").map(&:strip) end extensions = extensions.map {|s| s.downcase.sub(/\A\.+/, '')}.reject(&:blank?) extensions.include?(extension) end |
.find_by_token(token) ⇒ Object
Finds an attachment that matches the given token and that has no container
276 277 278 279 280 281 282 283 284 |
# File 'app/models/attachment.rb', line 276 def self.find_by_token(token) if token.to_s =~ /^(\d+)\.([0-9a-f]+)$/ , = $1, $2 = Attachment.find_by(:id => , :digest => ) if && .container.nil? end end end |
.latest_attach(attachments, filename) ⇒ Object
326 327 328 329 330 |
# File 'app/models/attachment.rb', line 326 def self.latest_attach(, filename) .sort_by(&:created_on).reverse.detect do |att| filename.casecmp(att.filename) == 0 end end |
.move_from_root_to_target_directory ⇒ Object
Moves existing attachments that are stored at the root of the files directory (ie. created before Redmine 2.3) to their target subdirectories
361 362 363 364 365 |
# File 'app/models/attachment.rb', line 361 def self.move_from_root_to_target_directory Attachment.where("disk_directory IS NULL OR disk_directory = ''").find_each do || .move_to_target_directory! end end |
.prune(age = 1.day) ⇒ Object
332 333 334 |
# File 'app/models/attachment.rb', line 332 def self.prune(age=1.day) Attachment.where("created_on < ? AND (container_type IS NULL OR container_type = '')", Time.now - age).destroy_all end |
.update_attachments(attachments, params) ⇒ Object
Updates the filename and description of a set of attachments with the given hash of attributes. Returns true if all attachments were updated.
Example:
Attachment.(, {
4 => {:filename => 'foo'},
7 => {:filename => 'bar', :description => 'file description'}
})
307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 |
# File 'app/models/attachment.rb', line 307 def self.(, params) params = params.transform_keys {|key| key.to_i} saved = true transaction do .each do || if p = params[.id] .filename = p[:filename] if p.key?(:filename) .description = p[:description] if p.key?(:description) saved &&= .save end end unless saved raise ActiveRecord::Rollback end end saved end |
.update_digests_to_sha256 ⇒ Object
Updates digests to SHA256 for all attachments that have a MD5 digest (ie. created before Redmine 3.4)
369 370 371 372 373 |
# File 'app/models/attachment.rb', line 369 def self.update_digests_to_sha256 Attachment.where("length(digest) < 64").find_each do || .update_digest_to_sha256! end end |
.valid_extension?(extension) ⇒ Boolean
Returns true if the extension is allowed regarding allowed/denied extensions defined in application settings, otherwise false
390 391 392 393 394 395 396 397 398 399 400 401 |
# File 'app/models/attachment.rb', line 390 def self.valid_extension?(extension) denied, allowed = [:attachment_extensions_denied, :attachment_extensions_allowed].map do |setting| Setting.send(setting) end if denied.present? && extension_in?(extension, denied) return false end if allowed.present? && !extension_in?(extension, allowed) return false end true end |
Instance Method Details
#copy(attributes = nil) ⇒ Object
Returns an unsaved copy of the attachment
63 64 65 66 67 68 |
# File 'app/models/attachment.rb', line 63 def copy(attributes=nil) copy = self.class.new copy.attributes = self.attributes.dup.except("id", "downloads") copy.attributes = attributes if attributes copy end |
#deletable?(user = User.current) ⇒ Boolean
189 190 191 192 193 194 195 |
# File 'app/models/attachment.rb', line 189 def deletable?(user=User.current) if container_id container && container.(user) else == user end end |
#delete_from_disk ⇒ Object
Deletes the file from the file system if it's not referenced by other attachments
146 147 148 149 150 |
# File 'app/models/attachment.rb', line 146 def delete_from_disk if Attachment.where("disk_filename = ? AND id <> ?", disk_filename, id).empty? delete_from_disk! end end |
#digest_type ⇒ Object
returns either MD5 or SHA256 depending on the way self.digest was computed
420 421 422 |
# File 'app/models/attachment.rb', line 420 def digest_type digest.size < 64 ? "MD5" : "SHA256" if digest.present? end |
#diskfile ⇒ Object
Returns file's location on disk
153 154 155 |
# File 'app/models/attachment.rb', line 153 def diskfile File.join(self.class.storage_path, disk_directory.to_s, disk_filename.to_s) end |
#editable?(user = User.current) ⇒ Boolean
181 182 183 184 185 186 187 |
# File 'app/models/attachment.rb', line 181 def editable?(user=User.current) if container_id container && container.(user) else == user end end |
#extension_in?(extensions) ⇒ Boolean
Returns true if attachment's extension belongs to extensions list.
415 416 417 |
# File 'app/models/attachment.rb', line 415 def extension_in?(extensions) self.class.extension_in?(File.extname(filename), extensions) end |
#file ⇒ Object
99 100 101 |
# File 'app/models/attachment.rb', line 99 def file nil end |
#file=(incoming_file) ⇒ Object
85 86 87 88 89 90 91 92 93 94 95 96 97 |
# File 'app/models/attachment.rb', line 85 def file=(incoming_file) unless incoming_file.nil? @temp_file = incoming_file if @temp_file.respond_to?(:original_filename) self.filename = @temp_file.original_filename self.filename.force_encoding("UTF-8") end if @temp_file.respond_to?(:content_type) self.content_type = @temp_file.content_type.to_s.chomp end self.filesize = @temp_file.size end end |
#filename=(arg) ⇒ Object
103 104 105 106 |
# File 'app/models/attachment.rb', line 103 def filename=(arg) write_attribute :filename, sanitize_filename(arg.to_s) filename end |
#files_to_final_location ⇒ Object
Copies the temporary file to its final location and computes its MD5 hash
110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 |
# File 'app/models/attachment.rb', line 110 def files_to_final_location if @temp_file self.disk_directory = target_directory self.disk_filename = Attachment.disk_filename(filename, disk_directory) logger.info("Saving attachment '#{self.diskfile}' (#{@temp_file.size} bytes)") if logger path = File.dirname(diskfile) unless File.directory?(path) FileUtils.mkdir_p(path) end sha = Digest::SHA256.new File.open(diskfile, "wb") do |f| if @temp_file.respond_to?(:read) buffer = "" while (buffer = @temp_file.read(8192)) f.write(buffer) sha.update(buffer) end else f.write(@temp_file) sha.update(@temp_file) end end self.digest = sha.hexdigest end @temp_file = nil if content_type.blank? && filename.present? self.content_type = Redmine::MimeType.of(filename) end # Don't save the content type if it's longer than the authorized length if self.content_type && self.content_type.length > 255 self.content_type = nil end end |
#image? ⇒ Boolean
197 198 199 |
# File 'app/models/attachment.rb', line 197 def image? !!(self.filename =~ /\.(bmp|gif|jpg|jpe|jpeg|png)$/i) end |
#increment_download ⇒ Object
165 166 167 |
# File 'app/models/attachment.rb', line 165 def increment_download increment!(:downloads) end |
#is_audio? ⇒ Boolean
257 258 259 |
# File 'app/models/attachment.rb', line 257 def is_audio? Redmine::MimeType.is_type?('audio', filename) end |
#is_diff? ⇒ Boolean
245 246 247 |
# File 'app/models/attachment.rb', line 245 def is_diff? self.filename =~ /\.(patch|diff)$/i end |
#is_image? ⇒ Boolean
241 242 243 |
# File 'app/models/attachment.rb', line 241 def is_image? Redmine::MimeType.is_type?('image', filename) end |
#is_pdf? ⇒ Boolean
249 250 251 |
# File 'app/models/attachment.rb', line 249 def is_pdf? Redmine::MimeType.of(filename) == "application/pdf" end |
#is_text? ⇒ Boolean
237 238 239 |
# File 'app/models/attachment.rb', line 237 def is_text? Redmine::MimeType.is_type?('text', filename) end |
#is_video? ⇒ Boolean
253 254 255 |
# File 'app/models/attachment.rb', line 253 def is_video? Redmine::MimeType.is_type?('video', filename) end |
#move_to_target_directory! ⇒ Object
Moves an existing attachment to its target directory
337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 |
# File 'app/models/attachment.rb', line 337 def move_to_target_directory! return unless !new_record? & readable? src = diskfile self.disk_directory = target_directory dest = diskfile return if src == dest if !FileUtils.mkdir_p(File.dirname(dest)) logger.error "Could not create directory #{File.dirname(dest)}" if logger return end if !FileUtils.mv(src, dest) logger.error "Could not move attachment from #{src} to #{dest}" if logger return end update_column :disk_directory, disk_directory end |
#previewable? ⇒ Boolean
261 262 263 |
# File 'app/models/attachment.rb', line 261 def previewable? is_text? || is_image? || is_video? || is_audio? end |
#project ⇒ Object
169 170 171 |
# File 'app/models/attachment.rb', line 169 def project container.try(:project) end |
#readable? ⇒ Boolean
Returns true if the file is readable
266 267 268 |
# File 'app/models/attachment.rb', line 266 def readable? disk_filename.present? && File.readable?(diskfile) end |
#thumbnail(options = {}) ⇒ Object
Returns the full path the attachment thumbnail, or nil if the thumbnail cannot be generated.
207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 |
# File 'app/models/attachment.rb', line 207 def thumbnail(={}) if thumbnailable? && readable? size = [:size].to_i if size > 0 # Limit the number of thumbnails per image size = (size / 50) * 50 # Maximum thumbnail size size = 800 if size > 800 else size = Setting.thumbnails_size.to_i end size = 100 unless size > 0 target = File.join(self.class.thumbnails_storage_path, "#{id}_#{digest}_#{size}.thumb") begin Redmine::Thumbnail.generate(self.diskfile, target, size) rescue => e logger.error "An error occured while generating thumbnail for #{disk_filename} to #{target}\nException was: #{e.}" if logger return nil end end end |
#thumbnailable? ⇒ Boolean
201 202 203 |
# File 'app/models/attachment.rb', line 201 def thumbnailable? image? end |
#title ⇒ Object
157 158 159 160 161 162 163 |
# File 'app/models/attachment.rb', line 157 def title title = filename.dup if description.present? title << " (#{description})" end title end |
#token ⇒ Object
Returns the attachment token
271 272 273 |
# File 'app/models/attachment.rb', line 271 def token "#{id}.#{digest}" end |
#update_digest_to_sha256! ⇒ Object
Updates attachment digest to SHA256
376 377 378 379 380 381 382 383 384 385 386 |
# File 'app/models/attachment.rb', line 376 def update_digest_to_sha256! if readable? sha = Digest::SHA256.new File.open(diskfile, 'rb') do |f| while buffer = f.read(8192) sha.update(buffer) end end update_column :digest, sha.hexdigest end end |
#validate_file_extension ⇒ Object
76 77 78 79 80 81 82 83 |
# File 'app/models/attachment.rb', line 76 def validate_file_extension if @temp_file extension = File.extname(filename) unless self.class.valid_extension?(extension) errors.add(:base, l(:error_attachment_extension_not_allowed, :extension => extension)) end end end |
#validate_max_file_size ⇒ Object
70 71 72 73 74 |
# File 'app/models/attachment.rb', line 70 def validate_max_file_size if @temp_file && self.filesize > Setting..to_i.kilobytes errors.add(:base, l(:error_attachment_too_big, :max_size => Setting..to_i.kilobytes)) end end |
#visible?(user = User.current) ⇒ Boolean
173 174 175 176 177 178 179 |
# File 'app/models/attachment.rb', line 173 def visible?(user=User.current) if container_id container && container.(user) else == user end end |