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