##// END OF EJS Templates
Code cleanup....
Jean-Philippe Lang -
r9534:e4332ba35fea
parent child
Show More
@@ -1,239 +1,239
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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 :filename, :author
24 validates_presence_of :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'] || File.join(Rails.root, "files")
47 @@storage_path = Redmine::Configuration['attachments_storage_path'] || File.join(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 after_destroy :delete_from_disk
51
51
52 # Returns an unsaved copy of the attachment
52 # Returns an unsaved copy of the attachment
53 def copy(attributes=nil)
53 def copy(attributes=nil)
54 copy = self.class.new
54 copy = self.class.new
55 copy.attributes = self.attributes.dup.except("id", "downloads")
55 copy.attributes = self.attributes.dup.except("id", "downloads")
56 copy.attributes = attributes if attributes
56 copy.attributes = attributes if attributes
57 copy
57 copy
58 end
58 end
59
59
60 def validate_max_file_size
60 def validate_max_file_size
61 if @temp_file && self.filesize > Setting.attachment_max_size.to_i.kilobytes
61 if @temp_file && self.filesize > Setting.attachment_max_size.to_i.kilobytes
62 errors.add(:base, l(:error_attachment_too_big, :max_size => Setting.attachment_max_size.to_i.kilobytes))
62 errors.add(:base, l(:error_attachment_too_big, :max_size => Setting.attachment_max_size.to_i.kilobytes))
63 end
63 end
64 end
64 end
65
65
66 def file=(incoming_file)
66 def file=(incoming_file)
67 unless incoming_file.nil?
67 unless incoming_file.nil?
68 @temp_file = incoming_file
68 @temp_file = incoming_file
69 if @temp_file.size > 0
69 if @temp_file.size > 0
70 if @temp_file.respond_to?(:original_filename)
70 if @temp_file.respond_to?(:original_filename)
71 self.filename = @temp_file.original_filename
71 self.filename = @temp_file.original_filename
72 self.filename.force_encoding("UTF-8") if filename.respond_to?(:force_encoding)
72 self.filename.force_encoding("UTF-8") if filename.respond_to?(:force_encoding)
73 end
73 end
74 if @temp_file.respond_to?(:content_type)
74 if @temp_file.respond_to?(:content_type)
75 self.content_type = @temp_file.content_type.to_s.chomp
75 self.content_type = @temp_file.content_type.to_s.chomp
76 end
76 end
77 if content_type.blank? && filename.present?
77 if content_type.blank? && filename.present?
78 self.content_type = Redmine::MimeType.of(filename)
78 self.content_type = Redmine::MimeType.of(filename)
79 end
79 end
80 self.filesize = @temp_file.size
80 self.filesize = @temp_file.size
81 end
81 end
82 end
82 end
83 end
83 end
84
84
85 def file
85 def file
86 nil
86 nil
87 end
87 end
88
88
89 def filename=(arg)
89 def filename=(arg)
90 write_attribute :filename, sanitize_filename(arg.to_s)
90 write_attribute :filename, sanitize_filename(arg.to_s)
91 if new_record? && disk_filename.blank?
91 if new_record? && disk_filename.blank?
92 self.disk_filename = Attachment.disk_filename(filename)
92 self.disk_filename = Attachment.disk_filename(filename)
93 end
93 end
94 filename
94 filename
95 end
95 end
96
96
97 # Copies the temporary file to its final location
97 # Copies the temporary file to its final location
98 # and computes its MD5 hash
98 # and computes its MD5 hash
99 def files_to_final_location
99 def files_to_final_location
100 if @temp_file && (@temp_file.size > 0)
100 if @temp_file && (@temp_file.size > 0)
101 logger.info("Saving attachment '#{self.diskfile}' (#{@temp_file.size} bytes)")
101 logger.info("Saving attachment '#{self.diskfile}' (#{@temp_file.size} bytes)")
102 md5 = Digest::MD5.new
102 md5 = Digest::MD5.new
103 File.open(diskfile, "wb") do |f|
103 File.open(diskfile, "wb") do |f|
104 if @temp_file.respond_to?(:read)
104 if @temp_file.respond_to?(:read)
105 buffer = ""
105 buffer = ""
106 while (buffer = @temp_file.read(8192))
106 while (buffer = @temp_file.read(8192))
107 f.write(buffer)
107 f.write(buffer)
108 md5.update(buffer)
108 md5.update(buffer)
109 end
109 end
110 else
110 else
111 f.write(@temp_file)
111 f.write(@temp_file)
112 md5.update(@temp_file)
112 md5.update(@temp_file)
113 end
113 end
114 end
114 end
115 self.digest = md5.hexdigest
115 self.digest = md5.hexdigest
116 end
116 end
117 @temp_file = nil
117 @temp_file = nil
118 # Don't save the content type if it's longer than the authorized length
118 # Don't save the content type if it's longer than the authorized length
119 if self.content_type && self.content_type.length > 255
119 if self.content_type && self.content_type.length > 255
120 self.content_type = nil
120 self.content_type = nil
121 end
121 end
122 end
122 end
123
123
124 # Deletes the file from the file system if it's not referenced by other attachments
124 # Deletes the file from the file system if it's not referenced by other attachments
125 def delete_from_disk
125 def delete_from_disk
126 if Attachment.first(:conditions => ["disk_filename = ? AND id <> ?", disk_filename, id]).nil?
126 if Attachment.where("disk_filename = ? AND id <> ?", disk_filename, id).empty?
127 delete_from_disk!
127 delete_from_disk!
128 end
128 end
129 end
129 end
130
130
131 # Returns file's location on disk
131 # Returns file's location on disk
132 def diskfile
132 def diskfile
133 File.join(self.class.storage_path, disk_filename.to_s)
133 File.join(self.class.storage_path, disk_filename.to_s)
134 end
134 end
135
135
136 def increment_download
136 def increment_download
137 increment!(:downloads)
137 increment!(:downloads)
138 end
138 end
139
139
140 def project
140 def project
141 container.try(:project)
141 container.try(:project)
142 end
142 end
143
143
144 def visible?(user=User.current)
144 def visible?(user=User.current)
145 container && container.attachments_visible?(user)
145 container && container.attachments_visible?(user)
146 end
146 end
147
147
148 def deletable?(user=User.current)
148 def deletable?(user=User.current)
149 container && container.attachments_deletable?(user)
149 container && container.attachments_deletable?(user)
150 end
150 end
151
151
152 def image?
152 def image?
153 self.filename =~ /\.(bmp|gif|jpg|jpe|jpeg|png)$/i
153 self.filename =~ /\.(bmp|gif|jpg|jpe|jpeg|png)$/i
154 end
154 end
155
155
156 def is_text?
156 def is_text?
157 Redmine::MimeType.is_type?('text', filename)
157 Redmine::MimeType.is_type?('text', filename)
158 end
158 end
159
159
160 def is_diff?
160 def is_diff?
161 self.filename =~ /\.(patch|diff)$/i
161 self.filename =~ /\.(patch|diff)$/i
162 end
162 end
163
163
164 # Returns true if the file is readable
164 # Returns true if the file is readable
165 def readable?
165 def readable?
166 File.readable?(diskfile)
166 File.readable?(diskfile)
167 end
167 end
168
168
169 # Returns the attachment token
169 # Returns the attachment token
170 def token
170 def token
171 "#{id}.#{digest}"
171 "#{id}.#{digest}"
172 end
172 end
173
173
174 # Finds an attachment that matches the given token and that has no container
174 # Finds an attachment that matches the given token and that has no container
175 def self.find_by_token(token)
175 def self.find_by_token(token)
176 if token.to_s =~ /^(\d+)\.([0-9a-f]+)$/
176 if token.to_s =~ /^(\d+)\.([0-9a-f]+)$/
177 attachment_id, attachment_digest = $1, $2
177 attachment_id, attachment_digest = $1, $2
178 attachment = Attachment.where(:id => attachment_id, :digest => attachment_digest).first
178 attachment = Attachment.where(:id => attachment_id, :digest => attachment_digest).first
179 if attachment && attachment.container.nil?
179 if attachment && attachment.container.nil?
180 attachment
180 attachment
181 end
181 end
182 end
182 end
183 end
183 end
184
184
185 # Bulk attaches a set of files to an object
185 # Bulk attaches a set of files to an object
186 #
186 #
187 # Returns a Hash of the results:
187 # Returns a Hash of the results:
188 # :files => array of the attached files
188 # :files => array of the attached files
189 # :unsaved => array of the files that could not be attached
189 # :unsaved => array of the files that could not be attached
190 def self.attach_files(obj, attachments)
190 def self.attach_files(obj, attachments)
191 result = obj.save_attachments(attachments, User.current)
191 result = obj.save_attachments(attachments, User.current)
192 obj.attach_saved_attachments
192 obj.attach_saved_attachments
193 result
193 result
194 end
194 end
195
195
196 def self.latest_attach(attachments, filename)
196 def self.latest_attach(attachments, filename)
197 attachments.sort_by(&:created_on).reverse.detect {
197 attachments.sort_by(&:created_on).reverse.detect {
198 |att| att.filename.downcase == filename.downcase
198 |att| att.filename.downcase == filename.downcase
199 }
199 }
200 end
200 end
201
201
202 def self.prune(age=1.day)
202 def self.prune(age=1.day)
203 Attachment.where("created_on < ? AND (container_type IS NULL OR container_type = '')", Time.now - age).destroy_all
203 Attachment.where("created_on < ? AND (container_type IS NULL OR container_type = '')", Time.now - age).destroy_all
204 end
204 end
205
205
206 private
206 private
207
207
208 # Physically deletes the file from the file system
208 # Physically deletes the file from the file system
209 def delete_from_disk!
209 def delete_from_disk!
210 if disk_filename.present? && File.exist?(diskfile)
210 if disk_filename.present? && File.exist?(diskfile)
211 File.delete(diskfile)
211 File.delete(diskfile)
212 end
212 end
213 end
213 end
214
214
215 def sanitize_filename(value)
215 def sanitize_filename(value)
216 # get only the filename, not the whole path
216 # get only the filename, not the whole path
217 just_filename = value.gsub(/^.*(\\|\/)/, '')
217 just_filename = value.gsub(/^.*(\\|\/)/, '')
218
218
219 # Finally, replace invalid characters with underscore
219 # Finally, replace invalid characters with underscore
220 @filename = just_filename.gsub(/[\/\?\%\*\:\|\"\'<>]+/, '_')
220 @filename = just_filename.gsub(/[\/\?\%\*\:\|\"\'<>]+/, '_')
221 end
221 end
222
222
223 # Returns an ASCII or hashed filename
223 # Returns an ASCII or hashed filename
224 def self.disk_filename(filename)
224 def self.disk_filename(filename)
225 timestamp = DateTime.now.strftime("%y%m%d%H%M%S")
225 timestamp = DateTime.now.strftime("%y%m%d%H%M%S")
226 ascii = ''
226 ascii = ''
227 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
227 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
228 ascii = filename
228 ascii = filename
229 else
229 else
230 ascii = Digest::MD5.hexdigest(filename)
230 ascii = Digest::MD5.hexdigest(filename)
231 # keep the extension if any
231 # keep the extension if any
232 ascii << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
232 ascii << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
233 end
233 end
234 while File.exist?(File.join(@@storage_path, "#{timestamp}_#{ascii}"))
234 while File.exist?(File.join(@@storage_path, "#{timestamp}_#{ascii}"))
235 timestamp.succ!
235 timestamp.succ!
236 end
236 end
237 "#{timestamp}_#{ascii}"
237 "#{timestamp}_#{ascii}"
238 end
238 end
239 end
239 end
General Comments 0
You need to be logged in to leave comments. Login now