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