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