##// END OF EJS Templates
Fix a nil error when a Project cannot save attachments....
Eric Davis -
r3658:c56c0f411c57
parent child
Show More
@@ -1,186 +1,192
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 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 :container, :filename, :author
24 validates_presence_of :container, :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
27
28 acts_as_event :title => :filename,
28 acts_as_event :title => :filename,
29 :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}}
29 :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}}
30
30
31 acts_as_activity_provider :type => 'files',
31 acts_as_activity_provider :type => 'files',
32 :permission => :view_files,
32 :permission => :view_files,
33 :author_key => :author_id,
33 :author_key => :author_id,
34 :find_options => {:select => "#{Attachment.table_name}.*",
34 :find_options => {:select => "#{Attachment.table_name}.*",
35 :joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " +
35 :joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " +
36 "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 )"}
36 "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 )"}
37
37
38 acts_as_activity_provider :type => 'documents',
38 acts_as_activity_provider :type => 'documents',
39 :permission => :view_documents,
39 :permission => :view_documents,
40 :author_key => :author_id,
40 :author_key => :author_id,
41 :find_options => {:select => "#{Attachment.table_name}.*",
41 :find_options => {:select => "#{Attachment.table_name}.*",
42 :joins => "LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " +
42 :joins => "LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " +
43 "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"}
43 "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"}
44
44
45 cattr_accessor :storage_path
45 cattr_accessor :storage_path
46 @@storage_path = "#{RAILS_ROOT}/files"
46 @@storage_path = "#{RAILS_ROOT}/files"
47
47
48 def validate
48 def validate
49 if self.filesize > Setting.attachment_max_size.to_i.kilobytes
49 if self.filesize > Setting.attachment_max_size.to_i.kilobytes
50 errors.add(:base, :too_long, :count => Setting.attachment_max_size.to_i.kilobytes)
50 errors.add(:base, :too_long, :count => Setting.attachment_max_size.to_i.kilobytes)
51 end
51 end
52 end
52 end
53
53
54 def file=(incoming_file)
54 def file=(incoming_file)
55 unless incoming_file.nil?
55 unless incoming_file.nil?
56 @temp_file = incoming_file
56 @temp_file = incoming_file
57 if @temp_file.size > 0
57 if @temp_file.size > 0
58 self.filename = sanitize_filename(@temp_file.original_filename)
58 self.filename = sanitize_filename(@temp_file.original_filename)
59 self.disk_filename = Attachment.disk_filename(filename)
59 self.disk_filename = Attachment.disk_filename(filename)
60 self.content_type = @temp_file.content_type.to_s.chomp
60 self.content_type = @temp_file.content_type.to_s.chomp
61 if content_type.blank?
61 if content_type.blank?
62 self.content_type = Redmine::MimeType.of(filename)
62 self.content_type = Redmine::MimeType.of(filename)
63 end
63 end
64 self.filesize = @temp_file.size
64 self.filesize = @temp_file.size
65 end
65 end
66 end
66 end
67 end
67 end
68
68
69 def file
69 def file
70 nil
70 nil
71 end
71 end
72
72
73 # Copies the temporary file to its final location
73 # Copies the temporary file to its final location
74 # and computes its MD5 hash
74 # and computes its MD5 hash
75 def before_save
75 def before_save
76 if @temp_file && (@temp_file.size > 0)
76 if @temp_file && (@temp_file.size > 0)
77 logger.debug("saving '#{self.diskfile}'")
77 logger.debug("saving '#{self.diskfile}'")
78 md5 = Digest::MD5.new
78 md5 = Digest::MD5.new
79 File.open(diskfile, "wb") do |f|
79 File.open(diskfile, "wb") do |f|
80 buffer = ""
80 buffer = ""
81 while (buffer = @temp_file.read(8192))
81 while (buffer = @temp_file.read(8192))
82 f.write(buffer)
82 f.write(buffer)
83 md5.update(buffer)
83 md5.update(buffer)
84 end
84 end
85 end
85 end
86 self.digest = md5.hexdigest
86 self.digest = md5.hexdigest
87 end
87 end
88 # Don't save the content type if it's longer than the authorized length
88 # Don't save the content type if it's longer than the authorized length
89 if self.content_type && self.content_type.length > 255
89 if self.content_type && self.content_type.length > 255
90 self.content_type = nil
90 self.content_type = nil
91 end
91 end
92 end
92 end
93
93
94 # Deletes file on the disk
94 # Deletes file on the disk
95 def after_destroy
95 def after_destroy
96 File.delete(diskfile) if !filename.blank? && File.exist?(diskfile)
96 File.delete(diskfile) if !filename.blank? && File.exist?(diskfile)
97 end
97 end
98
98
99 # Returns file's location on disk
99 # Returns file's location on disk
100 def diskfile
100 def diskfile
101 "#{@@storage_path}/#{self.disk_filename}"
101 "#{@@storage_path}/#{self.disk_filename}"
102 end
102 end
103
103
104 def increment_download
104 def increment_download
105 increment!(:downloads)
105 increment!(:downloads)
106 end
106 end
107
107
108 def project
108 def project
109 container.project
109 container.project
110 end
110 end
111
111
112 def visible?(user=User.current)
112 def visible?(user=User.current)
113 container.attachments_visible?(user)
113 container.attachments_visible?(user)
114 end
114 end
115
115
116 def deletable?(user=User.current)
116 def deletable?(user=User.current)
117 container.attachments_deletable?(user)
117 container.attachments_deletable?(user)
118 end
118 end
119
119
120 def image?
120 def image?
121 self.filename =~ /\.(jpe?g|gif|png)$/i
121 self.filename =~ /\.(jpe?g|gif|png)$/i
122 end
122 end
123
123
124 def is_text?
124 def is_text?
125 Redmine::MimeType.is_type?('text', filename)
125 Redmine::MimeType.is_type?('text', filename)
126 end
126 end
127
127
128 def is_diff?
128 def is_diff?
129 self.filename =~ /\.(patch|diff)$/i
129 self.filename =~ /\.(patch|diff)$/i
130 end
130 end
131
131
132 # Returns true if the file is readable
132 # Returns true if the file is readable
133 def readable?
133 def readable?
134 File.readable?(diskfile)
134 File.readable?(diskfile)
135 end
135 end
136
136
137 # Bulk attaches a set of files to an object
137 # Bulk attaches a set of files to an object
138 #
138 #
139 # Returns a Hash of the results:
139 # Returns a Hash of the results:
140 # :files => array of the attached files
140 # :files => array of the attached files
141 # :unsaved => array of the files that could not be attached
141 # :unsaved => array of the files that could not be attached
142 def self.attach_files(obj, attachments)
142 def self.attach_files(obj, attachments)
143 attached = []
143 attached = []
144 unsaved = []
144 unsaved = []
145 if attachments && attachments.is_a?(Hash)
145 if attachments && attachments.is_a?(Hash)
146 attachments.each_value do |attachment|
146 attachments.each_value do |attachment|
147 file = attachment['file']
147 file = attachment['file']
148 next unless file && file.size > 0
148 next unless file && file.size > 0
149 a = Attachment.create(:container => obj,
149 a = Attachment.create(:container => obj,
150 :file => file,
150 :file => file,
151 :description => attachment['description'].to_s.strip,
151 :description => attachment['description'].to_s.strip,
152 :author => User.current)
152 :author => User.current)
153 a.new_record? ? (obj.unsaved_attachments << a) : (attached << a)
153
154 if a.new_record?
155 obj.unsaved_attachments ||= []
156 obj.unsaved_attachments << a
157 else
158 attached << a
159 end
154 end
160 end
155 end
161 end
156 {:files => attached, :unsaved => obj.unsaved_attachments}
162 {:files => attached, :unsaved => obj.unsaved_attachments}
157 end
163 end
158
164
159 private
165 private
160 def sanitize_filename(value)
166 def sanitize_filename(value)
161 # get only the filename, not the whole path
167 # get only the filename, not the whole path
162 just_filename = value.gsub(/^.*(\\|\/)/, '')
168 just_filename = value.gsub(/^.*(\\|\/)/, '')
163 # NOTE: File.basename doesn't work right with Windows paths on Unix
169 # NOTE: File.basename doesn't work right with Windows paths on Unix
164 # INCORRECT: just_filename = File.basename(value.gsub('\\\\', '/'))
170 # INCORRECT: just_filename = File.basename(value.gsub('\\\\', '/'))
165
171
166 # Finally, replace all non alphanumeric, hyphens or periods with underscore
172 # Finally, replace all non alphanumeric, hyphens or periods with underscore
167 @filename = just_filename.gsub(/[^\w\.\-]/,'_')
173 @filename = just_filename.gsub(/[^\w\.\-]/,'_')
168 end
174 end
169
175
170 # Returns an ASCII or hashed filename
176 # Returns an ASCII or hashed filename
171 def self.disk_filename(filename)
177 def self.disk_filename(filename)
172 timestamp = DateTime.now.strftime("%y%m%d%H%M%S")
178 timestamp = DateTime.now.strftime("%y%m%d%H%M%S")
173 ascii = ''
179 ascii = ''
174 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
180 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
175 ascii = filename
181 ascii = filename
176 else
182 else
177 ascii = Digest::MD5.hexdigest(filename)
183 ascii = Digest::MD5.hexdigest(filename)
178 # keep the extension if any
184 # keep the extension if any
179 ascii << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
185 ascii << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
180 end
186 end
181 while File.exist?(File.join(@@storage_path, "#{timestamp}_#{ascii}"))
187 while File.exist?(File.join(@@storage_path, "#{timestamp}_#{ascii}"))
182 timestamp.succ!
188 timestamp.succ!
183 end
189 end
184 "#{timestamp}_#{ascii}"
190 "#{timestamp}_#{ascii}"
185 end
191 end
186 end
192 end
@@ -1,66 +1,92
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # redMine - project management software
3 # redMine - project management software
4 # Copyright (C) 2006-2007 Jean-Philippe Lang
4 # Copyright (C) 2006-2007 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.dirname(__FILE__) + '/../test_helper'
20 require File.dirname(__FILE__) + '/../test_helper'
21
21
22 class AttachmentTest < ActiveSupport::TestCase
22 class AttachmentTest < ActiveSupport::TestCase
23 fixtures :issues, :users
23 fixtures :issues, :users
24
24
25 def setup
25 def setup
26 end
26 end
27
27
28 def test_create
28 def test_create
29 a = Attachment.new(:container => Issue.find(1),
29 a = Attachment.new(:container => Issue.find(1),
30 :file => uploaded_test_file("testfile.txt", "text/plain"),
30 :file => uploaded_test_file("testfile.txt", "text/plain"),
31 :author => User.find(1))
31 :author => User.find(1))
32 assert a.save
32 assert a.save
33 assert_equal 'testfile.txt', a.filename
33 assert_equal 'testfile.txt', a.filename
34 assert_equal 59, a.filesize
34 assert_equal 59, a.filesize
35 assert_equal 'text/plain', a.content_type
35 assert_equal 'text/plain', a.content_type
36 assert_equal 0, a.downloads
36 assert_equal 0, a.downloads
37 assert_equal Digest::MD5.hexdigest(uploaded_test_file("testfile.txt", "text/plain").read), a.digest
37 assert_equal Digest::MD5.hexdigest(uploaded_test_file("testfile.txt", "text/plain").read), a.digest
38 assert File.exist?(a.diskfile)
38 assert File.exist?(a.diskfile)
39 end
39 end
40
40
41 def test_create_should_auto_assign_content_type
41 def test_create_should_auto_assign_content_type
42 a = Attachment.new(:container => Issue.find(1),
42 a = Attachment.new(:container => Issue.find(1),
43 :file => uploaded_test_file("testfile.txt", ""),
43 :file => uploaded_test_file("testfile.txt", ""),
44 :author => User.find(1))
44 :author => User.find(1))
45 assert a.save
45 assert a.save
46 assert_equal 'text/plain', a.content_type
46 assert_equal 'text/plain', a.content_type
47 end
47 end
48
48
49 def test_identical_attachments_at_the_same_time_should_not_overwrite
49 def test_identical_attachments_at_the_same_time_should_not_overwrite
50 a1 = Attachment.create!(:container => Issue.find(1),
50 a1 = Attachment.create!(:container => Issue.find(1),
51 :file => uploaded_test_file("testfile.txt", ""),
51 :file => uploaded_test_file("testfile.txt", ""),
52 :author => User.find(1))
52 :author => User.find(1))
53 a2 = Attachment.create!(:container => Issue.find(1),
53 a2 = Attachment.create!(:container => Issue.find(1),
54 :file => uploaded_test_file("testfile.txt", ""),
54 :file => uploaded_test_file("testfile.txt", ""),
55 :author => User.find(1))
55 :author => User.find(1))
56 assert a1.disk_filename != a2.disk_filename
56 assert a1.disk_filename != a2.disk_filename
57 end
57 end
58
58
59 def test_diskfilename
59 def test_diskfilename
60 assert Attachment.disk_filename("test_file.txt") =~ /^\d{12}_test_file.txt$/
60 assert Attachment.disk_filename("test_file.txt") =~ /^\d{12}_test_file.txt$/
61 assert_equal 'test_file.txt', Attachment.disk_filename("test_file.txt")[13..-1]
61 assert_equal 'test_file.txt', Attachment.disk_filename("test_file.txt")[13..-1]
62 assert_equal '770c509475505f37c2b8fb6030434d6b.txt', Attachment.disk_filename("test_accentué.txt")[13..-1]
62 assert_equal '770c509475505f37c2b8fb6030434d6b.txt', Attachment.disk_filename("test_accentué.txt")[13..-1]
63 assert_equal 'f8139524ebb8f32e51976982cd20a85d', Attachment.disk_filename("test_accentué")[13..-1]
63 assert_equal 'f8139524ebb8f32e51976982cd20a85d', Attachment.disk_filename("test_accentué")[13..-1]
64 assert_equal 'cbb5b0f30978ba03731d61f9f6d10011', Attachment.disk_filename("test_accentué.ça")[13..-1]
64 assert_equal 'cbb5b0f30978ba03731d61f9f6d10011', Attachment.disk_filename("test_accentué.ça")[13..-1]
65 end
65 end
66
67 context "Attachmnet#attach_files" do
68 should "add unsaved files to the object as unsaved attachments" do
69 # Max size of 0 to force Attachment creation failures
70 with_settings(:attachment_max_size => 0) do
71 # Mock out a file
72 @file = 'a_file.png'
73 @file.stubs(:size).returns(32)
74 @file.stubs(:original_filename).returns('a_file.png')
75 @file.stubs(:content_type).returns('image/png')
76 @file.stubs(:read).returns(false)
77
78 @project = Project.generate!
79 response = Attachment.attach_files(@project, {
80 '1' => {'file' => @file, 'description' => 'test'},
81 '2' => {'file' => @file, 'description' => 'test'}
82 })
83
84 assert response[:unsaved].present?
85 assert_equal 2, response[:unsaved].length
86 assert response[:unsaved].first.new_record?
87 assert response[:unsaved].second.new_record?
88 assert_equal response[:unsaved], @project.unsaved_attachments
89 end
90 end
91 end
66 end
92 end
General Comments 0
You need to be logged in to leave comments. Login now