##// END OF EJS Templates
Fixed: file uploads broken by r6312 (#8912)....
Jean-Philippe Lang -
r6200:6db66f718368
parent child
Show More
@@ -1,192 +1,193
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 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 = Redmine::Configuration['attachments_storage_path'] || "#{Rails.root}/files"
46 @@storage_path = Redmine::Configuration['attachments_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.info("Saving attachment '#{self.diskfile}' (#{@temp_file.size} bytes)")
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 @temp_file = nil
88 # Don't save the content type if it's longer than the authorized length
89 # Don't save the content type if it's longer than the authorized length
89 if self.content_type && self.content_type.length > 255
90 if self.content_type && self.content_type.length > 255
90 self.content_type = nil
91 self.content_type = nil
91 end
92 end
92 end
93 end
93
94
94 # Deletes file on the disk
95 # Deletes file on the disk
95 def after_destroy
96 def after_destroy
96 File.delete(diskfile) if !filename.blank? && File.exist?(diskfile)
97 File.delete(diskfile) if !filename.blank? && File.exist?(diskfile)
97 end
98 end
98
99
99 # Returns file's location on disk
100 # Returns file's location on disk
100 def diskfile
101 def diskfile
101 "#{@@storage_path}/#{self.disk_filename}"
102 "#{@@storage_path}/#{self.disk_filename}"
102 end
103 end
103
104
104 def increment_download
105 def increment_download
105 increment!(:downloads)
106 increment!(:downloads)
106 end
107 end
107
108
108 def project
109 def project
109 container.project
110 container.project
110 end
111 end
111
112
112 def visible?(user=User.current)
113 def visible?(user=User.current)
113 container.attachments_visible?(user)
114 container.attachments_visible?(user)
114 end
115 end
115
116
116 def deletable?(user=User.current)
117 def deletable?(user=User.current)
117 container.attachments_deletable?(user)
118 container.attachments_deletable?(user)
118 end
119 end
119
120
120 def image?
121 def image?
121 self.filename =~ /\.(jpe?g|gif|png)$/i
122 self.filename =~ /\.(jpe?g|gif|png)$/i
122 end
123 end
123
124
124 def is_text?
125 def is_text?
125 Redmine::MimeType.is_type?('text', filename)
126 Redmine::MimeType.is_type?('text', filename)
126 end
127 end
127
128
128 def is_diff?
129 def is_diff?
129 self.filename =~ /\.(patch|diff)$/i
130 self.filename =~ /\.(patch|diff)$/i
130 end
131 end
131
132
132 # Returns true if the file is readable
133 # Returns true if the file is readable
133 def readable?
134 def readable?
134 File.readable?(diskfile)
135 File.readable?(diskfile)
135 end
136 end
136
137
137 # Bulk attaches a set of files to an object
138 # Bulk attaches a set of files to an object
138 #
139 #
139 # Returns a Hash of the results:
140 # Returns a Hash of the results:
140 # :files => array of the attached files
141 # :files => array of the attached files
141 # :unsaved => array of the files that could not be attached
142 # :unsaved => array of the files that could not be attached
142 def self.attach_files(obj, attachments)
143 def self.attach_files(obj, attachments)
143 attached = []
144 attached = []
144 if attachments && attachments.is_a?(Hash)
145 if attachments && attachments.is_a?(Hash)
145 attachments.each_value do |attachment|
146 attachments.each_value do |attachment|
146 file = attachment['file']
147 file = attachment['file']
147 next unless file && file.size > 0
148 next unless file && file.size > 0
148 a = Attachment.create(:container => obj,
149 a = Attachment.create(:container => obj,
149 :file => file,
150 :file => file,
150 :description => attachment['description'].to_s.strip,
151 :description => attachment['description'].to_s.strip,
151 :author => User.current)
152 :author => User.current)
152 obj.attachments << a
153 obj.attachments << a
153
154
154 if a.new_record?
155 if a.new_record?
155 obj.unsaved_attachments ||= []
156 obj.unsaved_attachments ||= []
156 obj.unsaved_attachments << a
157 obj.unsaved_attachments << a
157 else
158 else
158 attached << a
159 attached << a
159 end
160 end
160 end
161 end
161 end
162 end
162 {:files => attached, :unsaved => obj.unsaved_attachments}
163 {:files => attached, :unsaved => obj.unsaved_attachments}
163 end
164 end
164
165
165 private
166 private
166 def sanitize_filename(value)
167 def sanitize_filename(value)
167 # get only the filename, not the whole path
168 # get only the filename, not the whole path
168 just_filename = value.gsub(/^.*(\\|\/)/, '')
169 just_filename = value.gsub(/^.*(\\|\/)/, '')
169 # NOTE: File.basename doesn't work right with Windows paths on Unix
170 # NOTE: File.basename doesn't work right with Windows paths on Unix
170 # INCORRECT: just_filename = File.basename(value.gsub('\\\\', '/'))
171 # INCORRECT: just_filename = File.basename(value.gsub('\\\\', '/'))
171
172
172 # Finally, replace all non alphanumeric, hyphens or periods with underscore
173 # Finally, replace all non alphanumeric, hyphens or periods with underscore
173 @filename = just_filename.gsub(/[^\w\.\-]/,'_')
174 @filename = just_filename.gsub(/[^\w\.\-]/,'_')
174 end
175 end
175
176
176 # Returns an ASCII or hashed filename
177 # Returns an ASCII or hashed filename
177 def self.disk_filename(filename)
178 def self.disk_filename(filename)
178 timestamp = DateTime.now.strftime("%y%m%d%H%M%S")
179 timestamp = DateTime.now.strftime("%y%m%d%H%M%S")
179 ascii = ''
180 ascii = ''
180 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
181 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
181 ascii = filename
182 ascii = filename
182 else
183 else
183 ascii = Digest::MD5.hexdigest(filename)
184 ascii = Digest::MD5.hexdigest(filename)
184 # keep the extension if any
185 # keep the extension if any
185 ascii << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
186 ascii << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
186 end
187 end
187 while File.exist?(File.join(@@storage_path, "#{timestamp}_#{ascii}"))
188 while File.exist?(File.join(@@storage_path, "#{timestamp}_#{ascii}"))
188 timestamp.succ!
189 timestamp.succ!
189 end
190 end
190 "#{timestamp}_#{ascii}"
191 "#{timestamp}_#{ascii}"
191 end
192 end
192 end
193 end
@@ -1,85 +1,106
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2011 Jean-Philippe Lang
4 # Copyright (C) 2006-2011 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 :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 '1478adae0d4eb06d35897518540e25d6', a.digest
37 assert_equal '1478adae0d4eb06d35897518540e25d6', a.digest
38 assert File.exist?(a.diskfile)
38 assert File.exist?(a.diskfile)
39 assert_equal 59, File.size(a.diskfile)
39 end
40 end
40
41
41 def test_create_should_auto_assign_content_type
42 def test_create_should_auto_assign_content_type
42 a = Attachment.new(:container => Issue.find(1),
43 a = Attachment.new(:container => Issue.find(1),
43 :file => uploaded_test_file("testfile.txt", ""),
44 :file => uploaded_test_file("testfile.txt", ""),
44 :author => User.find(1))
45 :author => User.find(1))
45 assert a.save
46 assert a.save
46 assert_equal 'text/plain', a.content_type
47 assert_equal 'text/plain', a.content_type
47 end
48 end
48
49
49 def test_identical_attachments_at_the_same_time_should_not_overwrite
50 def test_identical_attachments_at_the_same_time_should_not_overwrite
50 a1 = Attachment.create!(:container => Issue.find(1),
51 a1 = Attachment.create!(:container => Issue.find(1),
51 :file => uploaded_test_file("testfile.txt", ""),
52 :file => uploaded_test_file("testfile.txt", ""),
52 :author => User.find(1))
53 :author => User.find(1))
53 a2 = Attachment.create!(:container => Issue.find(1),
54 a2 = Attachment.create!(:container => Issue.find(1),
54 :file => uploaded_test_file("testfile.txt", ""),
55 :file => uploaded_test_file("testfile.txt", ""),
55 :author => User.find(1))
56 :author => User.find(1))
56 assert a1.disk_filename != a2.disk_filename
57 assert a1.disk_filename != a2.disk_filename
57 end
58 end
58
59
59 def test_diskfilename
60 def test_diskfilename
60 assert Attachment.disk_filename("test_file.txt") =~ /^\d{12}_test_file.txt$/
61 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]
62 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]
63 assert_equal '770c509475505f37c2b8fb6030434d6b.txt', Attachment.disk_filename("test_accentuΓ©.txt")[13..-1]
63 assert_equal 'f8139524ebb8f32e51976982cd20a85d', Attachment.disk_filename("test_accentuΓ©")[13..-1]
64 assert_equal 'f8139524ebb8f32e51976982cd20a85d', Attachment.disk_filename("test_accentuΓ©")[13..-1]
64 assert_equal 'cbb5b0f30978ba03731d61f9f6d10011', Attachment.disk_filename("test_accentuΓ©.Γ§a")[13..-1]
65 assert_equal 'cbb5b0f30978ba03731d61f9f6d10011', Attachment.disk_filename("test_accentuΓ©.Γ§a")[13..-1]
65 end
66 end
66
67
67 context "Attachmnet#attach_files" do
68 context "Attachmnet.attach_files" do
69 should "attach the file" do
70 issue = Issue.first
71 assert_difference 'Attachment.count' do
72 Attachment.attach_files(issue,
73 '1' => {
74 'file' => uploaded_test_file('testfile.txt', 'text/plain'),
75 'description' => 'test'
76 })
77 end
78
79 attachment = Attachment.first(:order => 'id DESC')
80 assert_equal issue, attachment.container
81 assert_equal 'testfile.txt', attachment.filename
82 assert_equal 59, attachment.filesize
83 assert_equal 'test', attachment.description
84 assert_equal 'text/plain', attachment.content_type
85 assert File.exists?(attachment.diskfile)
86 assert_equal 59, File.size(attachment.diskfile)
87 end
88
68 should "add unsaved files to the object as unsaved attachments" do
89 should "add unsaved files to the object as unsaved attachments" do
69 # Max size of 0 to force Attachment creation failures
90 # Max size of 0 to force Attachment creation failures
70 with_settings(:attachment_max_size => 0) do
91 with_settings(:attachment_max_size => 0) do
71 @project = Project.generate!
92 @project = Project.generate!
72 response = Attachment.attach_files(@project, {
93 response = Attachment.attach_files(@project, {
73 '1' => {'file' => mock_file, 'description' => 'test'},
94 '1' => {'file' => mock_file, 'description' => 'test'},
74 '2' => {'file' => mock_file, 'description' => 'test'}
95 '2' => {'file' => mock_file, 'description' => 'test'}
75 })
96 })
76
97
77 assert response[:unsaved].present?
98 assert response[:unsaved].present?
78 assert_equal 2, response[:unsaved].length
99 assert_equal 2, response[:unsaved].length
79 assert response[:unsaved].first.new_record?
100 assert response[:unsaved].first.new_record?
80 assert response[:unsaved].second.new_record?
101 assert response[:unsaved].second.new_record?
81 assert_equal response[:unsaved], @project.unsaved_attachments
102 assert_equal response[:unsaved], @project.unsaved_attachments
82 end
103 end
83 end
104 end
84 end
105 end
85 end
106 end
General Comments 0
You need to be logged in to leave comments. Login now