##// END OF EJS Templates
Store attachments in subdirectories (#5298)....
Jean-Philippe Lang -
r10761:c99b638d61cc
parent child
Show More
@@ -0,0 +1,9
1 class AddAttachmentsDiskDirectory < ActiveRecord::Migration
2 def up
3 add_column :attachments, :disk_directory, :string
4 end
5
6 def down
7 remove_column :attachments, :disk_directory
8 end
9 end
@@ -1,295 +1,326
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 require "fileutils"
19
20
20 class Attachment < ActiveRecord::Base
21 class Attachment < ActiveRecord::Base
21 belongs_to :container, :polymorphic => true
22 belongs_to :container, :polymorphic => true
22 belongs_to :author, :class_name => "User", :foreign_key => "author_id"
23 belongs_to :author, :class_name => "User", :foreign_key => "author_id"
23
24
24 validates_presence_of :filename, :author
25 validates_presence_of :filename, :author
25 validates_length_of :filename, :maximum => 255
26 validates_length_of :filename, :maximum => 255
26 validates_length_of :disk_filename, :maximum => 255
27 validates_length_of :disk_filename, :maximum => 255
27 validates_length_of :description, :maximum => 255
28 validates_length_of :description, :maximum => 255
28 validate :validate_max_file_size
29 validate :validate_max_file_size
29
30
30 acts_as_event :title => :filename,
31 acts_as_event :title => :filename,
31 :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}}
32
33
33 acts_as_activity_provider :type => 'files',
34 acts_as_activity_provider :type => 'files',
34 :permission => :view_files,
35 :permission => :view_files,
35 :author_key => :author_id,
36 :author_key => :author_id,
36 :find_options => {:select => "#{Attachment.table_name}.*",
37 :find_options => {:select => "#{Attachment.table_name}.*",
37 :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 " +
38 "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 )"}
39
40
40 acts_as_activity_provider :type => 'documents',
41 acts_as_activity_provider :type => 'documents',
41 :permission => :view_documents,
42 :permission => :view_documents,
42 :author_key => :author_id,
43 :author_key => :author_id,
43 :find_options => {:select => "#{Attachment.table_name}.*",
44 :find_options => {:select => "#{Attachment.table_name}.*",
44 :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 " +
45 "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"}
46
47
47 cattr_accessor :storage_path
48 cattr_accessor :storage_path
48 @@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")
49
50
50 cattr_accessor :thumbnails_storage_path
51 cattr_accessor :thumbnails_storage_path
51 @@thumbnails_storage_path = File.join(Rails.root, "tmp", "thumbnails")
52 @@thumbnails_storage_path = File.join(Rails.root, "tmp", "thumbnails")
52
53
53 before_save :files_to_final_location
54 before_save :files_to_final_location
54 after_destroy :delete_from_disk
55 after_destroy :delete_from_disk
55
56
56 # Returns an unsaved copy of the attachment
57 # Returns an unsaved copy of the attachment
57 def copy(attributes=nil)
58 def copy(attributes=nil)
58 copy = self.class.new
59 copy = self.class.new
59 copy.attributes = self.attributes.dup.except("id", "downloads")
60 copy.attributes = self.attributes.dup.except("id", "downloads")
60 copy.attributes = attributes if attributes
61 copy.attributes = attributes if attributes
61 copy
62 copy
62 end
63 end
63
64
64 def validate_max_file_size
65 def validate_max_file_size
65 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
66 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))
67 end
68 end
68 end
69 end
69
70
70 def file=(incoming_file)
71 def file=(incoming_file)
71 unless incoming_file.nil?
72 unless incoming_file.nil?
72 @temp_file = incoming_file
73 @temp_file = incoming_file
73 if @temp_file.size > 0
74 if @temp_file.size > 0
74 if @temp_file.respond_to?(:original_filename)
75 if @temp_file.respond_to?(:original_filename)
75 self.filename = @temp_file.original_filename
76 self.filename = @temp_file.original_filename
76 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)
77 end
78 end
78 if @temp_file.respond_to?(:content_type)
79 if @temp_file.respond_to?(:content_type)
79 self.content_type = @temp_file.content_type.to_s.chomp
80 self.content_type = @temp_file.content_type.to_s.chomp
80 end
81 end
81 if content_type.blank? && filename.present?
82 if content_type.blank? && filename.present?
82 self.content_type = Redmine::MimeType.of(filename)
83 self.content_type = Redmine::MimeType.of(filename)
83 end
84 end
84 self.filesize = @temp_file.size
85 self.filesize = @temp_file.size
85 end
86 end
86 end
87 end
87 end
88 end
88
89
89 def file
90 def file
90 nil
91 nil
91 end
92 end
92
93
93 def filename=(arg)
94 def filename=(arg)
94 write_attribute :filename, sanitize_filename(arg.to_s)
95 write_attribute :filename, sanitize_filename(arg.to_s)
95 if new_record? && disk_filename.blank?
96 self.disk_filename = Attachment.disk_filename(filename)
97 end
98 filename
96 filename
99 end
97 end
100
98
101 # Copies the temporary file to its final location
99 # Copies the temporary file to its final location
102 # and computes its MD5 hash
100 # and computes its MD5 hash
103 def files_to_final_location
101 def files_to_final_location
104 if @temp_file && (@temp_file.size > 0)
102 if @temp_file && (@temp_file.size > 0)
103 self.disk_directory = target_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)
107 unless File.directory?(path)
108 FileUtils.mkdir_p(path)
109 end
106 md5 = Digest::MD5.new
110 md5 = Digest::MD5.new
107 File.open(diskfile, "wb") do |f|
111 File.open(diskfile, "wb") do |f|
108 if @temp_file.respond_to?(:read)
112 if @temp_file.respond_to?(:read)
109 buffer = ""
113 buffer = ""
110 while (buffer = @temp_file.read(8192))
114 while (buffer = @temp_file.read(8192))
111 f.write(buffer)
115 f.write(buffer)
112 md5.update(buffer)
116 md5.update(buffer)
113 end
117 end
114 else
118 else
115 f.write(@temp_file)
119 f.write(@temp_file)
116 md5.update(@temp_file)
120 md5.update(@temp_file)
117 end
121 end
118 end
122 end
119 self.digest = md5.hexdigest
123 self.digest = md5.hexdigest
120 end
124 end
121 @temp_file = nil
125 @temp_file = nil
122 # 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
123 if self.content_type && self.content_type.length > 255
127 if self.content_type && self.content_type.length > 255
124 self.content_type = nil
128 self.content_type = nil
125 end
129 end
126 end
130 end
127
131
128 # 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
129 def delete_from_disk
133 def delete_from_disk
130 if Attachment.where("disk_filename = ? AND id <> ?", disk_filename, id).empty?
134 if Attachment.where("disk_filename = ? AND id <> ?", disk_filename, id).empty?
131 delete_from_disk!
135 delete_from_disk!
132 end
136 end
133 end
137 end
134
138
135 # Returns file's location on disk
139 # Returns file's location on disk
136 def diskfile
140 def diskfile
137 File.join(self.class.storage_path, disk_filename.to_s)
141 File.join(self.class.storage_path, disk_directory.to_s, disk_filename.to_s)
138 end
142 end
139
143
140 def title
144 def title
141 title = filename.to_s
145 title = filename.to_s
142 if description.present?
146 if description.present?
143 title << " (#{description})"
147 title << " (#{description})"
144 end
148 end
145 title
149 title
146 end
150 end
147
151
148 def increment_download
152 def increment_download
149 increment!(:downloads)
153 increment!(:downloads)
150 end
154 end
151
155
152 def project
156 def project
153 container.try(:project)
157 container.try(:project)
154 end
158 end
155
159
156 def visible?(user=User.current)
160 def visible?(user=User.current)
157 if container_id
161 if container_id
158 container && container.attachments_visible?(user)
162 container && container.attachments_visible?(user)
159 else
163 else
160 author == user
164 author == user
161 end
165 end
162 end
166 end
163
167
164 def deletable?(user=User.current)
168 def deletable?(user=User.current)
165 if container_id
169 if container_id
166 container && container.attachments_deletable?(user)
170 container && container.attachments_deletable?(user)
167 else
171 else
168 author == user
172 author == user
169 end
173 end
170 end
174 end
171
175
172 def image?
176 def image?
173 !!(self.filename =~ /\.(bmp|gif|jpg|jpe|jpeg|png)$/i)
177 !!(self.filename =~ /\.(bmp|gif|jpg|jpe|jpeg|png)$/i)
174 end
178 end
175
179
176 def thumbnailable?
180 def thumbnailable?
177 image?
181 image?
178 end
182 end
179
183
180 # Returns the full path the attachment thumbnail, or nil
184 # Returns the full path the attachment thumbnail, or nil
181 # if the thumbnail cannot be generated.
185 # if the thumbnail cannot be generated.
182 def thumbnail(options={})
186 def thumbnail(options={})
183 if thumbnailable? && readable?
187 if thumbnailable? && readable?
184 size = options[:size].to_i
188 size = options[:size].to_i
185 if size > 0
189 if size > 0
186 # Limit the number of thumbnails per image
190 # Limit the number of thumbnails per image
187 size = (size / 50) * 50
191 size = (size / 50) * 50
188 # Maximum thumbnail size
192 # Maximum thumbnail size
189 size = 800 if size > 800
193 size = 800 if size > 800
190 else
194 else
191 size = Setting.thumbnails_size.to_i
195 size = Setting.thumbnails_size.to_i
192 end
196 end
193 size = 100 unless size > 0
197 size = 100 unless size > 0
194 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")
195
199
196 begin
200 begin
197 Redmine::Thumbnail.generate(self.diskfile, target, size)
201 Redmine::Thumbnail.generate(self.diskfile, target, size)
198 rescue => e
202 rescue => e
199 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
200 return nil
204 return nil
201 end
205 end
202 end
206 end
203 end
207 end
204
208
205 # Deletes all thumbnails
209 # Deletes all thumbnails
206 def self.clear_thumbnails
210 def self.clear_thumbnails
207 Dir.glob(File.join(thumbnails_storage_path, "*.thumb")).each do |file|
211 Dir.glob(File.join(thumbnails_storage_path, "*.thumb")).each do |file|
208 File.delete file
212 File.delete file
209 end
213 end
210 end
214 end
211
215
212 def is_text?
216 def is_text?
213 Redmine::MimeType.is_type?('text', filename)
217 Redmine::MimeType.is_type?('text', filename)
214 end
218 end
215
219
216 def is_diff?
220 def is_diff?
217 self.filename =~ /\.(patch|diff)$/i
221 self.filename =~ /\.(patch|diff)$/i
218 end
222 end
219
223
220 # Returns true if the file is readable
224 # Returns true if the file is readable
221 def readable?
225 def readable?
222 File.readable?(diskfile)
226 File.readable?(diskfile)
223 end
227 end
224
228
225 # Returns the attachment token
229 # Returns the attachment token
226 def token
230 def token
227 "#{id}.#{digest}"
231 "#{id}.#{digest}"
228 end
232 end
229
233
230 # 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
231 def self.find_by_token(token)
235 def self.find_by_token(token)
232 if token.to_s =~ /^(\d+)\.([0-9a-f]+)$/
236 if token.to_s =~ /^(\d+)\.([0-9a-f]+)$/
233 attachment_id, attachment_digest = $1, $2
237 attachment_id, attachment_digest = $1, $2
234 attachment = Attachment.where(:id => attachment_id, :digest => attachment_digest).first
238 attachment = Attachment.where(:id => attachment_id, :digest => attachment_digest).first
235 if attachment && attachment.container.nil?
239 if attachment && attachment.container.nil?
236 attachment
240 attachment
237 end
241 end
238 end
242 end
239 end
243 end
240
244
241 # Bulk attaches a set of files to an object
245 # Bulk attaches a set of files to an object
242 #
246 #
243 # Returns a Hash of the results:
247 # Returns a Hash of the results:
244 # :files => array of the attached files
248 # :files => array of the attached files
245 # :unsaved => array of the files that could not be attached
249 # :unsaved => array of the files that could not be attached
246 def self.attach_files(obj, attachments)
250 def self.attach_files(obj, attachments)
247 result = obj.save_attachments(attachments, User.current)
251 result = obj.save_attachments(attachments, User.current)
248 obj.attach_saved_attachments
252 obj.attach_saved_attachments
249 result
253 result
250 end
254 end
251
255
252 def self.latest_attach(attachments, filename)
256 def self.latest_attach(attachments, filename)
253 attachments.sort_by(&:created_on).reverse.detect {
257 attachments.sort_by(&:created_on).reverse.detect {
254 |att| att.filename.downcase == filename.downcase
258 |att| att.filename.downcase == filename.downcase
255 }
259 }
256 end
260 end
257
261
258 def self.prune(age=1.day)
262 def self.prune(age=1.day)
259 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
260 end
264 end
261
265
266 # Moves an existing attachment to its target directory
267 def move_to_target_directory!
268 if !new_record? & readable?
269 src = diskfile
270 self.disk_directory = target_directory
271 dest = diskfile
272 if src != dest && FileUtils.mkdir_p(File.dirname(dest)) && FileUtils.mv(src, dest)
273 update_column :disk_directory, disk_directory
274 end
275 end
276 end
277
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
280 def self.move_from_root_to_target_directory
281 Attachment.where("disk_directory IS NULL OR disk_directory = ''").find_each do |attachment|
282 attachment.move_to_target_directory!
283 end
284 end
285
262 private
286 private
263
287
264 # Physically deletes the file from the file system
288 # Physically deletes the file from the file system
265 def delete_from_disk!
289 def delete_from_disk!
266 if disk_filename.present? && File.exist?(diskfile)
290 if disk_filename.present? && File.exist?(diskfile)
267 File.delete(diskfile)
291 File.delete(diskfile)
268 end
292 end
269 end
293 end
270
294
271 def sanitize_filename(value)
295 def sanitize_filename(value)
272 # get only the filename, not the whole path
296 # get only the filename, not the whole path
273 just_filename = value.gsub(/^.*(\\|\/)/, '')
297 just_filename = value.gsub(/^.*(\\|\/)/, '')
274
298
275 # Finally, replace invalid characters with underscore
299 # Finally, replace invalid characters with underscore
276 @filename = just_filename.gsub(/[\/\?\%\*\:\|\"\'<>]+/, '_')
300 @filename = just_filename.gsub(/[\/\?\%\*\:\|\"\'<>]+/, '_')
277 end
301 end
278
302
279 # Returns an ASCII or hashed filename
303 # Returns the subdirectory in which the attachment will be saved
280 def self.disk_filename(filename)
304 def target_directory
305 time = created_on || DateTime.now
306 time.strftime("%Y/%m")
307 end
308
309 # Returns an ASCII or hashed filename that do not
310 # exists yet in the given subdirectory
311 def self.disk_filename(filename, directory=nil)
281 timestamp = DateTime.now.strftime("%y%m%d%H%M%S")
312 timestamp = DateTime.now.strftime("%y%m%d%H%M%S")
282 ascii = ''
313 ascii = ''
283 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
314 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
284 ascii = filename
315 ascii = filename
285 else
316 else
286 ascii = Digest::MD5.hexdigest(filename)
317 ascii = Digest::MD5.hexdigest(filename)
287 # keep the extension if any
318 # keep the extension if any
288 ascii << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
319 ascii << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
289 end
320 end
290 while File.exist?(File.join(@@storage_path, "#{timestamp}_#{ascii}"))
321 while File.exist?(File.join(storage_path, directory.to_s, "#{timestamp}_#{ascii}"))
291 timestamp.succ!
322 timestamp.succ!
292 end
323 end
293 "#{timestamp}_#{ascii}"
324 "#{timestamp}_#{ascii}"
294 end
325 end
295 end
326 end
@@ -1,121 +1,126
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 namespace :redmine do
18 namespace :redmine do
19 namespace :attachments do
19 namespace :attachments do
20 desc 'Removes uploaded files left unattached after one day.'
20 desc 'Removes uploaded files left unattached after one day.'
21 task :prune => :environment do
21 task :prune => :environment do
22 Attachment.prune
22 Attachment.prune
23 end
23 end
24
25 desc 'Moves attachments stored at the root of the file directory (ie. created before Redmine 2.3) to their subdirectories'
26 task :move_to_subdirectories => :environment do
27 Attachment.move_from_root_to_target_directory
28 end
24 end
29 end
25
30
26 namespace :tokens do
31 namespace :tokens do
27 desc 'Removes expired tokens.'
32 desc 'Removes expired tokens.'
28 task :prune => :environment do
33 task :prune => :environment do
29 Token.destroy_expired
34 Token.destroy_expired
30 end
35 end
31 end
36 end
32
37
33 namespace :watchers do
38 namespace :watchers do
34 desc 'Removes watchers from what they can no longer view.'
39 desc 'Removes watchers from what they can no longer view.'
35 task :prune => :environment do
40 task :prune => :environment do
36 Watcher.prune
41 Watcher.prune
37 end
42 end
38 end
43 end
39
44
40 desc 'Fetch changesets from the repositories'
45 desc 'Fetch changesets from the repositories'
41 task :fetch_changesets => :environment do
46 task :fetch_changesets => :environment do
42 Repository.fetch_changesets
47 Repository.fetch_changesets
43 end
48 end
44
49
45 desc 'Migrates and copies plugins assets.'
50 desc 'Migrates and copies plugins assets.'
46 task :plugins do
51 task :plugins do
47 Rake::Task["redmine:plugins:migrate"].invoke
52 Rake::Task["redmine:plugins:migrate"].invoke
48 Rake::Task["redmine:plugins:assets"].invoke
53 Rake::Task["redmine:plugins:assets"].invoke
49 end
54 end
50
55
51 namespace :plugins do
56 namespace :plugins do
52 desc 'Migrates installed plugins.'
57 desc 'Migrates installed plugins.'
53 task :migrate => :environment do
58 task :migrate => :environment do
54 name = ENV['NAME']
59 name = ENV['NAME']
55 version = nil
60 version = nil
56 version_string = ENV['VERSION']
61 version_string = ENV['VERSION']
57 if version_string
62 if version_string
58 if version_string =~ /^\d+$/
63 if version_string =~ /^\d+$/
59 version = version_string.to_i
64 version = version_string.to_i
60 if name.nil?
65 if name.nil?
61 abort "The VERSION argument requires a plugin NAME."
66 abort "The VERSION argument requires a plugin NAME."
62 end
67 end
63 else
68 else
64 abort "Invalid VERSION #{version_string} given."
69 abort "Invalid VERSION #{version_string} given."
65 end
70 end
66 end
71 end
67
72
68 begin
73 begin
69 Redmine::Plugin.migrate(name, version)
74 Redmine::Plugin.migrate(name, version)
70 rescue Redmine::PluginNotFound
75 rescue Redmine::PluginNotFound
71 abort "Plugin #{name} was not found."
76 abort "Plugin #{name} was not found."
72 end
77 end
73
78
74 Rake::Task["db:schema:dump"].invoke
79 Rake::Task["db:schema:dump"].invoke
75 end
80 end
76
81
77 desc 'Copies plugins assets into the public directory.'
82 desc 'Copies plugins assets into the public directory.'
78 task :assets => :environment do
83 task :assets => :environment do
79 name = ENV['NAME']
84 name = ENV['NAME']
80
85
81 begin
86 begin
82 Redmine::Plugin.mirror_assets(name)
87 Redmine::Plugin.mirror_assets(name)
83 rescue Redmine::PluginNotFound
88 rescue Redmine::PluginNotFound
84 abort "Plugin #{name} was not found."
89 abort "Plugin #{name} was not found."
85 end
90 end
86 end
91 end
87
92
88 desc 'Runs the plugins tests.'
93 desc 'Runs the plugins tests.'
89 task :test do
94 task :test do
90 Rake::Task["redmine:plugins:test:units"].invoke
95 Rake::Task["redmine:plugins:test:units"].invoke
91 Rake::Task["redmine:plugins:test:functionals"].invoke
96 Rake::Task["redmine:plugins:test:functionals"].invoke
92 Rake::Task["redmine:plugins:test:integration"].invoke
97 Rake::Task["redmine:plugins:test:integration"].invoke
93 end
98 end
94
99
95 namespace :test do
100 namespace :test do
96 desc 'Runs the plugins unit tests.'
101 desc 'Runs the plugins unit tests.'
97 Rake::TestTask.new :units => "db:test:prepare" do |t|
102 Rake::TestTask.new :units => "db:test:prepare" do |t|
98 t.libs << "test"
103 t.libs << "test"
99 t.verbose = true
104 t.verbose = true
100 t.pattern = "plugins/#{ENV['NAME'] || '*'}/test/unit/**/*_test.rb"
105 t.pattern = "plugins/#{ENV['NAME'] || '*'}/test/unit/**/*_test.rb"
101 end
106 end
102
107
103 desc 'Runs the plugins functional tests.'
108 desc 'Runs the plugins functional tests.'
104 Rake::TestTask.new :functionals => "db:test:prepare" do |t|
109 Rake::TestTask.new :functionals => "db:test:prepare" do |t|
105 t.libs << "test"
110 t.libs << "test"
106 t.verbose = true
111 t.verbose = true
107 t.pattern = "plugins/#{ENV['NAME'] || '*'}/test/functional/**/*_test.rb"
112 t.pattern = "plugins/#{ENV['NAME'] || '*'}/test/functional/**/*_test.rb"
108 end
113 end
109
114
110 desc 'Runs the plugins integration tests.'
115 desc 'Runs the plugins integration tests.'
111 Rake::TestTask.new :integration => "db:test:prepare" do |t|
116 Rake::TestTask.new :integration => "db:test:prepare" do |t|
112 t.libs << "test"
117 t.libs << "test"
113 t.verbose = true
118 t.verbose = true
114 t.pattern = "plugins/#{ENV['NAME'] || '*'}/test/integration/**/*_test.rb"
119 t.pattern = "plugins/#{ENV['NAME'] || '*'}/test/integration/**/*_test.rb"
115 end
120 end
116 end
121 end
117 end
122 end
118 end
123 end
119
124
120 # Load plugins' rake tasks
125 # Load plugins' rake tasks
121 Dir[File.join(Rails.root, "plugins/*/lib/tasks/**/*.rake")].sort.each { |ext| load ext }
126 Dir[File.join(Rails.root, "plugins/*/lib/tasks/**/*.rake")].sort.each { |ext| load ext }
@@ -1,236 +1,269
1 ---
1 ---
2 attachments_001:
2 attachments_001:
3 created_on: 2006-07-19 21:07:27 +02:00
3 created_on: 2006-07-19 21:07:27 +02:00
4 downloads: 0
4 downloads: 0
5 content_type: text/plain
5 content_type: text/plain
6 disk_filename: 060719210727_error281.txt
6 disk_filename: 060719210727_error281.txt
7 disk_directory: "2006/07"
7 container_id: 3
8 container_id: 3
8 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
9 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
9 id: 1
10 id: 1
10 container_type: Issue
11 container_type: Issue
11 filesize: 28
12 filesize: 28
12 filename: error281.txt
13 filename: error281.txt
13 author_id: 2
14 author_id: 2
14 attachments_002:
15 attachments_002:
15 created_on: 2007-01-27 15:08:27 +01:00
16 created_on: 2007-01-27 15:08:27 +01:00
16 downloads: 0
17 downloads: 0
17 content_type: text/plain
18 content_type: text/plain
18 disk_filename: 060719210727_document.txt
19 disk_filename: 060719210727_document.txt
20 disk_directory: "2006/07"
19 container_id: 1
21 container_id: 1
20 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
22 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
21 id: 2
23 id: 2
22 container_type: Document
24 container_type: Document
23 filesize: 28
25 filesize: 28
24 filename: document.txt
26 filename: document.txt
25 author_id: 2
27 author_id: 2
26 attachments_003:
28 attachments_003:
27 created_on: 2006-07-19 21:07:27 +02:00
29 created_on: 2006-07-19 21:07:27 +02:00
28 downloads: 0
30 downloads: 0
29 content_type: image/gif
31 content_type: image/gif
30 disk_filename: 060719210727_logo.gif
32 disk_filename: 060719210727_logo.gif
33 disk_directory: "2006/07"
31 container_id: 4
34 container_id: 4
32 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
35 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
33 id: 3
36 id: 3
34 container_type: WikiPage
37 container_type: WikiPage
35 filesize: 280
38 filesize: 280
36 filename: logo.gif
39 filename: logo.gif
37 description: This is a logo
40 description: This is a logo
38 author_id: 2
41 author_id: 2
39 attachments_004:
42 attachments_004:
40 created_on: 2006-07-19 21:07:27 +02:00
43 created_on: 2006-07-19 21:07:27 +02:00
41 container_type: Issue
44 container_type: Issue
42 container_id: 3
45 container_id: 3
43 downloads: 0
46 downloads: 0
44 disk_filename: 060719210727_source.rb
47 disk_filename: 060719210727_source.rb
48 disk_directory: "2006/07"
45 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
49 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
46 id: 4
50 id: 4
47 filesize: 153
51 filesize: 153
48 filename: source.rb
52 filename: source.rb
49 author_id: 2
53 author_id: 2
50 description: This is a Ruby source file
54 description: This is a Ruby source file
51 content_type: application/x-ruby
55 content_type: application/x-ruby
52 attachments_005:
56 attachments_005:
53 created_on: 2006-07-19 21:07:27 +02:00
57 created_on: 2006-07-19 21:07:27 +02:00
54 container_type: Issue
58 container_type: Issue
55 container_id: 3
59 container_id: 3
56 downloads: 0
60 downloads: 0
57 disk_filename: 060719210727_changeset_iso8859-1.diff
61 disk_filename: 060719210727_changeset_iso8859-1.diff
62 disk_directory: "2006/07"
58 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
63 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
59 id: 5
64 id: 5
60 filesize: 687
65 filesize: 687
61 filename: changeset_iso8859-1.diff
66 filename: changeset_iso8859-1.diff
62 author_id: 2
67 author_id: 2
63 content_type: text/x-diff
68 content_type: text/x-diff
64 attachments_006:
69 attachments_006:
65 created_on: 2006-07-19 21:07:27 +02:00
70 created_on: 2006-07-19 21:07:27 +02:00
66 container_type: Issue
71 container_type: Issue
67 container_id: 3
72 container_id: 3
68 downloads: 0
73 downloads: 0
69 disk_filename: 060719210727_archive.zip
74 disk_filename: 060719210727_archive.zip
75 disk_directory: "2006/07"
70 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
76 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
71 id: 6
77 id: 6
72 filesize: 157
78 filesize: 157
73 filename: archive.zip
79 filename: archive.zip
74 author_id: 2
80 author_id: 2
75 content_type: application/octet-stream
81 content_type: application/octet-stream
76 attachments_007:
82 attachments_007:
77 created_on: 2006-07-19 21:07:27 +02:00
83 created_on: 2006-07-19 21:07:27 +02:00
78 container_type: Issue
84 container_type: Issue
79 container_id: 4
85 container_id: 4
80 downloads: 0
86 downloads: 0
81 disk_filename: 060719210727_archive.zip
87 disk_filename: 060719210727_archive.zip
88 disk_directory: "2006/07"
82 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
89 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
83 id: 7
90 id: 7
84 filesize: 157
91 filesize: 157
85 filename: archive.zip
92 filename: archive.zip
86 author_id: 1
93 author_id: 1
87 content_type: application/octet-stream
94 content_type: application/octet-stream
88 attachments_008:
95 attachments_008:
89 created_on: 2006-07-19 21:07:27 +02:00
96 created_on: 2006-07-19 21:07:27 +02:00
90 container_type: Project
97 container_type: Project
91 container_id: 1
98 container_id: 1
92 downloads: 0
99 downloads: 0
93 disk_filename: 060719210727_project_file.zip
100 disk_filename: 060719210727_project_file.zip
101 disk_directory: "2006/07"
94 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
102 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
95 id: 8
103 id: 8
96 filesize: 320
104 filesize: 320
97 filename: project_file.zip
105 filename: project_file.zip
98 author_id: 2
106 author_id: 2
99 content_type: application/octet-stream
107 content_type: application/octet-stream
100 attachments_009:
108 attachments_009:
101 created_on: 2006-07-19 21:07:27 +02:00
109 created_on: 2006-07-19 21:07:27 +02:00
102 container_type: Version
110 container_type: Version
103 container_id: 1
111 container_id: 1
104 downloads: 0
112 downloads: 0
105 disk_filename: 060719210727_archive.zip
113 disk_filename: 060719210727_archive.zip
114 disk_directory: "2006/07"
106 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
115 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
107 id: 9
116 id: 9
108 filesize: 452
117 filesize: 452
109 filename: version_file.zip
118 filename: version_file.zip
110 author_id: 2
119 author_id: 2
111 content_type: application/octet-stream
120 content_type: application/octet-stream
112 attachments_010:
121 attachments_010:
113 created_on: 2006-07-19 21:07:27 +02:00
122 created_on: 2006-07-19 21:07:27 +02:00
114 container_type: Issue
123 container_type: Issue
115 container_id: 2
124 container_id: 2
116 downloads: 0
125 downloads: 0
117 disk_filename: 060719210727_picture.jpg
126 disk_filename: 060719210727_picture.jpg
127 disk_directory: "2006/07"
118 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
128 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
119 id: 10
129 id: 10
120 filesize: 452
130 filesize: 452
121 filename: picture.jpg
131 filename: picture.jpg
122 author_id: 2
132 author_id: 2
123 content_type: image/jpeg
133 content_type: image/jpeg
124 attachments_011:
134 attachments_011:
125 created_on: 2007-02-12 15:08:27 +01:00
135 created_on: 2007-02-12 15:08:27 +01:00
126 container_type: Document
136 container_type: Document
127 container_id: 1
137 container_id: 1
128 downloads: 0
138 downloads: 0
129 disk_filename: 060719210727_picture.jpg
139 disk_filename: 060719210727_picture.jpg
140 disk_directory: "2006/07"
130 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
141 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
131 id: 11
142 id: 11
132 filesize: 452
143 filesize: 452
133 filename: picture.jpg
144 filename: picture.jpg
134 author_id: 2
145 author_id: 2
135 content_type: image/jpeg
146 content_type: image/jpeg
136 attachments_012:
147 attachments_012:
137 created_on: 2006-07-19 21:07:27 +02:00
148 created_on: 2006-07-19 21:07:27 +02:00
138 container_type: Version
149 container_type: Version
139 container_id: 1
150 container_id: 1
140 downloads: 0
151 downloads: 0
141 disk_filename: 060719210727_version_file.zip
152 disk_filename: 060719210727_version_file.zip
153 disk_directory: "2006/07"
142 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
154 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
143 id: 12
155 id: 12
144 filesize: 452
156 filesize: 452
145 filename: version_file.zip
157 filename: version_file.zip
146 author_id: 2
158 author_id: 2
147 content_type: application/octet-stream
159 content_type: application/octet-stream
148 attachments_013:
160 attachments_013:
149 created_on: 2006-07-19 21:07:27 +02:00
161 created_on: 2006-07-19 21:07:27 +02:00
150 container_type: Message
162 container_type: Message
151 container_id: 1
163 container_id: 1
152 downloads: 0
164 downloads: 0
153 disk_filename: 060719210727_foo.zip
165 disk_filename: 060719210727_foo.zip
166 disk_directory: "2006/07"
154 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
167 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
155 id: 13
168 id: 13
156 filesize: 452
169 filesize: 452
157 filename: foo.zip
170 filename: foo.zip
158 author_id: 2
171 author_id: 2
159 content_type: application/octet-stream
172 content_type: application/octet-stream
160 attachments_014:
173 attachments_014:
161 created_on: 2006-07-19 21:07:27 +02:00
174 created_on: 2006-07-19 21:07:27 +02:00
162 container_type: Issue
175 container_type: Issue
163 container_id: 3
176 container_id: 3
164 downloads: 0
177 downloads: 0
165 disk_filename: 060719210727_changeset_utf8.diff
178 disk_filename: 060719210727_changeset_utf8.diff
179 disk_directory: "2006/07"
166 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
180 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
167 id: 14
181 id: 14
168 filesize: 687
182 filesize: 687
169 filename: changeset_utf8.diff
183 filename: changeset_utf8.diff
170 author_id: 2
184 author_id: 2
171 content_type: text/x-diff
185 content_type: text/x-diff
172 attachments_015:
186 attachments_015:
173 id: 15
187 id: 15
174 created_on: 2010-07-19 21:07:27 +02:00
188 created_on: 2010-07-19 21:07:27 +02:00
175 container_type: Issue
189 container_type: Issue
176 container_id: 14
190 container_id: 14
177 downloads: 0
191 downloads: 0
178 disk_filename: 060719210727_changeset_utf8.diff
192 disk_filename: 060719210727_changeset_utf8.diff
193 disk_directory: "2006/07"
179 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
194 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
180 filesize: 687
195 filesize: 687
181 filename: private.diff
196 filename: private.diff
182 author_id: 2
197 author_id: 2
183 content_type: text/x-diff
198 content_type: text/x-diff
184 description: attachement of a private issue
199 description: attachement of a private issue
185 attachments_016:
200 attachments_016:
186 content_type: image/png
201 content_type: image/png
187 downloads: 0
202 downloads: 0
188 created_on: 2010-11-23 16:14:50 +09:00
203 created_on: 2010-11-23 16:14:50 +09:00
189 disk_filename: 101123161450_testfile_1.png
204 disk_filename: 101123161450_testfile_1.png
205 disk_directory: "2010/11"
190 container_id: 14
206 container_id: 14
191 digest: 8e0294de2441577c529f170b6fb8f638
207 digest: 8e0294de2441577c529f170b6fb8f638
192 id: 16
208 id: 16
193 container_type: Issue
209 container_type: Issue
194 description: ""
210 description: ""
195 filename: testfile.png
211 filename: testfile.png
196 filesize: 2654
212 filesize: 2654
197 author_id: 2
213 author_id: 2
198 attachments_017:
214 attachments_017:
199 content_type: image/png
215 content_type: image/png
200 downloads: 0
216 downloads: 0
201 created_on: 2010-12-23 16:14:50 +09:00
217 created_on: 2010-12-23 16:14:50 +09:00
202 disk_filename: 101223161450_testfile_2.png
218 disk_filename: 101223161450_testfile_2.png
219 disk_directory: "2010/12"
203 container_id: 14
220 container_id: 14
204 digest: 6bc2963e8d7ea0d3e68d12d1fba3d6ca
221 digest: 6bc2963e8d7ea0d3e68d12d1fba3d6ca
205 id: 17
222 id: 17
206 container_type: Issue
223 container_type: Issue
207 description: ""
224 description: ""
208 filename: testfile.PNG
225 filename: testfile.PNG
209 filesize: 3582
226 filesize: 3582
210 author_id: 2
227 author_id: 2
211 attachments_018:
228 attachments_018:
212 content_type: image/png
229 content_type: image/png
213 downloads: 0
230 downloads: 0
214 created_on: 2011-01-23 16:14:50 +09:00
231 created_on: 2011-01-23 16:14:50 +09:00
215 disk_filename: 101123161450_testfile_1.png
232 disk_filename: 101123161450_testfile_1.png
233 disk_directory: "2010/11"
216 container_id: 14
234 container_id: 14
217 digest: 8e0294de2441577c529f170b6fb8f638
235 digest: 8e0294de2441577c529f170b6fb8f638
218 id: 18
236 id: 18
219 container_type: Issue
237 container_type: Issue
220 description: ""
238 description: ""
221 filename: testγƒ†γ‚Ήγƒˆ.png
239 filename: testγƒ†γ‚Ήγƒˆ.png
222 filesize: 2654
240 filesize: 2654
223 author_id: 2
241 author_id: 2
224 attachments_019:
242 attachments_019:
225 content_type: image/png
243 content_type: image/png
226 downloads: 0
244 downloads: 0
227 created_on: 2011-02-23 16:14:50 +09:00
245 created_on: 2011-02-23 16:14:50 +09:00
228 disk_filename: 101223161450_testfile_2.png
246 disk_filename: 101223161450_testfile_2.png
247 disk_directory: "2010/12"
229 container_id: 14
248 container_id: 14
230 digest: 6bc2963e8d7ea0d3e68d12d1fba3d6ca
249 digest: 6bc2963e8d7ea0d3e68d12d1fba3d6ca
231 id: 19
250 id: 19
232 container_type: Issue
251 container_type: Issue
233 description: ""
252 description: ""
234 filename: Testγƒ†γ‚Ήγƒˆ.PNG
253 filename: Testγƒ†γ‚Ήγƒˆ.PNG
235 filesize: 3582
254 filesize: 3582
236 author_id: 2
255 author_id: 2
256 attachments_020:
257 content_type: text/plain
258 downloads: 0
259 created_on: 2012-05-12 16:14:50 +09:00
260 disk_filename: 120512161450_root_attachment.txt
261 disk_directory:
262 container_id: 14
263 digest: b0fe2abdb2599743d554a61d7da7ff74
264 id: 20
265 container_type: Issue
266 description: ""
267 filename: root_attachment.txt
268 filesize: 54
269 author_id: 2
1 NO CONTENT: file renamed from test/fixtures/files/060719210727_archive.zip to test/fixtures/files/2006/07/060719210727_archive.zip
NO CONTENT: file renamed from test/fixtures/files/060719210727_archive.zip to test/fixtures/files/2006/07/060719210727_archive.zip
1 NO CONTENT: file renamed from test/fixtures/files/060719210727_changeset_iso8859-1.diff to test/fixtures/files/2006/07/060719210727_changeset_iso8859-1.diff
NO CONTENT: file renamed from test/fixtures/files/060719210727_changeset_iso8859-1.diff to test/fixtures/files/2006/07/060719210727_changeset_iso8859-1.diff
1 NO CONTENT: file renamed from test/fixtures/files/060719210727_changeset_utf8.diff to test/fixtures/files/2006/07/060719210727_changeset_utf8.diff
NO CONTENT: file renamed from test/fixtures/files/060719210727_changeset_utf8.diff to test/fixtures/files/2006/07/060719210727_changeset_utf8.diff
1 NO CONTENT: file renamed from test/fixtures/files/060719210727_source.rb to test/fixtures/files/2006/07/060719210727_source.rb
NO CONTENT: file renamed from test/fixtures/files/060719210727_source.rb to test/fixtures/files/2006/07/060719210727_source.rb
1 NO CONTENT: file renamed from test/fixtures/files/101123161450_testfile_1.png to test/fixtures/files/2010/11/101123161450_testfile_1.png
NO CONTENT: file renamed from test/fixtures/files/101123161450_testfile_1.png to test/fixtures/files/2010/11/101123161450_testfile_1.png
1 NO CONTENT: file renamed from test/fixtures/files/101223161450_testfile_2.png to test/fixtures/files/2010/12/101223161450_testfile_2.png
NO CONTENT: file renamed from test/fixtures/files/101223161450_testfile_2.png to test/fixtures/files/2010/12/101223161450_testfile_2.png
@@ -1,255 +1,288
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 require File.expand_path('../../test_helper', __FILE__)
20 require File.expand_path('../../test_helper', __FILE__)
21
21
22 class AttachmentTest < ActiveSupport::TestCase
22 class AttachmentTest < ActiveSupport::TestCase
23 fixtures :users, :projects, :roles, :members, :member_roles,
23 fixtures :users, :projects, :roles, :members, :member_roles,
24 :enabled_modules, :issues, :trackers, :attachments
24 :enabled_modules, :issues, :trackers, :attachments
25
25
26 class MockFile
26 class MockFile
27 attr_reader :original_filename, :content_type, :content, :size
27 attr_reader :original_filename, :content_type, :content, :size
28
28
29 def initialize(attributes)
29 def initialize(attributes)
30 @original_filename = attributes[:original_filename]
30 @original_filename = attributes[:original_filename]
31 @content_type = attributes[:content_type]
31 @content_type = attributes[:content_type]
32 @content = attributes[:content] || "Content"
32 @content = attributes[:content] || "Content"
33 @size = content.size
33 @size = content.size
34 end
34 end
35 end
35 end
36
36
37 def setup
37 def setup
38 set_tmp_attachments_directory
38 set_tmp_attachments_directory
39 end
39 end
40
40
41 def test_container_for_new_attachment_should_be_nil
41 def test_container_for_new_attachment_should_be_nil
42 assert_nil Attachment.new.container
42 assert_nil Attachment.new.container
43 end
43 end
44
44
45 def test_create
45 def test_create
46 a = Attachment.new(:container => Issue.find(1),
46 a = Attachment.new(:container => Issue.find(1),
47 :file => uploaded_test_file("testfile.txt", "text/plain"),
47 :file => uploaded_test_file("testfile.txt", "text/plain"),
48 :author => User.find(1))
48 :author => User.find(1))
49 assert a.save
49 assert a.save
50 assert_equal 'testfile.txt', a.filename
50 assert_equal 'testfile.txt', a.filename
51 assert_equal 59, a.filesize
51 assert_equal 59, a.filesize
52 assert_equal 'text/plain', a.content_type
52 assert_equal 'text/plain', a.content_type
53 assert_equal 0, a.downloads
53 assert_equal 0, a.downloads
54 assert_equal '1478adae0d4eb06d35897518540e25d6', a.digest
54 assert_equal '1478adae0d4eb06d35897518540e25d6', a.digest
55
56 assert a.disk_directory
57 assert_match %r{\A\d{4}/\d{2}\z}, a.disk_directory
58
55 assert File.exist?(a.diskfile)
59 assert File.exist?(a.diskfile)
56 assert_equal 59, File.size(a.diskfile)
60 assert_equal 59, File.size(a.diskfile)
57 end
61 end
58
62
63 def test_copy_should_preserve_attributes
64 a = Attachment.find(1)
65 copy = a.copy
66
67 assert_save copy
68 copy = Attachment.order('id DESC').first
69 %w(filename filesize content_type author_id created_on description digest disk_filename disk_directory diskfile).each do |attribute|
70 assert_equal a.send(attribute), copy.send(attribute), "#{attribute} was different"
71 end
72 end
73
59 def test_size_should_be_validated_for_new_file
74 def test_size_should_be_validated_for_new_file
60 with_settings :attachment_max_size => 0 do
75 with_settings :attachment_max_size => 0 do
61 a = Attachment.new(:container => Issue.find(1),
76 a = Attachment.new(:container => Issue.find(1),
62 :file => uploaded_test_file("testfile.txt", "text/plain"),
77 :file => uploaded_test_file("testfile.txt", "text/plain"),
63 :author => User.find(1))
78 :author => User.find(1))
64 assert !a.save
79 assert !a.save
65 end
80 end
66 end
81 end
67
82
68 def test_size_should_not_be_validated_when_copying
83 def test_size_should_not_be_validated_when_copying
69 a = Attachment.create!(:container => Issue.find(1),
84 a = Attachment.create!(:container => Issue.find(1),
70 :file => uploaded_test_file("testfile.txt", "text/plain"),
85 :file => uploaded_test_file("testfile.txt", "text/plain"),
71 :author => User.find(1))
86 :author => User.find(1))
72 with_settings :attachment_max_size => 0 do
87 with_settings :attachment_max_size => 0 do
73 copy = a.copy
88 copy = a.copy
74 assert copy.save
89 assert copy.save
75 end
90 end
76 end
91 end
77
92
78 def test_description_length_should_be_validated
93 def test_description_length_should_be_validated
79 a = Attachment.new(:description => 'a' * 300)
94 a = Attachment.new(:description => 'a' * 300)
80 assert !a.save
95 assert !a.save
81 assert_not_nil a.errors[:description]
96 assert_not_nil a.errors[:description]
82 end
97 end
83
98
84 def test_destroy
99 def test_destroy
85 a = Attachment.new(:container => Issue.find(1),
100 a = Attachment.new(:container => Issue.find(1),
86 :file => uploaded_test_file("testfile.txt", "text/plain"),
101 :file => uploaded_test_file("testfile.txt", "text/plain"),
87 :author => User.find(1))
102 :author => User.find(1))
88 assert a.save
103 assert a.save
89 assert_equal 'testfile.txt', a.filename
104 assert_equal 'testfile.txt', a.filename
90 assert_equal 59, a.filesize
105 assert_equal 59, a.filesize
91 assert_equal 'text/plain', a.content_type
106 assert_equal 'text/plain', a.content_type
92 assert_equal 0, a.downloads
107 assert_equal 0, a.downloads
93 assert_equal '1478adae0d4eb06d35897518540e25d6', a.digest
108 assert_equal '1478adae0d4eb06d35897518540e25d6', a.digest
94 diskfile = a.diskfile
109 diskfile = a.diskfile
95 assert File.exist?(diskfile)
110 assert File.exist?(diskfile)
96 assert_equal 59, File.size(a.diskfile)
111 assert_equal 59, File.size(a.diskfile)
97 assert a.destroy
112 assert a.destroy
98 assert !File.exist?(diskfile)
113 assert !File.exist?(diskfile)
99 end
114 end
100
115
101 def test_destroy_should_not_delete_file_referenced_by_other_attachment
116 def test_destroy_should_not_delete_file_referenced_by_other_attachment
102 a = Attachment.create!(:container => Issue.find(1),
117 a = Attachment.create!(:container => Issue.find(1),
103 :file => uploaded_test_file("testfile.txt", "text/plain"),
118 :file => uploaded_test_file("testfile.txt", "text/plain"),
104 :author => User.find(1))
119 :author => User.find(1))
105 diskfile = a.diskfile
120 diskfile = a.diskfile
106
121
107 copy = a.copy
122 copy = a.copy
108 copy.save!
123 copy.save!
109
124
110 assert File.exists?(diskfile)
125 assert File.exists?(diskfile)
111 a.destroy
126 a.destroy
112 assert File.exists?(diskfile)
127 assert File.exists?(diskfile)
113 copy.destroy
128 copy.destroy
114 assert !File.exists?(diskfile)
129 assert !File.exists?(diskfile)
115 end
130 end
116
131
117 def test_create_should_auto_assign_content_type
132 def test_create_should_auto_assign_content_type
118 a = Attachment.new(:container => Issue.find(1),
133 a = Attachment.new(:container => Issue.find(1),
119 :file => uploaded_test_file("testfile.txt", ""),
134 :file => uploaded_test_file("testfile.txt", ""),
120 :author => User.find(1))
135 :author => User.find(1))
121 assert a.save
136 assert a.save
122 assert_equal 'text/plain', a.content_type
137 assert_equal 'text/plain', a.content_type
123 end
138 end
124
139
125 def test_identical_attachments_at_the_same_time_should_not_overwrite
140 def test_identical_attachments_at_the_same_time_should_not_overwrite
141 time = DateTime.now
142 DateTime.stubs(:now).returns(time)
143
126 a1 = Attachment.create!(:container => Issue.find(1),
144 a1 = Attachment.create!(:container => Issue.find(1),
127 :file => uploaded_test_file("testfile.txt", ""),
145 :file => uploaded_test_file("testfile.txt", ""),
128 :author => User.find(1))
146 :author => User.find(1))
129 a2 = Attachment.create!(:container => Issue.find(1),
147 a2 = Attachment.create!(:container => Issue.find(1),
130 :file => uploaded_test_file("testfile.txt", ""),
148 :file => uploaded_test_file("testfile.txt", ""),
131 :author => User.find(1))
149 :author => User.find(1))
132 assert a1.disk_filename != a2.disk_filename
150 assert a1.disk_filename != a2.disk_filename
133 end
151 end
134
152
135 def test_filename_should_be_basenamed
153 def test_filename_should_be_basenamed
136 a = Attachment.new(:file => MockFile.new(:original_filename => "path/to/the/file"))
154 a = Attachment.new(:file => MockFile.new(:original_filename => "path/to/the/file"))
137 assert_equal 'file', a.filename
155 assert_equal 'file', a.filename
138 end
156 end
139
157
140 def test_filename_should_be_sanitized
158 def test_filename_should_be_sanitized
141 a = Attachment.new(:file => MockFile.new(:original_filename => "valid:[] invalid:?%*|\"'<>chars"))
159 a = Attachment.new(:file => MockFile.new(:original_filename => "valid:[] invalid:?%*|\"'<>chars"))
142 assert_equal 'valid_[] invalid_chars', a.filename
160 assert_equal 'valid_[] invalid_chars', a.filename
143 end
161 end
144
162
145 def test_diskfilename
163 def test_diskfilename
146 assert Attachment.disk_filename("test_file.txt") =~ /^\d{12}_test_file.txt$/
164 assert Attachment.disk_filename("test_file.txt") =~ /^\d{12}_test_file.txt$/
147 assert_equal 'test_file.txt', Attachment.disk_filename("test_file.txt")[13..-1]
165 assert_equal 'test_file.txt', Attachment.disk_filename("test_file.txt")[13..-1]
148 assert_equal '770c509475505f37c2b8fb6030434d6b.txt', Attachment.disk_filename("test_accentuΓ©.txt")[13..-1]
166 assert_equal '770c509475505f37c2b8fb6030434d6b.txt', Attachment.disk_filename("test_accentuΓ©.txt")[13..-1]
149 assert_equal 'f8139524ebb8f32e51976982cd20a85d', Attachment.disk_filename("test_accentuΓ©")[13..-1]
167 assert_equal 'f8139524ebb8f32e51976982cd20a85d', Attachment.disk_filename("test_accentuΓ©")[13..-1]
150 assert_equal 'cbb5b0f30978ba03731d61f9f6d10011', Attachment.disk_filename("test_accentuΓ©.Γ§a")[13..-1]
168 assert_equal 'cbb5b0f30978ba03731d61f9f6d10011', Attachment.disk_filename("test_accentuΓ©.Γ§a")[13..-1]
151 end
169 end
152
170
153 def test_title
171 def test_title
154 a = Attachment.new(:filename => "test.png")
172 a = Attachment.new(:filename => "test.png")
155 assert_equal "test.png", a.title
173 assert_equal "test.png", a.title
156
174
157 a = Attachment.new(:filename => "test.png", :description => "Cool image")
175 a = Attachment.new(:filename => "test.png", :description => "Cool image")
158 assert_equal "test.png (Cool image)", a.title
176 assert_equal "test.png (Cool image)", a.title
159 end
177 end
160
178
161 def test_prune_should_destroy_old_unattached_attachments
179 def test_prune_should_destroy_old_unattached_attachments
162 Attachment.create!(:file => uploaded_test_file("testfile.txt", ""), :author_id => 1, :created_on => 2.days.ago)
180 Attachment.create!(:file => uploaded_test_file("testfile.txt", ""), :author_id => 1, :created_on => 2.days.ago)
163 Attachment.create!(:file => uploaded_test_file("testfile.txt", ""), :author_id => 1, :created_on => 2.days.ago)
181 Attachment.create!(:file => uploaded_test_file("testfile.txt", ""), :author_id => 1, :created_on => 2.days.ago)
164 Attachment.create!(:file => uploaded_test_file("testfile.txt", ""), :author_id => 1)
182 Attachment.create!(:file => uploaded_test_file("testfile.txt", ""), :author_id => 1)
165
183
166 assert_difference 'Attachment.count', -2 do
184 assert_difference 'Attachment.count', -2 do
167 Attachment.prune
185 Attachment.prune
168 end
186 end
169 end
187 end
170
188
189 def test_move_from_root_to_target_directory_should_move_root_files
190 a = Attachment.find(20)
191 assert a.disk_directory.blank?
192 # Create a real file for this fixture
193 File.open(a.diskfile, "w") do |f|
194 f.write "test file at the root of files directory"
195 end
196 assert a.readable?
197 Attachment.move_from_root_to_target_directory
198
199 a.reload
200 assert_equal '2012/05', a.disk_directory
201 assert a.readable?
202 end
203
171 context "Attachmnet.attach_files" do
204 context "Attachmnet.attach_files" do
172 should "attach the file" do
205 should "attach the file" do
173 issue = Issue.first
206 issue = Issue.first
174 assert_difference 'Attachment.count' do
207 assert_difference 'Attachment.count' do
175 Attachment.attach_files(issue,
208 Attachment.attach_files(issue,
176 '1' => {
209 '1' => {
177 'file' => uploaded_test_file('testfile.txt', 'text/plain'),
210 'file' => uploaded_test_file('testfile.txt', 'text/plain'),
178 'description' => 'test'
211 'description' => 'test'
179 })
212 })
180 end
213 end
181
214
182 attachment = Attachment.first(:order => 'id DESC')
215 attachment = Attachment.first(:order => 'id DESC')
183 assert_equal issue, attachment.container
216 assert_equal issue, attachment.container
184 assert_equal 'testfile.txt', attachment.filename
217 assert_equal 'testfile.txt', attachment.filename
185 assert_equal 59, attachment.filesize
218 assert_equal 59, attachment.filesize
186 assert_equal 'test', attachment.description
219 assert_equal 'test', attachment.description
187 assert_equal 'text/plain', attachment.content_type
220 assert_equal 'text/plain', attachment.content_type
188 assert File.exists?(attachment.diskfile)
221 assert File.exists?(attachment.diskfile)
189 assert_equal 59, File.size(attachment.diskfile)
222 assert_equal 59, File.size(attachment.diskfile)
190 end
223 end
191
224
192 should "add unsaved files to the object as unsaved attachments" do
225 should "add unsaved files to the object as unsaved attachments" do
193 # Max size of 0 to force Attachment creation failures
226 # Max size of 0 to force Attachment creation failures
194 with_settings(:attachment_max_size => 0) do
227 with_settings(:attachment_max_size => 0) do
195 @project = Project.find(1)
228 @project = Project.find(1)
196 response = Attachment.attach_files(@project, {
229 response = Attachment.attach_files(@project, {
197 '1' => {'file' => mock_file, 'description' => 'test'},
230 '1' => {'file' => mock_file, 'description' => 'test'},
198 '2' => {'file' => mock_file, 'description' => 'test'}
231 '2' => {'file' => mock_file, 'description' => 'test'}
199 })
232 })
200
233
201 assert response[:unsaved].present?
234 assert response[:unsaved].present?
202 assert_equal 2, response[:unsaved].length
235 assert_equal 2, response[:unsaved].length
203 assert response[:unsaved].first.new_record?
236 assert response[:unsaved].first.new_record?
204 assert response[:unsaved].second.new_record?
237 assert response[:unsaved].second.new_record?
205 assert_equal response[:unsaved], @project.unsaved_attachments
238 assert_equal response[:unsaved], @project.unsaved_attachments
206 end
239 end
207 end
240 end
208 end
241 end
209
242
210 def test_latest_attach
243 def test_latest_attach
211 set_fixtures_attachments_directory
244 set_fixtures_attachments_directory
212 a1 = Attachment.find(16)
245 a1 = Attachment.find(16)
213 assert_equal "testfile.png", a1.filename
246 assert_equal "testfile.png", a1.filename
214 assert a1.readable?
247 assert a1.readable?
215 assert (! a1.visible?(User.anonymous))
248 assert (! a1.visible?(User.anonymous))
216 assert a1.visible?(User.find(2))
249 assert a1.visible?(User.find(2))
217 a2 = Attachment.find(17)
250 a2 = Attachment.find(17)
218 assert_equal "testfile.PNG", a2.filename
251 assert_equal "testfile.PNG", a2.filename
219 assert a2.readable?
252 assert a2.readable?
220 assert (! a2.visible?(User.anonymous))
253 assert (! a2.visible?(User.anonymous))
221 assert a2.visible?(User.find(2))
254 assert a2.visible?(User.find(2))
222 assert a1.created_on < a2.created_on
255 assert a1.created_on < a2.created_on
223
256
224 la1 = Attachment.latest_attach([a1, a2], "testfile.png")
257 la1 = Attachment.latest_attach([a1, a2], "testfile.png")
225 assert_equal 17, la1.id
258 assert_equal 17, la1.id
226 la2 = Attachment.latest_attach([a1, a2], "Testfile.PNG")
259 la2 = Attachment.latest_attach([a1, a2], "Testfile.PNG")
227 assert_equal 17, la2.id
260 assert_equal 17, la2.id
228
261
229 set_tmp_attachments_directory
262 set_tmp_attachments_directory
230 end
263 end
231
264
232 def test_thumbnailable_should_be_true_for_images
265 def test_thumbnailable_should_be_true_for_images
233 assert_equal true, Attachment.new(:filename => 'test.jpg').thumbnailable?
266 assert_equal true, Attachment.new(:filename => 'test.jpg').thumbnailable?
234 end
267 end
235
268
236 def test_thumbnailable_should_be_true_for_non_images
269 def test_thumbnailable_should_be_true_for_non_images
237 assert_equal false, Attachment.new(:filename => 'test.txt').thumbnailable?
270 assert_equal false, Attachment.new(:filename => 'test.txt').thumbnailable?
238 end
271 end
239
272
240 if convert_installed?
273 if convert_installed?
241 def test_thumbnail_should_generate_the_thumbnail
274 def test_thumbnail_should_generate_the_thumbnail
242 set_fixtures_attachments_directory
275 set_fixtures_attachments_directory
243 attachment = Attachment.find(16)
276 attachment = Attachment.find(16)
244 Attachment.clear_thumbnails
277 Attachment.clear_thumbnails
245
278
246 assert_difference "Dir.glob(File.join(Attachment.thumbnails_storage_path, '*.thumb')).size" do
279 assert_difference "Dir.glob(File.join(Attachment.thumbnails_storage_path, '*.thumb')).size" do
247 thumbnail = attachment.thumbnail
280 thumbnail = attachment.thumbnail
248 assert_equal "16_8e0294de2441577c529f170b6fb8f638_100.thumb", File.basename(thumbnail)
281 assert_equal "16_8e0294de2441577c529f170b6fb8f638_100.thumb", File.basename(thumbnail)
249 assert File.exists?(thumbnail)
282 assert File.exists?(thumbnail)
250 end
283 end
251 end
284 end
252 else
285 else
253 puts '(ImageMagick convert not available)'
286 puts '(ImageMagick convert not available)'
254 end
287 end
255 end
288 end
General Comments 0
You need to be logged in to leave comments. Login now