summaryrefslogtreecommitdiff
path: root/lib/tempfile.rb
diff options
context:
space:
mode:
Diffstat (limited to 'lib/tempfile.rb')
-rw-r--r--lib/tempfile.rb307
1 files changed, 262 insertions, 45 deletions
diff --git a/lib/tempfile.rb b/lib/tempfile.rb
index c3263ed3c6..cd512bb1c5 100644
--- a/lib/tempfile.rb
+++ b/lib/tempfile.rb
@@ -8,18 +8,61 @@
require 'delegate'
require 'tmpdir'
-# A utility class for managing temporary files. When you create a Tempfile
-# object, it will create a temporary file with a unique filename. A Tempfile
-# objects behaves just like a File object, and you can perform all the usual
-# file operations on it: reading data, writing data, changing its permissions,
-# etc. So although this class does not explicitly document all instance methods
-# supported by File, you can in fact call any File instance method on a
-# Tempfile object.
+# A utility class for managing temporary files.
+#
+# There are two kind of methods of creating a temporary file:
+#
+# - Tempfile.create (recommended)
+# - Tempfile.new and Tempfile.open (mostly for backward compatibility, not recommended)
+#
+# Tempfile.create creates a usual \File object.
+# The timing of file deletion is predictable.
+# Also, it supports open-and-unlink technique which
+# removes the temporary file immediately after creation.
+#
+# Tempfile.new and Tempfile.open creates a \Tempfile object.
+# The created file is removed by the GC (finalizer).
+# The timing of file deletion is not predictable.
#
# == Synopsis
#
# require 'tempfile'
#
+# # Tempfile.create with a block
+# # The filename are chosen automatically.
+# # (You can specify the prefix and suffix of the filename by an optional argument.)
+# Tempfile.create {|f|
+# f.puts "foo"
+# f.rewind
+# f.read # => "foo\n"
+# } # The file is removed at block exit.
+#
+# # Tempfile.create without a block
+# # You need to unlink the file in non-block form.
+# f = Tempfile.create
+# f.puts "foo"
+# f.close
+# File.unlink(f.path) # You need to unlink the file.
+#
+# # Tempfile.create(anonymous: true) without a block
+# 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) with a block
+# 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"
+# }
+#
+# # Not recommended: Tempfile.new without a block
# file = Tempfile.new('foo')
# file.path # => A unique filename in the OS's temp directory,
# # e.g.: "/tmp/foo.24722.0"
@@ -30,7 +73,27 @@ require 'tmpdir'
# file.close
# file.unlink # deletes the temp file
#
-# == Good practices
+# == About Tempfile.new and Tempfile.open
+#
+# This section does not apply to Tempfile.create because
+# it returns a File object (not a Tempfile object).
+#
+# When you create a Tempfile object,
+# it will create a temporary file with a unique filename. A Tempfile
+# objects behaves just like a File object, and you can perform all the usual
+# file operations on it: reading data, writing data, changing its permissions,
+# etc. So although this class does not explicitly document all instance methods
+# supported by File, you can in fact call any File instance method on a
+# Tempfile object.
+#
+# A Tempfile object has a finalizer to remove the temporary file.
+# This means that the temporary file is removed via GC.
+# This can cause several problems:
+#
+# - Long GC intervals and conservative GC can accumulate temporary files that are not removed.
+# - Temporary files are not removed if Ruby exits abnormally (such as SIGKILL, SEGV).
+#
+# There are legacy good practices for Tempfile.new and Tempfile.open as follows.
#
# === Explicit close
#
@@ -57,7 +120,7 @@ require 'tmpdir'
# Note that Tempfile.create returns a File instance instead of a Tempfile, which
# also avoids the overhead and complications of delegation.
#
-# Tempfile.open('foo') do |file|
+# Tempfile.create('foo') do |file|
# # ...do something with file...
# end
#
@@ -71,12 +134,17 @@ require 'tmpdir'
# be able to read from or write to the Tempfile, and you do not need to
# know the Tempfile's filename either.
#
+# Also, this guarantees the temporary file is removed even if Ruby exits abnormally.
+# The OS reclaims the storage for the temporary file when the file is closed or
+# the Ruby process exits (normally or abnormally).
+#
# For example, a practical use case for unlink-after-creation would be this:
# you need a large byte buffer that's too large to comfortably fit in RAM,
# e.g. when you're writing a web server and you want to buffer the client's
# file upload data.
#
-# Please refer to #unlink for more information and a code example.
+# `Tempfile.create(anonymous: true)` supports this behavior.
+# It also works on Windows.
#
# == Minor notes
#
@@ -88,6 +156,9 @@ require 'tmpdir'
# mutex.
class Tempfile < DelegateClass(File)
+ # The version
+ VERSION = "0.3.1"
+
# Creates a file in the underlying file system;
# returns a new \Tempfile object based on that file.
#
@@ -150,26 +221,52 @@ class Tempfile < DelegateClass(File)
@unlinked = false
@mode = mode|File::RDWR|File::CREAT|File::EXCL
+ tmpfile = nil
::Dir::Tmpname.create(basename, tmpdir, **options) do |tmpname, n, opts|
opts[:perm] = 0600
- @tmpfile = File.open(tmpname, @mode, **opts)
+ tmpfile = File.open(tmpname, @mode, **opts)
@opts = opts.freeze
end
- ObjectSpace.define_finalizer(self, Remover.new(@tmpfile))
- super(@tmpfile)
+ super(tmpfile)
+
+ @finalizer_manager = FinalizerManager.new(__getobj__.path)
+ @finalizer_manager.register(self, __getobj__)
+ end
+
+ def initialize_dup(other) # :nodoc:
+ initialize_copy_iv(other)
+ super(other)
+ @finalizer_manager.register(self, __getobj__)
+ end
+
+ def initialize_clone(other) # :nodoc:
+ initialize_copy_iv(other)
+ super(other)
+ @finalizer_manager.register(self, __getobj__)
+ end
+
+ private def initialize_copy_iv(other) # :nodoc:
+ @unlinked = other.unlinked
+ @mode = other.mode
+ @opts = other.opts
+ @finalizer_manager = other.finalizer_manager
end
# Opens or reopens the file with mode "r+".
def open
_close
+
mode = @mode & ~(File::CREAT|File::EXCL)
- @tmpfile = File.open(@tmpfile.path, mode, **@opts)
- __setobj__(@tmpfile)
+ __setobj__(File.open(__getobj__.path, mode, **@opts))
+
+ @finalizer_manager.register(self, __getobj__)
+
+ __getobj__
end
def _close # :nodoc:
- @tmpfile.close
+ __getobj__.close
end
protected :_close
@@ -226,13 +323,15 @@ class Tempfile < DelegateClass(File)
def unlink
return if @unlinked
begin
- File.unlink(@tmpfile.path)
+ File.unlink(__getobj__.path)
rescue Errno::ENOENT
rescue Errno::EACCES
# may not be able to unlink on Windows; just ignore
return
end
- ObjectSpace.undefine_finalizer(self)
+
+ @finalizer_manager.unlinked = true
+
@unlinked = true
end
alias delete unlink
@@ -240,47 +339,61 @@ class Tempfile < DelegateClass(File)
# Returns the full path name of the temporary file.
# This will be nil if #unlink has been called.
def path
- @unlinked ? nil : @tmpfile.path
+ @unlinked ? nil : __getobj__.path
end
# Returns the size of the temporary file. As a side effect, the IO
# buffer is flushed before determining the size.
def size
- if !@tmpfile.closed?
- @tmpfile.size # File#size calls rb_io_flush_raw()
+ if !__getobj__.closed?
+ __getobj__.size # File#size calls rb_io_flush_raw()
else
- File.size(@tmpfile.path)
+ File.size(__getobj__.path)
end
end
alias length size
# :stopdoc:
def inspect
- if @tmpfile.closed?
+ if __getobj__.closed?
"#<#{self.class}:#{path} (closed)>"
else
"#<#{self.class}:#{path}>"
end
end
+ alias to_s inspect
- class Remover # :nodoc:
- def initialize(tmpfile)
+ protected
+
+ attr_reader :unlinked, :mode, :opts, :finalizer_manager
+
+ class FinalizerManager # :nodoc:
+ attr_accessor :unlinked
+
+ def initialize(path)
+ @open_files = {}
+ @path = path
@pid = Process.pid
- @tmpfile = tmpfile
+ @unlinked = false
end
- def call(*args)
- return if @pid != Process.pid
+ def register(obj, file)
+ ObjectSpace.undefine_finalizer(obj)
+ ObjectSpace.define_finalizer(obj, self)
+ @open_files[obj.object_id] = file
+ end
- $stderr.puts "removing #{@tmpfile.path}..." if $DEBUG
+ def call(object_id)
+ @open_files.delete(object_id).close
- @tmpfile.close
- begin
- File.unlink(@tmpfile.path)
- rescue Errno::ENOENT
+ if @open_files.empty? && !@unlinked && Process.pid == @pid
+ $stderr.puts "removing #{@path}..." if $DEBUG
+ begin
+ File.unlink(@path)
+ rescue Errno::ENOENT
+ end
+ $stderr.puts "done" if $DEBUG
end
-
- $stderr.puts "done" if $DEBUG
end
end
@@ -351,8 +464,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:
#
@@ -360,11 +474,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+:
#
@@ -375,27 +514,59 @@ 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.
+#
+# - <tt>anonymous=false</tt> (default) without a block: the file is not removed.
+# - <tt>anonymous=false</tt> (default) with a block: the file is removed after the block exits.
+# - <tt>anonymous=true</tt> without a block: the file is removed before returning.
+# - <tt>anonymous=true</tt> with a block: the file is removed before the block is called.
+#
+# In the first case (<tt>anonymous=false</tt> 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 <tt>anonymous=true</tt> 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
+# :stopdoc:
+
+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
@@ -423,3 +594,49 @@ def Tempfile.create(basename="", tmpdir=nil, mode: 0, **options)
tmpfile
end
end
+
+if RUBY_VERSION < "3.2"
+ module PathAttr # :nodoc:
+ attr_reader :path
+
+ def self.set_path(file, path)
+ file.extend(self).instance_variable_set(:@path, path)
+ end
+ 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)
+ tmppath = tmpfile.path
+ end
+ path = File.join(tmpdir, '')
+ unless tmppath == path
+ # clear path.
+ tmpfile.autoclose = false
+ tmpfile = File.new(tmpfile.fileno, mode: File::RDWR, path: path)
+ PathAttr.set_path(tmpfile, path) if defined?(PathAttr)
+ end
+ if block
+ begin
+ yield tmpfile
+ ensure
+ tmpfile.close
+ end
+ else
+ tmpfile
+ end
+end
+end