]> source.dussan.org Git - redmine.git/commitdiff
Ensure unique attachment filenames (#35539).
authorGo MAEDA <maeda@farend.jp>
Mon, 19 Jul 2021 13:27:59 +0000 (13:27 +0000)
committerGo MAEDA <maeda@farend.jp>
Mon, 19 Jul 2021 13:27:59 +0000 (13:27 +0000)
Patch by Jens Krämer.

git-svn-id: http://svn.redmine.org/redmine/trunk@21071 e93f8b46-1217-0410-a6f0-8f06a7374b81

app/models/attachment.rb
test/unit/attachment_test.rb

index c3c3fc8b3ef77b48238b94d21e372d9f38a716f3..c65c5672203d7ca5a3e35bffc7639a6bb9380375 100644 (file)
@@ -138,14 +138,10 @@ class Attachment < ActiveRecord::Base
   def files_to_final_location
     if @temp_file
       self.disk_directory = target_directory
-      self.disk_filename = Attachment.disk_filename(filename, disk_directory)
-      logger.info("Saving attachment '#{self.diskfile}' (#{@temp_file.size} bytes)") if logger
-      path = File.dirname(diskfile)
-      unless File.directory?(path)
-        FileUtils.mkdir_p(path)
-      end
       sha = Digest::SHA256.new
-      File.open(diskfile, "wb") do |f|
+      Attachment.create_diskfile(filename, disk_directory) do |f|
+        self.disk_filename = File.basename f.path
+        logger.info("Saving attachment '#{self.diskfile}' (#{@temp_file.size} bytes)") if logger
         if @temp_file.respond_to?(:read)
           buffer = ""
           while (buffer = @temp_file.read(8192))
@@ -557,9 +553,8 @@ class Attachment < ActiveRecord::Base
 
   # Singleton class method is public
   class << self
-    # Returns an ASCII or hashed filename that do not
-    # exists yet in the given subdirectory
-    def disk_filename(filename, directory=nil)
+    # Claims a unique ASCII or hashed filename, yields the open file handle
+    def create_diskfile(filename, directory=nil, &block)
       timestamp = DateTime.now.strftime("%y%m%d%H%M%S")
       ascii = ''
       if %r{^[a-zA-Z0-9_\.\-]*$}.match?(filename) && filename.length <= 50
@@ -569,11 +564,21 @@ class Attachment < ActiveRecord::Base
         # keep the extension if any
         ascii << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
       end
-      while File.exist?(File.join(storage_path, directory.to_s,
-                                  "#{timestamp}_#{ascii}"))
+
+      path = File.join storage_path, directory.to_s
+      FileUtils.mkdir_p(path) unless File.directory?(path)
+      begin
+        name = "#{timestamp}_#{ascii}"
+        File.open(
+          File.join(path, name),
+          flags: File::CREAT | File::EXCL | File::BINARY | File::WRONLY,
+          binmode: true,
+          &block
+        )
+      rescue Errno::EEXIST
         timestamp.succ!
+        retry
       end
-      "#{timestamp}_#{ascii}"
     end
   end
 end
index 9484c93600074af59b37dd4c84beddc40751c795..2aee87964a621d73b25c6ea5517430a8b50e96a4 100644 (file)
@@ -275,12 +275,28 @@ class AttachmentTest < ActiveSupport::TestCase
     assert_equal 'valid_[] invalid_chars', a.filename
   end
 
-  def test_diskfilename
-    assert Attachment.disk_filename("test_file.txt") =~ /^\d{12}_test_file.txt$/
-    assert_equal 'test_file.txt', Attachment.disk_filename("test_file.txt")[13..-1]
-    assert_equal '770c509475505f37c2b8fb6030434d6b.txt', Attachment.disk_filename("test_accentué.txt")[13..-1]
-    assert_equal 'f8139524ebb8f32e51976982cd20a85d', Attachment.disk_filename("test_accentué")[13..-1]
-    assert_equal 'cbb5b0f30978ba03731d61f9f6d10011', Attachment.disk_filename("test_accentué.ça")[13..-1]
+  def test_create_diskfile
+    Attachment.create_diskfile("test_file.txt") do |f|
+      path = f.path
+      assert_match(/^\d{12}_test_file.txt$/, File.basename(path))
+      assert_equal 'test_file.txt', File.basename(path)[13..-1]
+      File.unlink f.path
+    end
+
+    Attachment.create_diskfile("test_accentué.txt") do |f|
+      assert_equal '770c509475505f37c2b8fb6030434d6b.txt', File.basename(f.path)[13..-1]
+      File.unlink f.path
+    end
+
+    Attachment.create_diskfile("test_accentué") do |f|
+      assert_equal 'f8139524ebb8f32e51976982cd20a85d', File.basename(f.path)[13..-1]
+      File.unlink f.path
+    end
+
+    Attachment.create_diskfile("test_accentué.ça") do |f|
+      assert_equal 'cbb5b0f30978ba03731d61f9f6d10011', File.basename(f.path)[13..-1]
+      File.unlink f.path
+    end
   end
 
   def test_title