##// END OF EJS Templates
Validate attachment description length (#11365)....
Jean-Philippe Lang -
r9801:7f0bb136ad11
parent child
Show More
@@ -1,270 +1,271
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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
19
20 class Attachment < ActiveRecord::Base
20 class Attachment < ActiveRecord::Base
21 belongs_to :container, :polymorphic => true
21 belongs_to :container, :polymorphic => true
22 belongs_to :author, :class_name => "User", :foreign_key => "author_id"
22 belongs_to :author, :class_name => "User", :foreign_key => "author_id"
23
23
24 validates_presence_of :filename, :author
24 validates_presence_of :filename, :author
25 validates_length_of :filename, :maximum => 255
25 validates_length_of :filename, :maximum => 255
26 validates_length_of :disk_filename, :maximum => 255
26 validates_length_of :disk_filename, :maximum => 255
27 validates_length_of :description, :maximum => 255
27 validate :validate_max_file_size
28 validate :validate_max_file_size
28
29
29 acts_as_event :title => :filename,
30 acts_as_event :title => :filename,
30 :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}}
31 :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}}
31
32
32 acts_as_activity_provider :type => 'files',
33 acts_as_activity_provider :type => 'files',
33 :permission => :view_files,
34 :permission => :view_files,
34 :author_key => :author_id,
35 :author_key => :author_id,
35 :find_options => {:select => "#{Attachment.table_name}.*",
36 :find_options => {:select => "#{Attachment.table_name}.*",
36 :joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " +
37 :joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " +
37 "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 )"}
38 "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 )"}
38
39
39 acts_as_activity_provider :type => 'documents',
40 acts_as_activity_provider :type => 'documents',
40 :permission => :view_documents,
41 :permission => :view_documents,
41 :author_key => :author_id,
42 :author_key => :author_id,
42 :find_options => {:select => "#{Attachment.table_name}.*",
43 :find_options => {:select => "#{Attachment.table_name}.*",
43 :joins => "LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " +
44 :joins => "LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " +
44 "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"}
45 "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"}
45
46
46 cattr_accessor :storage_path
47 cattr_accessor :storage_path
47 @@storage_path = Redmine::Configuration['attachments_storage_path'] || File.join(Rails.root, "files")
48 @@storage_path = Redmine::Configuration['attachments_storage_path'] || File.join(Rails.root, "files")
48
49
49 cattr_accessor :thumbnails_storage_path
50 cattr_accessor :thumbnails_storage_path
50 @@thumbnails_storage_path = File.join(Rails.root, "tmp", "thumbnails")
51 @@thumbnails_storage_path = File.join(Rails.root, "tmp", "thumbnails")
51
52
52 before_save :files_to_final_location
53 before_save :files_to_final_location
53 after_destroy :delete_from_disk
54 after_destroy :delete_from_disk
54
55
55 # Returns an unsaved copy of the attachment
56 # Returns an unsaved copy of the attachment
56 def copy(attributes=nil)
57 def copy(attributes=nil)
57 copy = self.class.new
58 copy = self.class.new
58 copy.attributes = self.attributes.dup.except("id", "downloads")
59 copy.attributes = self.attributes.dup.except("id", "downloads")
59 copy.attributes = attributes if attributes
60 copy.attributes = attributes if attributes
60 copy
61 copy
61 end
62 end
62
63
63 def validate_max_file_size
64 def validate_max_file_size
64 if @temp_file && self.filesize > Setting.attachment_max_size.to_i.kilobytes
65 if @temp_file && self.filesize > Setting.attachment_max_size.to_i.kilobytes
65 errors.add(:base, l(:error_attachment_too_big, :max_size => Setting.attachment_max_size.to_i.kilobytes))
66 errors.add(:base, l(:error_attachment_too_big, :max_size => Setting.attachment_max_size.to_i.kilobytes))
66 end
67 end
67 end
68 end
68
69
69 def file=(incoming_file)
70 def file=(incoming_file)
70 unless incoming_file.nil?
71 unless incoming_file.nil?
71 @temp_file = incoming_file
72 @temp_file = incoming_file
72 if @temp_file.size > 0
73 if @temp_file.size > 0
73 if @temp_file.respond_to?(:original_filename)
74 if @temp_file.respond_to?(:original_filename)
74 self.filename = @temp_file.original_filename
75 self.filename = @temp_file.original_filename
75 self.filename.force_encoding("UTF-8") if filename.respond_to?(:force_encoding)
76 self.filename.force_encoding("UTF-8") if filename.respond_to?(:force_encoding)
76 end
77 end
77 if @temp_file.respond_to?(:content_type)
78 if @temp_file.respond_to?(:content_type)
78 self.content_type = @temp_file.content_type.to_s.chomp
79 self.content_type = @temp_file.content_type.to_s.chomp
79 end
80 end
80 if content_type.blank? && filename.present?
81 if content_type.blank? && filename.present?
81 self.content_type = Redmine::MimeType.of(filename)
82 self.content_type = Redmine::MimeType.of(filename)
82 end
83 end
83 self.filesize = @temp_file.size
84 self.filesize = @temp_file.size
84 end
85 end
85 end
86 end
86 end
87 end
87
88
88 def file
89 def file
89 nil
90 nil
90 end
91 end
91
92
92 def filename=(arg)
93 def filename=(arg)
93 write_attribute :filename, sanitize_filename(arg.to_s)
94 write_attribute :filename, sanitize_filename(arg.to_s)
94 if new_record? && disk_filename.blank?
95 if new_record? && disk_filename.blank?
95 self.disk_filename = Attachment.disk_filename(filename)
96 self.disk_filename = Attachment.disk_filename(filename)
96 end
97 end
97 filename
98 filename
98 end
99 end
99
100
100 # Copies the temporary file to its final location
101 # Copies the temporary file to its final location
101 # and computes its MD5 hash
102 # and computes its MD5 hash
102 def files_to_final_location
103 def files_to_final_location
103 if @temp_file && (@temp_file.size > 0)
104 if @temp_file && (@temp_file.size > 0)
104 logger.info("Saving attachment '#{self.diskfile}' (#{@temp_file.size} bytes)")
105 logger.info("Saving attachment '#{self.diskfile}' (#{@temp_file.size} bytes)")
105 md5 = Digest::MD5.new
106 md5 = Digest::MD5.new
106 File.open(diskfile, "wb") do |f|
107 File.open(diskfile, "wb") do |f|
107 if @temp_file.respond_to?(:read)
108 if @temp_file.respond_to?(:read)
108 buffer = ""
109 buffer = ""
109 while (buffer = @temp_file.read(8192))
110 while (buffer = @temp_file.read(8192))
110 f.write(buffer)
111 f.write(buffer)
111 md5.update(buffer)
112 md5.update(buffer)
112 end
113 end
113 else
114 else
114 f.write(@temp_file)
115 f.write(@temp_file)
115 md5.update(@temp_file)
116 md5.update(@temp_file)
116 end
117 end
117 end
118 end
118 self.digest = md5.hexdigest
119 self.digest = md5.hexdigest
119 end
120 end
120 @temp_file = nil
121 @temp_file = nil
121 # Don't save the content type if it's longer than the authorized length
122 # Don't save the content type if it's longer than the authorized length
122 if self.content_type && self.content_type.length > 255
123 if self.content_type && self.content_type.length > 255
123 self.content_type = nil
124 self.content_type = nil
124 end
125 end
125 end
126 end
126
127
127 # Deletes the file from the file system if it's not referenced by other attachments
128 # Deletes the file from the file system if it's not referenced by other attachments
128 def delete_from_disk
129 def delete_from_disk
129 if Attachment.where("disk_filename = ? AND id <> ?", disk_filename, id).empty?
130 if Attachment.where("disk_filename = ? AND id <> ?", disk_filename, id).empty?
130 delete_from_disk!
131 delete_from_disk!
131 end
132 end
132 end
133 end
133
134
134 # Returns file's location on disk
135 # Returns file's location on disk
135 def diskfile
136 def diskfile
136 File.join(self.class.storage_path, disk_filename.to_s)
137 File.join(self.class.storage_path, disk_filename.to_s)
137 end
138 end
138
139
139 def increment_download
140 def increment_download
140 increment!(:downloads)
141 increment!(:downloads)
141 end
142 end
142
143
143 def project
144 def project
144 container.try(:project)
145 container.try(:project)
145 end
146 end
146
147
147 def visible?(user=User.current)
148 def visible?(user=User.current)
148 container && container.attachments_visible?(user)
149 container && container.attachments_visible?(user)
149 end
150 end
150
151
151 def deletable?(user=User.current)
152 def deletable?(user=User.current)
152 container && container.attachments_deletable?(user)
153 container && container.attachments_deletable?(user)
153 end
154 end
154
155
155 def image?
156 def image?
156 !!(self.filename =~ /\.(bmp|gif|jpg|jpe|jpeg|png)$/i)
157 !!(self.filename =~ /\.(bmp|gif|jpg|jpe|jpeg|png)$/i)
157 end
158 end
158
159
159 def thumbnailable?
160 def thumbnailable?
160 image?
161 image?
161 end
162 end
162
163
163 # Returns the full path the attachment thumbnail, or nil
164 # Returns the full path the attachment thumbnail, or nil
164 # if the thumbnail cannot be generated.
165 # if the thumbnail cannot be generated.
165 def thumbnail
166 def thumbnail
166 if thumbnailable? && readable?
167 if thumbnailable? && readable?
167 size = Setting.thumbnails_size.to_i
168 size = Setting.thumbnails_size.to_i
168 size = 100 unless size > 0
169 size = 100 unless size > 0
169 target = File.join(self.class.thumbnails_storage_path, "#{id}_#{digest}_#{size}.thumb")
170 target = File.join(self.class.thumbnails_storage_path, "#{id}_#{digest}_#{size}.thumb")
170
171
171 begin
172 begin
172 Redmine::Thumbnail.generate(self.diskfile, target, size)
173 Redmine::Thumbnail.generate(self.diskfile, target, size)
173 rescue => e
174 rescue => e
174 logger.error "An error occured while generating thumbnail for #{disk_filename} to #{target}\nException was: #{e.message}" if logger
175 logger.error "An error occured while generating thumbnail for #{disk_filename} to #{target}\nException was: #{e.message}" if logger
175 return nil
176 return nil
176 end
177 end
177 end
178 end
178 end
179 end
179
180
180 # Deletes all thumbnails
181 # Deletes all thumbnails
181 def self.clear_thumbnails
182 def self.clear_thumbnails
182 Dir.glob(File.join(thumbnails_storage_path, "*.thumb")).each do |file|
183 Dir.glob(File.join(thumbnails_storage_path, "*.thumb")).each do |file|
183 File.delete file
184 File.delete file
184 end
185 end
185 end
186 end
186
187
187 def is_text?
188 def is_text?
188 Redmine::MimeType.is_type?('text', filename)
189 Redmine::MimeType.is_type?('text', filename)
189 end
190 end
190
191
191 def is_diff?
192 def is_diff?
192 self.filename =~ /\.(patch|diff)$/i
193 self.filename =~ /\.(patch|diff)$/i
193 end
194 end
194
195
195 # Returns true if the file is readable
196 # Returns true if the file is readable
196 def readable?
197 def readable?
197 File.readable?(diskfile)
198 File.readable?(diskfile)
198 end
199 end
199
200
200 # Returns the attachment token
201 # Returns the attachment token
201 def token
202 def token
202 "#{id}.#{digest}"
203 "#{id}.#{digest}"
203 end
204 end
204
205
205 # Finds an attachment that matches the given token and that has no container
206 # Finds an attachment that matches the given token and that has no container
206 def self.find_by_token(token)
207 def self.find_by_token(token)
207 if token.to_s =~ /^(\d+)\.([0-9a-f]+)$/
208 if token.to_s =~ /^(\d+)\.([0-9a-f]+)$/
208 attachment_id, attachment_digest = $1, $2
209 attachment_id, attachment_digest = $1, $2
209 attachment = Attachment.where(:id => attachment_id, :digest => attachment_digest).first
210 attachment = Attachment.where(:id => attachment_id, :digest => attachment_digest).first
210 if attachment && attachment.container.nil?
211 if attachment && attachment.container.nil?
211 attachment
212 attachment
212 end
213 end
213 end
214 end
214 end
215 end
215
216
216 # Bulk attaches a set of files to an object
217 # Bulk attaches a set of files to an object
217 #
218 #
218 # Returns a Hash of the results:
219 # Returns a Hash of the results:
219 # :files => array of the attached files
220 # :files => array of the attached files
220 # :unsaved => array of the files that could not be attached
221 # :unsaved => array of the files that could not be attached
221 def self.attach_files(obj, attachments)
222 def self.attach_files(obj, attachments)
222 result = obj.save_attachments(attachments, User.current)
223 result = obj.save_attachments(attachments, User.current)
223 obj.attach_saved_attachments
224 obj.attach_saved_attachments
224 result
225 result
225 end
226 end
226
227
227 def self.latest_attach(attachments, filename)
228 def self.latest_attach(attachments, filename)
228 attachments.sort_by(&:created_on).reverse.detect {
229 attachments.sort_by(&:created_on).reverse.detect {
229 |att| att.filename.downcase == filename.downcase
230 |att| att.filename.downcase == filename.downcase
230 }
231 }
231 end
232 end
232
233
233 def self.prune(age=1.day)
234 def self.prune(age=1.day)
234 Attachment.where("created_on < ? AND (container_type IS NULL OR container_type = '')", Time.now - age).destroy_all
235 Attachment.where("created_on < ? AND (container_type IS NULL OR container_type = '')", Time.now - age).destroy_all
235 end
236 end
236
237
237 private
238 private
238
239
239 # Physically deletes the file from the file system
240 # Physically deletes the file from the file system
240 def delete_from_disk!
241 def delete_from_disk!
241 if disk_filename.present? && File.exist?(diskfile)
242 if disk_filename.present? && File.exist?(diskfile)
242 File.delete(diskfile)
243 File.delete(diskfile)
243 end
244 end
244 end
245 end
245
246
246 def sanitize_filename(value)
247 def sanitize_filename(value)
247 # get only the filename, not the whole path
248 # get only the filename, not the whole path
248 just_filename = value.gsub(/^.*(\\|\/)/, '')
249 just_filename = value.gsub(/^.*(\\|\/)/, '')
249
250
250 # Finally, replace invalid characters with underscore
251 # Finally, replace invalid characters with underscore
251 @filename = just_filename.gsub(/[\/\?\%\*\:\|\"\'<>]+/, '_')
252 @filename = just_filename.gsub(/[\/\?\%\*\:\|\"\'<>]+/, '_')
252 end
253 end
253
254
254 # Returns an ASCII or hashed filename
255 # Returns an ASCII or hashed filename
255 def self.disk_filename(filename)
256 def self.disk_filename(filename)
256 timestamp = DateTime.now.strftime("%y%m%d%H%M%S")
257 timestamp = DateTime.now.strftime("%y%m%d%H%M%S")
257 ascii = ''
258 ascii = ''
258 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
259 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
259 ascii = filename
260 ascii = filename
260 else
261 else
261 ascii = Digest::MD5.hexdigest(filename)
262 ascii = Digest::MD5.hexdigest(filename)
262 # keep the extension if any
263 # keep the extension if any
263 ascii << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
264 ascii << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
264 end
265 end
265 while File.exist?(File.join(@@storage_path, "#{timestamp}_#{ascii}"))
266 while File.exist?(File.join(@@storage_path, "#{timestamp}_#{ascii}"))
266 timestamp.succ!
267 timestamp.succ!
267 end
268 end
268 "#{timestamp}_#{ascii}"
269 "#{timestamp}_#{ascii}"
269 end
270 end
270 end
271 end
@@ -1,241 +1,247
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2012 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_create
45 def test_create
46 a = Attachment.new(:container => Issue.find(1),
46 a = Attachment.new(:container => Issue.find(1),
47 :file => uploaded_test_file("testfile.txt", "text/plain"),
47 :file => uploaded_test_file("testfile.txt", "text/plain"),
48 :author => User.find(1))
48 :author => User.find(1))
49 assert a.save
49 assert a.save
50 assert_equal 'testfile.txt', a.filename
50 assert_equal 'testfile.txt', a.filename
51 assert_equal 59, a.filesize
51 assert_equal 59, a.filesize
52 assert_equal 'text/plain', a.content_type
52 assert_equal 'text/plain', a.content_type
53 assert_equal 0, a.downloads
53 assert_equal 0, a.downloads
54 assert_equal '1478adae0d4eb06d35897518540e25d6', a.digest
54 assert_equal '1478adae0d4eb06d35897518540e25d6', a.digest
55 assert File.exist?(a.diskfile)
55 assert File.exist?(a.diskfile)
56 assert_equal 59, File.size(a.diskfile)
56 assert_equal 59, File.size(a.diskfile)
57 end
57 end
58
58
59 def test_size_should_be_validated_for_new_file
59 def test_size_should_be_validated_for_new_file
60 with_settings :attachment_max_size => 0 do
60 with_settings :attachment_max_size => 0 do
61 a = Attachment.new(:container => Issue.find(1),
61 a = Attachment.new(:container => Issue.find(1),
62 :file => uploaded_test_file("testfile.txt", "text/plain"),
62 :file => uploaded_test_file("testfile.txt", "text/plain"),
63 :author => User.find(1))
63 :author => User.find(1))
64 assert !a.save
64 assert !a.save
65 end
65 end
66 end
66 end
67
67
68 def test_size_should_not_be_validated_when_copying
68 def test_size_should_not_be_validated_when_copying
69 a = Attachment.create!(:container => Issue.find(1),
69 a = Attachment.create!(:container => Issue.find(1),
70 :file => uploaded_test_file("testfile.txt", "text/plain"),
70 :file => uploaded_test_file("testfile.txt", "text/plain"),
71 :author => User.find(1))
71 :author => User.find(1))
72 with_settings :attachment_max_size => 0 do
72 with_settings :attachment_max_size => 0 do
73 copy = a.copy
73 copy = a.copy
74 assert copy.save
74 assert copy.save
75 end
75 end
76 end
76 end
77
77
78 def test_description_length_should_be_validated
79 a = Attachment.new(:description => 'a' * 300)
80 assert !a.save
81 assert_not_nil a.errors[:description]
82 end
83
78 def test_destroy
84 def test_destroy
79 a = Attachment.new(:container => Issue.find(1),
85 a = Attachment.new(:container => Issue.find(1),
80 :file => uploaded_test_file("testfile.txt", "text/plain"),
86 :file => uploaded_test_file("testfile.txt", "text/plain"),
81 :author => User.find(1))
87 :author => User.find(1))
82 assert a.save
88 assert a.save
83 assert_equal 'testfile.txt', a.filename
89 assert_equal 'testfile.txt', a.filename
84 assert_equal 59, a.filesize
90 assert_equal 59, a.filesize
85 assert_equal 'text/plain', a.content_type
91 assert_equal 'text/plain', a.content_type
86 assert_equal 0, a.downloads
92 assert_equal 0, a.downloads
87 assert_equal '1478adae0d4eb06d35897518540e25d6', a.digest
93 assert_equal '1478adae0d4eb06d35897518540e25d6', a.digest
88 diskfile = a.diskfile
94 diskfile = a.diskfile
89 assert File.exist?(diskfile)
95 assert File.exist?(diskfile)
90 assert_equal 59, File.size(a.diskfile)
96 assert_equal 59, File.size(a.diskfile)
91 assert a.destroy
97 assert a.destroy
92 assert !File.exist?(diskfile)
98 assert !File.exist?(diskfile)
93 end
99 end
94
100
95 def test_destroy_should_not_delete_file_referenced_by_other_attachment
101 def test_destroy_should_not_delete_file_referenced_by_other_attachment
96 a = Attachment.create!(:container => Issue.find(1),
102 a = Attachment.create!(:container => Issue.find(1),
97 :file => uploaded_test_file("testfile.txt", "text/plain"),
103 :file => uploaded_test_file("testfile.txt", "text/plain"),
98 :author => User.find(1))
104 :author => User.find(1))
99 diskfile = a.diskfile
105 diskfile = a.diskfile
100
106
101 copy = a.copy
107 copy = a.copy
102 copy.save!
108 copy.save!
103
109
104 assert File.exists?(diskfile)
110 assert File.exists?(diskfile)
105 a.destroy
111 a.destroy
106 assert File.exists?(diskfile)
112 assert File.exists?(diskfile)
107 copy.destroy
113 copy.destroy
108 assert !File.exists?(diskfile)
114 assert !File.exists?(diskfile)
109 end
115 end
110
116
111 def test_create_should_auto_assign_content_type
117 def test_create_should_auto_assign_content_type
112 a = Attachment.new(:container => Issue.find(1),
118 a = Attachment.new(:container => Issue.find(1),
113 :file => uploaded_test_file("testfile.txt", ""),
119 :file => uploaded_test_file("testfile.txt", ""),
114 :author => User.find(1))
120 :author => User.find(1))
115 assert a.save
121 assert a.save
116 assert_equal 'text/plain', a.content_type
122 assert_equal 'text/plain', a.content_type
117 end
123 end
118
124
119 def test_identical_attachments_at_the_same_time_should_not_overwrite
125 def test_identical_attachments_at_the_same_time_should_not_overwrite
120 a1 = Attachment.create!(:container => Issue.find(1),
126 a1 = Attachment.create!(:container => Issue.find(1),
121 :file => uploaded_test_file("testfile.txt", ""),
127 :file => uploaded_test_file("testfile.txt", ""),
122 :author => User.find(1))
128 :author => User.find(1))
123 a2 = Attachment.create!(:container => Issue.find(1),
129 a2 = Attachment.create!(:container => Issue.find(1),
124 :file => uploaded_test_file("testfile.txt", ""),
130 :file => uploaded_test_file("testfile.txt", ""),
125 :author => User.find(1))
131 :author => User.find(1))
126 assert a1.disk_filename != a2.disk_filename
132 assert a1.disk_filename != a2.disk_filename
127 end
133 end
128
134
129 def test_filename_should_be_basenamed
135 def test_filename_should_be_basenamed
130 a = Attachment.new(:file => MockFile.new(:original_filename => "path/to/the/file"))
136 a = Attachment.new(:file => MockFile.new(:original_filename => "path/to/the/file"))
131 assert_equal 'file', a.filename
137 assert_equal 'file', a.filename
132 end
138 end
133
139
134 def test_filename_should_be_sanitized
140 def test_filename_should_be_sanitized
135 a = Attachment.new(:file => MockFile.new(:original_filename => "valid:[] invalid:?%*|\"'<>chars"))
141 a = Attachment.new(:file => MockFile.new(:original_filename => "valid:[] invalid:?%*|\"'<>chars"))
136 assert_equal 'valid_[] invalid_chars', a.filename
142 assert_equal 'valid_[] invalid_chars', a.filename
137 end
143 end
138
144
139 def test_diskfilename
145 def test_diskfilename
140 assert Attachment.disk_filename("test_file.txt") =~ /^\d{12}_test_file.txt$/
146 assert Attachment.disk_filename("test_file.txt") =~ /^\d{12}_test_file.txt$/
141 assert_equal 'test_file.txt', Attachment.disk_filename("test_file.txt")[13..-1]
147 assert_equal 'test_file.txt', Attachment.disk_filename("test_file.txt")[13..-1]
142 assert_equal '770c509475505f37c2b8fb6030434d6b.txt', Attachment.disk_filename("test_accentuΓ©.txt")[13..-1]
148 assert_equal '770c509475505f37c2b8fb6030434d6b.txt', Attachment.disk_filename("test_accentuΓ©.txt")[13..-1]
143 assert_equal 'f8139524ebb8f32e51976982cd20a85d', Attachment.disk_filename("test_accentuΓ©")[13..-1]
149 assert_equal 'f8139524ebb8f32e51976982cd20a85d', Attachment.disk_filename("test_accentuΓ©")[13..-1]
144 assert_equal 'cbb5b0f30978ba03731d61f9f6d10011', Attachment.disk_filename("test_accentuΓ©.Γ§a")[13..-1]
150 assert_equal 'cbb5b0f30978ba03731d61f9f6d10011', Attachment.disk_filename("test_accentuΓ©.Γ§a")[13..-1]
145 end
151 end
146
152
147 def test_prune_should_destroy_old_unattached_attachments
153 def test_prune_should_destroy_old_unattached_attachments
148 Attachment.create!(:file => uploaded_test_file("testfile.txt", ""), :author_id => 1, :created_on => 2.days.ago)
154 Attachment.create!(:file => uploaded_test_file("testfile.txt", ""), :author_id => 1, :created_on => 2.days.ago)
149 Attachment.create!(:file => uploaded_test_file("testfile.txt", ""), :author_id => 1, :created_on => 2.days.ago)
155 Attachment.create!(:file => uploaded_test_file("testfile.txt", ""), :author_id => 1, :created_on => 2.days.ago)
150 Attachment.create!(:file => uploaded_test_file("testfile.txt", ""), :author_id => 1)
156 Attachment.create!(:file => uploaded_test_file("testfile.txt", ""), :author_id => 1)
151
157
152 assert_difference 'Attachment.count', -2 do
158 assert_difference 'Attachment.count', -2 do
153 Attachment.prune
159 Attachment.prune
154 end
160 end
155 end
161 end
156
162
157 context "Attachmnet.attach_files" do
163 context "Attachmnet.attach_files" do
158 should "attach the file" do
164 should "attach the file" do
159 issue = Issue.first
165 issue = Issue.first
160 assert_difference 'Attachment.count' do
166 assert_difference 'Attachment.count' do
161 Attachment.attach_files(issue,
167 Attachment.attach_files(issue,
162 '1' => {
168 '1' => {
163 'file' => uploaded_test_file('testfile.txt', 'text/plain'),
169 'file' => uploaded_test_file('testfile.txt', 'text/plain'),
164 'description' => 'test'
170 'description' => 'test'
165 })
171 })
166 end
172 end
167
173
168 attachment = Attachment.first(:order => 'id DESC')
174 attachment = Attachment.first(:order => 'id DESC')
169 assert_equal issue, attachment.container
175 assert_equal issue, attachment.container
170 assert_equal 'testfile.txt', attachment.filename
176 assert_equal 'testfile.txt', attachment.filename
171 assert_equal 59, attachment.filesize
177 assert_equal 59, attachment.filesize
172 assert_equal 'test', attachment.description
178 assert_equal 'test', attachment.description
173 assert_equal 'text/plain', attachment.content_type
179 assert_equal 'text/plain', attachment.content_type
174 assert File.exists?(attachment.diskfile)
180 assert File.exists?(attachment.diskfile)
175 assert_equal 59, File.size(attachment.diskfile)
181 assert_equal 59, File.size(attachment.diskfile)
176 end
182 end
177
183
178 should "add unsaved files to the object as unsaved attachments" do
184 should "add unsaved files to the object as unsaved attachments" do
179 # Max size of 0 to force Attachment creation failures
185 # Max size of 0 to force Attachment creation failures
180 with_settings(:attachment_max_size => 0) do
186 with_settings(:attachment_max_size => 0) do
181 @project = Project.find(1)
187 @project = Project.find(1)
182 response = Attachment.attach_files(@project, {
188 response = Attachment.attach_files(@project, {
183 '1' => {'file' => mock_file, 'description' => 'test'},
189 '1' => {'file' => mock_file, 'description' => 'test'},
184 '2' => {'file' => mock_file, 'description' => 'test'}
190 '2' => {'file' => mock_file, 'description' => 'test'}
185 })
191 })
186
192
187 assert response[:unsaved].present?
193 assert response[:unsaved].present?
188 assert_equal 2, response[:unsaved].length
194 assert_equal 2, response[:unsaved].length
189 assert response[:unsaved].first.new_record?
195 assert response[:unsaved].first.new_record?
190 assert response[:unsaved].second.new_record?
196 assert response[:unsaved].second.new_record?
191 assert_equal response[:unsaved], @project.unsaved_attachments
197 assert_equal response[:unsaved], @project.unsaved_attachments
192 end
198 end
193 end
199 end
194 end
200 end
195
201
196 def test_latest_attach
202 def test_latest_attach
197 set_fixtures_attachments_directory
203 set_fixtures_attachments_directory
198 a1 = Attachment.find(16)
204 a1 = Attachment.find(16)
199 assert_equal "testfile.png", a1.filename
205 assert_equal "testfile.png", a1.filename
200 assert a1.readable?
206 assert a1.readable?
201 assert (! a1.visible?(User.anonymous))
207 assert (! a1.visible?(User.anonymous))
202 assert a1.visible?(User.find(2))
208 assert a1.visible?(User.find(2))
203 a2 = Attachment.find(17)
209 a2 = Attachment.find(17)
204 assert_equal "testfile.PNG", a2.filename
210 assert_equal "testfile.PNG", a2.filename
205 assert a2.readable?
211 assert a2.readable?
206 assert (! a2.visible?(User.anonymous))
212 assert (! a2.visible?(User.anonymous))
207 assert a2.visible?(User.find(2))
213 assert a2.visible?(User.find(2))
208 assert a1.created_on < a2.created_on
214 assert a1.created_on < a2.created_on
209
215
210 la1 = Attachment.latest_attach([a1, a2], "testfile.png")
216 la1 = Attachment.latest_attach([a1, a2], "testfile.png")
211 assert_equal 17, la1.id
217 assert_equal 17, la1.id
212 la2 = Attachment.latest_attach([a1, a2], "Testfile.PNG")
218 la2 = Attachment.latest_attach([a1, a2], "Testfile.PNG")
213 assert_equal 17, la2.id
219 assert_equal 17, la2.id
214
220
215 set_tmp_attachments_directory
221 set_tmp_attachments_directory
216 end
222 end
217
223
218 def test_thumbnailable_should_be_true_for_images
224 def test_thumbnailable_should_be_true_for_images
219 assert_equal true, Attachment.new(:filename => 'test.jpg').thumbnailable?
225 assert_equal true, Attachment.new(:filename => 'test.jpg').thumbnailable?
220 end
226 end
221
227
222 def test_thumbnailable_should_be_true_for_non_images
228 def test_thumbnailable_should_be_true_for_non_images
223 assert_equal false, Attachment.new(:filename => 'test.txt').thumbnailable?
229 assert_equal false, Attachment.new(:filename => 'test.txt').thumbnailable?
224 end
230 end
225
231
226 if convert_installed?
232 if convert_installed?
227 def test_thumbnail_should_generate_the_thumbnail
233 def test_thumbnail_should_generate_the_thumbnail
228 set_fixtures_attachments_directory
234 set_fixtures_attachments_directory
229 attachment = Attachment.find(16)
235 attachment = Attachment.find(16)
230 Attachment.clear_thumbnails
236 Attachment.clear_thumbnails
231
237
232 assert_difference "Dir.glob(File.join(Attachment.thumbnails_storage_path, '*.thumb')).size" do
238 assert_difference "Dir.glob(File.join(Attachment.thumbnails_storage_path, '*.thumb')).size" do
233 thumbnail = attachment.thumbnail
239 thumbnail = attachment.thumbnail
234 assert_equal "16_8e0294de2441577c529f170b6fb8f638_100.thumb", File.basename(thumbnail)
240 assert_equal "16_8e0294de2441577c529f170b6fb8f638_100.thumb", File.basename(thumbnail)
235 assert File.exists?(thumbnail)
241 assert File.exists?(thumbnail)
236 end
242 end
237 end
243 end
238 else
244 else
239 puts '(ImageMagick convert not available)'
245 puts '(ImageMagick convert not available)'
240 end
246 end
241 end
247 end
General Comments 0
You need to be logged in to leave comments. Login now