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_diff? ⇒ Boolean
- #is_image? ⇒ Boolean
- #is_pdf? ⇒ Boolean
- #is_text? ⇒ 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
284 285 286 287 288 |
# File 'app/models/attachment.rb', line 284 def self.attach_files(obj, ) result = obj.(, User.current) obj. result end |
.clear_thumbnails ⇒ Object
Deletes all thumbnails
232 233 234 235 236 |
# File 'app/models/attachment.rb', line 232 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.
397 398 399 400 401 402 403 404 405 |
# File 'app/models/attachment.rb', line 397 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
269 270 271 272 273 274 275 276 277 |
# File 'app/models/attachment.rb', line 269 def self.find_by_token(token) if token.to_s =~ /^(\d+)\.([0-9a-f]+)$/ , = $1, $2 = Attachment.where(:id => , :digest => ).first if && .container.nil? end end end |
.latest_attach(attachments, filename) ⇒ Object
319 320 321 322 323 |
# File 'app/models/attachment.rb', line 319 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
354 355 356 357 358 |
# File 'app/models/attachment.rb', line 354 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
325 326 327 |
# File 'app/models/attachment.rb', line 325 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'}
})
300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 |
# File 'app/models/attachment.rb', line 300 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)
362 363 364 365 366 |
# File 'app/models/attachment.rb', line 362 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
383 384 385 386 387 388 389 390 391 392 393 394 |
# File 'app/models/attachment.rb', line 383 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
64 65 66 67 68 69 |
# File 'app/models/attachment.rb', line 64 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
190 191 192 193 194 195 196 |
# File 'app/models/attachment.rb', line 190 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
147 148 149 150 151 |
# File 'app/models/attachment.rb', line 147 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
413 414 415 |
# File 'app/models/attachment.rb', line 413 def digest_type digest.size < 64 ? "MD5" : "SHA256" if digest.present? end |
#diskfile ⇒ Object
Returns file's location on disk
154 155 156 |
# File 'app/models/attachment.rb', line 154 def diskfile File.join(self.class.storage_path, disk_directory.to_s, disk_filename.to_s) end |
#editable?(user = User.current) ⇒ Boolean
182 183 184 185 186 187 188 |
# File 'app/models/attachment.rb', line 182 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.
408 409 410 |
# File 'app/models/attachment.rb', line 408 def extension_in?(extensions) self.class.extension_in?(File.extname(filename), extensions) end |
#file ⇒ Object
100 101 102 |
# File 'app/models/attachment.rb', line 100 def file nil end |
#file=(incoming_file) ⇒ Object
86 87 88 89 90 91 92 93 94 95 96 97 98 |
# File 'app/models/attachment.rb', line 86 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
104 105 106 107 |
# File 'app/models/attachment.rb', line 104 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
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 144 |
# File 'app/models/attachment.rb', line 111 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
198 199 200 |
# File 'app/models/attachment.rb', line 198 def image? !!(self.filename =~ /\.(bmp|gif|jpg|jpe|jpeg|png)$/i) end |
#increment_download ⇒ Object
166 167 168 |
# File 'app/models/attachment.rb', line 166 def increment_download increment!(:downloads) end |
#is_diff? ⇒ Boolean
246 247 248 |
# File 'app/models/attachment.rb', line 246 def is_diff? self.filename =~ /\.(patch|diff)$/i end |
#is_image? ⇒ Boolean
242 243 244 |
# File 'app/models/attachment.rb', line 242 def is_image? Redmine::MimeType.is_type?('image', filename) end |
#is_pdf? ⇒ Boolean
250 251 252 |
# File 'app/models/attachment.rb', line 250 def is_pdf? Redmine::MimeType.of(filename) == "application/pdf" end |
#is_text? ⇒ Boolean
238 239 240 |
# File 'app/models/attachment.rb', line 238 def is_text? Redmine::MimeType.is_type?('text', filename) end |
#move_to_target_directory! ⇒ Object
Moves an existing attachment to its target directory
330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 |
# File 'app/models/attachment.rb', line 330 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
254 255 256 |
# File 'app/models/attachment.rb', line 254 def previewable? is_text? || is_image? end |
#project ⇒ Object
170 171 172 |
# File 'app/models/attachment.rb', line 170 def project container.try(:project) end |
#readable? ⇒ Boolean
Returns true if the file is readable
259 260 261 |
# File 'app/models/attachment.rb', line 259 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.
208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 |
# File 'app/models/attachment.rb', line 208 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
202 203 204 |
# File 'app/models/attachment.rb', line 202 def thumbnailable? image? end |
#title ⇒ Object
158 159 160 161 162 163 164 |
# File 'app/models/attachment.rb', line 158 def title title = filename.dup if description.present? title << " (#{description})" end title end |
#token ⇒ Object
Returns the attachment token
264 265 266 |
# File 'app/models/attachment.rb', line 264 def token "#{id}.#{digest}" end |
#update_digest_to_sha256! ⇒ Object
Updates attachment digest to SHA256
369 370 371 372 373 374 375 376 377 378 379 |
# File 'app/models/attachment.rb', line 369 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
77 78 79 80 81 82 83 84 |
# File 'app/models/attachment.rb', line 77 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
71 72 73 74 75 |
# File 'app/models/attachment.rb', line 71 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
174 175 176 177 178 179 180 |
# File 'app/models/attachment.rb', line 174 def visible?(user=User.current) if container_id container && container.(user) else == user end end |