Class: Attachment

Inherits:
ActiveRecord::Base
  • Object
show all
Includes:
Redmine::SafeAttributes
Defined in:
app/models/attachment.rb

Overview

Since:

  • 0.4.0

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

Instance Method Summary collapse

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

Since:

  • 1.0.0



291
292
293
294
295
# File 'app/models/attachment.rb', line 291

def self.attach_files(obj, attachments)
  result = obj.save_attachments(attachments, User.current)
  obj.attach_saved_attachments
  result
end

.clear_thumbnailsObject

Deletes all thumbnails

Since:

  • 2.1.0



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.

Returns:

  • (Boolean)

Since:

  • 3.4.0



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

Since:

  • 1.4.0



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]+)$/
    attachment_id, attachment_digest = $1, $2
    attachment = Attachment.find_by(:id => attachment_id, :digest => attachment_digest)
    if attachment && attachment.container.nil?
      attachment
    end
  end
end

.latest_attach(attachments, filename) ⇒ Object

Since:

  • 1.3.0



326
327
328
329
330
# File 'app/models/attachment.rb', line 326

def self.latest_attach(attachments, filename)
  attachments.sort_by(&:created_on).reverse.detect do |att|
    filename.casecmp(att.filename) == 0
  end
end

.move_from_root_to_target_directoryObject

Moves existing attachments that are stored at the root of the files directory (ie. created before Redmine 2.3) to their target subdirectories

Since:

  • 2.3.0



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 |attachment|
    attachment.move_to_target_directory!
  end
end

.prune(age = 1.day) ⇒ Object

Since:

  • 1.4.0



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.update_attachments(attachments, {
  4 => {:filename => 'foo'},
  7 => {:filename => 'bar', :description => 'file description'}
})

Since:

  • 3.0.0



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.update_attachments(attachments, params)
  params = params.transform_keys {|key| key.to_i}

  saved = true
  transaction do
    attachments.each do |attachment|
      if p = params[attachment.id]
        attachment.filename = p[:filename] if p.key?(:filename)
        attachment.description = p[:description] if p.key?(:description)
        saved &&= attachment.save
      end
    end
    unless saved
      raise ActiveRecord::Rollback
    end
  end
  saved
end

.update_digests_to_sha256Object

Updates digests to SHA256 for all attachments that have a MD5 digest (ie. created before Redmine 3.4)

Since:

  • 3.4.0



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 |attachment|
    attachment.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

Returns:

  • (Boolean)

Since:

  • 3.2.0



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

Since:

  • 1.4.0



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

Returns:

  • (Boolean)

Since:

  • 0.8.1



189
190
191
192
193
194
195
# File 'app/models/attachment.rb', line 189

def deletable?(user=User.current)
  if container_id
    container && container.attachments_deletable?(user)
  else
    author == user
  end
end

#delete_from_diskObject

Deletes the file from the file system if it's not referenced by other attachments

Since:

  • 1.3.0



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_typeObject

returns either MD5 or SHA256 depending on the way self.digest was computed

Since:

  • 3.4.0



420
421
422
# File 'app/models/attachment.rb', line 420

def digest_type
  digest.size < 64 ? "MD5" : "SHA256" if digest.present?
end

#diskfileObject

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

Returns:

  • (Boolean)

Since:

  • 3.0.0



181
182
183
184
185
186
187
# File 'app/models/attachment.rb', line 181

def editable?(user=User.current)
  if container_id
    container && container.attachments_editable?(user)
  else
    author == user
  end
end

#extension_in?(extensions) ⇒ Boolean

Returns true if attachment's extension belongs to extensions list.

Returns:

  • (Boolean)

Since:

  • 3.4.0



415
416
417
# File 'app/models/attachment.rb', line 415

def extension_in?(extensions)
  self.class.extension_in?(File.extname(filename), extensions)
end

#fileObject



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

Since:

  • 1.4.0



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_locationObject

Copies the temporary file to its final location and computes its MD5 hash

Since:

  • 1.3.0



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

Returns:

  • (Boolean)

Since:

  • 0.6.0



197
198
199
# File 'app/models/attachment.rb', line 197

def image?
  !!(self.filename =~ /\.(bmp|gif|jpg|jpe|jpeg|png)$/i)
end

#increment_downloadObject



165
166
167
# File 'app/models/attachment.rb', line 165

def increment_download
  increment!(:downloads)
end

#is_audio?Boolean

Returns:

  • (Boolean)

Since:

  • 4.0.0



257
258
259
# File 'app/models/attachment.rb', line 257

def is_audio?
  Redmine::MimeType.is_type?('audio', filename)
end

#is_diff?Boolean

Returns:

  • (Boolean)

Since:

  • 0.8.0



245
246
247
# File 'app/models/attachment.rb', line 245

def is_diff?
  self.filename =~ /\.(patch|diff)$/i
end

#is_image?Boolean

Returns:

  • (Boolean)

Since:

  • 3.3.0



241
242
243
# File 'app/models/attachment.rb', line 241

def is_image?
  Redmine::MimeType.is_type?('image', filename)
end

#is_pdf?Boolean

Returns:

  • (Boolean)

Since:

  • 3.3.0



249
250
251
# File 'app/models/attachment.rb', line 249

def is_pdf?
  Redmine::MimeType.of(filename) == "application/pdf"
end

#is_text?Boolean

Returns:

  • (Boolean)

Since:

  • 0.8.0



237
238
239
# File 'app/models/attachment.rb', line 237

def is_text?
  Redmine::MimeType.is_type?('text', filename)
end

#is_video?Boolean

Returns:

  • (Boolean)

Since:

  • 4.0.0



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

Since:

  • 2.3.0



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

Returns:

  • (Boolean)

Since:

  • 3.4.0



261
262
263
# File 'app/models/attachment.rb', line 261

def previewable?
  is_text? || is_image? || is_video? || is_audio?
end

#projectObject

Since:

  • 0.5.1



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

Returns:

  • (Boolean)

Since:

  • 0.9.0



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.

Since:

  • 2.1.0



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(options={})
  if thumbnailable? && readable?
    size = options[: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.message}" if logger
      return nil
    end
  end
end

#thumbnailable?Boolean

Returns:

  • (Boolean)

Since:

  • 2.1.0



201
202
203
# File 'app/models/attachment.rb', line 201

def thumbnailable?
  image?
end

#titleObject

Since:

  • 2.1.0



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

#tokenObject

Returns the attachment token

Since:

  • 1.4.0



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

Since:

  • 3.4.0



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_extensionObject

Since:

  • 3.2.0



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_sizeObject

Since:

  • 1.3.0



70
71
72
73
74
# File 'app/models/attachment.rb', line 70

def validate_max_file_size
  if @temp_file && self.filesize > Setting.attachment_max_size.to_i.kilobytes
    errors.add(:base, l(:error_attachment_too_big, :max_size => Setting.attachment_max_size.to_i.kilobytes))
  end
end

#visible?(user = User.current) ⇒ Boolean

Returns:

  • (Boolean)

Since:

  • 0.8.1



173
174
175
176
177
178
179
# File 'app/models/attachment.rb', line 173

def visible?(user=User.current)
  if container_id
    container && container.attachments_visible?(user)
  else
    author == user
  end
end