summaryrefslogtreecommitdiff
path: root/lib/tempfile.rb
diff options
context:
space:
mode:
Diffstat (limited to 'lib/tempfile.rb')
-rw-r--r--lib/tempfile.rb224
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