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