1# frozen_string_literal: true
2#
3# tempfile - manipulates temporary files
4#
5# $Id: tempfile.rb 66415 2018-12-16 12:09:08Z nobu $
6#
7
8require 'delegate'
9require 'tmpdir'
10
11# A utility class for managing temporary files. When you create a Tempfile
12# object, it will create a temporary file with a unique filename. A Tempfile
13# objects behaves just like a File object, and you can perform all the usual
14# file operations on it: reading data, writing data, changing its permissions,
15# etc. So although this class does not explicitly document all instance methods
16# supported by File, you can in fact call any File instance method on a
17# Tempfile object.
18#
19# == Synopsis
20#
21#   require 'tempfile'
22#
23#   file = Tempfile.new('foo')
24#   file.path      # => A unique filename in the OS's temp directory,
25#                  #    e.g.: "/tmp/foo.24722.0"
26#                  #    This filename contains 'foo' in its basename.
27#   file.write("hello world")
28#   file.rewind
29#   file.read      # => "hello world"
30#   file.close
31#   file.unlink    # deletes the temp file
32#
33# == Good practices
34#
35# === Explicit close
36#
37# When a Tempfile object is garbage collected, or when the Ruby interpreter
38# exits, its associated temporary file is automatically deleted. This means
39# that's it's unnecessary to explicitly delete a Tempfile after use, though
40# it's good practice to do so: not explicitly deleting unused Tempfiles can
41# potentially leave behind large amounts of tempfiles on the filesystem
42# until they're garbage collected. The existence of these temp files can make
43# it harder to determine a new Tempfile filename.
44#
45# Therefore, one should always call #unlink or close in an ensure block, like
46# this:
47#
48#   file = Tempfile.new('foo')
49#   begin
50#      # ...do something with file...
51#   ensure
52#      file.close
53#      file.unlink   # deletes the temp file
54#   end
55#
56# === Unlink after creation
57#
58# On POSIX systems, it's possible to unlink a file right after creating it,
59# and before closing it. This removes the filesystem entry without closing
60# the file handle, so it ensures that only the processes that already had
61# the file handle open can access the file's contents. It's strongly
62# recommended that you do this if you do not want any other processes to
63# be able to read from or write to the Tempfile, and you do not need to
64# know the Tempfile's filename either.
65#
66# For example, a practical use case for unlink-after-creation would be this:
67# you need a large byte buffer that's too large to comfortably fit in RAM,
68# e.g. when you're writing a web server and you want to buffer the client's
69# file upload data.
70#
71# Please refer to #unlink for more information and a code example.
72#
73# == Minor notes
74#
75# Tempfile's filename picking method is both thread-safe and inter-process-safe:
76# it guarantees that no other threads or processes will pick the same filename.
77#
78# Tempfile itself however may not be entirely thread-safe. If you access the
79# same Tempfile object from multiple threads then you should protect it with a
80# mutex.
81class Tempfile < DelegateClass(File)
82  # Creates a temporary file with permissions 0600 (= only readable and
83  # writable by the owner) and opens it with mode "w+".
84  #
85  # The +basename+ parameter is used to determine the name of the
86  # temporary file. You can either pass a String or an Array with
87  # 2 String elements. In the former form, the temporary file's base
88  # name will begin with the given string. In the latter form,
89  # the temporary file's base name will begin with the array's first
90  # element, and end with the second element. For example:
91  #
92  #   file = Tempfile.new('hello')
93  #   file.path  # => something like: "/tmp/hello2843-8392-92849382--0"
94  #
95  #   # Use the Array form to enforce an extension in the filename:
96  #   file = Tempfile.new(['hello', '.jpg'])
97  #   file.path  # => something like: "/tmp/hello2843-8392-92849382--0.jpg"
98  #
99  # The temporary file will be placed in the directory as specified
100  # by the +tmpdir+ parameter. By default, this is +Dir.tmpdir+.
101  # When $SAFE > 0 and the given +tmpdir+ is tainted, it uses
102  # '/tmp' as the temporary directory. Please note that ENV values
103  # are tainted by default, and +Dir.tmpdir+'s return value might
104  # come from environment variables (e.g. <tt>$TMPDIR</tt>).
105  #
106  #   file = Tempfile.new('hello', '/home/aisaka')
107  #   file.path  # => something like: "/home/aisaka/hello2843-8392-92849382--0"
108  #
109  # You can also pass an options hash. Under the hood, Tempfile creates
110  # the temporary file using +File.open+. These options will be passed to
111  # +File.open+. This is mostly useful for specifying encoding
112  # options, e.g.:
113  #
114  #   Tempfile.new('hello', '/home/aisaka', encoding: 'ascii-8bit')
115  #
116  #   # You can also omit the 'tmpdir' parameter:
117  #   Tempfile.new('hello', encoding: 'ascii-8bit')
118  #
119  # Note: +mode+ keyword argument, as accepted by Tempfile, can only be
120  # numeric, combination of the modes defined in File::Constants.
121  #
122  # === Exceptions
123  #
124  # If Tempfile.new cannot find a unique filename within a limited
125  # number of tries, then it will raise an exception.
126  def initialize(basename="", tmpdir=nil, mode: 0, **options)
127    warn "Tempfile.new doesn't call the given block.", uplevel: 1 if block_given?
128
129    @unlinked = false
130    @mode = mode|File::RDWR|File::CREAT|File::EXCL
131    ::Dir::Tmpname.create(basename, tmpdir, options) do |tmpname, n, opts|
132      opts[:perm] = 0600
133      @tmpfile = File.open(tmpname, @mode, opts)
134      @opts = opts.freeze
135    end
136    ObjectSpace.define_finalizer(self, Remover.new(@tmpfile))
137
138    super(@tmpfile)
139  end
140
141  # Opens or reopens the file with mode "r+".
142  def open
143    _close
144    mode = @mode & ~(File::CREAT|File::EXCL)
145    @tmpfile = File.open(@tmpfile.path, mode, @opts)
146    __setobj__(@tmpfile)
147  end
148
149  def _close    # :nodoc:
150    @tmpfile.close
151  end
152  protected :_close
153
154  # Closes the file. If +unlink_now+ is true, then the file will be unlinked
155  # (deleted) after closing. Of course, you can choose to later call #unlink
156  # if you do not unlink it now.
157  #
158  # If you don't explicitly unlink the temporary file, the removal
159  # will be delayed until the object is finalized.
160  def close(unlink_now=false)
161    _close
162    unlink if unlink_now
163  end
164
165  # Closes and unlinks (deletes) the file. Has the same effect as called
166  # <tt>close(true)</tt>.
167  def close!
168    close(true)
169  end
170
171  # Unlinks (deletes) the file from the filesystem. One should always unlink
172  # the file after using it, as is explained in the "Explicit close" good
173  # practice section in the Tempfile overview:
174  #
175  #   file = Tempfile.new('foo')
176  #   begin
177  #      # ...do something with file...
178  #   ensure
179  #      file.close
180  #      file.unlink   # deletes the temp file
181  #   end
182  #
183  # === Unlink-before-close
184  #
185  # On POSIX systems it's possible to unlink a file before closing it. This
186  # practice is explained in detail in the Tempfile overview (section
187  # "Unlink after creation"); please refer there for more information.
188  #
189  # However, unlink-before-close may not be supported on non-POSIX operating
190  # systems. Microsoft Windows is the most notable case: unlinking a non-closed
191  # file will result in an error, which this method will silently ignore. If
192  # you want to practice unlink-before-close whenever possible, then you should
193  # write code like this:
194  #
195  #   file = Tempfile.new('foo')
196  #   file.unlink   # On Windows this silently fails.
197  #   begin
198  #      # ... do something with file ...
199  #   ensure
200  #      file.close!   # Closes the file handle. If the file wasn't unlinked
201  #                    # because #unlink failed, then this method will attempt
202  #                    # to do so again.
203  #   end
204  def unlink
205    return if @unlinked
206    begin
207      File.unlink(@tmpfile.path)
208    rescue Errno::ENOENT
209    rescue Errno::EACCES
210      # may not be able to unlink on Windows; just ignore
211      return
212    end
213    ObjectSpace.undefine_finalizer(self)
214    @unlinked = true
215  end
216  alias delete unlink
217
218  # Returns the full path name of the temporary file.
219  # This will be nil if #unlink has been called.
220  def path
221    @unlinked ? nil : @tmpfile.path
222  end
223
224  # Returns the size of the temporary file.  As a side effect, the IO
225  # buffer is flushed before determining the size.
226  def size
227    if !@tmpfile.closed?
228      @tmpfile.size # File#size calls rb_io_flush_raw()
229    else
230      File.size(@tmpfile.path)
231    end
232  end
233  alias length size
234
235  # :stopdoc:
236  def inspect
237    if closed?
238      "#<#{self.class}:#{path} (closed)>"
239    else
240      "#<#{self.class}:#{path}>"
241    end
242  end
243
244  class Remover # :nodoc:
245    def initialize(tmpfile)
246      @pid = Process.pid
247      @tmpfile = tmpfile
248    end
249
250    def call(*args)
251      return if @pid != Process.pid
252
253      $stderr.puts "removing #{@tmpfile.path}..." if $DEBUG
254
255      @tmpfile.close
256      begin
257        File.unlink(@tmpfile.path)
258      rescue Errno::ENOENT
259      end
260
261      $stderr.puts "done" if $DEBUG
262    end
263  end
264
265  class << self
266    # :startdoc:
267
268    # Creates a new Tempfile.
269    #
270    # If no block is given, this is a synonym for Tempfile.new.
271    #
272    # If a block is given, then a Tempfile object will be constructed,
273    # and the block is run with said object as argument. The Tempfile
274    # object will be automatically closed after the block terminates.
275    # The call returns the value of the block.
276    #
277    # In any case, all arguments (<code>*args</code>) will be passed to Tempfile.new.
278    #
279    #   Tempfile.open('foo', '/home/temp') do |f|
280    #      # ... do something with f ...
281    #   end
282    #
283    #   # Equivalent:
284    #   f = Tempfile.open('foo', '/home/temp')
285    #   begin
286    #      # ... do something with f ...
287    #   ensure
288    #      f.close
289    #   end
290    def open(*args)
291      tempfile = new(*args)
292
293      if block_given?
294        begin
295          yield(tempfile)
296        ensure
297          tempfile.close
298        end
299      else
300        tempfile
301      end
302    end
303  end
304end
305
306# Creates a temporary file as usual File object (not Tempfile).
307# It doesn't use finalizer and delegation.
308#
309# If no block is given, this is similar to Tempfile.new except
310# creating File instead of Tempfile.
311# The created file is not removed automatically.
312# You should use File.unlink to remove it.
313#
314# If a block is given, then a File object will be constructed,
315# and the block is invoked with the object as the argument.
316# The File object will be automatically closed and
317# the temporary file is removed after the block terminates.
318# The call returns the value of the block.
319#
320# In any case, all arguments (+basename+, +tmpdir+, +mode+, and
321# <code>**options</code>) will be treated as Tempfile.new.
322#
323#   Tempfile.create('foo', '/home/temp') do |f|
324#      # ... do something with f ...
325#   end
326#
327def Tempfile.create(basename="", tmpdir=nil, mode: 0, **options)
328  tmpfile = nil
329  Dir::Tmpname.create(basename, tmpdir, options) do |tmpname, n, opts|
330    mode |= File::RDWR|File::CREAT|File::EXCL
331    opts[:perm] = 0600
332    tmpfile = File.open(tmpname, mode, opts)
333  end
334  if block_given?
335    begin
336      yield tmpfile
337    ensure
338      unless tmpfile.closed?
339        if File.identical?(tmpfile, tmpfile.path)
340          unlinked = File.unlink tmpfile.path rescue nil
341        end
342        tmpfile.close
343      end
344      unless unlinked
345        begin
346          File.unlink tmpfile.path
347        rescue Errno::ENOENT
348        end
349      end
350    end
351  else
352    tmpfile
353  end
354end
355