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