Tempfile #
Every application that processes data occasionally needs temporary files — buffers for data being processed, places to store intermediate results before an operation finishes, or files created during testing that must be cleaned up afterward. Creating temporary files manually (File.open("/tmp/myapp_#{Time.now.to_i}.tmp", "w")) has several problems: names can collide if multiple processes run concurrently, files aren’t automatically deleted if an error occurs mid-process, and choosing the right directory (writable, on the correct filesystem) isn’t trivial across operating systems. Tempfile solves all these problems — it creates files with unique names guaranteed not to collide, in the proper temporary directory for the running OS, and automatically deletes them when the object is garbage-collected or when close! is called.
Creating Tempfiles #
require "tempfile"
# The simplest tempfile
tmp = Tempfile.new
# => #<Tempfile:/tmp/20240115-12345-1a2b3c>
# The file name contains the PID and a random string — guaranteed unique
tmp.path # => "/tmp/20240115-12345-1a2b3c"
tmp.size # => 0 (empty file)
# With a prefix — helps identification when debugging
tmp = Tempfile.new("report")
tmp.path # => "/tmp/report20240115-12345-1a2b3c"
# With a prefix and suffix (two-element array)
tmp = Tempfile.new(["data_import", ".csv"])
tmp.path # => "/tmp/data_import20240115-12345-1a2b3c.csv"
# In a specific directory
tmp = Tempfile.new("cache", "/var/cache/myapp")
tmp.path # => "/var/cache/myapp/cache20240115-12345-1a2b3c"
# Tempfile implements IO — all IO methods are available
tmp = Tempfile.new(["upload", ".jpg"])
tmp.write("image data here")
tmp.flush # make sure the data is written to disk
tmp.rewind # back to the beginning of the file
tmp.read # => "image data here"
tmp.size # => 19
Lifecycle and Automatic Deletion #
This is the most important aspect of Tempfile and often misunderstood — when the file is deleted and how to ensure it definitely gets deleted.
require "tempfile"
# ANTI-PATTERN: creating a Tempfile without a block
tmp = Tempfile.new("data")
tmp.write("important content")
# ... do something with tmp ...
tmp.close # Closes the file handle, BUT the file still exists on disk!
# If an exception occurs before close, the file isn't deleted!
# Manual alternatives
tmp.close! # close + delete the file
# or
tmp.unlink # delete the file (the file handle is still open)
tmp.close
# CORRECT: use a block — the file is guaranteed deleted after the block
Tempfile.create("data") do |tmp|
tmp.write("important content")
tmp.flush
# Do operations with the file
process_file(tmp.path)
end
# The file is automatically deleted after the block, even on exceptions!
# Tempfile.new with manual ensure (if access outside the block is needed)
tmp = Tempfile.new("data")
begin
tmp.write("content")
tmp.flush
use_file(tmp.path)
ensure
tmp.close! # always delete, even on exceptions
end
flowchart TD
A[Tempfile.create] --> B{Use a block?}
B -- Yes --> C[File created]
C --> D[Block executes]
D --> E{Exception?}
E -- Yes --> F[File still deleted]
E -- No --> G[File deleted normally]
B -- No --> H[Tempfile.new]
H --> I[File created]
I --> J[Use the file]
J --> K{close! called?}
K -- Yes --> L[File deleted]
K -- No --> M[File deleted at GC\nor process end]
M --> N[But the timing isn't guaranteed!]Tempfile.create vs Tempfile.new #
require "tempfile"
# Tempfile.create — recommended for Ruby 2.6+
# Automatically deleted at the end of the block (or program end without a block)
Tempfile.create(["prefix", ".ext"]) do |file|
file.write("data")
file.path # use this path for other operations
end
# the file is already deleted here
# Tempfile.new — older, needs manual management
# The finalizer automatically deletes at GC, but the timing is unpredictable
tmp = Tempfile.new("prefix")
# ... use it ...
tmp.close! # explicitly delete
# For new code: always prefer Tempfile.create with a block
Reading and Writing #
Tempfile inherits from File, which inherits from IO — all IO methods are available.
require "tempfile"
Tempfile.create(["report", ".txt"]) do |tmp|
# Writing
tmp.write("First line\n")
tmp.puts("Second line")
tmp.print("No newline")
# Flush to disk before another process reads
tmp.flush
# Reading from the beginning
tmp.rewind
puts tmp.read # all content
tmp.rewind
tmp.each_line { |line| puts line.chomp }
# Position
tmp.pos # current position in bytes
tmp.seek(0) # to the beginning
tmp.seek(0, IO::SEEK_END) # to the end
# Binary files
tmp.binmode
tmp.write("\x89PNG\r\n\x1a\n") # PNG header
end
The Atomic Write Pattern #
One of the most important patterns using Tempfile is atomic write — writing to a temp file then renaming to the target, so the target file is never in a half-written state.
require "tempfile"
require "fileutils"
# ANTI-PATTERN: writing directly to the target file
def save_config_unsafe(path, data)
File.write(path, data.to_json)
# If the process crashes mid-write, the target file is corrupt!
end
# CORRECT: atomic write with Tempfile
def save_config(path, data)
dir = File.dirname(path)
Tempfile.create("config", dir) do |tmp|
tmp.write(JSON.pretty_generate(data))
tmp.flush
tmp.fsync # make sure the data truly reaches the disk (not just the OS buffer)
# rename is an atomic operation on Unix filesystems
# the target file is never in a half-written state
FileUtils.mv(tmp.path, path)
end
# Tempfile.create won't delete a file that's already been renamed
# because its path has changed
end
# The same pattern for various formats
def export_csv_safe(path, data)
Tempfile.create(["export", ".csv"], File.dirname(path)) do |tmp|
CSV.open(tmp.path, "w") do |csv|
csv << data.first.keys
data.each { |row| csv << row.values }
end
FileUtils.mv(tmp.path, path)
end
end
Use in Testing #
Tempfile is very useful in unit tests — you need real files to test against but don’t want to litter the project directory.
require "tempfile"
require "minitest/autorun"
class FileProcessorTest < Minitest::Test
def setup
# Create a tempfile accessible during one test
@tempfile = Tempfile.new(["test_input", ".csv"])
@tempfile.write("nama,umur\nAlice,28\nBob,35\n")
@tempfile.flush
@tempfile.rewind
end
def teardown
@tempfile.close! # delete after the test finishes
end
def test_read_csv
result = FileProcessor.read(@tempfile.path)
assert_equal 2, result.length
assert_equal "Alice", result[0]["nama"]
end
def test_process_empty
Tempfile.create("empty") do |f|
# an empty file
result = FileProcessor.read(f.path)
assert_empty result
end
end
end
# With RSpec
RSpec.describe FileProcessor do
around do |example|
Tempfile.create(["spec", ".json"]) do |f|
@file = f
example.run
end
end
it "reads JSON correctly" do
@file.write('{"key": "value"}')
@file.flush
@file.rewind
result = FileProcessor.read_json(@file.path)
expect(result["key"]).to eq("value")
end
end
Tempfile as a Buffer #
Tempfile is useful as a buffer for large data that doesn’t fit in memory.
require "tempfile"
require "net/http"
# Download a large file to a tempfile, process, then save to the destination
def download_and_process(url, destination)
uri = URI.parse(url)
Tempfile.create(["download", File.extname(uri.path)]) do |tmp|
tmp.binmode
# Streaming download into the tempfile
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
http.request(Net::HTTP::Get.new(uri)) do |response|
response.read_body { |chunk| tmp.write(chunk) }
end
end
tmp.flush
tmp.rewind
# Process the downloaded file
process_file(tmp, destination)
end
end
# A buffer for large data transformations
def transform_large_csv(input_path, output_path)
Tempfile.create(["transform", ".csv"]) do |buffer|
# Read input, transform, write to the buffer first
CSV.foreach(input_path, headers: true) do |row|
transformed = transform(row.to_h)
buffer.puts(transformed.values.join(","))
end
buffer.flush
# When done, atomic move to the output
FileUtils.mv(buffer.path, output_path)
end
end
Integration with Pathname #
Tempfile can be combined with Pathname for more expressive operations.
require "tempfile"
require "pathname"
# Get a Pathname from a Tempfile
Tempfile.create(["data", ".json"]) do |tmp|
path = Pathname.new(tmp.path)
tmp.write('{"key": "value"}')
tmp.flush
# Now all Pathname methods can be used
path.size # => 17
path.extname # => ".json"
path.dirname # => Pathname("/tmp")
path.exist? # => true
# Copy to another location using Pathname
output = Pathname.new("/tmp/output.json")
FileUtils.cp(path.to_s, output.to_s)
end
# Helper to get a Tempfile as a Pathname
def tempfile_path(prefix, suffix = "")
tmp = Tempfile.new([prefix, suffix])
Pathname.new(tmp.path).tap do |path|
ObjectSpace.define_finalizer(path, proc { tmp.close! })
end
end
Case Study: File Upload Processing #
A common pattern in web applications — uploaded files need processing before being stored.
require "tempfile"
class UploadProcessor
def self.process(uploaded_file, type:)
# Save the upload to a tempfile first
Tempfile.create(["upload", File.extname(uploaded_file.original_filename)]) do |tmp|
tmp.binmode
tmp.write(uploaded_file.read)
tmp.flush
tmp.rewind
case type
when :image
process_image(tmp.path)
when :csv
process_csv(tmp.path)
when :pdf
process_pdf(tmp.path)
end
end
# The tempfile is automatically deleted, no files left behind
end
private
def self.process_image(path)
# Resize, compress, generate thumbnails, etc.
result_path = "/var/uploads/images/#{SecureRandom.uuid}.jpg"
# Process the image into result_path
# ImageMagick, libvips, etc. work with file paths
`convert #{path} -resize 800x600 #{result_path}`
result_path
end
def self.process_csv(path)
rows = []
CSV.foreach(path, headers: true) do |row|
rows << row.to_h
end
rows
end
end
Summary #
- Always use
Tempfile.createwith a block — the file is guaranteed deleted after the block finishes, even if an exception occurs mid-process.Tempfile.newneeds manualclose!— if not using a block, useensureto callclose!(not justclose) so the file is truly deleted.tmp.flushbefore another process reads the file — written data may still be in the OS buffer;flushensures the data is on disk before its path is used.- Create tempfiles in the same directory as the target — for the atomic write pattern with
File.rename, source and destination must be on the same filesystem; create the tempfile inFile.dirname(target_path).fsyncfor critical data —flushonly empties Ruby’s buffer into the OS;fsyncensures the data reaches the storage hardware (slower but safer for important data).- Ideal for testing — create a tempfile in
setup, delete it inteardown; this ensures tests leave no leftover files and don’t depend on pre-existing files.- Use as a streaming buffer — for large files that don’t fit in memory, stream the data to a Tempfile before processing; more efficient than building a huge string in memory.
Tempfile.createis the modern pattern — safer thanTempfile.newbecause of more predictable lifecycle; use it for new code.