##// END OF EJS Templates
Removes attachment from disk after rollback on create (#21125)....
Jean-Philippe Lang -
r14703:3ffdc863dec4
parent child
Show More
@@ -0,0 +1,76
1 # encoding: utf-8
2 #
3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
5 #
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
20 require File.expand_path('../../test_helper', __FILE__)
21
22 class AttachmentTest < ActiveSupport::TestCase
23 fixtures :users, :email_addresses, :projects, :roles, :members, :member_roles,
24 :enabled_modules, :issues, :trackers, :attachments
25
26 self.use_transactional_fixtures = false
27
28 def setup
29 set_tmp_attachments_directory
30 end
31
32 def test_rollback_after_create_should_remove_file_from_disk
33 diskfile = nil
34
35 Attachment.transaction do
36 a = Attachment.new(:container => Issue.find(1),
37 :file => uploaded_test_file("testfile.txt", "text/plain"),
38 :author => User.find(1))
39 a.save!
40 diskfile = a.diskfile
41 assert File.exist?(diskfile)
42 raise ActiveRecord::Rollback
43 end
44 assert !File.exist?(diskfile)
45 end
46
47 def test_destroy_should_remove_file_from_disk_after_commit
48 a = Attachment.new(:container => Issue.find(1),
49 :file => uploaded_test_file("testfile.txt", "text/plain"),
50 :author => User.find(1))
51 a.save!
52 diskfile = a.diskfile
53 assert File.exist?(diskfile)
54
55 Attachment.transaction do
56 a.destroy
57 assert File.exist?(diskfile)
58 end
59 assert !File.exist?(diskfile)
60 end
61
62 def test_rollback_after_destroy_should_not_remove_file_from_disk
63 a = Attachment.new(:container => Issue.find(1),
64 :file => uploaded_test_file("testfile.txt", "text/plain"),
65 :author => User.find(1))
66 a.save!
67 diskfile = a.diskfile
68 assert File.exist?(diskfile)
69
70 Attachment.transaction do
71 a.destroy
72 raise ActiveRecord::Rollback
73 end
74 assert File.exist?(diskfile)
75 end
76 end
@@ -1,401 +1,402
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require "digest/md5"
19 19 require "fileutils"
20 20
21 21 class Attachment < ActiveRecord::Base
22 22 belongs_to :container, :polymorphic => true
23 23 belongs_to :author, :class_name => "User"
24 24
25 25 validates_presence_of :filename, :author
26 26 validates_length_of :filename, :maximum => 255
27 27 validates_length_of :disk_filename, :maximum => 255
28 28 validates_length_of :description, :maximum => 255
29 29 validate :validate_max_file_size, :validate_file_extension
30 30 attr_protected :id
31 31
32 32 acts_as_event :title => :filename,
33 33 :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}}
34 34
35 35 acts_as_activity_provider :type => 'files',
36 36 :permission => :view_files,
37 37 :author_key => :author_id,
38 38 :scope => select("#{Attachment.table_name}.*").
39 39 joins("LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " +
40 40 "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 )")
41 41
42 42 acts_as_activity_provider :type => 'documents',
43 43 :permission => :view_documents,
44 44 :author_key => :author_id,
45 45 :scope => select("#{Attachment.table_name}.*").
46 46 joins("LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " +
47 47 "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id")
48 48
49 49 cattr_accessor :storage_path
50 50 @@storage_path = Redmine::Configuration['attachments_storage_path'] || File.join(Rails.root, "files")
51 51
52 52 cattr_accessor :thumbnails_storage_path
53 53 @@thumbnails_storage_path = File.join(Rails.root, "tmp", "thumbnails")
54 54
55 55 before_create :files_to_final_location
56 after_rollback :delete_from_disk, :on => :create
56 57 after_commit :delete_from_disk, :on => :destroy
57 58
58 59 # Returns an unsaved copy of the attachment
59 60 def copy(attributes=nil)
60 61 copy = self.class.new
61 62 copy.attributes = self.attributes.dup.except("id", "downloads")
62 63 copy.attributes = attributes if attributes
63 64 copy
64 65 end
65 66
66 67 def validate_max_file_size
67 68 if @temp_file && self.filesize > Setting.attachment_max_size.to_i.kilobytes
68 69 errors.add(:base, l(:error_attachment_too_big, :max_size => Setting.attachment_max_size.to_i.kilobytes))
69 70 end
70 71 end
71 72
72 73 def validate_file_extension
73 74 if @temp_file
74 75 extension = File.extname(filename)
75 76 unless self.class.valid_extension?(extension)
76 77 errors.add(:base, l(:error_attachment_extension_not_allowed, :extension => extension))
77 78 end
78 79 end
79 80 end
80 81
81 82 def file=(incoming_file)
82 83 unless incoming_file.nil?
83 84 @temp_file = incoming_file
84 85 if @temp_file.size > 0
85 86 if @temp_file.respond_to?(:original_filename)
86 87 self.filename = @temp_file.original_filename
87 88 self.filename.force_encoding("UTF-8")
88 89 end
89 90 if @temp_file.respond_to?(:content_type)
90 91 self.content_type = @temp_file.content_type.to_s.chomp
91 92 end
92 93 self.filesize = @temp_file.size
93 94 end
94 95 end
95 96 end
96 97
97 98 def file
98 99 nil
99 100 end
100 101
101 102 def filename=(arg)
102 103 write_attribute :filename, sanitize_filename(arg.to_s)
103 104 filename
104 105 end
105 106
106 107 # Copies the temporary file to its final location
107 108 # and computes its MD5 hash
108 109 def files_to_final_location
109 110 if @temp_file && (@temp_file.size > 0)
110 111 self.disk_directory = target_directory
111 112 self.disk_filename = Attachment.disk_filename(filename, disk_directory)
112 113 logger.info("Saving attachment '#{self.diskfile}' (#{@temp_file.size} bytes)") if logger
113 114 path = File.dirname(diskfile)
114 115 unless File.directory?(path)
115 116 FileUtils.mkdir_p(path)
116 117 end
117 118 md5 = Digest::MD5.new
118 119 File.open(diskfile, "wb") do |f|
119 120 if @temp_file.respond_to?(:read)
120 121 buffer = ""
121 122 while (buffer = @temp_file.read(8192))
122 123 f.write(buffer)
123 124 md5.update(buffer)
124 125 end
125 126 else
126 127 f.write(@temp_file)
127 128 md5.update(@temp_file)
128 129 end
129 130 end
130 131 self.digest = md5.hexdigest
131 132 end
132 133 @temp_file = nil
133 134
134 135 if content_type.blank? && filename.present?
135 136 self.content_type = Redmine::MimeType.of(filename)
136 137 end
137 138 # Don't save the content type if it's longer than the authorized length
138 139 if self.content_type && self.content_type.length > 255
139 140 self.content_type = nil
140 141 end
141 142 end
142 143
143 144 # Deletes the file from the file system if it's not referenced by other attachments
144 145 def delete_from_disk
145 146 if Attachment.where("disk_filename = ? AND id <> ?", disk_filename, id).empty?
146 147 delete_from_disk!
147 148 end
148 149 end
149 150
150 151 # Returns file's location on disk
151 152 def diskfile
152 153 File.join(self.class.storage_path, disk_directory.to_s, disk_filename.to_s)
153 154 end
154 155
155 156 def title
156 157 title = filename.to_s
157 158 if description.present?
158 159 title << " (#{description})"
159 160 end
160 161 title
161 162 end
162 163
163 164 def increment_download
164 165 increment!(:downloads)
165 166 end
166 167
167 168 def project
168 169 container.try(:project)
169 170 end
170 171
171 172 def visible?(user=User.current)
172 173 if container_id
173 174 container && container.attachments_visible?(user)
174 175 else
175 176 author == user
176 177 end
177 178 end
178 179
179 180 def editable?(user=User.current)
180 181 if container_id
181 182 container && container.attachments_editable?(user)
182 183 else
183 184 author == user
184 185 end
185 186 end
186 187
187 188 def deletable?(user=User.current)
188 189 if container_id
189 190 container && container.attachments_deletable?(user)
190 191 else
191 192 author == user
192 193 end
193 194 end
194 195
195 196 def image?
196 197 !!(self.filename =~ /\.(bmp|gif|jpg|jpe|jpeg|png)$/i)
197 198 end
198 199
199 200 def thumbnailable?
200 201 image?
201 202 end
202 203
203 204 # Returns the full path the attachment thumbnail, or nil
204 205 # if the thumbnail cannot be generated.
205 206 def thumbnail(options={})
206 207 if thumbnailable? && readable?
207 208 size = options[:size].to_i
208 209 if size > 0
209 210 # Limit the number of thumbnails per image
210 211 size = (size / 50) * 50
211 212 # Maximum thumbnail size
212 213 size = 800 if size > 800
213 214 else
214 215 size = Setting.thumbnails_size.to_i
215 216 end
216 217 size = 100 unless size > 0
217 218 target = File.join(self.class.thumbnails_storage_path, "#{id}_#{digest}_#{size}.thumb")
218 219
219 220 begin
220 221 Redmine::Thumbnail.generate(self.diskfile, target, size)
221 222 rescue => e
222 223 logger.error "An error occured while generating thumbnail for #{disk_filename} to #{target}\nException was: #{e.message}" if logger
223 224 return nil
224 225 end
225 226 end
226 227 end
227 228
228 229 # Deletes all thumbnails
229 230 def self.clear_thumbnails
230 231 Dir.glob(File.join(thumbnails_storage_path, "*.thumb")).each do |file|
231 232 File.delete file
232 233 end
233 234 end
234 235
235 236 def is_text?
236 237 Redmine::MimeType.is_type?('text', filename)
237 238 end
238 239
239 240 def is_diff?
240 241 self.filename =~ /\.(patch|diff)$/i
241 242 end
242 243
243 244 # Returns true if the file is readable
244 245 def readable?
245 246 File.readable?(diskfile)
246 247 end
247 248
248 249 # Returns the attachment token
249 250 def token
250 251 "#{id}.#{digest}"
251 252 end
252 253
253 254 # Finds an attachment that matches the given token and that has no container
254 255 def self.find_by_token(token)
255 256 if token.to_s =~ /^(\d+)\.([0-9a-f]+)$/
256 257 attachment_id, attachment_digest = $1, $2
257 258 attachment = Attachment.where(:id => attachment_id, :digest => attachment_digest).first
258 259 if attachment && attachment.container.nil?
259 260 attachment
260 261 end
261 262 end
262 263 end
263 264
264 265 # Bulk attaches a set of files to an object
265 266 #
266 267 # Returns a Hash of the results:
267 268 # :files => array of the attached files
268 269 # :unsaved => array of the files that could not be attached
269 270 def self.attach_files(obj, attachments)
270 271 result = obj.save_attachments(attachments, User.current)
271 272 obj.attach_saved_attachments
272 273 result
273 274 end
274 275
275 276 # Updates the filename and description of a set of attachments
276 277 # with the given hash of attributes. Returns true if all
277 278 # attachments were updated.
278 279 #
279 280 # Example:
280 281 # Attachment.update_attachments(attachments, {
281 282 # 4 => {:filename => 'foo'},
282 283 # 7 => {:filename => 'bar', :description => 'file description'}
283 284 # })
284 285 #
285 286 def self.update_attachments(attachments, params)
286 287 params = params.transform_keys {|key| key.to_i}
287 288
288 289 saved = true
289 290 transaction do
290 291 attachments.each do |attachment|
291 292 if p = params[attachment.id]
292 293 attachment.filename = p[:filename] if p.key?(:filename)
293 294 attachment.description = p[:description] if p.key?(:description)
294 295 saved &&= attachment.save
295 296 end
296 297 end
297 298 unless saved
298 299 raise ActiveRecord::Rollback
299 300 end
300 301 end
301 302 saved
302 303 end
303 304
304 305 def self.latest_attach(attachments, filename)
305 306 attachments.sort_by(&:created_on).reverse.detect do |att|
306 307 filename.casecmp(att.filename) == 0
307 308 end
308 309 end
309 310
310 311 def self.prune(age=1.day)
311 312 Attachment.where("created_on < ? AND (container_type IS NULL OR container_type = '')", Time.now - age).destroy_all
312 313 end
313 314
314 315 # Moves an existing attachment to its target directory
315 316 def move_to_target_directory!
316 317 return unless !new_record? & readable?
317 318
318 319 src = diskfile
319 320 self.disk_directory = target_directory
320 321 dest = diskfile
321 322
322 323 return if src == dest
323 324
324 325 if !FileUtils.mkdir_p(File.dirname(dest))
325 326 logger.error "Could not create directory #{File.dirname(dest)}" if logger
326 327 return
327 328 end
328 329
329 330 if !FileUtils.mv(src, dest)
330 331 logger.error "Could not move attachment from #{src} to #{dest}" if logger
331 332 return
332 333 end
333 334
334 335 update_column :disk_directory, disk_directory
335 336 end
336 337
337 338 # Moves existing attachments that are stored at the root of the files
338 339 # directory (ie. created before Redmine 2.3) to their target subdirectories
339 340 def self.move_from_root_to_target_directory
340 341 Attachment.where("disk_directory IS NULL OR disk_directory = ''").find_each do |attachment|
341 342 attachment.move_to_target_directory!
342 343 end
343 344 end
344 345
345 346 # Returns true if the extension is allowed, otherwise false
346 347 def self.valid_extension?(extension)
347 348 extension = extension.downcase.sub(/\A\.+/, '')
348 349
349 350 denied, allowed = [:attachment_extensions_denied, :attachment_extensions_allowed].map do |setting|
350 351 Setting.send(setting).to_s.split(",").map {|s| s.strip.downcase.sub(/\A\.+/, '')}.reject(&:blank?)
351 352 end
352 353 if denied.present? && denied.include?(extension)
353 354 return false
354 355 end
355 356 unless allowed.blank? || allowed.include?(extension)
356 357 return false
357 358 end
358 359 true
359 360 end
360 361
361 362 private
362 363
363 364 # Physically deletes the file from the file system
364 365 def delete_from_disk!
365 366 if disk_filename.present? && File.exist?(diskfile)
366 367 File.delete(diskfile)
367 368 end
368 369 end
369 370
370 371 def sanitize_filename(value)
371 372 # get only the filename, not the whole path
372 373 just_filename = value.gsub(/\A.*(\\|\/)/m, '')
373 374
374 375 # Finally, replace invalid characters with underscore
375 376 just_filename.gsub(/[\/\?\%\*\:\|\"\'<>\n\r]+/, '_')
376 377 end
377 378
378 379 # Returns the subdirectory in which the attachment will be saved
379 380 def target_directory
380 381 time = created_on || DateTime.now
381 382 time.strftime("%Y/%m")
382 383 end
383 384
384 385 # Returns an ASCII or hashed filename that do not
385 386 # exists yet in the given subdirectory
386 387 def self.disk_filename(filename, directory=nil)
387 388 timestamp = DateTime.now.strftime("%y%m%d%H%M%S")
388 389 ascii = ''
389 390 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
390 391 ascii = filename
391 392 else
392 393 ascii = Digest::MD5.hexdigest(filename)
393 394 # keep the extension if any
394 395 ascii << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
395 396 end
396 397 while File.exist?(File.join(storage_path, directory.to_s, "#{timestamp}_#{ascii}"))
397 398 timestamp.succ!
398 399 end
399 400 "#{timestamp}_#{ascii}"
400 401 end
401 402 end
@@ -1,421 +1,421
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2015 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 require File.expand_path('../../test_helper', __FILE__)
21 21
22 22 class AttachmentTest < ActiveSupport::TestCase
23 fixtures :users, :projects, :roles, :members, :member_roles,
23 fixtures :users, :email_addresses, :projects, :roles, :members, :member_roles,
24 24 :enabled_modules, :issues, :trackers, :attachments
25 25
26 26 # TODO: remove this with Rails 5 that supports after_commit callbacks
27 27 # in transactional fixtures (https://github.com/rails/rails/pull/18458)
28 28 self.use_transactional_fixtures = false
29 29
30 30 class MockFile
31 31 attr_reader :original_filename, :content_type, :content, :size
32 32
33 33 def initialize(attributes)
34 34 @original_filename = attributes[:original_filename]
35 35 @content_type = attributes[:content_type]
36 36 @content = attributes[:content] || "Content"
37 37 @size = content.size
38 38 end
39 39 end
40 40
41 41 def setup
42 42 set_tmp_attachments_directory
43 43 end
44 44
45 45 def test_container_for_new_attachment_should_be_nil
46 46 assert_nil Attachment.new.container
47 47 end
48 48
49 49 def test_filename_should_remove_eols
50 50 assert_equal "line_feed", Attachment.new(:filename => "line\nfeed").filename
51 51 assert_equal "line_feed", Attachment.new(:filename => "some\npath/line\nfeed").filename
52 52 assert_equal "carriage_return", Attachment.new(:filename => "carriage\rreturn").filename
53 53 assert_equal "carriage_return", Attachment.new(:filename => "some\rpath/carriage\rreturn").filename
54 54 end
55 55
56 56 def test_create
57 57 a = Attachment.new(:container => Issue.find(1),
58 58 :file => uploaded_test_file("testfile.txt", "text/plain"),
59 59 :author => User.find(1))
60 60 assert a.save
61 61 assert_equal 'testfile.txt', a.filename
62 62 assert_equal 59, a.filesize
63 63 assert_equal 'text/plain', a.content_type
64 64 assert_equal 0, a.downloads
65 65 assert_equal '1478adae0d4eb06d35897518540e25d6', a.digest
66 66
67 67 assert a.disk_directory
68 68 assert_match %r{\A\d{4}/\d{2}\z}, a.disk_directory
69 69
70 70 assert File.exist?(a.diskfile)
71 71 assert_equal 59, File.size(a.diskfile)
72 72 end
73 73
74 74 def test_create_should_clear_content_type_if_too_long
75 75 a = Attachment.new(:container => Issue.find(1),
76 76 :file => uploaded_test_file("testfile.txt", "text/plain"),
77 77 :author => User.find(1),
78 78 :content_type => 'a'*300)
79 79 assert a.save
80 80 a.reload
81 81 assert_nil a.content_type
82 82 end
83 83
84 84 def test_copy_should_preserve_attributes
85 85 a = Attachment.find(1)
86 86 copy = a.copy
87 87
88 88 assert_save copy
89 89 copy = Attachment.order('id DESC').first
90 90 %w(filename filesize content_type author_id created_on description digest disk_filename disk_directory diskfile).each do |attribute|
91 91 assert_equal a.send(attribute), copy.send(attribute), "#{attribute} was different"
92 92 end
93 93 end
94 94
95 95 def test_size_should_be_validated_for_new_file
96 96 with_settings :attachment_max_size => 0 do
97 97 a = Attachment.new(:container => Issue.find(1),
98 98 :file => uploaded_test_file("testfile.txt", "text/plain"),
99 99 :author => User.find(1))
100 100 assert !a.save
101 101 end
102 102 end
103 103
104 104 def test_size_should_not_be_validated_when_copying
105 105 a = Attachment.create!(:container => Issue.find(1),
106 106 :file => uploaded_test_file("testfile.txt", "text/plain"),
107 107 :author => User.find(1))
108 108 with_settings :attachment_max_size => 0 do
109 109 copy = a.copy
110 110 assert copy.save
111 111 end
112 112 end
113 113
114 114 def test_filesize_greater_than_2gb_should_be_supported
115 115 with_settings :attachment_max_size => (50.gigabyte / 1024) do
116 116 a = Attachment.create!(:container => Issue.find(1),
117 117 :file => uploaded_test_file("testfile.txt", "text/plain"),
118 118 :author => User.find(1))
119 119 a.filesize = 20.gigabyte
120 120 a.save!
121 121 assert_equal 20.gigabyte, a.reload.filesize
122 122 end
123 123 end
124 124
125 125 def test_extension_should_be_validated_against_allowed_extensions
126 126 with_settings :attachment_extensions_allowed => "txt, png" do
127 127 a = Attachment.new(:container => Issue.find(1),
128 128 :file => mock_file_with_options(:original_filename => "test.png"),
129 129 :author => User.find(1))
130 130 assert_save a
131 131
132 132 a = Attachment.new(:container => Issue.find(1),
133 133 :file => mock_file_with_options(:original_filename => "test.jpeg"),
134 134 :author => User.find(1))
135 135 assert !a.save
136 136 end
137 137 end
138 138
139 139 def test_extension_should_be_validated_against_denied_extensions
140 140 with_settings :attachment_extensions_denied => "txt, png" do
141 141 a = Attachment.new(:container => Issue.find(1),
142 142 :file => mock_file_with_options(:original_filename => "test.jpeg"),
143 143 :author => User.find(1))
144 144 assert_save a
145 145
146 146 a = Attachment.new(:container => Issue.find(1),
147 147 :file => mock_file_with_options(:original_filename => "test.png"),
148 148 :author => User.find(1))
149 149 assert !a.save
150 150 end
151 151 end
152 152
153 153 def test_valid_extension_should_be_case_insensitive
154 154 with_settings :attachment_extensions_allowed => "txt, Png" do
155 155 assert Attachment.valid_extension?(".pnG")
156 156 assert !Attachment.valid_extension?(".jpeg")
157 157 end
158 158 with_settings :attachment_extensions_denied => "txt, Png" do
159 159 assert !Attachment.valid_extension?(".pnG")
160 160 assert Attachment.valid_extension?(".jpeg")
161 161 end
162 162 end
163 163
164 164 def test_description_length_should_be_validated
165 165 a = Attachment.new(:description => 'a' * 300)
166 166 assert !a.save
167 167 assert_not_equal [], a.errors[:description]
168 168 end
169 169
170 170 def test_destroy
171 171 a = Attachment.new(:container => Issue.find(1),
172 172 :file => uploaded_test_file("testfile.txt", "text/plain"),
173 173 :author => User.find(1))
174 174 assert a.save
175 175 assert_equal 'testfile.txt', a.filename
176 176 assert_equal 59, a.filesize
177 177 assert_equal 'text/plain', a.content_type
178 178 assert_equal 0, a.downloads
179 179 assert_equal '1478adae0d4eb06d35897518540e25d6', a.digest
180 180 diskfile = a.diskfile
181 181 assert File.exist?(diskfile)
182 182 assert_equal 59, File.size(a.diskfile)
183 183 assert a.destroy
184 184 assert !File.exist?(diskfile)
185 185 end
186 186
187 187 def test_destroy_should_not_delete_file_referenced_by_other_attachment
188 188 a = Attachment.create!(:container => Issue.find(1),
189 189 :file => uploaded_test_file("testfile.txt", "text/plain"),
190 190 :author => User.find(1))
191 191 diskfile = a.diskfile
192 192
193 193 copy = a.copy
194 194 copy.save!
195 195
196 196 assert File.exists?(diskfile)
197 197 a.destroy
198 198 assert File.exists?(diskfile)
199 199 copy.destroy
200 200 assert !File.exists?(diskfile)
201 201 end
202 202
203 203 def test_create_should_auto_assign_content_type
204 204 a = Attachment.new(:container => Issue.find(1),
205 205 :file => uploaded_test_file("testfile.txt", ""),
206 206 :author => User.find(1))
207 207 assert a.save
208 208 assert_equal 'text/plain', a.content_type
209 209 end
210 210
211 211 def test_identical_attachments_at_the_same_time_should_not_overwrite
212 212 a1 = Attachment.create!(:container => Issue.find(1),
213 213 :file => uploaded_test_file("testfile.txt", ""),
214 214 :author => User.find(1))
215 215 a2 = Attachment.create!(:container => Issue.find(1),
216 216 :file => uploaded_test_file("testfile.txt", ""),
217 217 :author => User.find(1))
218 218 assert a1.disk_filename != a2.disk_filename
219 219 end
220 220
221 221 def test_filename_should_be_basenamed
222 222 a = Attachment.new(:file => MockFile.new(:original_filename => "path/to/the/file"))
223 223 assert_equal 'file', a.filename
224 224 end
225 225
226 226 def test_filename_should_be_sanitized
227 227 a = Attachment.new(:file => MockFile.new(:original_filename => "valid:[] invalid:?%*|\"'<>chars"))
228 228 assert_equal 'valid_[] invalid_chars', a.filename
229 229 end
230 230
231 231 def test_diskfilename
232 232 assert Attachment.disk_filename("test_file.txt") =~ /^\d{12}_test_file.txt$/
233 233 assert_equal 'test_file.txt', Attachment.disk_filename("test_file.txt")[13..-1]
234 234 assert_equal '770c509475505f37c2b8fb6030434d6b.txt', Attachment.disk_filename("test_accentuΓ©.txt")[13..-1]
235 235 assert_equal 'f8139524ebb8f32e51976982cd20a85d', Attachment.disk_filename("test_accentuΓ©")[13..-1]
236 236 assert_equal 'cbb5b0f30978ba03731d61f9f6d10011', Attachment.disk_filename("test_accentuΓ©.Γ§a")[13..-1]
237 237 end
238 238
239 239 def test_title
240 240 a = Attachment.new(:filename => "test.png")
241 241 assert_equal "test.png", a.title
242 242
243 243 a = Attachment.new(:filename => "test.png", :description => "Cool image")
244 244 assert_equal "test.png (Cool image)", a.title
245 245 end
246 246
247 247 def test_new_attachment_should_be_editable_by_author
248 248 user = User.find(1)
249 249 a = Attachment.new(:author => user)
250 250 assert_equal true, a.editable?(user)
251 251 end
252 252
253 253 def test_prune_should_destroy_old_unattached_attachments
254 254 Attachment.create!(:file => uploaded_test_file("testfile.txt", ""), :author_id => 1, :created_on => 2.days.ago)
255 255 Attachment.create!(:file => uploaded_test_file("testfile.txt", ""), :author_id => 1, :created_on => 2.days.ago)
256 256 Attachment.create!(:file => uploaded_test_file("testfile.txt", ""), :author_id => 1)
257 257
258 258 assert_difference 'Attachment.count', -2 do
259 259 Attachment.prune
260 260 end
261 261 end
262 262
263 263 def test_move_from_root_to_target_directory_should_move_root_files
264 264 a = Attachment.find(20)
265 265 assert a.disk_directory.blank?
266 266 # Create a real file for this fixture
267 267 File.open(a.diskfile, "w") do |f|
268 268 f.write "test file at the root of files directory"
269 269 end
270 270 assert a.readable?
271 271 Attachment.move_from_root_to_target_directory
272 272
273 273 a.reload
274 274 assert_equal '2012/05', a.disk_directory
275 275 assert a.readable?
276 276 end
277 277
278 278 test "Attachmnet.attach_files should attach the file" do
279 279 issue = Issue.first
280 280 assert_difference 'Attachment.count' do
281 281 Attachment.attach_files(issue,
282 282 '1' => {
283 283 'file' => uploaded_test_file('testfile.txt', 'text/plain'),
284 284 'description' => 'test'
285 285 })
286 286 end
287 287 attachment = Attachment.order('id DESC').first
288 288 assert_equal issue, attachment.container
289 289 assert_equal 'testfile.txt', attachment.filename
290 290 assert_equal 59, attachment.filesize
291 291 assert_equal 'test', attachment.description
292 292 assert_equal 'text/plain', attachment.content_type
293 293 assert File.exists?(attachment.diskfile)
294 294 assert_equal 59, File.size(attachment.diskfile)
295 295 end
296 296
297 297 test "Attachmnet.attach_files should add unsaved files to the object as unsaved attachments" do
298 298 # Max size of 0 to force Attachment creation failures
299 299 with_settings(:attachment_max_size => 0) do
300 300 @project = Project.find(1)
301 301 response = Attachment.attach_files(@project, {
302 302 '1' => {'file' => mock_file, 'description' => 'test'},
303 303 '2' => {'file' => mock_file, 'description' => 'test'}
304 304 })
305 305
306 306 assert response[:unsaved].present?
307 307 assert_equal 2, response[:unsaved].length
308 308 assert response[:unsaved].first.new_record?
309 309 assert response[:unsaved].second.new_record?
310 310 assert_equal response[:unsaved], @project.unsaved_attachments
311 311 end
312 312 end
313 313
314 314 test "Attachment.attach_files should preserve the content_type of attachments added by token" do
315 315 @project = Project.find(1)
316 316 attachment = Attachment.create!(:file => uploaded_test_file("testfile.txt", ""), :author_id => 1, :created_on => 2.days.ago)
317 317 assert_equal 'text/plain', attachment.content_type
318 318 Attachment.attach_files(@project, { '1' => {'token' => attachment.token } })
319 319 attachment.reload
320 320 assert_equal 'text/plain', attachment.content_type
321 321 end
322 322
323 323 def test_update_attachments
324 324 attachments = Attachment.where(:id => [2, 3]).to_a
325 325
326 326 assert Attachment.update_attachments(attachments, {
327 327 '2' => {:filename => 'newname.txt', :description => 'New description'},
328 328 3 => {:filename => 'othername.txt'}
329 329 })
330 330
331 331 attachment = Attachment.find(2)
332 332 assert_equal 'newname.txt', attachment.filename
333 333 assert_equal 'New description', attachment.description
334 334
335 335 attachment = Attachment.find(3)
336 336 assert_equal 'othername.txt', attachment.filename
337 337 end
338 338
339 339 def test_update_attachments_with_failure
340 340 attachments = Attachment.where(:id => [2, 3]).to_a
341 341
342 342 assert !Attachment.update_attachments(attachments, {
343 343 '2' => {:filename => '', :description => 'New description'},
344 344 3 => {:filename => 'othername.txt'}
345 345 })
346 346
347 347 attachment = Attachment.find(3)
348 348 assert_equal 'logo.gif', attachment.filename
349 349 end
350 350
351 351 def test_update_attachments_should_sanitize_filename
352 352 attachments = Attachment.where(:id => 2).to_a
353 353
354 354 assert Attachment.update_attachments(attachments, {
355 355 2 => {:filename => 'newname?.txt'},
356 356 })
357 357
358 358 attachment = Attachment.find(2)
359 359 assert_equal 'newname_.txt', attachment.filename
360 360 end
361 361
362 362 def test_latest_attach
363 363 set_fixtures_attachments_directory
364 364 a1 = Attachment.find(16)
365 365 assert_equal "testfile.png", a1.filename
366 366 assert a1.readable?
367 367 assert (! a1.visible?(User.anonymous))
368 368 assert a1.visible?(User.find(2))
369 369 a2 = Attachment.find(17)
370 370 assert_equal "testfile.PNG", a2.filename
371 371 assert a2.readable?
372 372 assert (! a2.visible?(User.anonymous))
373 373 assert a2.visible?(User.find(2))
374 374 assert a1.created_on < a2.created_on
375 375
376 376 la1 = Attachment.latest_attach([a1, a2], "testfile.png")
377 377 assert_equal 17, la1.id
378 378 la2 = Attachment.latest_attach([a1, a2], "Testfile.PNG")
379 379 assert_equal 17, la2.id
380 380
381 381 set_tmp_attachments_directory
382 382 end
383 383
384 384 def test_latest_attach_should_not_error_with_string_with_invalid_encoding
385 385 string = "width:50\xFE-Image.jpg".force_encoding('UTF-8')
386 386 assert_equal false, string.valid_encoding?
387 387
388 388 Attachment.latest_attach(Attachment.limit(2).to_a, string)
389 389 end
390 390
391 391 def test_thumbnailable_should_be_true_for_images
392 392 assert_equal true, Attachment.new(:filename => 'test.jpg').thumbnailable?
393 393 end
394 394
395 395 def test_thumbnailable_should_be_true_for_non_images
396 396 assert_equal false, Attachment.new(:filename => 'test.txt').thumbnailable?
397 397 end
398 398
399 399 if convert_installed?
400 400 def test_thumbnail_should_generate_the_thumbnail
401 401 set_fixtures_attachments_directory
402 402 attachment = Attachment.find(16)
403 403 Attachment.clear_thumbnails
404 404
405 405 assert_difference "Dir.glob(File.join(Attachment.thumbnails_storage_path, '*.thumb')).size" do
406 406 thumbnail = attachment.thumbnail
407 407 assert_equal "16_8e0294de2441577c529f170b6fb8f638_100.thumb", File.basename(thumbnail)
408 408 assert File.exists?(thumbnail)
409 409 end
410 410 end
411 411
412 412 def test_thumbnail_should_return_nil_if_generation_fails
413 413 Redmine::Thumbnail.expects(:generate).raises(SystemCallError, 'Something went wrong')
414 414 set_fixtures_attachments_directory
415 415 attachment = Attachment.find(16)
416 416 assert_nil attachment.thumbnail
417 417 end
418 418 else
419 419 puts '(ImageMagick convert not available)'
420 420 end
421 421 end
General Comments 0
You need to be logged in to leave comments. Login now