Class: Attachment

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

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

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


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

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

.clear_thumbnailsObject

Deletes all thumbnails


219
220
221
222
223
# File 'app/models/attachment.rb', line 219

def self.clear_thumbnails
  Dir.glob(File.join(thumbnails_storage_path, "*.thumb")).each do |file|
    File.delete file
  end
end

.find_by_token(token) ⇒ Object

Finds an attachment that matches the given token and that has no container


244
245
246
247
248
249
250
251
252
# File 'app/models/attachment.rb', line 244

def self.find_by_token(token)
  if token.to_s =~ /^(\d+)\.([0-9a-f]+)$/
    attachment_id, attachment_digest = $1, $2
    attachment = Attachment.where(:id => attachment_id, :digest => attachment_digest).first
    if attachment && attachment.container.nil?
      attachment
    end
  end
end

.latest_attach(attachments, filename) ⇒ Object


294
295
296
297
298
# File 'app/models/attachment.rb', line 294

def self.latest_attach(attachments, filename)
  attachments.sort_by(&:created_on).reverse.detect do |att|
    att.filename.downcase == filename.downcase
  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


329
330
331
332
333
# File 'app/models/attachment.rb', line 329

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


300
301
302
# File 'app/models/attachment.rb', line 300

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'}
})

275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
# File 'app/models/attachment.rb', line 275

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

Instance Method Details

#copy(attributes = nil) ⇒ Object

Returns an unsaved copy of the attachment


59
60
61
62
63
64
# File 'app/models/attachment.rb', line 59

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


177
178
179
180
181
182
183
# File 'app/models/attachment.rb', line 177

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


134
135
136
137
138
# File 'app/models/attachment.rb', line 134

def delete_from_disk
  if Attachment.where("disk_filename = ? AND id <> ?", disk_filename, id).empty?
    delete_from_disk!
  end
end

#diskfileObject

Returns file's location on disk


141
142
143
# File 'app/models/attachment.rb', line 141

def diskfile
  File.join(self.class.storage_path, disk_directory.to_s, disk_filename.to_s)
end

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


169
170
171
172
173
174
175
# File 'app/models/attachment.rb', line 169

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

#fileObject


91
92
93
# File 'app/models/attachment.rb', line 91

def file
  nil
end

#file=(incoming_file) ⇒ Object


72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'app/models/attachment.rb', line 72

def file=(incoming_file)
  unless incoming_file.nil?
    @temp_file = incoming_file
    if @temp_file.size > 0
      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
      if content_type.blank? && filename.present?
        self.content_type = Redmine::MimeType.of(filename)
      end
      self.filesize = @temp_file.size
    end
  end
end

#filename=(arg) ⇒ Object


95
96
97
98
# File 'app/models/attachment.rb', line 95

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


102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'app/models/attachment.rb', line 102

def files_to_final_location
  if @temp_file && (@temp_file.size > 0)
    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
    md5 = Digest::MD5.new
    File.open(diskfile, "wb") do |f|
      if @temp_file.respond_to?(:read)
        buffer = ""
        while (buffer = @temp_file.read(8192))
          f.write(buffer)
          md5.update(buffer)
        end
      else
        f.write(@temp_file)
        md5.update(@temp_file)
      end
    end
    self.digest = md5.hexdigest
  end
  @temp_file = nil
  # 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


185
186
187
# File 'app/models/attachment.rb', line 185

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

#increment_downloadObject


153
154
155
# File 'app/models/attachment.rb', line 153

def increment_download
  increment!(:downloads)
end

#is_diff?Boolean


229
230
231
# File 'app/models/attachment.rb', line 229

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

#is_text?Boolean


225
226
227
# File 'app/models/attachment.rb', line 225

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

#move_to_target_directory!Object

Moves an existing attachment to its target directory


305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
# File 'app/models/attachment.rb', line 305

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

#projectObject


157
158
159
# File 'app/models/attachment.rb', line 157

def project
  container.try(:project)
end

#readable?Boolean

Returns true if the file is readable


234
235
236
# File 'app/models/attachment.rb', line 234

def readable?
  File.readable?(diskfile)
end

#thumbnail(options = {}) ⇒ Object

Returns the full path the attachment thumbnail, or nil if the thumbnail cannot be generated.


195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
# File 'app/models/attachment.rb', line 195

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


189
190
191
# File 'app/models/attachment.rb', line 189

def thumbnailable?
  image?
end

#titleObject


145
146
147
148
149
150
151
# File 'app/models/attachment.rb', line 145

def title
  title = filename.to_s
  if description.present?
    title << " (#{description})"
  end
  title
end

#tokenObject

Returns the attachment token


239
240
241
# File 'app/models/attachment.rb', line 239

def token
  "#{id}.#{digest}"
end

#validate_max_file_sizeObject


66
67
68
69
70
# File 'app/models/attachment.rb', line 66

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


161
162
163
164
165
166
167
# File 'app/models/attachment.rb', line 161

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