Tempfile

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.create dengan blok — file dijamin terhapus setelah blok selesai, bahkan jika terjadi exception di tengah proses.
  • Tempfile.new perlu close! manual — jika tidak menggunakan blok, gunakan ensure untuk memanggil close! (bukan hanya close) agar file benar-benar terhapus.
  • tmp.flush sebelum file dibaca proses lain — data yang ditulis mungkin masih di buffer OS; flush memastikan 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 di File.dirname(target_path).
  • fsync untuk data kritisflush hanya mengosongkan buffer Ruby ke OS; fsync memastikan data sampai ke storage hardware (lebih lambat tapi lebih aman untuk data penting).
  • Ideal untuk testing — buat tempfile di setup, hapus di teardown; 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.create adalah pola modern — lebih safe dari Tempfile.new karena lifecycle yang lebih terprediksi; gunakan ini untuk kode baru.

← Sebelumnya: Net::HTTP   Berikutnya: Benchmark →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact