##// END OF EJS Templates
Attachment content type not set when uploading attachment (#18667)....
Jean-Philippe Lang -
r13405:64763bece35c
parent child
Show More
@@ -1,375 +1,376
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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
29 validate :validate_max_file_size
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_save :files_to_final_location
55 before_create :files_to_final_location
56 after_destroy :delete_from_disk
56 after_destroy :delete_from_disk
57
57
58 # Returns an unsaved copy of the attachment
58 # Returns an unsaved copy of the attachment
59 def copy(attributes=nil)
59 def copy(attributes=nil)
60 copy = self.class.new
60 copy = self.class.new
61 copy.attributes = self.attributes.dup.except("id", "downloads")
61 copy.attributes = self.attributes.dup.except("id", "downloads")
62 copy.attributes = attributes if attributes
62 copy.attributes = attributes if attributes
63 copy
63 copy
64 end
64 end
65
65
66 def validate_max_file_size
66 def validate_max_file_size
67 if @temp_file && self.filesize > Setting.attachment_max_size.to_i.kilobytes
67 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))
68 errors.add(:base, l(:error_attachment_too_big, :max_size => Setting.attachment_max_size.to_i.kilobytes))
69 end
69 end
70 end
70 end
71
71
72 def file=(incoming_file)
72 def file=(incoming_file)
73 unless incoming_file.nil?
73 unless incoming_file.nil?
74 @temp_file = incoming_file
74 @temp_file = incoming_file
75 if @temp_file.size > 0
75 if @temp_file.size > 0
76 if @temp_file.respond_to?(:original_filename)
76 if @temp_file.respond_to?(:original_filename)
77 self.filename = @temp_file.original_filename
77 self.filename = @temp_file.original_filename
78 self.filename.force_encoding("UTF-8")
78 self.filename.force_encoding("UTF-8")
79 end
79 end
80 if @temp_file.respond_to?(:content_type)
80 if @temp_file.respond_to?(:content_type)
81 self.content_type = @temp_file.content_type.to_s.chomp
81 self.content_type = @temp_file.content_type.to_s.chomp
82 end
82 end
83 if content_type.blank? && filename.present?
84 self.content_type = Redmine::MimeType.of(filename)
85 end
86 self.filesize = @temp_file.size
83 self.filesize = @temp_file.size
87 end
84 end
88 end
85 end
89 end
86 end
90
87
91 def file
88 def file
92 nil
89 nil
93 end
90 end
94
91
95 def filename=(arg)
92 def filename=(arg)
96 write_attribute :filename, sanitize_filename(arg.to_s)
93 write_attribute :filename, sanitize_filename(arg.to_s)
97 filename
94 filename
98 end
95 end
99
96
100 # Copies the temporary file to its final location
97 # Copies the temporary file to its final location
101 # and computes its MD5 hash
98 # and computes its MD5 hash
102 def files_to_final_location
99 def files_to_final_location
103 if @temp_file && (@temp_file.size > 0)
100 if @temp_file && (@temp_file.size > 0)
104 self.disk_directory = target_directory
101 self.disk_directory = target_directory
105 self.disk_filename = Attachment.disk_filename(filename, disk_directory)
102 self.disk_filename = Attachment.disk_filename(filename, disk_directory)
106 logger.info("Saving attachment '#{self.diskfile}' (#{@temp_file.size} bytes)") if logger
103 logger.info("Saving attachment '#{self.diskfile}' (#{@temp_file.size} bytes)") if logger
107 path = File.dirname(diskfile)
104 path = File.dirname(diskfile)
108 unless File.directory?(path)
105 unless File.directory?(path)
109 FileUtils.mkdir_p(path)
106 FileUtils.mkdir_p(path)
110 end
107 end
111 md5 = Digest::MD5.new
108 md5 = Digest::MD5.new
112 File.open(diskfile, "wb") do |f|
109 File.open(diskfile, "wb") do |f|
113 if @temp_file.respond_to?(:read)
110 if @temp_file.respond_to?(:read)
114 buffer = ""
111 buffer = ""
115 while (buffer = @temp_file.read(8192))
112 while (buffer = @temp_file.read(8192))
116 f.write(buffer)
113 f.write(buffer)
117 md5.update(buffer)
114 md5.update(buffer)
118 end
115 end
119 else
116 else
120 f.write(@temp_file)
117 f.write(@temp_file)
121 md5.update(@temp_file)
118 md5.update(@temp_file)
122 end
119 end
123 end
120 end
124 self.digest = md5.hexdigest
121 self.digest = md5.hexdigest
125 end
122 end
126 @temp_file = nil
123 @temp_file = nil
124
125 if content_type.blank? && filename.present?
126 self.content_type = Redmine::MimeType.of(filename)
127 end
127 # Don't save the content type if it's longer than the authorized length
128 # Don't save the content type if it's longer than the authorized length
128 if self.content_type && self.content_type.length > 255
129 if self.content_type && self.content_type.length > 255
129 self.content_type = nil
130 self.content_type = nil
130 end
131 end
131 end
132 end
132
133
133 # Deletes the file from the file system if it's not referenced by other attachments
134 # Deletes the file from the file system if it's not referenced by other attachments
134 def delete_from_disk
135 def delete_from_disk
135 if Attachment.where("disk_filename = ? AND id <> ?", disk_filename, id).empty?
136 if Attachment.where("disk_filename = ? AND id <> ?", disk_filename, id).empty?
136 delete_from_disk!
137 delete_from_disk!
137 end
138 end
138 end
139 end
139
140
140 # Returns file's location on disk
141 # Returns file's location on disk
141 def diskfile
142 def diskfile
142 File.join(self.class.storage_path, disk_directory.to_s, disk_filename.to_s)
143 File.join(self.class.storage_path, disk_directory.to_s, disk_filename.to_s)
143 end
144 end
144
145
145 def title
146 def title
146 title = filename.to_s
147 title = filename.to_s
147 if description.present?
148 if description.present?
148 title << " (#{description})"
149 title << " (#{description})"
149 end
150 end
150 title
151 title
151 end
152 end
152
153
153 def increment_download
154 def increment_download
154 increment!(:downloads)
155 increment!(:downloads)
155 end
156 end
156
157
157 def project
158 def project
158 container.try(:project)
159 container.try(:project)
159 end
160 end
160
161
161 def visible?(user=User.current)
162 def visible?(user=User.current)
162 if container_id
163 if container_id
163 container && container.attachments_visible?(user)
164 container && container.attachments_visible?(user)
164 else
165 else
165 author == user
166 author == user
166 end
167 end
167 end
168 end
168
169
169 def editable?(user=User.current)
170 def editable?(user=User.current)
170 if container_id
171 if container_id
171 container && container.attachments_editable?(user)
172 container && container.attachments_editable?(user)
172 else
173 else
173 author == user
174 author == user
174 end
175 end
175 end
176 end
176
177
177 def deletable?(user=User.current)
178 def deletable?(user=User.current)
178 if container_id
179 if container_id
179 container && container.attachments_deletable?(user)
180 container && container.attachments_deletable?(user)
180 else
181 else
181 author == user
182 author == user
182 end
183 end
183 end
184 end
184
185
185 def image?
186 def image?
186 !!(self.filename =~ /\.(bmp|gif|jpg|jpe|jpeg|png)$/i)
187 !!(self.filename =~ /\.(bmp|gif|jpg|jpe|jpeg|png)$/i)
187 end
188 end
188
189
189 def thumbnailable?
190 def thumbnailable?
190 image?
191 image?
191 end
192 end
192
193
193 # Returns the full path the attachment thumbnail, or nil
194 # Returns the full path the attachment thumbnail, or nil
194 # if the thumbnail cannot be generated.
195 # if the thumbnail cannot be generated.
195 def thumbnail(options={})
196 def thumbnail(options={})
196 if thumbnailable? && readable?
197 if thumbnailable? && readable?
197 size = options[:size].to_i
198 size = options[:size].to_i
198 if size > 0
199 if size > 0
199 # Limit the number of thumbnails per image
200 # Limit the number of thumbnails per image
200 size = (size / 50) * 50
201 size = (size / 50) * 50
201 # Maximum thumbnail size
202 # Maximum thumbnail size
202 size = 800 if size > 800
203 size = 800 if size > 800
203 else
204 else
204 size = Setting.thumbnails_size.to_i
205 size = Setting.thumbnails_size.to_i
205 end
206 end
206 size = 100 unless size > 0
207 size = 100 unless size > 0
207 target = File.join(self.class.thumbnails_storage_path, "#{id}_#{digest}_#{size}.thumb")
208 target = File.join(self.class.thumbnails_storage_path, "#{id}_#{digest}_#{size}.thumb")
208
209
209 begin
210 begin
210 Redmine::Thumbnail.generate(self.diskfile, target, size)
211 Redmine::Thumbnail.generate(self.diskfile, target, size)
211 rescue => e
212 rescue => e
212 logger.error "An error occured while generating thumbnail for #{disk_filename} to #{target}\nException was: #{e.message}" if logger
213 logger.error "An error occured while generating thumbnail for #{disk_filename} to #{target}\nException was: #{e.message}" if logger
213 return nil
214 return nil
214 end
215 end
215 end
216 end
216 end
217 end
217
218
218 # Deletes all thumbnails
219 # Deletes all thumbnails
219 def self.clear_thumbnails
220 def self.clear_thumbnails
220 Dir.glob(File.join(thumbnails_storage_path, "*.thumb")).each do |file|
221 Dir.glob(File.join(thumbnails_storage_path, "*.thumb")).each do |file|
221 File.delete file
222 File.delete file
222 end
223 end
223 end
224 end
224
225
225 def is_text?
226 def is_text?
226 Redmine::MimeType.is_type?('text', filename)
227 Redmine::MimeType.is_type?('text', filename)
227 end
228 end
228
229
229 def is_diff?
230 def is_diff?
230 self.filename =~ /\.(patch|diff)$/i
231 self.filename =~ /\.(patch|diff)$/i
231 end
232 end
232
233
233 # Returns true if the file is readable
234 # Returns true if the file is readable
234 def readable?
235 def readable?
235 File.readable?(diskfile)
236 File.readable?(diskfile)
236 end
237 end
237
238
238 # Returns the attachment token
239 # Returns the attachment token
239 def token
240 def token
240 "#{id}.#{digest}"
241 "#{id}.#{digest}"
241 end
242 end
242
243
243 # Finds an attachment that matches the given token and that has no container
244 # Finds an attachment that matches the given token and that has no container
244 def self.find_by_token(token)
245 def self.find_by_token(token)
245 if token.to_s =~ /^(\d+)\.([0-9a-f]+)$/
246 if token.to_s =~ /^(\d+)\.([0-9a-f]+)$/
246 attachment_id, attachment_digest = $1, $2
247 attachment_id, attachment_digest = $1, $2
247 attachment = Attachment.where(:id => attachment_id, :digest => attachment_digest).first
248 attachment = Attachment.where(:id => attachment_id, :digest => attachment_digest).first
248 if attachment && attachment.container.nil?
249 if attachment && attachment.container.nil?
249 attachment
250 attachment
250 end
251 end
251 end
252 end
252 end
253 end
253
254
254 # Bulk attaches a set of files to an object
255 # Bulk attaches a set of files to an object
255 #
256 #
256 # Returns a Hash of the results:
257 # Returns a Hash of the results:
257 # :files => array of the attached files
258 # :files => array of the attached files
258 # :unsaved => array of the files that could not be attached
259 # :unsaved => array of the files that could not be attached
259 def self.attach_files(obj, attachments)
260 def self.attach_files(obj, attachments)
260 result = obj.save_attachments(attachments, User.current)
261 result = obj.save_attachments(attachments, User.current)
261 obj.attach_saved_attachments
262 obj.attach_saved_attachments
262 result
263 result
263 end
264 end
264
265
265 # Updates the filename and description of a set of attachments
266 # Updates the filename and description of a set of attachments
266 # with the given hash of attributes. Returns true if all
267 # with the given hash of attributes. Returns true if all
267 # attachments were updated.
268 # attachments were updated.
268 #
269 #
269 # Example:
270 # Example:
270 # Attachment.update_attachments(attachments, {
271 # Attachment.update_attachments(attachments, {
271 # 4 => {:filename => 'foo'},
272 # 4 => {:filename => 'foo'},
272 # 7 => {:filename => 'bar', :description => 'file description'}
273 # 7 => {:filename => 'bar', :description => 'file description'}
273 # })
274 # })
274 #
275 #
275 def self.update_attachments(attachments, params)
276 def self.update_attachments(attachments, params)
276 params = params.transform_keys {|key| key.to_i}
277 params = params.transform_keys {|key| key.to_i}
277
278
278 saved = true
279 saved = true
279 transaction do
280 transaction do
280 attachments.each do |attachment|
281 attachments.each do |attachment|
281 if p = params[attachment.id]
282 if p = params[attachment.id]
282 attachment.filename = p[:filename] if p.key?(:filename)
283 attachment.filename = p[:filename] if p.key?(:filename)
283 attachment.description = p[:description] if p.key?(:description)
284 attachment.description = p[:description] if p.key?(:description)
284 saved &&= attachment.save
285 saved &&= attachment.save
285 end
286 end
286 end
287 end
287 unless saved
288 unless saved
288 raise ActiveRecord::Rollback
289 raise ActiveRecord::Rollback
289 end
290 end
290 end
291 end
291 saved
292 saved
292 end
293 end
293
294
294 def self.latest_attach(attachments, filename)
295 def self.latest_attach(attachments, filename)
295 attachments.sort_by(&:created_on).reverse.detect do |att|
296 attachments.sort_by(&:created_on).reverse.detect do |att|
296 att.filename.downcase == filename.downcase
297 att.filename.downcase == filename.downcase
297 end
298 end
298 end
299 end
299
300
300 def self.prune(age=1.day)
301 def self.prune(age=1.day)
301 Attachment.where("created_on < ? AND (container_type IS NULL OR container_type = '')", Time.now - age).destroy_all
302 Attachment.where("created_on < ? AND (container_type IS NULL OR container_type = '')", Time.now - age).destroy_all
302 end
303 end
303
304
304 # Moves an existing attachment to its target directory
305 # Moves an existing attachment to its target directory
305 def move_to_target_directory!
306 def move_to_target_directory!
306 return unless !new_record? & readable?
307 return unless !new_record? & readable?
307
308
308 src = diskfile
309 src = diskfile
309 self.disk_directory = target_directory
310 self.disk_directory = target_directory
310 dest = diskfile
311 dest = diskfile
311
312
312 return if src == dest
313 return if src == dest
313
314
314 if !FileUtils.mkdir_p(File.dirname(dest))
315 if !FileUtils.mkdir_p(File.dirname(dest))
315 logger.error "Could not create directory #{File.dirname(dest)}" if logger
316 logger.error "Could not create directory #{File.dirname(dest)}" if logger
316 return
317 return
317 end
318 end
318
319
319 if !FileUtils.mv(src, dest)
320 if !FileUtils.mv(src, dest)
320 logger.error "Could not move attachment from #{src} to #{dest}" if logger
321 logger.error "Could not move attachment from #{src} to #{dest}" if logger
321 return
322 return
322 end
323 end
323
324
324 update_column :disk_directory, disk_directory
325 update_column :disk_directory, disk_directory
325 end
326 end
326
327
327 # Moves existing attachments that are stored at the root of the files
328 # Moves existing attachments that are stored at the root of the files
328 # directory (ie. created before Redmine 2.3) to their target subdirectories
329 # directory (ie. created before Redmine 2.3) to their target subdirectories
329 def self.move_from_root_to_target_directory
330 def self.move_from_root_to_target_directory
330 Attachment.where("disk_directory IS NULL OR disk_directory = ''").find_each do |attachment|
331 Attachment.where("disk_directory IS NULL OR disk_directory = ''").find_each do |attachment|
331 attachment.move_to_target_directory!
332 attachment.move_to_target_directory!
332 end
333 end
333 end
334 end
334
335
335 private
336 private
336
337
337 # Physically deletes the file from the file system
338 # Physically deletes the file from the file system
338 def delete_from_disk!
339 def delete_from_disk!
339 if disk_filename.present? && File.exist?(diskfile)
340 if disk_filename.present? && File.exist?(diskfile)
340 File.delete(diskfile)
341 File.delete(diskfile)
341 end
342 end
342 end
343 end
343
344
344 def sanitize_filename(value)
345 def sanitize_filename(value)
345 # get only the filename, not the whole path
346 # get only the filename, not the whole path
346 just_filename = value.gsub(/\A.*(\\|\/)/m, '')
347 just_filename = value.gsub(/\A.*(\\|\/)/m, '')
347
348
348 # Finally, replace invalid characters with underscore
349 # Finally, replace invalid characters with underscore
349 just_filename.gsub(/[\/\?\%\*\:\|\"\'<>\n\r]+/, '_')
350 just_filename.gsub(/[\/\?\%\*\:\|\"\'<>\n\r]+/, '_')
350 end
351 end
351
352
352 # Returns the subdirectory in which the attachment will be saved
353 # Returns the subdirectory in which the attachment will be saved
353 def target_directory
354 def target_directory
354 time = created_on || DateTime.now
355 time = created_on || DateTime.now
355 time.strftime("%Y/%m")
356 time.strftime("%Y/%m")
356 end
357 end
357
358
358 # Returns an ASCII or hashed filename that do not
359 # Returns an ASCII or hashed filename that do not
359 # exists yet in the given subdirectory
360 # exists yet in the given subdirectory
360 def self.disk_filename(filename, directory=nil)
361 def self.disk_filename(filename, directory=nil)
361 timestamp = DateTime.now.strftime("%y%m%d%H%M%S")
362 timestamp = DateTime.now.strftime("%y%m%d%H%M%S")
362 ascii = ''
363 ascii = ''
363 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
364 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
364 ascii = filename
365 ascii = filename
365 else
366 else
366 ascii = Digest::MD5.hexdigest(filename)
367 ascii = Digest::MD5.hexdigest(filename)
367 # keep the extension if any
368 # keep the extension if any
368 ascii << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
369 ascii << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
369 end
370 end
370 while File.exist?(File.join(storage_path, directory.to_s, "#{timestamp}_#{ascii}"))
371 while File.exist?(File.join(storage_path, directory.to_s, "#{timestamp}_#{ascii}"))
371 timestamp.succ!
372 timestamp.succ!
372 end
373 end
373 "#{timestamp}_#{ascii}"
374 "#{timestamp}_#{ascii}"
374 end
375 end
375 end
376 end
@@ -1,132 +1,142
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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 File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class AttachmentsTest < Redmine::IntegrationTest
20 class AttachmentsTest < Redmine::IntegrationTest
21 fixtures :projects, :enabled_modules,
21 fixtures :projects, :enabled_modules,
22 :users, :roles, :members, :member_roles,
22 :users, :roles, :members, :member_roles,
23 :trackers, :projects_trackers,
23 :trackers, :projects_trackers,
24 :issue_statuses, :enumerations
24 :issue_statuses, :enumerations
25
25
26 def test_upload_should_set_default_content_type
27 log_user('jsmith', 'jsmith')
28 assert_difference 'Attachment.count' do
29 post "/uploads.js?attachment_id=1&filename=foo.txt", "File content", {"CONTENT_TYPE" => 'application/octet-stream'}
30 assert_response :success
31 end
32 attachment = Attachment.order(:id => :desc).first
33 assert_equal 'text/plain', attachment.content_type
34 end
35
26 def test_upload_as_js_and_attach_to_an_issue
36 def test_upload_as_js_and_attach_to_an_issue
27 log_user('jsmith', 'jsmith')
37 log_user('jsmith', 'jsmith')
28
38
29 token = ajax_upload('myupload.txt', 'File content')
39 token = ajax_upload('myupload.txt', 'File content')
30
40
31 assert_difference 'Issue.count' do
41 assert_difference 'Issue.count' do
32 post '/projects/ecookbook/issues', {
42 post '/projects/ecookbook/issues', {
33 :issue => {:tracker_id => 1, :subject => 'Issue with upload'},
43 :issue => {:tracker_id => 1, :subject => 'Issue with upload'},
34 :attachments => {'1' => {:filename => 'myupload.txt', :description => 'My uploaded file', :token => token}}
44 :attachments => {'1' => {:filename => 'myupload.txt', :description => 'My uploaded file', :token => token}}
35 }
45 }
36 assert_response 302
46 assert_response 302
37 end
47 end
38
48
39 issue = Issue.order('id DESC').first
49 issue = Issue.order('id DESC').first
40 assert_equal 'Issue with upload', issue.subject
50 assert_equal 'Issue with upload', issue.subject
41 assert_equal 1, issue.attachments.count
51 assert_equal 1, issue.attachments.count
42
52
43 attachment = issue.attachments.first
53 attachment = issue.attachments.first
44 assert_equal 'myupload.txt', attachment.filename
54 assert_equal 'myupload.txt', attachment.filename
45 assert_equal 'My uploaded file', attachment.description
55 assert_equal 'My uploaded file', attachment.description
46 assert_equal 'File content'.length, attachment.filesize
56 assert_equal 'File content'.length, attachment.filesize
47 end
57 end
48
58
49 def test_upload_as_js_and_preview_as_inline_attachment
59 def test_upload_as_js_and_preview_as_inline_attachment
50 log_user('jsmith', 'jsmith')
60 log_user('jsmith', 'jsmith')
51
61
52 token = ajax_upload('myupload.jpg', 'JPEG content')
62 token = ajax_upload('myupload.jpg', 'JPEG content')
53
63
54 post '/issues/preview/new/ecookbook', {
64 post '/issues/preview/new/ecookbook', {
55 :issue => {:tracker_id => 1, :description => 'Inline upload: !myupload.jpg!'},
65 :issue => {:tracker_id => 1, :description => 'Inline upload: !myupload.jpg!'},
56 :attachments => {'1' => {:filename => 'myupload.jpg', :description => 'My uploaded file', :token => token}}
66 :attachments => {'1' => {:filename => 'myupload.jpg', :description => 'My uploaded file', :token => token}}
57 }
67 }
58 assert_response :success
68 assert_response :success
59
69
60 attachment_path = response.body.match(%r{<img src="(/attachments/download/\d+/myupload.jpg)"})[1]
70 attachment_path = response.body.match(%r{<img src="(/attachments/download/\d+/myupload.jpg)"})[1]
61 assert_not_nil token, "No attachment path found in response:\n#{response.body}"
71 assert_not_nil token, "No attachment path found in response:\n#{response.body}"
62
72
63 get attachment_path
73 get attachment_path
64 assert_response :success
74 assert_response :success
65 assert_equal 'JPEG content', response.body
75 assert_equal 'JPEG content', response.body
66 end
76 end
67
77
68 def test_upload_and_resubmit_after_validation_failure
78 def test_upload_and_resubmit_after_validation_failure
69 log_user('jsmith', 'jsmith')
79 log_user('jsmith', 'jsmith')
70
80
71 token = ajax_upload('myupload.txt', 'File content')
81 token = ajax_upload('myupload.txt', 'File content')
72
82
73 assert_no_difference 'Issue.count' do
83 assert_no_difference 'Issue.count' do
74 post '/projects/ecookbook/issues', {
84 post '/projects/ecookbook/issues', {
75 :issue => {:tracker_id => 1, :subject => ''},
85 :issue => {:tracker_id => 1, :subject => ''},
76 :attachments => {'1' => {:filename => 'myupload.txt', :description => 'My uploaded file', :token => token}}
86 :attachments => {'1' => {:filename => 'myupload.txt', :description => 'My uploaded file', :token => token}}
77 }
87 }
78 assert_response :success
88 assert_response :success
79 end
89 end
80 assert_select 'input[type=hidden][name=?][value=?]', 'attachments[p0][token]', token
90 assert_select 'input[type=hidden][name=?][value=?]', 'attachments[p0][token]', token
81 assert_select 'input[name=?][value=?]', 'attachments[p0][filename]', 'myupload.txt'
91 assert_select 'input[name=?][value=?]', 'attachments[p0][filename]', 'myupload.txt'
82 assert_select 'input[name=?][value=?]', 'attachments[p0][description]', 'My uploaded file'
92 assert_select 'input[name=?][value=?]', 'attachments[p0][description]', 'My uploaded file'
83
93
84 assert_difference 'Issue.count' do
94 assert_difference 'Issue.count' do
85 post '/projects/ecookbook/issues', {
95 post '/projects/ecookbook/issues', {
86 :issue => {:tracker_id => 1, :subject => 'Issue with upload'},
96 :issue => {:tracker_id => 1, :subject => 'Issue with upload'},
87 :attachments => {'p0' => {:filename => 'myupload.txt', :description => 'My uploaded file', :token => token}}
97 :attachments => {'p0' => {:filename => 'myupload.txt', :description => 'My uploaded file', :token => token}}
88 }
98 }
89 assert_response 302
99 assert_response 302
90 end
100 end
91
101
92 issue = Issue.order('id DESC').first
102 issue = Issue.order('id DESC').first
93 assert_equal 'Issue with upload', issue.subject
103 assert_equal 'Issue with upload', issue.subject
94 assert_equal 1, issue.attachments.count
104 assert_equal 1, issue.attachments.count
95
105
96 attachment = issue.attachments.first
106 attachment = issue.attachments.first
97 assert_equal 'myupload.txt', attachment.filename
107 assert_equal 'myupload.txt', attachment.filename
98 assert_equal 'My uploaded file', attachment.description
108 assert_equal 'My uploaded file', attachment.description
99 assert_equal 'File content'.length, attachment.filesize
109 assert_equal 'File content'.length, attachment.filesize
100 end
110 end
101
111
102 def test_upload_as_js_and_destroy
112 def test_upload_as_js_and_destroy
103 log_user('jsmith', 'jsmith')
113 log_user('jsmith', 'jsmith')
104
114
105 token = ajax_upload('myupload.txt', 'File content')
115 token = ajax_upload('myupload.txt', 'File content')
106
116
107 attachment = Attachment.order('id DESC').first
117 attachment = Attachment.order('id DESC').first
108 attachment_path = "/attachments/#{attachment.id}.js?attachment_id=1"
118 attachment_path = "/attachments/#{attachment.id}.js?attachment_id=1"
109 assert_include "href: '#{attachment_path}'", response.body, "Path to attachment: #{attachment_path} not found in response:\n#{response.body}"
119 assert_include "href: '#{attachment_path}'", response.body, "Path to attachment: #{attachment_path} not found in response:\n#{response.body}"
110
120
111 assert_difference 'Attachment.count', -1 do
121 assert_difference 'Attachment.count', -1 do
112 delete attachment_path
122 delete attachment_path
113 assert_response :success
123 assert_response :success
114 end
124 end
115
125
116 assert_include "$('#attachments_1').remove();", response.body
126 assert_include "$('#attachments_1').remove();", response.body
117 end
127 end
118
128
119 private
129 private
120
130
121 def ajax_upload(filename, content, attachment_id=1)
131 def ajax_upload(filename, content, attachment_id=1)
122 assert_difference 'Attachment.count' do
132 assert_difference 'Attachment.count' do
123 post "/uploads.js?attachment_id=#{attachment_id}&filename=#{filename}", content, {"CONTENT_TYPE" => 'application/octet-stream'}
133 post "/uploads.js?attachment_id=#{attachment_id}&filename=#{filename}", content, {"CONTENT_TYPE" => 'application/octet-stream'}
124 assert_response :success
134 assert_response :success
125 assert_equal 'text/javascript', response.content_type
135 assert_equal 'text/javascript', response.content_type
126 end
136 end
127
137
128 token = response.body.match(/\.val\('(\d+\.[0-9a-f]+)'\)/)[1]
138 token = response.body.match(/\.val\('(\d+\.[0-9a-f]+)'\)/)[1]
129 assert_not_nil token, "No upload token found in response:\n#{response.body}"
139 assert_not_nil token, "No upload token found in response:\n#{response.body}"
130 token
140 token
131 end
141 end
132 end
142 end
General Comments 0
You need to be logged in to leave comments. Login now