##// END OF EJS Templates
Fixes memory consumption on file upload (#3116)....
Jean-Philippe Lang -
r2578:a59854ef9d41
parent child
Show More
@@ -1,153 +1,152
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 self.filesize = @temp_file.size
61 self.filesize = @temp_file.size
62 end
62 end
63 end
63 end
64 end
64 end
65
65
66 def file
66 def file
67 nil
67 nil
68 end
68 end
69
69
70 # Copy temp file to its final location
70 # Copies the temporary file to its final location
71 # and computes its MD5 hash
71 def before_save
72 def before_save
72 if @temp_file && (@temp_file.size > 0)
73 if @temp_file && (@temp_file.size > 0)
73 logger.debug("saving '#{self.diskfile}'")
74 logger.debug("saving '#{self.diskfile}'")
75 md5 = Digest::MD5.new
74 File.open(diskfile, "wb") do |f|
76 File.open(diskfile, "wb") do |f|
75 f.write(@temp_file.read)
77 buffer = ""
78 while (buffer = @temp_file.read(8192))
79 f.write(buffer)
80 md5.update(buffer)
81 end
76 end
82 end
77 self.digest = self.class.digest(diskfile)
83 self.digest = md5.hexdigest
78 end
84 end
79 # Don't save the content type if it's longer than the authorized length
85 # Don't save the content type if it's longer than the authorized length
80 if self.content_type && self.content_type.length > 255
86 if self.content_type && self.content_type.length > 255
81 self.content_type = nil
87 self.content_type = nil
82 end
88 end
83 end
89 end
84
90
85 # Deletes file on the disk
91 # Deletes file on the disk
86 def after_destroy
92 def after_destroy
87 File.delete(diskfile) if !filename.blank? && File.exist?(diskfile)
93 File.delete(diskfile) if !filename.blank? && File.exist?(diskfile)
88 end
94 end
89
95
90 # Returns file's location on disk
96 # Returns file's location on disk
91 def diskfile
97 def diskfile
92 "#{@@storage_path}/#{self.disk_filename}"
98 "#{@@storage_path}/#{self.disk_filename}"
93 end
99 end
94
100
95 def increment_download
101 def increment_download
96 increment!(:downloads)
102 increment!(:downloads)
97 end
103 end
98
104
99 def project
105 def project
100 container.project
106 container.project
101 end
107 end
102
108
103 def visible?(user=User.current)
109 def visible?(user=User.current)
104 container.attachments_visible?(user)
110 container.attachments_visible?(user)
105 end
111 end
106
112
107 def deletable?(user=User.current)
113 def deletable?(user=User.current)
108 container.attachments_deletable?(user)
114 container.attachments_deletable?(user)
109 end
115 end
110
116
111 def image?
117 def image?
112 self.filename =~ /\.(jpe?g|gif|png)$/i
118 self.filename =~ /\.(jpe?g|gif|png)$/i
113 end
119 end
114
120
115 def is_text?
121 def is_text?
116 Redmine::MimeType.is_type?('text', filename)
122 Redmine::MimeType.is_type?('text', filename)
117 end
123 end
118
124
119 def is_diff?
125 def is_diff?
120 self.filename =~ /\.(patch|diff)$/i
126 self.filename =~ /\.(patch|diff)$/i
121 end
127 end
122
128
123 private
129 private
124 def sanitize_filename(value)
130 def sanitize_filename(value)
125 # get only the filename, not the whole path
131 # get only the filename, not the whole path
126 just_filename = value.gsub(/^.*(\\|\/)/, '')
132 just_filename = value.gsub(/^.*(\\|\/)/, '')
127 # NOTE: File.basename doesn't work right with Windows paths on Unix
133 # NOTE: File.basename doesn't work right with Windows paths on Unix
128 # INCORRECT: just_filename = File.basename(value.gsub('\\\\', '/'))
134 # INCORRECT: just_filename = File.basename(value.gsub('\\\\', '/'))
129
135
130 # Finally, replace all non alphanumeric, hyphens or periods with underscore
136 # Finally, replace all non alphanumeric, hyphens or periods with underscore
131 @filename = just_filename.gsub(/[^\w\.\-]/,'_')
137 @filename = just_filename.gsub(/[^\w\.\-]/,'_')
132 end
138 end
133
139
134 # Returns an ASCII or hashed filename
140 # Returns an ASCII or hashed filename
135 def self.disk_filename(filename)
141 def self.disk_filename(filename)
136 df = DateTime.now.strftime("%y%m%d%H%M%S") + "_"
142 df = DateTime.now.strftime("%y%m%d%H%M%S") + "_"
137 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
143 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
138 df << filename
144 df << filename
139 else
145 else
140 df << Digest::MD5.hexdigest(filename)
146 df << Digest::MD5.hexdigest(filename)
141 # keep the extension if any
147 # keep the extension if any
142 df << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
148 df << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
143 end
149 end
144 df
150 df
145 end
151 end
146
147 # Returns the MD5 digest of the file at given path
148 def self.digest(filename)
149 File.open(filename, 'rb') do |f|
150 Digest::MD5.hexdigest(f.read)
151 end
152 end
153 end
152 end
@@ -1,37 +1,46
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 File.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19
19
20 class AttachmentTest < Test::Unit::TestCase
20 class AttachmentTest < Test::Unit::TestCase
21 fixtures :issues, :users
21 fixtures :issues, :users
22
22
23 def setup
23 def setup
24 end
24 end
25
26 def test_create
27 a = Attachment.new(:container => Issue.find(1),
28 :file => test_uploaded_file("testfile.txt", "text/plain"),
29 :author => User.find(1))
30 assert a.save
31 assert_equal 'testfile.txt', a.filename
32 assert_equal 59, a.filesize
33 assert_equal 'text/plain', a.content_type
34 assert_equal 0, a.downloads
35 assert_equal Digest::MD5.hexdigest(test_uploaded_file("testfile.txt", "text/plain").read), a.digest
36 assert File.exist?(a.diskfile)
37 end
25
38
26 def test_diskfilename
39 def test_diskfilename
27 assert Attachment.disk_filename("test_file.txt") =~ /^\d{12}_test_file.txt$/
40 assert Attachment.disk_filename("test_file.txt") =~ /^\d{12}_test_file.txt$/
28 assert_equal 'test_file.txt', Attachment.disk_filename("test_file.txt")[13..-1]
41 assert_equal 'test_file.txt', Attachment.disk_filename("test_file.txt")[13..-1]
29 assert_equal '770c509475505f37c2b8fb6030434d6b.txt', Attachment.disk_filename("test_accentué.txt")[13..-1]
42 assert_equal '770c509475505f37c2b8fb6030434d6b.txt', Attachment.disk_filename("test_accentué.txt")[13..-1]
30 assert_equal 'f8139524ebb8f32e51976982cd20a85d', Attachment.disk_filename("test_accentué")[13..-1]
43 assert_equal 'f8139524ebb8f32e51976982cd20a85d', Attachment.disk_filename("test_accentué")[13..-1]
31 assert_equal 'cbb5b0f30978ba03731d61f9f6d10011', Attachment.disk_filename("test_accentué.ça")[13..-1]
44 assert_equal 'cbb5b0f30978ba03731d61f9f6d10011', Attachment.disk_filename("test_accentué.ça")[13..-1]
32 end
45 end
33
34 def test_digest
35 assert_equal '1478adae0d4eb06d35897518540e25d6', Attachment.digest(Test::Unit::TestCase.fixture_path + "/files/testfile.txt")
36 end
37 end
46 end
General Comments 0
You need to be logged in to leave comments. Login now