diff options
Diffstat (limited to 'lib/tempfile.rb')
-rw-r--r-- | lib/tempfile.rb | 224 |
1 files changed, 157 insertions, 67 deletions
diff --git a/lib/tempfile.rb b/lib/tempfile.rb index c1b696c4cf..1d7b80a74d 100644 --- a/lib/tempfile.rb +++ b/lib/tempfile.rb @@ -57,7 +57,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 # @@ -87,75 +87,116 @@ 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.2.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). # - # It is recommended to use Tempfile.create { ... } instead when possible, - # because that method avoids the cost of delegation and does not rely on a - # finalizer to close and unlink the file, which is unreliable. + # 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 + @finalizer_obj = Object.new + 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)) + ObjectSpace.define_finalizer(@finalizer_obj, Remover.new(tmpfile.path)) + ObjectSpace.define_finalizer(self, Closer.new(tmpfile)) + + super(tmpfile) + end + + def initialize_dup(other) # :nodoc: + initialize_copy_iv(other) + super(other) + ObjectSpace.define_finalizer(self, Closer.new(__getobj__)) + end + + def initialize_clone(other) # :nodoc: + initialize_copy_iv(other) + super(other) + ObjectSpace.define_finalizer(self, Closer.new(__getobj__)) + end - super(@tmpfile) + private def initialize_copy_iv(other) # :nodoc: + @unlinked = other.unlinked + @mode = other.mode + @opts = other.opts + @finalizer_obj = other.finalizer_obj end # Opens or reopens the file with mode "r+". def open _close + ObjectSpace.undefine_finalizer(self) mode = @mode & ~(File::CREAT|File::EXCL) - @tmpfile = File.open(@tmpfile.path, mode, **@opts) - __setobj__(@tmpfile) + __setobj__(File.open(__getobj__.path, mode, **@opts)) + ObjectSpace.define_finalizer(self, Closer.new(__getobj__)) + __getobj__ end def _close # :nodoc: - @tmpfile.close + __getobj__.close end protected :_close @@ -212,13 +253,13 @@ 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) + ObjectSpace.undefine_finalizer(@finalizer_obj) @unlinked = true end alias delete unlink @@ -226,43 +267,57 @@ 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: + protected + + attr_reader :unlinked, :mode, :opts, :finalizer_obj + + class Closer # :nodoc: def initialize(tmpfile) - @pid = Process.pid @tmpfile = tmpfile end def call(*args) + @tmpfile.close + end + end + + class Remover # :nodoc: + def initialize(path) + @pid = Process.pid + @path = path + end + + def call(*args) return if @pid != Process.pid - $stderr.puts "removing #{@tmpfile.path}..." if $DEBUG + $stderr.puts "removing #{@path}..." if $DEBUG - @tmpfile.close begin - File.unlink(@tmpfile.path) + File.unlink(@path) rescue Errno::ENOENT end @@ -325,26 +380,61 @@ class Tempfile < DelegateClass(File) end end -# Creates a temporary file as a usual File object (not a Tempfile). -# It does not use finalizer and delegation, which makes it more efficient and reliable. +# 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. In that case, 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, -# releasing all resources that the block created. -# 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 the same as for Tempfile.new. +# With no block, the file is not removed automatically, +# and so should be explicitly removed. # -# 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" +# File.exist?(f.path) # => true +# File.unlink(f.path) +# File.exist?(f.path) # => false +# +# Argument +basename+, if given, may be one of: +# +# - 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 directory +tmpdir+: +# +# Tempfile.create('foo', '.') # => #<File:./foo20220505-9795-1emu6g8> +# +# 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]. +# +# 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: +# +# Tempfile.create {|file| file.path } # => "/tmp/20220505-9795-rkists" +# +# Related: Tempfile.new. # def Tempfile.create(basename="", tmpdir=nil, mode: 0, **options) tmpfile = nil |