diff options
Diffstat (limited to 'lib/tempfile.rb')
| -rw-r--r-- | lib/tempfile.rb | 460 |
1 files changed, 376 insertions, 84 deletions
diff --git a/lib/tempfile.rb b/lib/tempfile.rb index efb0b1bcd6..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,15 +73,35 @@ 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 # # When a Tempfile object is garbage collected, or when the Ruby interpreter # exits, its associated temporary file is automatically deleted. This means -# that's it's unnecessary to explicitly delete a Tempfile after use, though -# it's good practice to do so: not explicitly deleting unused Tempfiles can -# potentially leave behind large amounts of tempfiles on the filesystem +# that it's unnecessary to explicitly delete a Tempfile after use, though +# it's a good practice to do so: not explicitly deleting unused Tempfiles can +# potentially leave behind a large number of temp files on the filesystem # until they're garbage collected. The existence of these temp files can make # it harder to determine a new Tempfile filename. # @@ -53,6 +116,14 @@ require 'tmpdir' # file.unlink # deletes the temp file # end # +# Tempfile.create { ... } exists for this purpose and is more convenient to use. +# Note that Tempfile.create returns a File instance instead of a Tempfile, which +# also avoids the overhead and complications of delegation. +# +# Tempfile.create('foo') do |file| +# # ...do something with file... +# end +# # === Unlink after creation # # On POSIX systems, it's possible to unlink a file right after creating it, @@ -63,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 # @@ -79,71 +155,118 @@ require 'tmpdir' # same Tempfile object from multiple threads then you should protect it with a # mutex. class Tempfile < DelegateClass(File) - # Creates a temporary file with permissions 0600 (= only readable and - # writable by the owner) and opens it with mode "w+". + + # The version + VERSION = "0.3.1" + + # Creates a file in the underlying file system; + # returns a new \Tempfile object based on that file. + # + # If possible, consider instead using Tempfile.create, which: + # + # - Avoids the performance cost of delegation, + # incurred when Tempfile.new calls its superclass <tt>DelegateClass(File)</tt>. + # - Does not rely on a finalizer to close and unlink the file, + # which can be unreliable. + # + # Creates and returns file whose: + # + # - Class is \Tempfile (not \File, as in Tempfile.create). + # - Directory is the system temporary directory (system-dependent). + # - Generated filename is unique in that directory. + # - Permissions are <tt>0600</tt>; + # see {File Permissions}[rdoc-ref:File@File+Permissions]. + # - Mode is <tt>'w+'</tt> (read/write mode, positioned at the end). + # + # The underlying file is removed when the \Tempfile object dies + # and is reclaimed by the garbage collector. # - # The +basename+ parameter is used to determine the name of the - # temporary file. You can either pass a String or an Array with - # 2 String elements. In the former form, the temporary file's base - # name will begin with the given string. In the latter form, - # the temporary file's base name will begin with the array's first - # element, and end with the second element. For example: + # Example: # - # file = Tempfile.new('hello') - # file.path # => something like: "/tmp/hello2843-8392-92849382--0" + # f = Tempfile.new # => #<Tempfile:/tmp/20220505-17839-1s0kt30> + # f.class # => Tempfile + # f.path # => "/tmp/20220505-17839-1s0kt30" + # f.stat.mode.to_s(8) # => "100600" + # File.exist?(f.path) # => true + # File.unlink(f.path) # + # File.exist?(f.path) # => false # - # # Use the Array form to enforce an extension in the filename: - # file = Tempfile.new(['hello', '.jpg']) - # file.path # => something like: "/tmp/hello2843-8392-92849382--0.jpg" + # Argument +basename+, if given, may be one of: # - # The temporary file will be placed in the directory as specified - # by the +tmpdir+ parameter. By default, this is +Dir.tmpdir+. + # - A string: the generated filename begins with +basename+: # - # file = Tempfile.new('hello', '/home/aisaka') - # file.path # => something like: "/home/aisaka/hello2843-8392-92849382--0" + # Tempfile.new('foo') # => #<Tempfile:/tmp/foo20220505-17839-1whk2f> # - # You can also pass an options hash. Under the hood, Tempfile creates - # the temporary file using +File.open+. These options will be passed to - # +File.open+. This is mostly useful for specifying encoding - # options, e.g.: + # - An array of two strings <tt>[prefix, suffix]</tt>: + # the generated filename begins with +prefix+ and ends with +suffix+: # - # Tempfile.new('hello', '/home/aisaka', encoding: 'ascii-8bit') + # Tempfile.new(%w/foo .jpg/) # => #<Tempfile:/tmp/foo20220505-17839-58xtfi.jpg> # - # # You can also omit the 'tmpdir' parameter: - # Tempfile.new('hello', encoding: 'ascii-8bit') + # With arguments +basename+ and +tmpdir+, the file is created in directory +tmpdir+: # - # Note: +mode+ keyword argument, as accepted by Tempfile, can only be - # numeric, combination of the modes defined in File::Constants. + # Tempfile.new('foo', '.') # => #<Tempfile:./foo20220505-17839-xfstr8> # - # === Exceptions + # Keyword arguments +mode+ and +options+ are passed directly to method + # {File.open}[rdoc-ref:File.open]: + # + # - The value given with +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]. + # + # Related: Tempfile.create. # - # If Tempfile.new cannot find a unique filename within a limited - # number of tries, then it will raise an exception. def initialize(basename="", tmpdir=nil, mode: 0, **options) warn "Tempfile.new doesn't call the given block.", uplevel: 1 if block_given? @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 @@ -200,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 @@ -214,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 @@ -263,11 +402,25 @@ class Tempfile < DelegateClass(File) # Creates a new Tempfile. # + # This method is not recommended and exists mostly for backward compatibility. + # Please use Tempfile.create instead, which avoids the cost of delegation, + # does not rely on a finalizer, and also unlinks the file when given a block. + # + # Tempfile.open is still appropriate if you need the Tempfile to be unlinked + # by a finalizer and you cannot explicitly know where in the program the + # Tempfile can be unlinked safely. + # # If no block is given, this is a synonym for Tempfile.new. # # If a block is given, then a Tempfile object will be constructed, - # and the block is run with said object as argument. The Tempfile + # and the block is run with the Tempfile object as argument. The Tempfile # object will be automatically closed after the block terminates. + # However, the file will *not* be unlinked and needs to be manually unlinked + # with Tempfile#close! or Tempfile#unlink. The finalizer will try to unlink + # but should not be relied upon as it can keep the file on the disk much + # longer than intended. For instance, on CRuby, finalizers can be delayed + # due to conservative stack scanning and references left in unused memory. + # # The call returns the value of the block. # # In any case, all arguments (<code>*args</code>) will be passed to Tempfile.new. @@ -299,28 +452,121 @@ class Tempfile < DelegateClass(File) end end -# Creates a temporary file as usual File object (not Tempfile). -# It doesn't use finalizer and delegation. +# Creates a file in the underlying file system; +# returns a new \File object based on that file. # -# If no block is given, this is similar to Tempfile.new except -# creating File instead of Tempfile. -# The created file is not removed automatically. -# You should use File.unlink to remove it. +# With no block given and no arguments, creates and returns file whose: # -# If a block is given, then a File object will be constructed, -# and the block is invoked with the object as the argument. -# The File object will be automatically closed and -# the temporary file is removed after the block terminates. -# The call returns the value of the block. +# - Class is {File}[rdoc-ref:File] (not \Tempfile). +# - Directory is the system temporary directory (system-dependent). +# - Generated filename is unique in that directory. +# - Permissions are <tt>0600</tt>; +# see {File Permissions}[rdoc-ref:File@File+Permissions]. +# - Mode is <tt>'w+'</tt> (read/write mode, positioned at the end). # -# In any case, all arguments (+basename+, +tmpdir+, +mode+, and -# <code>**options</code>) will be treated as Tempfile.new. +# 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. # -# Tempfile.create('foo', '/home/temp') do |f| -# # ... do something with f ... -# end +# Example: +# +# f = Tempfile.create # => #<File:/tmp/20220505-9795-17ky6f6> +# 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 +# +# 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+: +# +# Tempfile.create('foo') # => #<File:/tmp/foo20220505-9795-1gok8l9> +# +# - An array of two strings <tt>[prefix, suffix]</tt>: +# the generated filename begins with +prefix+ and ends with +suffix+: +# +# Tempfile.create(%w/foo .jpg/) # => #<File:/tmp/foo20220505-17839-tnjchh.jpg> +# +# 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 the method +# {File.open}[rdoc-ref:File.open]: +# +# - 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]. +# +# 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. # -def Tempfile.create(basename="", tmpdir=nil, mode: 0, **options) +# Related: Tempfile.new. +# +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 @@ -348,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 |
