##// END OF EJS Templates
Rails3: replace deprecate 'validate' instance method to declared validation method at Attachment model....
Toshi MARUYAMA -
r6594:af7ec57f1500
parent child
Show More
@@ -1,193 +1,194
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 validate :validate_max_file_size
27
28
28 acts_as_event :title => :filename,
29 acts_as_event :title => :filename,
29 :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}}
30 :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}}
30
31
31 acts_as_activity_provider :type => 'files',
32 acts_as_activity_provider :type => 'files',
32 :permission => :view_files,
33 :permission => :view_files,
33 :author_key => :author_id,
34 :author_key => :author_id,
34 :find_options => {:select => "#{Attachment.table_name}.*",
35 :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 " +
36 :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 )"}
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 )"}
37
38
38 acts_as_activity_provider :type => 'documents',
39 acts_as_activity_provider :type => 'documents',
39 :permission => :view_documents,
40 :permission => :view_documents,
40 :author_key => :author_id,
41 :author_key => :author_id,
41 :find_options => {:select => "#{Attachment.table_name}.*",
42 :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 " +
43 :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"}
44 "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"}
44
45
45 cattr_accessor :storage_path
46 cattr_accessor :storage_path
46 @@storage_path = Redmine::Configuration['attachments_storage_path'] || "#{Rails.root}/files"
47 @@storage_path = Redmine::Configuration['attachments_storage_path'] || "#{Rails.root}/files"
47
48
48 def validate
49 def validate_max_file_size
49 if self.filesize > Setting.attachment_max_size.to_i.kilobytes
50 if self.filesize > Setting.attachment_max_size.to_i.kilobytes
50 errors.add(:base, :too_long, :count => Setting.attachment_max_size.to_i.kilobytes)
51 errors.add(:base, :too_long, :count => Setting.attachment_max_size.to_i.kilobytes)
51 end
52 end
52 end
53 end
53
54
54 def file=(incoming_file)
55 def file=(incoming_file)
55 unless incoming_file.nil?
56 unless incoming_file.nil?
56 @temp_file = incoming_file
57 @temp_file = incoming_file
57 if @temp_file.size > 0
58 if @temp_file.size > 0
58 self.filename = sanitize_filename(@temp_file.original_filename)
59 self.filename = sanitize_filename(@temp_file.original_filename)
59 self.disk_filename = Attachment.disk_filename(filename)
60 self.disk_filename = Attachment.disk_filename(filename)
60 self.content_type = @temp_file.content_type.to_s.chomp
61 self.content_type = @temp_file.content_type.to_s.chomp
61 if content_type.blank?
62 if content_type.blank?
62 self.content_type = Redmine::MimeType.of(filename)
63 self.content_type = Redmine::MimeType.of(filename)
63 end
64 end
64 self.filesize = @temp_file.size
65 self.filesize = @temp_file.size
65 end
66 end
66 end
67 end
67 end
68 end
68
69
69 def file
70 def file
70 nil
71 nil
71 end
72 end
72
73
73 # Copies the temporary file to its final location
74 # Copies the temporary file to its final location
74 # and computes its MD5 hash
75 # and computes its MD5 hash
75 def before_save
76 def before_save
76 if @temp_file && (@temp_file.size > 0)
77 if @temp_file && (@temp_file.size > 0)
77 logger.info("Saving attachment '#{self.diskfile}' (#{@temp_file.size} bytes)")
78 logger.info("Saving attachment '#{self.diskfile}' (#{@temp_file.size} bytes)")
78 md5 = Digest::MD5.new
79 md5 = Digest::MD5.new
79 File.open(diskfile, "wb") do |f|
80 File.open(diskfile, "wb") do |f|
80 buffer = ""
81 buffer = ""
81 while (buffer = @temp_file.read(8192))
82 while (buffer = @temp_file.read(8192))
82 f.write(buffer)
83 f.write(buffer)
83 md5.update(buffer)
84 md5.update(buffer)
84 end
85 end
85 end
86 end
86 self.digest = md5.hexdigest
87 self.digest = md5.hexdigest
87 end
88 end
88 @temp_file = nil
89 @temp_file = nil
89 # Don't save the content type if it's longer than the authorized length
90 # Don't save the content type if it's longer than the authorized length
90 if self.content_type && self.content_type.length > 255
91 if self.content_type && self.content_type.length > 255
91 self.content_type = nil
92 self.content_type = nil
92 end
93 end
93 end
94 end
94
95
95 # Deletes file on the disk
96 # Deletes file on the disk
96 def after_destroy
97 def after_destroy
97 File.delete(diskfile) if !filename.blank? && File.exist?(diskfile)
98 File.delete(diskfile) if !filename.blank? && File.exist?(diskfile)
98 end
99 end
99
100
100 # Returns file's location on disk
101 # Returns file's location on disk
101 def diskfile
102 def diskfile
102 "#{@@storage_path}/#{self.disk_filename}"
103 "#{@@storage_path}/#{self.disk_filename}"
103 end
104 end
104
105
105 def increment_download
106 def increment_download
106 increment!(:downloads)
107 increment!(:downloads)
107 end
108 end
108
109
109 def project
110 def project
110 container.project
111 container.project
111 end
112 end
112
113
113 def visible?(user=User.current)
114 def visible?(user=User.current)
114 container.attachments_visible?(user)
115 container.attachments_visible?(user)
115 end
116 end
116
117
117 def deletable?(user=User.current)
118 def deletable?(user=User.current)
118 container.attachments_deletable?(user)
119 container.attachments_deletable?(user)
119 end
120 end
120
121
121 def image?
122 def image?
122 self.filename =~ /\.(jpe?g|gif|png)$/i
123 self.filename =~ /\.(jpe?g|gif|png)$/i
123 end
124 end
124
125
125 def is_text?
126 def is_text?
126 Redmine::MimeType.is_type?('text', filename)
127 Redmine::MimeType.is_type?('text', filename)
127 end
128 end
128
129
129 def is_diff?
130 def is_diff?
130 self.filename =~ /\.(patch|diff)$/i
131 self.filename =~ /\.(patch|diff)$/i
131 end
132 end
132
133
133 # Returns true if the file is readable
134 # Returns true if the file is readable
134 def readable?
135 def readable?
135 File.readable?(diskfile)
136 File.readable?(diskfile)
136 end
137 end
137
138
138 # Bulk attaches a set of files to an object
139 # Bulk attaches a set of files to an object
139 #
140 #
140 # Returns a Hash of the results:
141 # Returns a Hash of the results:
141 # :files => array of the attached files
142 # :files => array of the attached files
142 # :unsaved => array of the files that could not be attached
143 # :unsaved => array of the files that could not be attached
143 def self.attach_files(obj, attachments)
144 def self.attach_files(obj, attachments)
144 attached = []
145 attached = []
145 if attachments && attachments.is_a?(Hash)
146 if attachments && attachments.is_a?(Hash)
146 attachments.each_value do |attachment|
147 attachments.each_value do |attachment|
147 file = attachment['file']
148 file = attachment['file']
148 next unless file && file.size > 0
149 next unless file && file.size > 0
149 a = Attachment.create(:container => obj,
150 a = Attachment.create(:container => obj,
150 :file => file,
151 :file => file,
151 :description => attachment['description'].to_s.strip,
152 :description => attachment['description'].to_s.strip,
152 :author => User.current)
153 :author => User.current)
153 obj.attachments << a
154 obj.attachments << a
154
155
155 if a.new_record?
156 if a.new_record?
156 obj.unsaved_attachments ||= []
157 obj.unsaved_attachments ||= []
157 obj.unsaved_attachments << a
158 obj.unsaved_attachments << a
158 else
159 else
159 attached << a
160 attached << a
160 end
161 end
161 end
162 end
162 end
163 end
163 {:files => attached, :unsaved => obj.unsaved_attachments}
164 {:files => attached, :unsaved => obj.unsaved_attachments}
164 end
165 end
165
166
166 private
167 private
167 def sanitize_filename(value)
168 def sanitize_filename(value)
168 # get only the filename, not the whole path
169 # get only the filename, not the whole path
169 just_filename = value.gsub(/^.*(\\|\/)/, '')
170 just_filename = value.gsub(/^.*(\\|\/)/, '')
170 # NOTE: File.basename doesn't work right with Windows paths on Unix
171 # NOTE: File.basename doesn't work right with Windows paths on Unix
171 # INCORRECT: just_filename = File.basename(value.gsub('\\\\', '/'))
172 # INCORRECT: just_filename = File.basename(value.gsub('\\\\', '/'))
172
173
173 # Finally, replace all non alphanumeric, hyphens or periods with underscore
174 # Finally, replace all non alphanumeric, hyphens or periods with underscore
174 @filename = just_filename.gsub(/[^\w\.\-]/,'_')
175 @filename = just_filename.gsub(/[^\w\.\-]/,'_')
175 end
176 end
176
177
177 # Returns an ASCII or hashed filename
178 # Returns an ASCII or hashed filename
178 def self.disk_filename(filename)
179 def self.disk_filename(filename)
179 timestamp = DateTime.now.strftime("%y%m%d%H%M%S")
180 timestamp = DateTime.now.strftime("%y%m%d%H%M%S")
180 ascii = ''
181 ascii = ''
181 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
182 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
182 ascii = filename
183 ascii = filename
183 else
184 else
184 ascii = Digest::MD5.hexdigest(filename)
185 ascii = Digest::MD5.hexdigest(filename)
185 # keep the extension if any
186 # keep the extension if any
186 ascii << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
187 ascii << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
187 end
188 end
188 while File.exist?(File.join(@@storage_path, "#{timestamp}_#{ascii}"))
189 while File.exist?(File.join(@@storage_path, "#{timestamp}_#{ascii}"))
189 timestamp.succ!
190 timestamp.succ!
190 end
191 end
191 "#{timestamp}_#{ascii}"
192 "#{timestamp}_#{ascii}"
192 end
193 end
193 end
194 end
General Comments 0
You need to be logged in to leave comments. Login now