##// 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
@@ -16,6 +16,7
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
@@ -92,9 +93,6 class Attachment < ActiveRecord::Base
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
@@ -102,7 +100,13 class Attachment < ActiveRecord::Base
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)
@@ -134,7 +138,7 class Attachment < ActiveRecord::Base
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
@@ -259,6 +263,26 class Attachment < ActiveRecord::Base
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
@@ -276,8 +300,15 class Attachment < ActiveRecord::Base
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_\.\-]*$}
@@ -287,7 +318,7 class Attachment < ActiveRecord::Base
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}"
@@ -21,6 +21,11 namespace :redmine do
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
@@ -4,6 +4,7 attachments_001:
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
@@ -16,6 +17,7 attachments_002:
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
@@ -28,6 +30,7 attachments_003:
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
@@ -42,6 +45,7 attachments_004:
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
@@ -55,6 +59,7 attachments_005:
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
@@ -67,6 +72,7 attachments_006:
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
@@ -79,6 +85,7 attachments_007:
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
@@ -91,6 +98,7 attachments_008:
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
@@ -103,6 +111,7 attachments_009:
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
@@ -115,6 +124,7 attachments_010:
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
@@ -127,6 +137,7 attachments_011:
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
@@ -139,6 +150,7 attachments_012:
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
@@ -151,6 +163,7 attachments_013:
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
@@ -163,6 +176,7 attachments_014:
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
@@ -176,6 +190,7 attachments_015:
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
@@ -187,6 +202,7 attachments_016:
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
@@ -200,6 +216,7 attachments_017:
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
@@ -213,6 +230,7 attachments_018:
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
@@ -226,6 +244,7 attachments_019:
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
@@ -234,3 +253,17 attachments_019:
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
@@ -52,10 +52,25 class AttachmentTest < ActiveSupport::TestCase
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),
@@ -123,6 +138,9 class AttachmentTest < ActiveSupport::TestCase
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))
@@ -168,6 +186,21 class AttachmentTest < ActiveSupport::TestCase
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
General Comments 0
You need to be logged in to leave comments. Login now