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