##// END OF EJS Templates
Merged r13787 (#18667)....
Jean-Philippe Lang -
r13421:7ca5df08360e
parent child
Show More
@@ -1,337 +1,338
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", :foreign_key => "author_id"
23 belongs_to :author, :class_name => "User", :foreign_key => "author_id"
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
30
31 acts_as_event :title => :filename,
31 acts_as_event :title => :filename,
32 :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}}
32 :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}}
33
33
34 acts_as_activity_provider :type => 'files',
34 acts_as_activity_provider :type => 'files',
35 :permission => :view_files,
35 :permission => :view_files,
36 :author_key => :author_id,
36 :author_key => :author_id,
37 :find_options => {:select => "#{Attachment.table_name}.*",
37 :find_options => {:select => "#{Attachment.table_name}.*",
38 :joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " +
38 :joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " +
39 "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 )"}
39 "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
40
41 acts_as_activity_provider :type => 'documents',
41 acts_as_activity_provider :type => 'documents',
42 :permission => :view_documents,
42 :permission => :view_documents,
43 :author_key => :author_id,
43 :author_key => :author_id,
44 :find_options => {:select => "#{Attachment.table_name}.*",
44 :find_options => {:select => "#{Attachment.table_name}.*",
45 :joins => "LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " +
45 :joins => "LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " +
46 "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"}
46 "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"}
47
47
48 cattr_accessor :storage_path
48 cattr_accessor :storage_path
49 @@storage_path = Redmine::Configuration['attachments_storage_path'] || File.join(Rails.root, "files")
49 @@storage_path = Redmine::Configuration['attachments_storage_path'] || File.join(Rails.root, "files")
50
50
51 cattr_accessor :thumbnails_storage_path
51 cattr_accessor :thumbnails_storage_path
52 @@thumbnails_storage_path = File.join(Rails.root, "tmp", "thumbnails")
52 @@thumbnails_storage_path = File.join(Rails.root, "tmp", "thumbnails")
53
53
54 before_save :files_to_final_location
54 before_create :files_to_final_location
55 after_destroy :delete_from_disk
55 after_destroy :delete_from_disk
56
56
57 # Returns an unsaved copy of the attachment
57 # Returns an unsaved copy of the attachment
58 def copy(attributes=nil)
58 def copy(attributes=nil)
59 copy = self.class.new
59 copy = self.class.new
60 copy.attributes = self.attributes.dup.except("id", "downloads")
60 copy.attributes = self.attributes.dup.except("id", "downloads")
61 copy.attributes = attributes if attributes
61 copy.attributes = attributes if attributes
62 copy
62 copy
63 end
63 end
64
64
65 def validate_max_file_size
65 def validate_max_file_size
66 if @temp_file && self.filesize > Setting.attachment_max_size.to_i.kilobytes
66 if @temp_file && self.filesize > Setting.attachment_max_size.to_i.kilobytes
67 errors.add(:base, l(:error_attachment_too_big, :max_size => Setting.attachment_max_size.to_i.kilobytes))
67 errors.add(:base, l(:error_attachment_too_big, :max_size => Setting.attachment_max_size.to_i.kilobytes))
68 end
68 end
69 end
69 end
70
70
71 def file=(incoming_file)
71 def file=(incoming_file)
72 unless incoming_file.nil?
72 unless incoming_file.nil?
73 @temp_file = incoming_file
73 @temp_file = incoming_file
74 if @temp_file.size > 0
74 if @temp_file.size > 0
75 if @temp_file.respond_to?(:original_filename)
75 if @temp_file.respond_to?(:original_filename)
76 self.filename = @temp_file.original_filename
76 self.filename = @temp_file.original_filename
77 self.filename.force_encoding("UTF-8") if filename.respond_to?(:force_encoding)
77 self.filename.force_encoding("UTF-8") if filename.respond_to?(:force_encoding)
78 end
78 end
79 if @temp_file.respond_to?(:content_type)
79 if @temp_file.respond_to?(:content_type)
80 self.content_type = @temp_file.content_type.to_s.chomp
80 self.content_type = @temp_file.content_type.to_s.chomp
81 end
81 end
82 if content_type.blank? && filename.present?
83 self.content_type = Redmine::MimeType.of(filename)
84 end
85 self.filesize = @temp_file.size
82 self.filesize = @temp_file.size
86 end
83 end
87 end
84 end
88 end
85 end
89
86
90 def file
87 def file
91 nil
88 nil
92 end
89 end
93
90
94 def filename=(arg)
91 def filename=(arg)
95 write_attribute :filename, sanitize_filename(arg.to_s)
92 write_attribute :filename, sanitize_filename(arg.to_s)
96 filename
93 filename
97 end
94 end
98
95
99 # Copies the temporary file to its final location
96 # Copies the temporary file to its final location
100 # and computes its MD5 hash
97 # and computes its MD5 hash
101 def files_to_final_location
98 def files_to_final_location
102 if @temp_file && (@temp_file.size > 0)
99 if @temp_file && (@temp_file.size > 0)
103 self.disk_directory = target_directory
100 self.disk_directory = target_directory
104 self.disk_filename = Attachment.disk_filename(filename, disk_directory)
101 self.disk_filename = Attachment.disk_filename(filename, disk_directory)
105 logger.info("Saving attachment '#{self.diskfile}' (#{@temp_file.size} bytes)") if logger
102 logger.info("Saving attachment '#{self.diskfile}' (#{@temp_file.size} bytes)") if logger
106 path = File.dirname(diskfile)
103 path = File.dirname(diskfile)
107 unless File.directory?(path)
104 unless File.directory?(path)
108 FileUtils.mkdir_p(path)
105 FileUtils.mkdir_p(path)
109 end
106 end
110 md5 = Digest::MD5.new
107 md5 = Digest::MD5.new
111 File.open(diskfile, "wb") do |f|
108 File.open(diskfile, "wb") do |f|
112 if @temp_file.respond_to?(:read)
109 if @temp_file.respond_to?(:read)
113 buffer = ""
110 buffer = ""
114 while (buffer = @temp_file.read(8192))
111 while (buffer = @temp_file.read(8192))
115 f.write(buffer)
112 f.write(buffer)
116 md5.update(buffer)
113 md5.update(buffer)
117 end
114 end
118 else
115 else
119 f.write(@temp_file)
116 f.write(@temp_file)
120 md5.update(@temp_file)
117 md5.update(@temp_file)
121 end
118 end
122 end
119 end
123 self.digest = md5.hexdigest
120 self.digest = md5.hexdigest
124 end
121 end
125 @temp_file = nil
122 @temp_file = nil
123
124 if content_type.blank? && filename.present?
125 self.content_type = Redmine::MimeType.of(filename)
126 end
126 # Don't save the content type if it's longer than the authorized length
127 # Don't save the content type if it's longer than the authorized length
127 if self.content_type && self.content_type.length > 255
128 if self.content_type && self.content_type.length > 255
128 self.content_type = nil
129 self.content_type = nil
129 end
130 end
130 end
131 end
131
132
132 # Deletes the file from the file system if it's not referenced by other attachments
133 # Deletes the file from the file system if it's not referenced by other attachments
133 def delete_from_disk
134 def delete_from_disk
134 if Attachment.where("disk_filename = ? AND id <> ?", disk_filename, id).empty?
135 if Attachment.where("disk_filename = ? AND id <> ?", disk_filename, id).empty?
135 delete_from_disk!
136 delete_from_disk!
136 end
137 end
137 end
138 end
138
139
139 # Returns file's location on disk
140 # Returns file's location on disk
140 def diskfile
141 def diskfile
141 File.join(self.class.storage_path, disk_directory.to_s, disk_filename.to_s)
142 File.join(self.class.storage_path, disk_directory.to_s, disk_filename.to_s)
142 end
143 end
143
144
144 def title
145 def title
145 title = filename.to_s
146 title = filename.to_s
146 if description.present?
147 if description.present?
147 title << " (#{description})"
148 title << " (#{description})"
148 end
149 end
149 title
150 title
150 end
151 end
151
152
152 def increment_download
153 def increment_download
153 increment!(:downloads)
154 increment!(:downloads)
154 end
155 end
155
156
156 def project
157 def project
157 container.try(:project)
158 container.try(:project)
158 end
159 end
159
160
160 def visible?(user=User.current)
161 def visible?(user=User.current)
161 if container_id
162 if container_id
162 container && container.attachments_visible?(user)
163 container && container.attachments_visible?(user)
163 else
164 else
164 author == user
165 author == user
165 end
166 end
166 end
167 end
167
168
168 def deletable?(user=User.current)
169 def deletable?(user=User.current)
169 if container_id
170 if container_id
170 container && container.attachments_deletable?(user)
171 container && container.attachments_deletable?(user)
171 else
172 else
172 author == user
173 author == user
173 end
174 end
174 end
175 end
175
176
176 def image?
177 def image?
177 !!(self.filename =~ /\.(bmp|gif|jpg|jpe|jpeg|png)$/i)
178 !!(self.filename =~ /\.(bmp|gif|jpg|jpe|jpeg|png)$/i)
178 end
179 end
179
180
180 def thumbnailable?
181 def thumbnailable?
181 image?
182 image?
182 end
183 end
183
184
184 # Returns the full path the attachment thumbnail, or nil
185 # Returns the full path the attachment thumbnail, or nil
185 # if the thumbnail cannot be generated.
186 # if the thumbnail cannot be generated.
186 def thumbnail(options={})
187 def thumbnail(options={})
187 if thumbnailable? && readable?
188 if thumbnailable? && readable?
188 size = options[:size].to_i
189 size = options[:size].to_i
189 if size > 0
190 if size > 0
190 # Limit the number of thumbnails per image
191 # Limit the number of thumbnails per image
191 size = (size / 50) * 50
192 size = (size / 50) * 50
192 # Maximum thumbnail size
193 # Maximum thumbnail size
193 size = 800 if size > 800
194 size = 800 if size > 800
194 else
195 else
195 size = Setting.thumbnails_size.to_i
196 size = Setting.thumbnails_size.to_i
196 end
197 end
197 size = 100 unless size > 0
198 size = 100 unless size > 0
198 target = File.join(self.class.thumbnails_storage_path, "#{id}_#{digest}_#{size}.thumb")
199 target = File.join(self.class.thumbnails_storage_path, "#{id}_#{digest}_#{size}.thumb")
199
200
200 begin
201 begin
201 Redmine::Thumbnail.generate(self.diskfile, target, size)
202 Redmine::Thumbnail.generate(self.diskfile, target, size)
202 rescue => e
203 rescue => e
203 logger.error "An error occured while generating thumbnail for #{disk_filename} to #{target}\nException was: #{e.message}" if logger
204 logger.error "An error occured while generating thumbnail for #{disk_filename} to #{target}\nException was: #{e.message}" if logger
204 return nil
205 return nil
205 end
206 end
206 end
207 end
207 end
208 end
208
209
209 # Deletes all thumbnails
210 # Deletes all thumbnails
210 def self.clear_thumbnails
211 def self.clear_thumbnails
211 Dir.glob(File.join(thumbnails_storage_path, "*.thumb")).each do |file|
212 Dir.glob(File.join(thumbnails_storage_path, "*.thumb")).each do |file|
212 File.delete file
213 File.delete file
213 end
214 end
214 end
215 end
215
216
216 def is_text?
217 def is_text?
217 Redmine::MimeType.is_type?('text', filename)
218 Redmine::MimeType.is_type?('text', filename)
218 end
219 end
219
220
220 def is_diff?
221 def is_diff?
221 self.filename =~ /\.(patch|diff)$/i
222 self.filename =~ /\.(patch|diff)$/i
222 end
223 end
223
224
224 # Returns true if the file is readable
225 # Returns true if the file is readable
225 def readable?
226 def readable?
226 File.readable?(diskfile)
227 File.readable?(diskfile)
227 end
228 end
228
229
229 # Returns the attachment token
230 # Returns the attachment token
230 def token
231 def token
231 "#{id}.#{digest}"
232 "#{id}.#{digest}"
232 end
233 end
233
234
234 # Finds an attachment that matches the given token and that has no container
235 # Finds an attachment that matches the given token and that has no container
235 def self.find_by_token(token)
236 def self.find_by_token(token)
236 if token.to_s =~ /^(\d+)\.([0-9a-f]+)$/
237 if token.to_s =~ /^(\d+)\.([0-9a-f]+)$/
237 attachment_id, attachment_digest = $1, $2
238 attachment_id, attachment_digest = $1, $2
238 attachment = Attachment.where(:id => attachment_id, :digest => attachment_digest).first
239 attachment = Attachment.where(:id => attachment_id, :digest => attachment_digest).first
239 if attachment && attachment.container.nil?
240 if attachment && attachment.container.nil?
240 attachment
241 attachment
241 end
242 end
242 end
243 end
243 end
244 end
244
245
245 # Bulk attaches a set of files to an object
246 # Bulk attaches a set of files to an object
246 #
247 #
247 # Returns a Hash of the results:
248 # Returns a Hash of the results:
248 # :files => array of the attached files
249 # :files => array of the attached files
249 # :unsaved => array of the files that could not be attached
250 # :unsaved => array of the files that could not be attached
250 def self.attach_files(obj, attachments)
251 def self.attach_files(obj, attachments)
251 result = obj.save_attachments(attachments, User.current)
252 result = obj.save_attachments(attachments, User.current)
252 obj.attach_saved_attachments
253 obj.attach_saved_attachments
253 result
254 result
254 end
255 end
255
256
256 def self.latest_attach(attachments, filename)
257 def self.latest_attach(attachments, filename)
257 attachments.sort_by(&:created_on).reverse.detect {
258 attachments.sort_by(&:created_on).reverse.detect {
258 |att| att.filename.downcase == filename.downcase
259 |att| att.filename.downcase == filename.downcase
259 }
260 }
260 end
261 end
261
262
262 def self.prune(age=1.day)
263 def self.prune(age=1.day)
263 Attachment.where("created_on < ? AND (container_type IS NULL OR container_type = '')", Time.now - age).destroy_all
264 Attachment.where("created_on < ? AND (container_type IS NULL OR container_type = '')", Time.now - age).destroy_all
264 end
265 end
265
266
266 # Moves an existing attachment to its target directory
267 # Moves an existing attachment to its target directory
267 def move_to_target_directory!
268 def move_to_target_directory!
268 return unless !new_record? & readable?
269 return unless !new_record? & readable?
269
270
270 src = diskfile
271 src = diskfile
271 self.disk_directory = target_directory
272 self.disk_directory = target_directory
272 dest = diskfile
273 dest = diskfile
273
274
274 return if src == dest
275 return if src == dest
275
276
276 if !FileUtils.mkdir_p(File.dirname(dest))
277 if !FileUtils.mkdir_p(File.dirname(dest))
277 logger.error "Could not create directory #{File.dirname(dest)}" if logger
278 logger.error "Could not create directory #{File.dirname(dest)}" if logger
278 return
279 return
279 end
280 end
280
281
281 if !FileUtils.mv(src, dest)
282 if !FileUtils.mv(src, dest)
282 logger.error "Could not move attachment from #{src} to #{dest}" if logger
283 logger.error "Could not move attachment from #{src} to #{dest}" if logger
283 return
284 return
284 end
285 end
285
286
286 update_column :disk_directory, disk_directory
287 update_column :disk_directory, disk_directory
287 end
288 end
288
289
289 # Moves existing attachments that are stored at the root of the files
290 # Moves existing attachments that are stored at the root of the files
290 # directory (ie. created before Redmine 2.3) to their target subdirectories
291 # directory (ie. created before Redmine 2.3) to their target subdirectories
291 def self.move_from_root_to_target_directory
292 def self.move_from_root_to_target_directory
292 Attachment.where("disk_directory IS NULL OR disk_directory = ''").find_each do |attachment|
293 Attachment.where("disk_directory IS NULL OR disk_directory = ''").find_each do |attachment|
293 attachment.move_to_target_directory!
294 attachment.move_to_target_directory!
294 end
295 end
295 end
296 end
296
297
297 private
298 private
298
299
299 # Physically deletes the file from the file system
300 # Physically deletes the file from the file system
300 def delete_from_disk!
301 def delete_from_disk!
301 if disk_filename.present? && File.exist?(diskfile)
302 if disk_filename.present? && File.exist?(diskfile)
302 File.delete(diskfile)
303 File.delete(diskfile)
303 end
304 end
304 end
305 end
305
306
306 def sanitize_filename(value)
307 def sanitize_filename(value)
307 # get only the filename, not the whole path
308 # get only the filename, not the whole path
308 just_filename = value.gsub(/\A.*(\\|\/)/m, '')
309 just_filename = value.gsub(/\A.*(\\|\/)/m, '')
309
310
310 # Finally, replace invalid characters with underscore
311 # Finally, replace invalid characters with underscore
311 @filename = just_filename.gsub(/[\/\?\%\*\:\|\"\'<>\n\r]+/, '_')
312 @filename = just_filename.gsub(/[\/\?\%\*\:\|\"\'<>\n\r]+/, '_')
312 end
313 end
313
314
314 # Returns the subdirectory in which the attachment will be saved
315 # Returns the subdirectory in which the attachment will be saved
315 def target_directory
316 def target_directory
316 time = created_on || DateTime.now
317 time = created_on || DateTime.now
317 time.strftime("%Y/%m")
318 time.strftime("%Y/%m")
318 end
319 end
319
320
320 # Returns an ASCII or hashed filename that do not
321 # Returns an ASCII or hashed filename that do not
321 # exists yet in the given subdirectory
322 # exists yet in the given subdirectory
322 def self.disk_filename(filename, directory=nil)
323 def self.disk_filename(filename, directory=nil)
323 timestamp = DateTime.now.strftime("%y%m%d%H%M%S")
324 timestamp = DateTime.now.strftime("%y%m%d%H%M%S")
324 ascii = ''
325 ascii = ''
325 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
326 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
326 ascii = filename
327 ascii = filename
327 else
328 else
328 ascii = Digest::MD5.hexdigest(filename)
329 ascii = Digest::MD5.hexdigest(filename)
329 # keep the extension if any
330 # keep the extension if any
330 ascii << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
331 ascii << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
331 end
332 end
332 while File.exist?(File.join(storage_path, directory.to_s, "#{timestamp}_#{ascii}"))
333 while File.exist?(File.join(storage_path, directory.to_s, "#{timestamp}_#{ascii}"))
333 timestamp.succ!
334 timestamp.succ!
334 end
335 end
335 "#{timestamp}_#{ascii}"
336 "#{timestamp}_#{ascii}"
336 end
337 end
337 end
338 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 < ActionController::IntegrationTest
20 class AttachmentsTest < ActionController::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