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]
This commit is contained in:
akr 2024-06-01 15:11:19 +09:00 committed by GitHub
parent 5308da5e1c
commit 3ee83c73c3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 150 additions and 10 deletions

View File

@ -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

View File

@ -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