##// 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 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require "digest/md5"
19 require "fileutils"
19 20
20 21 class Attachment < ActiveRecord::Base
21 22 belongs_to :container, :polymorphic => true
@@ -92,9 +93,6 class Attachment < ActiveRecord::Base
92 93
93 94 def filename=(arg)
94 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 96 filename
99 97 end
100 98
@@ -102,7 +100,13 class Attachment < ActiveRecord::Base
102 100 # and computes its MD5 hash
103 101 def files_to_final_location
104 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 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 110 md5 = Digest::MD5.new
107 111 File.open(diskfile, "wb") do |f|
108 112 if @temp_file.respond_to?(:read)
@@ -134,7 +138,7 class Attachment < ActiveRecord::Base
134 138
135 139 # Returns file's location on disk
136 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 142 end
139 143
140 144 def title
@@ -259,6 +263,26 class Attachment < ActiveRecord::Base
259 263 Attachment.where("created_on < ? AND (container_type IS NULL OR container_type = '')", Time.now - age).destroy_all
260 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 286 private
263 287
264 288 # Physically deletes the file from the file system
@@ -276,8 +300,15 class Attachment < ActiveRecord::Base
276 300 @filename = just_filename.gsub(/[\/\?\%\*\:\|\"\'<>]+/, '_')
277 301 end
278 302
279 # Returns an ASCII or hashed filename
280 def self.disk_filename(filename)
303 # Returns the subdirectory in which the attachment will be saved
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 312 timestamp = DateTime.now.strftime("%y%m%d%H%M%S")
282 313 ascii = ''
283 314 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
@@ -287,7 +318,7 class Attachment < ActiveRecord::Base
287 318 # keep the extension if any
288 319 ascii << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
289 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 322 timestamp.succ!
292 323 end
293 324 "#{timestamp}_#{ascii}"
@@ -21,6 +21,11 namespace :redmine do
21 21 task :prune => :environment do
22 22 Attachment.prune
23 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 29 end
25 30
26 31 namespace :tokens do
@@ -4,6 +4,7 attachments_001:
4 4 downloads: 0
5 5 content_type: text/plain
6 6 disk_filename: 060719210727_error281.txt
7 disk_directory: "2006/07"
7 8 container_id: 3
8 9 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
9 10 id: 1
@@ -16,6 +17,7 attachments_002:
16 17 downloads: 0
17 18 content_type: text/plain
18 19 disk_filename: 060719210727_document.txt
20 disk_directory: "2006/07"
19 21 container_id: 1
20 22 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
21 23 id: 2
@@ -28,6 +30,7 attachments_003:
28 30 downloads: 0
29 31 content_type: image/gif
30 32 disk_filename: 060719210727_logo.gif
33 disk_directory: "2006/07"
31 34 container_id: 4
32 35 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
33 36 id: 3
@@ -42,6 +45,7 attachments_004:
42 45 container_id: 3
43 46 downloads: 0
44 47 disk_filename: 060719210727_source.rb
48 disk_directory: "2006/07"
45 49 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
46 50 id: 4
47 51 filesize: 153
@@ -55,6 +59,7 attachments_005:
55 59 container_id: 3
56 60 downloads: 0
57 61 disk_filename: 060719210727_changeset_iso8859-1.diff
62 disk_directory: "2006/07"
58 63 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
59 64 id: 5
60 65 filesize: 687
@@ -67,6 +72,7 attachments_006:
67 72 container_id: 3
68 73 downloads: 0
69 74 disk_filename: 060719210727_archive.zip
75 disk_directory: "2006/07"
70 76 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
71 77 id: 6
72 78 filesize: 157
@@ -79,6 +85,7 attachments_007:
79 85 container_id: 4
80 86 downloads: 0
81 87 disk_filename: 060719210727_archive.zip
88 disk_directory: "2006/07"
82 89 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
83 90 id: 7
84 91 filesize: 157
@@ -91,6 +98,7 attachments_008:
91 98 container_id: 1
92 99 downloads: 0
93 100 disk_filename: 060719210727_project_file.zip
101 disk_directory: "2006/07"
94 102 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
95 103 id: 8
96 104 filesize: 320
@@ -103,6 +111,7 attachments_009:
103 111 container_id: 1
104 112 downloads: 0
105 113 disk_filename: 060719210727_archive.zip
114 disk_directory: "2006/07"
106 115 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
107 116 id: 9
108 117 filesize: 452
@@ -115,6 +124,7 attachments_010:
115 124 container_id: 2
116 125 downloads: 0
117 126 disk_filename: 060719210727_picture.jpg
127 disk_directory: "2006/07"
118 128 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
119 129 id: 10
120 130 filesize: 452
@@ -127,6 +137,7 attachments_011:
127 137 container_id: 1
128 138 downloads: 0
129 139 disk_filename: 060719210727_picture.jpg
140 disk_directory: "2006/07"
130 141 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
131 142 id: 11
132 143 filesize: 452
@@ -139,6 +150,7 attachments_012:
139 150 container_id: 1
140 151 downloads: 0
141 152 disk_filename: 060719210727_version_file.zip
153 disk_directory: "2006/07"
142 154 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
143 155 id: 12
144 156 filesize: 452
@@ -151,6 +163,7 attachments_013:
151 163 container_id: 1
152 164 downloads: 0
153 165 disk_filename: 060719210727_foo.zip
166 disk_directory: "2006/07"
154 167 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
155 168 id: 13
156 169 filesize: 452
@@ -163,6 +176,7 attachments_014:
163 176 container_id: 3
164 177 downloads: 0
165 178 disk_filename: 060719210727_changeset_utf8.diff
179 disk_directory: "2006/07"
166 180 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
167 181 id: 14
168 182 filesize: 687
@@ -176,6 +190,7 attachments_015:
176 190 container_id: 14
177 191 downloads: 0
178 192 disk_filename: 060719210727_changeset_utf8.diff
193 disk_directory: "2006/07"
179 194 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
180 195 filesize: 687
181 196 filename: private.diff
@@ -187,6 +202,7 attachments_016:
187 202 downloads: 0
188 203 created_on: 2010-11-23 16:14:50 +09:00
189 204 disk_filename: 101123161450_testfile_1.png
205 disk_directory: "2010/11"
190 206 container_id: 14
191 207 digest: 8e0294de2441577c529f170b6fb8f638
192 208 id: 16
@@ -200,6 +216,7 attachments_017:
200 216 downloads: 0
201 217 created_on: 2010-12-23 16:14:50 +09:00
202 218 disk_filename: 101223161450_testfile_2.png
219 disk_directory: "2010/12"
203 220 container_id: 14
204 221 digest: 6bc2963e8d7ea0d3e68d12d1fba3d6ca
205 222 id: 17
@@ -213,6 +230,7 attachments_018:
213 230 downloads: 0
214 231 created_on: 2011-01-23 16:14:50 +09:00
215 232 disk_filename: 101123161450_testfile_1.png
233 disk_directory: "2010/11"
216 234 container_id: 14
217 235 digest: 8e0294de2441577c529f170b6fb8f638
218 236 id: 18
@@ -226,6 +244,7 attachments_019:
226 244 downloads: 0
227 245 created_on: 2011-02-23 16:14:50 +09:00
228 246 disk_filename: 101223161450_testfile_2.png
247 disk_directory: "2010/12"
229 248 container_id: 14
230 249 digest: 6bc2963e8d7ea0d3e68d12d1fba3d6ca
231 250 id: 19
@@ -234,3 +253,17 attachments_019:
234 253 filename: Testγƒ†γ‚Ήγƒˆ.PNG
235 254 filesize: 3582
236 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
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
1 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
1 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
@@ -52,10 +52,25 class AttachmentTest < ActiveSupport::TestCase
52 52 assert_equal 'text/plain', a.content_type
53 53 assert_equal 0, a.downloads
54 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 59 assert File.exist?(a.diskfile)
56 60 assert_equal 59, File.size(a.diskfile)
57 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 74 def test_size_should_be_validated_for_new_file
60 75 with_settings :attachment_max_size => 0 do
61 76 a = Attachment.new(:container => Issue.find(1),
@@ -123,6 +138,9 class AttachmentTest < ActiveSupport::TestCase
123 138 end
124 139
125 140 def test_identical_attachments_at_the_same_time_should_not_overwrite
141 time = DateTime.now
142 DateTime.stubs(:now).returns(time)
143
126 144 a1 = Attachment.create!(:container => Issue.find(1),
127 145 :file => uploaded_test_file("testfile.txt", ""),
128 146 :author => User.find(1))
@@ -168,6 +186,21 class AttachmentTest < ActiveSupport::TestCase
168 186 end
169 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 204 context "Attachmnet.attach_files" do
172 205 should "attach the file" do
173 206 issue = Issue.first
General Comments 0
You need to be logged in to leave comments. Login now