diff options
author | akr <[email protected]> | 2024-06-01 15:11:19 +0900 |
---|---|---|
committer | GitHub <[email protected]> | 2024-06-01 15:11:19 +0900 |
commit | 3ee83c73c38070d695537d4322ce4decb970a54a (patch) | |
tree | 5473bb0b99515adfd2b62888e226e25d489c2a93 | |
parent | 5308da5e1c53839b27cc4c0081bb965b46e0d052 (diff) |
Tempfile.create(anonymous: true) implemented. (#10803)
The keyword argument `anonymous` is implemented for `Tempfile.create`
The default is `anonymous: false`.
The behavior is not changed as before.
The created temporary file is immediately removed if `anonymous: true` is specified.
So applications don't need to remove the file.
The actual storage of the file is reclaimed by the OS when the file is closed.
It uses `O_TMPFILE` for Linux 3.11 or later.
It creates an anonymous file from the beginning.
It uses FILE_SHARE_DELETE for Windows.
It makes it possible to remove the opened file.
[Feature #20497]
-rw-r--r-- | lib/tempfile.rb | 110 | ||||
-rw-r--r-- | test/test_tempfile.rb | 50 |
2 files changed, 150 insertions, 10 deletions
diff --git a/lib/tempfile.rb b/lib/tempfile.rb index 1d7b80a74d..908f451dfc 100644 --- a/lib/tempfile.rb +++ b/lib/tempfile.rb @@ -392,8 +392,9 @@ end # see {File Permissions}[rdoc-ref:File@File+Permissions]. # - Mode is <tt>'w+'</tt> (read/write mode, positioned at the end). # -# With no block, the file is not removed automatically, -# and so should be explicitly removed. +# The temporary file removal depends on the keyword argument +anonymous+ and +# whether a block is given or not. +# See the description about the +anonymous+ keyword argument later. # # Example: # @@ -401,11 +402,36 @@ end # f.class # => File # f.path # => "/tmp/20220505-9795-17ky6f6" # f.stat.mode.to_s(8) # => "100600" +# f.close # File.exist?(f.path) # => true # File.unlink(f.path) # File.exist?(f.path) # => false # -# Argument +basename+, if given, may be one of: +# Tempfile.create {|f| +# f.puts "foo" +# f.rewind +# f.read # => "foo\n" +# f.path # => "/tmp/20240524-380207-oma0ny" +# File.exist?(f.path) # => true +# } # The file is removed at block exit. +# +# f = Tempfile.create(anonymous: true) +# # The file is already removed because anonymous +# f.path # => "/tmp/" (no filename since no file) +# f.puts "foo" +# f.rewind +# f.read # => "foo\n" +# f.close +# +# Tempfile.create(anonymous: true) {|f| +# # The file is already removed because anonymous +# f.path # => "/tmp/" (no filename since no file) +# f.puts "foo" +# f.rewind +# f.read # => "foo\n" +# } +# +# The argument +basename+, if given, may be one of the following: # # - A string: the generated filename begins with +basename+: # @@ -416,27 +442,57 @@ end # # Tempfile.create(%w/foo .jpg/) # => #<File:/tmp/foo20220505-17839-tnjchh.jpg> # -# With arguments +basename+ and +tmpdir+, the file is created in directory +tmpdir+: +# With arguments +basename+ and +tmpdir+, the file is created in the directory +tmpdir+: # # Tempfile.create('foo', '.') # => #<File:./foo20220505-9795-1emu6g8> # -# Keyword arguments +mode+ and +options+ are passed directly to method +# Keyword arguments +mode+ and +options+ are passed directly to the method # {File.open}[rdoc-ref:File.open]: # -# - The value given with +mode+ must be an integer, +# - The value given for +mode+ must be an integer # and may be expressed as the logical OR of constants defined in # {File::Constants}[rdoc-ref:File::Constants]. # - For +options+, see {Open Options}[rdoc-ref:IO@Open+Options]. # -# With a block given, creates the file as above, passes it to the block, -# and returns the block's value; -# before the return, the file object is closed and the underlying file is removed: +# The keyword argument +anonymous+ specifies when the file is removed. +# +# - +anonymous=false+ (default) without a block: the file is not removed. +# - +anonymous=false+ (default) with a block: the file is removed after the block exits. +# - +anonymous=true+ without a block: the file is removed before returning. +# - +anonymous=true+ with a block: the file is removed before the block is called. +# +# In the first case (+anonymous=false+ without a block), +# the file is not removed automatically. +# It should be explicitly closed. +# It can be used to rename to the desired filename. +# If the file is not needed, it should be explicitly removed. +# +# The +File#path+ method of the created file object returns the temporary directory with a trailing slash +# when +anonymous+ is true. +# +# When a block is given, it creates the file as described above, passes it to the block, +# and returns the block's value. +# Before the returning, the file object is closed and the underlying file is removed: # # Tempfile.create {|file| file.path } # => "/tmp/20220505-9795-rkists" # +# Implementation note: +# +# The keyword argument +anonymous=true+ is implemented using FILE_SHARE_DELETE on Windows. +# O_TMPFILE is used on Linux. +# # Related: Tempfile.new. # -def Tempfile.create(basename="", tmpdir=nil, mode: 0, **options) +def Tempfile.create(basename="", tmpdir=nil, mode: 0, anonymous: false, **options, &block) + if anonymous + create_anonymous(basename, tmpdir, mode: mode, **options, &block) + else + create_with_filename(basename, tmpdir, mode: mode, **options, &block) + end +end + +class << Tempfile +private def create_with_filename(basename="", tmpdir=nil, mode: 0, **options) tmpfile = nil Dir::Tmpname.create(basename, tmpdir, **options) do |tmpname, n, opts| mode |= File::RDWR|File::CREAT|File::EXCL @@ -464,3 +520,37 @@ def Tempfile.create(basename="", tmpdir=nil, mode: 0, **options) tmpfile end end + +private def create_anonymous(basename="", tmpdir=nil, mode: 0, **options, &block) + tmpfile = nil + tmpdir = Dir.tmpdir() if tmpdir.nil? + if defined?(File::TMPFILE) # O_TMPFILE since Linux 3.11 + begin + tmpfile = File.open(tmpdir, File::RDWR | File::TMPFILE, 0600) + rescue Errno::EISDIR, Errno::ENOENT, Errno::EOPNOTSUPP + # kernel or the filesystem does not support O_TMPFILE + # fallback to create-and-unlink + end + end + if tmpfile.nil? + mode |= File::SHARE_DELETE | File::BINARY # Windows needs them to unlink the opened file. + tmpfile = create_with_filename(basename, tmpdir, mode: mode, **options) + File.unlink(tmpfile.path) + end + path = File.join(tmpdir, '') + if tmpfile.path != path + # clear path. + tmpfile.autoclose = false + tmpfile = File.new(tmpfile.fileno, mode: File::RDWR, path: path) + end + if block + begin + yield tmpfile + ensure + tmpfile.close + end + else + tmpfile + end +end +end diff --git a/test/test_tempfile.rb b/test/test_tempfile.rb index eddbac5d75..d4ae7d4b3f 100644 --- a/test/test_tempfile.rb +++ b/test/test_tempfile.rb @@ -425,4 +425,54 @@ puts Tempfile.new('foo').path assert_not_send([File.absolute_path(actual), :start_with?, target]) end end + + def test_create_anonymous_without_block + t = Tempfile.create(anonymous: true) + assert_equal(File, t.class) + assert_equal(0600, t.stat.mode & 0777) unless /mswin|mingw/ =~ RUBY_PLATFORM + t.puts "foo" + t.rewind + assert_equal("foo\n", t.read) + t.close + ensure + t.close if t + end + + def test_create_anonymous_with_block + result = Tempfile.create(anonymous: true) {|t| + assert_equal(File, t.class) + assert_equal(0600, t.stat.mode & 0777) unless /mswin|mingw/ =~ RUBY_PLATFORM + t.puts "foo" + t.rewind + assert_equal("foo\n", t.read) + :result + } + assert_equal(:result, result) + end + + def test_create_anonymous_removes_file + Dir.mktmpdir {|d| + t = Tempfile.create("", d, anonymous: true) + t.close + assert_equal([], Dir.children(d)) + } + end + + def test_create_anonymous_path + Dir.mktmpdir {|d| + begin + t = Tempfile.create("", d, anonymous: true) + assert_equal(File.join(d, ""), t.path) + ensure + t.close if t + end + } + end + + def test_create_anonymous_autoclose + Tempfile.create(anonymous: true) {|t| + assert_equal(true, t.autoclose?) + } + end + end |