Tempfile #
Setiap aplikasi yang memproses data sesekali membutuhkan file sementara — buffer untuk data yang sedang diproses, tempat menyimpan hasil intermediate sebelum operasi selesai, atau file yang dibuat selama testing dan harus bersih setelahnya. Membuat file sementara secara manual (File.open("/tmp/myapp_#{Time.now.to_i}.tmp", "w")) mengandung beberapa masalah: nama bisa bertabrakan jika ada beberapa proses berjalan bersamaan, file tidak otomatis terhapus jika terjadi error di tengah proses, dan pemilihan direktori yang tepat (yang writable, di filesystem yang benar) tidak trivial di berbagai sistem operasi. Tempfile menyelesaikan semua masalah ini — ia membuat file dengan nama unik yang dijamin tidak bertabrakan, di direktori temporary yang tepat untuk sistem operasi yang berjalan, dan otomatis dihapus ketika objek di-garbage collect atau ketika close! dipanggil.
Membuat Tempfile #
require "tempfile"
# Membuat tempfile paling sederhana
tmp = Tempfile.new
# => #<Tempfile:/tmp/20240115-12345-1a2b3c>
# Nama file berisi PID dan random string — dijamin unik
tmp.path # => "/tmp/20240115-12345-1a2b3c"
tmp.size # => 0 (file kosong)
# Dengan prefix — membantu identifikasi saat debugging
tmp = Tempfile.new("laporan")
tmp.path # => "/tmp/laporan20240115-12345-1a2b3c"
# Dengan prefix dan suffix (array dua elemen)
tmp = Tempfile.new(["data_import", ".csv"])
tmp.path # => "/tmp/data_import20240115-12345-1a2b3c.csv"
# Di direktori tertentu
tmp = Tempfile.new("cache", "/var/cache/myapp")
tmp.path # => "/var/cache/myapp/cache20240115-12345-1a2b3c"
# Tempfile mengimplementasikan IO — semua method IO tersedia
tmp = Tempfile.new(["upload", ".jpg"])
tmp.write("data gambar di sini")
tmp.flush # pastikan data ditulis ke disk
tmp.rewind # kembali ke awal file
tmp.read # => "data gambar di sini"
tmp.size # => 19
Lifecycle dan Penghapusan Otomatis #
Ini adalah aspek terpenting dari Tempfile yang sering disalahpahami — kapan file dihapus dan bagaimana memastikan file pasti terhapus.
require "tempfile"
# ANTI-PATTERN: membuat Tempfile tanpa blok
tmp = Tempfile.new("data")
tmp.write("konten penting")
# ... lakukan sesuatu dengan tmp ...
tmp.close # Menutup file handle, TAPI file masih ada di disk!
# Jika terjadi exception sebelum close, file tidak terhapus!
# Alternatif manual
tmp.close! # close + hapus file
# atau
tmp.unlink # hapus file (file handle masih terbuka)
tmp.close
# BENAR: gunakan blok — file dijamin terhapus setelah blok
Tempfile.create("data") do |tmp|
tmp.write("konten penting")
tmp.flush
# Lakukan operasi dengan file
proses_file(tmp.path)
end
# File otomatis terhapus setelah blok, bahkan jika terjadi exception!
# Tempfile.new dengan ensure manual (jika perlu akses di luar blok)
tmp = Tempfile.new("data")
begin
tmp.write("konten")
tmp.flush
gunakan_file(tmp.path)
ensure
tmp.close! # selalu hapus, bahkan jika exception
end
flowchart TD
A[Tempfile.create] --> B{Gunakan blok?}
B -- Ya --> C[File dibuat]
C --> D[Blok dieksekusi]
D --> E{Exception?}
E -- Ya --> F[File tetap terhapus]
E -- Tidak --> G[File terhapus normal]
B -- Tidak --> H[Tempfile.new]
H --> I[File dibuat]
I --> J[Gunakan file]
J --> K{close! dipanggil?}
K -- Ya --> L[File terhapus]
K -- Tidak --> M[File dihapus saat GC\natau proses selesai]
M --> N[Tapi timing tidak terjamin!]Tempfile.create vs Tempfile.new #
require "tempfile"
# Tempfile.create — direkomendasikan untuk Ruby 2.6+
# Otomatis hapus di akhir blok (atau di akhir program jika tanpa blok)
Tempfile.create(["prefix", ".ext"]) do |file|
file.write("data")
file.path # gunakan path ini untuk operasi lain
end
# file sudah terhapus di sini
# Tempfile.new — lebih lama, perlu manajemen manual
# Finalizer otomatis menghapus saat GC, tapi waktunya tidak terprediksi
tmp = Tempfile.new("prefix")
# ... gunakan ...
tmp.close! # eksplisit hapus
# Untuk kode baru: selalu prefer Tempfile.create dengan blok
Membaca dan Menulis #
Tempfile mewarisi dari File yang mewarisi dari IO — semua method IO tersedia.
require "tempfile"
Tempfile.create(["report", ".txt"]) do |tmp|
# Menulis
tmp.write("Baris pertama\n")
tmp.puts("Baris kedua")
tmp.print("Tanpa newline")
# Flush ke disk sebelum dibaca proses lain
tmp.flush
# Membaca dari awal
tmp.rewind
puts tmp.read # semua konten
tmp.rewind
tmp.each_line { |baris| puts baris.chomp }
# Posisi
tmp.pos # posisi saat ini dalam byte
tmp.seek(0) # ke awal
tmp.seek(0, IO::SEEK_END) # ke akhir
# File binary
tmp.binmode
tmp.write("\x89PNG\r\n\x1a\n") # PNG header
end
Pola Atomic Write #
Salah satu pola paling penting menggunakan Tempfile adalah atomic write — menulis ke file sementara lalu rename ke target, sehingga file target tidak pernah dalam kondisi setengah-tertulis.
require "tempfile"
require "fileutils"
# ANTI-PATTERN: tulis langsung ke file target
def simpan_konfigurasi_tidak_aman(path, data)
File.write(path, data.to_json)
# Jika proses crash di tengah penulisan, file target corrupt!
end
# BENAR: atomic write dengan Tempfile
def simpan_konfigurasi(path, data)
dir = File.dirname(path)
Tempfile.create("config", dir) do |tmp|
tmp.write(JSON.pretty_generate(data))
tmp.flush
tmp.fsync # pastikan data benar-benar sampai ke disk (bukan hanya OS buffer)
# rename adalah operasi atomic di Unix filesystem
# file target tidak pernah dalam kondisi setengah-tertulis
FileUtils.mv(tmp.path, path)
end
# Tempfile.create tidak akan hapus file yang sudah di-rename
# karena path-nya sudah berubah
end
# Pola yang sama untuk berbagai format
def ekspor_csv_aman(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
Penggunaan dalam Testing #
Tempfile sangat berguna dalam unit test — kamu perlu file nyata untuk diuji tapi tidak ingin mengotori direktori proyek.
require "tempfile"
require "minitest/autorun"
class FileProcessorTest < Minitest::Test
def setup
# Buat tempfile yang bisa diakses selama satu 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! # hapus setelah test selesai
end
def test_baca_csv
hasil = FileProcessor.baca(@tempfile.path)
assert_equal 2, hasil.length
assert_equal "Alice", hasil[0]["nama"]
end
def test_proses_kosong
Tempfile.create("kosong") do |f|
# file kosong
hasil = FileProcessor.baca(f.path)
assert_empty hasil
end
end
end
# Dengan RSpec
RSpec.describe FileProcessor do
around do |example|
Tempfile.create(["spec", ".json"]) do |f|
@file = f
example.run
end
end
it "membaca JSON dengan benar" do
@file.write('{"key": "value"}')
@file.flush
@file.rewind
hasil = FileProcessor.baca_json(@file.path)
expect(hasil["key"]).to eq("value")
end
end
Tempfile sebagai Buffer #
Tempfile berguna sebagai buffer untuk data besar yang tidak muat di memori.
require "tempfile"
require "net/http"
# Download file besar ke tempfile, proses, lalu simpan ke tujuan
def download_dan_proses(url, tujuan)
uri = URI.parse(url)
Tempfile.create(["download", File.extname(uri.path)]) do |tmp|
tmp.binmode
# Streaming download ke 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
# Proses file yang sudah didownload
proses_file(tmp, tujuan)
end
end
# Buffer untuk transformasi data besar
def transform_csv_besar(input_path, output_path)
Tempfile.create(["transform", ".csv"]) do |buffer|
# Baca input, transform, tulis ke buffer dulu
CSV.foreach(input_path, headers: true) do |baris|
transformed = transform(baris.to_h)
buffer.puts(transformed.values.join(","))
end
buffer.flush
# Setelah selesai, atomic move ke output
FileUtils.mv(buffer.path, output_path)
end
end
Integrasi dengan Pathname #
Tempfile bisa dikombinasikan dengan Pathname untuk operasi yang lebih ekspresif.
require "tempfile"
require "pathname"
# Dapatkan Pathname dari Tempfile
Tempfile.create(["data", ".json"]) do |tmp|
path = Pathname.new(tmp.path)
tmp.write('{"key": "value"}')
tmp.flush
# Sekarang bisa gunakan semua method Pathname
path.size # => 17
path.extname # => ".json"
path.dirname # => Pathname("/tmp")
path.exist? # => true
# Copy ke lokasi lain menggunakan Pathname
output = Pathname.new("/tmp/output.json")
FileUtils.cp(path.to_s, output.to_s)
end
# Helper untuk mendapatkan Tempfile sebagai 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
Kasus: Proses Upload File #
Pola umum di web application — file yang diupload perlu diproses sebelum disimpan.
require "tempfile"
class UploadProcessor
def self.proses(uploaded_file, tipe:)
# Simpan upload ke tempfile dulu
Tempfile.create(["upload", File.extname(uploaded_file.original_filename)]) do |tmp|
tmp.binmode
tmp.write(uploaded_file.read)
tmp.flush
tmp.rewind
case tipe
when :gambar
proses_gambar(tmp.path)
when :csv
proses_csv(tmp.path)
when :pdf
proses_pdf(tmp.path)
end
end
# Tempfile otomatis terhapus, tidak ada file yang tertinggal
end
private
def self.proses_gambar(path)
# Resize, compress, generate thumbnail, dll
hasil_path = "/var/uploads/images/#{SecureRandom.uuid}.jpg"
# Proses gambar ke hasil_path
# ImageMagick, libvips, dll bekerja dengan path file
`convert #{path} -resize 800x600 #{hasil_path}`
hasil_path
end
def self.proses_csv(path)
baris = []
CSV.foreach(path, headers: true) do |row|
baris << row.to_h
end
baris
end
end
Ringkasan #
- Selalu gunakan
Tempfile.createdengan blok — file dijamin terhapus setelah blok selesai, bahkan jika terjadi exception di tengah proses.Tempfile.newperluclose!manual — jika tidak menggunakan blok, gunakanensureuntuk memanggilclose!(bukan hanyaclose) agar file benar-benar terhapus.tmp.flushsebelum file dibaca proses lain — data yang ditulis mungkin masih di buffer OS;flushmemastikan data sudah ditulis ke disk sebelum path-nya digunakan.- Buat tempfile di direktori yang sama dengan target — untuk pola atomic write dengan
File.rename, sumber dan tujuan harus di filesystem yang sama; buat tempfile diFile.dirname(target_path).fsyncuntuk data kritis —flushhanya mengosongkan buffer Ruby ke OS;fsyncmemastikan data sampai ke storage hardware (lebih lambat tapi lebih aman untuk data penting).- Ideal untuk testing — buat tempfile di
setup, hapus diteardown; ini memastikan test tidak meninggalkan file sisa dan tidak bergantung pada file yang mungkin sudah ada.- Gunakan sebagai streaming buffer — untuk file besar yang tidak muat di memori, stream data ke Tempfile dahulu sebelum diproses; lebih efisien dari membangun string besar di memori.
Tempfile.createadalah pola modern — lebih safe dariTempfile.newkarena lifecycle yang lebih terprediksi; gunakan ini untuk kode baru.