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