##// END OF EJS Templates
remove trailing white-space from app/models/attachment.rb...
Toshi MARUYAMA -
r11037:3d7819ed822e
parent child
Show More
@@ -1,326 +1,326
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 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 require "fileutils"
19 require "fileutils"
20
20
21 class Attachment < ActiveRecord::Base
21 class Attachment < ActiveRecord::Base
22 belongs_to :container, :polymorphic => true
22 belongs_to :container, :polymorphic => true
23 belongs_to :author, :class_name => "User", :foreign_key => "author_id"
23 belongs_to :author, :class_name => "User", :foreign_key => "author_id"
24
24
25 validates_presence_of :filename, :author
25 validates_presence_of :filename, :author
26 validates_length_of :filename, :maximum => 255
26 validates_length_of :filename, :maximum => 255
27 validates_length_of :disk_filename, :maximum => 255
27 validates_length_of :disk_filename, :maximum => 255
28 validates_length_of :description, :maximum => 255
28 validates_length_of :description, :maximum => 255
29 validate :validate_max_file_size
29 validate :validate_max_file_size
30
30
31 acts_as_event :title => :filename,
31 acts_as_event :title => :filename,
32 :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}}
32 :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}}
33
33
34 acts_as_activity_provider :type => 'files',
34 acts_as_activity_provider :type => 'files',
35 :permission => :view_files,
35 :permission => :view_files,
36 :author_key => :author_id,
36 :author_key => :author_id,
37 :find_options => {:select => "#{Attachment.table_name}.*",
37 :find_options => {:select => "#{Attachment.table_name}.*",
38 :joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " +
38 :joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " +
39 "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 )"}
39 "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 )"}
40
40
41 acts_as_activity_provider :type => 'documents',
41 acts_as_activity_provider :type => 'documents',
42 :permission => :view_documents,
42 :permission => :view_documents,
43 :author_key => :author_id,
43 :author_key => :author_id,
44 :find_options => {:select => "#{Attachment.table_name}.*",
44 :find_options => {:select => "#{Attachment.table_name}.*",
45 :joins => "LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " +
45 :joins => "LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " +
46 "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"}
46 "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"}
47
47
48 cattr_accessor :storage_path
48 cattr_accessor :storage_path
49 @@storage_path = Redmine::Configuration['attachments_storage_path'] || File.join(Rails.root, "files")
49 @@storage_path = Redmine::Configuration['attachments_storage_path'] || File.join(Rails.root, "files")
50
50
51 cattr_accessor :thumbnails_storage_path
51 cattr_accessor :thumbnails_storage_path
52 @@thumbnails_storage_path = File.join(Rails.root, "tmp", "thumbnails")
52 @@thumbnails_storage_path = File.join(Rails.root, "tmp", "thumbnails")
53
53
54 before_save :files_to_final_location
54 before_save :files_to_final_location
55 after_destroy :delete_from_disk
55 after_destroy :delete_from_disk
56
56
57 # Returns an unsaved copy of the attachment
57 # Returns an unsaved copy of the attachment
58 def copy(attributes=nil)
58 def copy(attributes=nil)
59 copy = self.class.new
59 copy = self.class.new
60 copy.attributes = self.attributes.dup.except("id", "downloads")
60 copy.attributes = self.attributes.dup.except("id", "downloads")
61 copy.attributes = attributes if attributes
61 copy.attributes = attributes if attributes
62 copy
62 copy
63 end
63 end
64
64
65 def validate_max_file_size
65 def validate_max_file_size
66 if @temp_file && self.filesize > Setting.attachment_max_size.to_i.kilobytes
66 if @temp_file && self.filesize > Setting.attachment_max_size.to_i.kilobytes
67 errors.add(:base, l(:error_attachment_too_big, :max_size => Setting.attachment_max_size.to_i.kilobytes))
67 errors.add(:base, l(:error_attachment_too_big, :max_size => Setting.attachment_max_size.to_i.kilobytes))
68 end
68 end
69 end
69 end
70
70
71 def file=(incoming_file)
71 def file=(incoming_file)
72 unless incoming_file.nil?
72 unless incoming_file.nil?
73 @temp_file = incoming_file
73 @temp_file = incoming_file
74 if @temp_file.size > 0
74 if @temp_file.size > 0
75 if @temp_file.respond_to?(:original_filename)
75 if @temp_file.respond_to?(:original_filename)
76 self.filename = @temp_file.original_filename
76 self.filename = @temp_file.original_filename
77 self.filename.force_encoding("UTF-8") if filename.respond_to?(:force_encoding)
77 self.filename.force_encoding("UTF-8") if filename.respond_to?(:force_encoding)
78 end
78 end
79 if @temp_file.respond_to?(:content_type)
79 if @temp_file.respond_to?(:content_type)
80 self.content_type = @temp_file.content_type.to_s.chomp
80 self.content_type = @temp_file.content_type.to_s.chomp
81 end
81 end
82 if content_type.blank? && filename.present?
82 if content_type.blank? && filename.present?
83 self.content_type = Redmine::MimeType.of(filename)
83 self.content_type = Redmine::MimeType.of(filename)
84 end
84 end
85 self.filesize = @temp_file.size
85 self.filesize = @temp_file.size
86 end
86 end
87 end
87 end
88 end
88 end
89
89
90 def file
90 def file
91 nil
91 nil
92 end
92 end
93
93
94 def filename=(arg)
94 def filename=(arg)
95 write_attribute :filename, sanitize_filename(arg.to_s)
95 write_attribute :filename, sanitize_filename(arg.to_s)
96 filename
96 filename
97 end
97 end
98
98
99 # Copies the temporary file to its final location
99 # Copies the temporary file to its final location
100 # and computes its MD5 hash
100 # and computes its MD5 hash
101 def files_to_final_location
101 def files_to_final_location
102 if @temp_file && (@temp_file.size > 0)
102 if @temp_file && (@temp_file.size > 0)
103 self.disk_directory = target_directory
103 self.disk_directory = target_directory
104 self.disk_filename = Attachment.disk_filename(filename, disk_directory)
104 self.disk_filename = Attachment.disk_filename(filename, disk_directory)
105 logger.info("Saving attachment '#{self.diskfile}' (#{@temp_file.size} bytes)")
105 logger.info("Saving attachment '#{self.diskfile}' (#{@temp_file.size} bytes)")
106 path = File.dirname(diskfile)
106 path = File.dirname(diskfile)
107 unless File.directory?(path)
107 unless File.directory?(path)
108 FileUtils.mkdir_p(path)
108 FileUtils.mkdir_p(path)
109 end
109 end
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 if @temp_file.respond_to?(:read)
112 if @temp_file.respond_to?(:read)
113 buffer = ""
113 buffer = ""
114 while (buffer = @temp_file.read(8192))
114 while (buffer = @temp_file.read(8192))
115 f.write(buffer)
115 f.write(buffer)
116 md5.update(buffer)
116 md5.update(buffer)
117 end
117 end
118 else
118 else
119 f.write(@temp_file)
119 f.write(@temp_file)
120 md5.update(@temp_file)
120 md5.update(@temp_file)
121 end
121 end
122 end
122 end
123 self.digest = md5.hexdigest
123 self.digest = md5.hexdigest
124 end
124 end
125 @temp_file = nil
125 @temp_file = nil
126 # Don't save the content type if it's longer than the authorized length
126 # Don't save the content type if it's longer than the authorized length
127 if self.content_type && self.content_type.length > 255
127 if self.content_type && self.content_type.length > 255
128 self.content_type = nil
128 self.content_type = nil
129 end
129 end
130 end
130 end
131
131
132 # Deletes the file from the file system if it's not referenced by other attachments
132 # Deletes the file from the file system if it's not referenced by other attachments
133 def delete_from_disk
133 def delete_from_disk
134 if Attachment.where("disk_filename = ? AND id <> ?", disk_filename, id).empty?
134 if Attachment.where("disk_filename = ? AND id <> ?", disk_filename, id).empty?
135 delete_from_disk!
135 delete_from_disk!
136 end
136 end
137 end
137 end
138
138
139 # Returns file's location on disk
139 # Returns file's location on disk
140 def diskfile
140 def diskfile
141 File.join(self.class.storage_path, disk_directory.to_s, disk_filename.to_s)
141 File.join(self.class.storage_path, disk_directory.to_s, disk_filename.to_s)
142 end
142 end
143
143
144 def title
144 def title
145 title = filename.to_s
145 title = filename.to_s
146 if description.present?
146 if description.present?
147 title << " (#{description})"
147 title << " (#{description})"
148 end
148 end
149 title
149 title
150 end
150 end
151
151
152 def increment_download
152 def increment_download
153 increment!(:downloads)
153 increment!(:downloads)
154 end
154 end
155
155
156 def project
156 def project
157 container.try(:project)
157 container.try(:project)
158 end
158 end
159
159
160 def visible?(user=User.current)
160 def visible?(user=User.current)
161 if container_id
161 if container_id
162 container && container.attachments_visible?(user)
162 container && container.attachments_visible?(user)
163 else
163 else
164 author == user
164 author == user
165 end
165 end
166 end
166 end
167
167
168 def deletable?(user=User.current)
168 def deletable?(user=User.current)
169 if container_id
169 if container_id
170 container && container.attachments_deletable?(user)
170 container && container.attachments_deletable?(user)
171 else
171 else
172 author == user
172 author == user
173 end
173 end
174 end
174 end
175
175
176 def image?
176 def image?
177 !!(self.filename =~ /\.(bmp|gif|jpg|jpe|jpeg|png)$/i)
177 !!(self.filename =~ /\.(bmp|gif|jpg|jpe|jpeg|png)$/i)
178 end
178 end
179
179
180 def thumbnailable?
180 def thumbnailable?
181 image?
181 image?
182 end
182 end
183
183
184 # Returns the full path the attachment thumbnail, or nil
184 # Returns the full path the attachment thumbnail, or nil
185 # if the thumbnail cannot be generated.
185 # if the thumbnail cannot be generated.
186 def thumbnail(options={})
186 def thumbnail(options={})
187 if thumbnailable? && readable?
187 if thumbnailable? && readable?
188 size = options[:size].to_i
188 size = options[:size].to_i
189 if size > 0
189 if size > 0
190 # Limit the number of thumbnails per image
190 # Limit the number of thumbnails per image
191 size = (size / 50) * 50
191 size = (size / 50) * 50
192 # Maximum thumbnail size
192 # Maximum thumbnail size
193 size = 800 if size > 800
193 size = 800 if size > 800
194 else
194 else
195 size = Setting.thumbnails_size.to_i
195 size = Setting.thumbnails_size.to_i
196 end
196 end
197 size = 100 unless size > 0
197 size = 100 unless size > 0
198 target = File.join(self.class.thumbnails_storage_path, "#{id}_#{digest}_#{size}.thumb")
198 target = File.join(self.class.thumbnails_storage_path, "#{id}_#{digest}_#{size}.thumb")
199
199
200 begin
200 begin
201 Redmine::Thumbnail.generate(self.diskfile, target, size)
201 Redmine::Thumbnail.generate(self.diskfile, target, size)
202 rescue => e
202 rescue => e
203 logger.error "An error occured while generating thumbnail for #{disk_filename} to #{target}\nException was: #{e.message}" if logger
203 logger.error "An error occured while generating thumbnail for #{disk_filename} to #{target}\nException was: #{e.message}" if logger
204 return nil
204 return nil
205 end
205 end
206 end
206 end
207 end
207 end
208
208
209 # Deletes all thumbnails
209 # Deletes all thumbnails
210 def self.clear_thumbnails
210 def self.clear_thumbnails
211 Dir.glob(File.join(thumbnails_storage_path, "*.thumb")).each do |file|
211 Dir.glob(File.join(thumbnails_storage_path, "*.thumb")).each do |file|
212 File.delete file
212 File.delete file
213 end
213 end
214 end
214 end
215
215
216 def is_text?
216 def is_text?
217 Redmine::MimeType.is_type?('text', filename)
217 Redmine::MimeType.is_type?('text', filename)
218 end
218 end
219
219
220 def is_diff?
220 def is_diff?
221 self.filename =~ /\.(patch|diff)$/i
221 self.filename =~ /\.(patch|diff)$/i
222 end
222 end
223
223
224 # Returns true if the file is readable
224 # Returns true if the file is readable
225 def readable?
225 def readable?
226 File.readable?(diskfile)
226 File.readable?(diskfile)
227 end
227 end
228
228
229 # Returns the attachment token
229 # Returns the attachment token
230 def token
230 def token
231 "#{id}.#{digest}"
231 "#{id}.#{digest}"
232 end
232 end
233
233
234 # Finds an attachment that matches the given token and that has no container
234 # Finds an attachment that matches the given token and that has no container
235 def self.find_by_token(token)
235 def self.find_by_token(token)
236 if token.to_s =~ /^(\d+)\.([0-9a-f]+)$/
236 if token.to_s =~ /^(\d+)\.([0-9a-f]+)$/
237 attachment_id, attachment_digest = $1, $2
237 attachment_id, attachment_digest = $1, $2
238 attachment = Attachment.where(:id => attachment_id, :digest => attachment_digest).first
238 attachment = Attachment.where(:id => attachment_id, :digest => attachment_digest).first
239 if attachment && attachment.container.nil?
239 if attachment && attachment.container.nil?
240 attachment
240 attachment
241 end
241 end
242 end
242 end
243 end
243 end
244
244
245 # Bulk attaches a set of files to an object
245 # Bulk attaches a set of files to an object
246 #
246 #
247 # Returns a Hash of the results:
247 # Returns a Hash of the results:
248 # :files => array of the attached files
248 # :files => array of the attached files
249 # :unsaved => array of the files that could not be attached
249 # :unsaved => array of the files that could not be attached
250 def self.attach_files(obj, attachments)
250 def self.attach_files(obj, attachments)
251 result = obj.save_attachments(attachments, User.current)
251 result = obj.save_attachments(attachments, User.current)
252 obj.attach_saved_attachments
252 obj.attach_saved_attachments
253 result
253 result
254 end
254 end
255
255
256 def self.latest_attach(attachments, filename)
256 def self.latest_attach(attachments, filename)
257 attachments.sort_by(&:created_on).reverse.detect {
257 attachments.sort_by(&:created_on).reverse.detect {
258 |att| att.filename.downcase == filename.downcase
258 |att| att.filename.downcase == filename.downcase
259 }
259 }
260 end
260 end
261
261
262 def self.prune(age=1.day)
262 def self.prune(age=1.day)
263 Attachment.where("created_on < ? AND (container_type IS NULL OR container_type = '')", Time.now - age).destroy_all
263 Attachment.where("created_on < ? AND (container_type IS NULL OR container_type = '')", Time.now - age).destroy_all
264 end
264 end
265
265
266 # Moves an existing attachment to its target directory
266 # Moves an existing attachment to its target directory
267 def move_to_target_directory!
267 def move_to_target_directory!
268 if !new_record? & readable?
268 if !new_record? & readable?
269 src = diskfile
269 src = diskfile
270 self.disk_directory = target_directory
270 self.disk_directory = target_directory
271 dest = diskfile
271 dest = diskfile
272 if src != dest && FileUtils.mkdir_p(File.dirname(dest)) && FileUtils.mv(src, dest)
272 if src != dest && FileUtils.mkdir_p(File.dirname(dest)) && FileUtils.mv(src, dest)
273 update_column :disk_directory, disk_directory
273 update_column :disk_directory, disk_directory
274 end
274 end
275 end
275 end
276 end
276 end
277
277
278 # Moves existing attachments that are stored at the root of the files
278 # Moves existing attachments that are stored at the root of the files
279 # directory (ie. created before Redmine 2.3) to their target subdirectories
279 # directory (ie. created before Redmine 2.3) to their target subdirectories
280 def self.move_from_root_to_target_directory
280 def self.move_from_root_to_target_directory
281 Attachment.where("disk_directory IS NULL OR disk_directory = ''").find_each do |attachment|
281 Attachment.where("disk_directory IS NULL OR disk_directory = ''").find_each do |attachment|
282 attachment.move_to_target_directory!
282 attachment.move_to_target_directory!
283 end
283 end
284 end
284 end
285
285
286 private
286 private
287
287
288 # Physically deletes the file from the file system
288 # Physically deletes the file from the file system
289 def delete_from_disk!
289 def delete_from_disk!
290 if disk_filename.present? && File.exist?(diskfile)
290 if disk_filename.present? && File.exist?(diskfile)
291 File.delete(diskfile)
291 File.delete(diskfile)
292 end
292 end
293 end
293 end
294
294
295 def sanitize_filename(value)
295 def sanitize_filename(value)
296 # get only the filename, not the whole path
296 # get only the filename, not the whole path
297 just_filename = value.gsub(/^.*(\\|\/)/, '')
297 just_filename = value.gsub(/^.*(\\|\/)/, '')
298
298
299 # Finally, replace invalid characters with underscore
299 # Finally, replace invalid characters with underscore
300 @filename = just_filename.gsub(/[\/\?\%\*\:\|\"\'<>]+/, '_')
300 @filename = just_filename.gsub(/[\/\?\%\*\:\|\"\'<>]+/, '_')
301 end
301 end
302
302
303 # Returns the subdirectory in which the attachment will be saved
303 # Returns the subdirectory in which the attachment will be saved
304 def target_directory
304 def target_directory
305 time = created_on || DateTime.now
305 time = created_on || DateTime.now
306 time.strftime("%Y/%m")
306 time.strftime("%Y/%m")
307 end
307 end
308
308
309 # Returns an ASCII or hashed filename that do not
309 # Returns an ASCII or hashed filename that do not
310 # exists yet in the given subdirectory
310 # exists yet in the given subdirectory
311 def self.disk_filename(filename, directory=nil)
311 def self.disk_filename(filename, directory=nil)
312 timestamp = DateTime.now.strftime("%y%m%d%H%M%S")
312 timestamp = DateTime.now.strftime("%y%m%d%H%M%S")
313 ascii = ''
313 ascii = ''
314 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
314 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
315 ascii = filename
315 ascii = filename
316 else
316 else
317 ascii = Digest::MD5.hexdigest(filename)
317 ascii = Digest::MD5.hexdigest(filename)
318 # keep the extension if any
318 # keep the extension if any
319 ascii << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
319 ascii << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
320 end
320 end
321 while File.exist?(File.join(storage_path, directory.to_s, "#{timestamp}_#{ascii}"))
321 while File.exist?(File.join(storage_path, directory.to_s, "#{timestamp}_#{ascii}"))
322 timestamp.succ!
322 timestamp.succ!
323 end
323 end
324 "#{timestamp}_#{ascii}"
324 "#{timestamp}_#{ascii}"
325 end
325 end
326 end
326 end
General Comments 0
You need to be logged in to leave comments. Login now