diff options
Diffstat (limited to 'lib/tempfile.rb')
| -rw-r--r-- | lib/tempfile.rb | 307 |
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 |
