FileUtils

FileUtils #

Ruby memiliki File dan Dir untuk operasi file system, tapi keduanya sering membutuhkan banyak kode boilerplate untuk tugas-tugas umum seperti menyalin direktori secara rekursif, membuat struktur folder bertingkat, atau menghapus tree direktori. FileUtils hadir untuk mengisi celah ini — ia menyediakan operasi file system tingkat tinggi yang terinspirasi dari perintah Unix (cp, mv, rm, mkdir, chmod). Satu baris FileUtils.cp_r(sumber, tujuan) menggantikan puluhan baris kode manual untuk copy rekursif. Artikel ini membahas seluruh API FileUtils, mode verbose dan noop yang berguna untuk debugging, dan pola aman untuk manipulasi file system di skrip Ruby.

Memulai dengan FileUtils #

FileUtils tersedia setelah require "fileutils". Semua method tersedia sebagai method modul — kamu bisa memanggilnya langsung sebagai FileUtils.nama_method atau meng-include modulnya ke kelas.

require "fileutils"

# Penggunaan langsung sebagai method modul
FileUtils.mkdir_p("/tmp/test/sub/dir")
FileUtils.touch("/tmp/test/file.txt")

# Atau include ke kelas
class DeployScript
  include FileUtils

  def jalankan
    mkdir_p("releases/current")
    cp_r("dist/.", "releases/current")
    chmod_R(0755, "releases/current/bin")
  end
end

# Mode verbose — cetak setiap perintah yang dieksekusi (seperti shell dengan -v)
FileUtils.mkdir_p("/tmp/test", verbose: true)
# => mkdir -p /tmp/test

FileUtils.cp("a.txt", "b.txt", verbose: true)
# => cp a.txt b.txt

# Mode noop — simulasi tanpa eksekusi nyata (dry run)
FileUtils.rm_rf("/tmp/penting", noop: true, verbose: true)
# => rm -rf /tmp/penting
# Tidak ada yang benar-benar dihapus!
flowchart TD
    A[FileUtils] --> B[Operasi Direktori]
    A --> C[Operasi File]
    A --> D[Permission & Atribut]
    A --> E[Mode Khusus]

    B --> B1[mkdir / mkdir_p]
    B --> B2[rmdir / rm_rf]
    B --> B3[cd / pwd]

    C --> C1[cp / cp_r]
    C --> C2[mv]
    C --> C3[rm / rm_f / rm_rf]
    C --> C4[touch / install]
    C --> C5[ln / ln_s / ln_sf]

    D --> D1[chmod / chmod_R]
    D --> D2[chown / chown_R]

    E --> E1[verbose: true]
    E --> E2[noop: true]
    E --> E3[FileUtils::Verbose]
    E --> E4[FileUtils::NoWrite]
    E --> E5[FileUtils::DryRun]

Operasi Direktori #

Membuat Direktori #

require "fileutils"

# mkdir — membuat satu direktori (parent harus sudah ada)
FileUtils.mkdir("/tmp/satu_level")

# mkdir_p — membuat direktori beserta semua parent yang belum ada
# Inilah yang paling sering dipakai
FileUtils.mkdir_p("/tmp/level1/level2/level3")
# Membuat /tmp/level1, /tmp/level1/level2, dan /tmp/level1/level2/level3

# Membuat beberapa direktori sekaligus
FileUtils.mkdir_p(["log", "tmp/pids", "tmp/sockets", "public/uploads"])

# mkdir_p tidak error jika direktori sudah ada — aman dipanggil berulang kali
FileUtils.mkdir_p("/tmp/level1/level2/level3")   # tidak error!

# rmdir — hapus direktori kosong
FileUtils.rmdir("/tmp/kosong")   # error jika tidak kosong

# Menghapus direktori kosong secara rekursif dari daun ke atas
# (jarang dipakai, biasanya pakai rm_rf saja)
FileUtils.remove_dir("/tmp/level1")

Berpindah Direktori #

# cd — berpindah working directory untuk durasi blok
FileUtils.cd("/tmp") do
  # working directory adalah /tmp selama blok ini berjalan
  FileUtils.touch("file_di_tmp.txt")
  puts Dir.pwd   # => /tmp
end
# Setelah blok selesai, kembali ke working directory semula

# cd tanpa blok — ubah working directory secara permanen
# Hati-hati: ini mengubah state global proses
FileUtils.cd("/tmp")
puts Dir.pwd   # => /tmp

Copy File dan Direktori #

cp — Copy File #

require "fileutils"

# cp — copy satu file
FileUtils.cp("sumber.txt", "tujuan.txt")
FileUtils.cp("sumber.txt", "/tmp/")   # copy ke direktori, nama sama

# Copy beberapa file ke direktori tujuan
FileUtils.cp(["a.txt", "b.txt", "c.txt"], "/tmp/backup/")

# cp dengan preserve — pertahankan timestamp dan permission
FileUtils.cp("sumber.txt", "tujuan.txt", preserve: true)

# cp_lr — copy dengan hard link (lebih cepat, hemat disk)
# File source dan dest berbagi inode yang sama
FileUtils.cp_lr("sumber.txt", "link.txt")

cp_r — Copy Rekursif #

cp_r adalah method yang paling sering dipakai untuk menyalin direktori beserta seluruh isinya — file, subdirektori, dan sub-subdirektori.

# cp_r — copy direktori secara rekursif
FileUtils.cp_r("direktori_sumber", "direktori_tujuan")

# Jika tujuan tidak ada, dibuat dengan nama tersebut
# Jika tujuan sudah ada, sumber disalin ke dalamnya

# Copy isi direktori (dengan trailing slash atau /.)
FileUtils.cp_r("dist/.", "public/")   # salin isi dist ke dalam public

# Contoh deploy: salin hasil build ke direktori release
def buat_release(versi)
  release_dir = "releases/#{versi}"
  FileUtils.mkdir_p(release_dir)
  FileUtils.cp_r("dist/.", release_dir)
  FileUtils.cp("config/production.yml", "#{release_dir}/config/")
  release_dir
end

# cp_r dengan daftar eksklusi — tidak ada opsi bawaan, perlu manual
def cp_r_kecuali(sumber, tujuan, excludes: [])
  FileUtils.mkdir_p(tujuan)
  Pathname.new(sumber).glob("**/*").each do |src_path|
    next if excludes.any? { |pola| src_path.to_s.match?(pola) }
    next if src_path.directory?

    relative = src_path.relative_path_from(sumber)
    dst_path = Pathname.new(tujuan) / relative
    FileUtils.mkdir_p(dst_path.dirname)
    FileUtils.cp(src_path.to_s, dst_path.to_s)
  end
end

cp_r_kecuali("proyek", "backup",
  excludes: [/\.git/, /node_modules/, /\.DS_Store/])

Memindahkan dan Mengganti Nama File #

require "fileutils"

# mv — pindahkan atau ganti nama file/direktori
FileUtils.mv("lama.txt", "baru.txt")         # rename di tempat
FileUtils.mv("file.txt", "/tmp/")            # pindah ke direktori lain
FileUtils.mv("file.txt", "/tmp/nama_baru.txt")  # pindah sekaligus rename

# mv beberapa file ke direktori tujuan
FileUtils.mv(["a.txt", "b.txt"], "/tmp/arsip/")

# Pola: backup sebelum replace
def replace_dengan_backup(file_asli, file_baru)
  backup = "#{file_asli}.bak"
  FileUtils.cp(file_asli, backup) if File.exist?(file_asli)
  FileUtils.mv(file_baru, file_asli)
  backup
end

# mv aman dengan cek keberadaan
def pindah_jika_ada(sumber, tujuan)
  if File.exist?(sumber)
    FileUtils.mkdir_p(File.dirname(tujuan))
    FileUtils.mv(sumber, tujuan)
    true
  else
    false
  end
end

Menghapus File dan Direktori #

Penghapusan adalah operasi yang paling perlu berhati-hati — tidak ada recycle bin di file system!

require "fileutils"

# rm — hapus file (error jika tidak ada)
FileUtils.rm("file.txt")

# rm_f — hapus file, abaikan jika tidak ada (force)
FileUtils.rm_f("mungkin_tidak_ada.txt")

# rm dengan beberapa file sekaligus
FileUtils.rm(["a.txt", "b.txt", "c.txt"])
FileUtils.rm_f(["a.tmp", "b.tmp"])

# rm_r — hapus direktori rekursif (error jika tidak ada)
FileUtils.rm_r("direktori_lama")

# rm_rf — hapus direktori rekursif, abaikan jika tidak ada (yang paling sering dipakai)
FileUtils.rm_rf("direktori_lama")
FileUtils.rm_rf("/tmp/build_output")

rm_rf tidak bisa dibatalkan. File yang dihapus dengan rm_rf tidak masuk recycle bin — langsung hilang dari disk. Selalu validasi path sebelum menjalankan rm_rf, terutama jika path datang dari variabel atau input pengguna.

# ANTI-PATTERN: hapus langsung tanpa validasi
def bersihkan_build(build_dir)
  FileUtils.rm_rf(build_dir)   # berbahaya jika build_dir adalah "/" atau ""
end

# BENAR: validasi path sebelum hapus
def bersihkan_build(build_dir)
  path = Pathname.new(build_dir).expand_path

  # Pastikan tidak menghapus root atau home
  raise "Path terlalu pendek, mungkin salah!" if path.to_s.split("/").length < 3
  raise "Tidak boleh hapus home directory!" if path.to_s.start_with?(Dir.home)

  FileUtils.rm_rf(path.to_s) if path.directory?
end
# Pola aman: preview dulu dengan noop, eksekusi setelah konfirmasi
def hapus_dengan_konfirmasi(path)
  puts "Akan menghapus:"
  FileUtils.rm_rf(path, noop: true, verbose: true)

  print "Lanjutkan? (y/n): "
  return unless gets.chomp.downcase == "y"

  FileUtils.rm_rf(path)
  puts "Selesai."
end

require "fileutils"

# ln — hard link (hanya untuk file, bukan direktori)
FileUtils.ln("target.txt", "link.txt")

# ln_s — symbolic link (symlink)
FileUtils.ln_s("target.txt", "link.txt")
FileUtils.ln_s("/absolute/path/target", "link")

# ln_sf — symbolic link dengan force (timpa jika sudah ada)
FileUtils.ln_sf("target_baru.txt", "link.txt")   # update symlink yang ada

# Pola deploy: current symlink ke release terbaru
def update_current_symlink(release_path)
  current = "releases/current"
  FileUtils.rm(current) if File.symlink?(current)
  FileUtils.ln_s(File.expand_path(release_path), current)
end

Permission dan Kepemilikan #

require "fileutils"

# chmod — ubah permission satu file/direktori
FileUtils.chmod(0644, "file.txt")     # rw-r--r--
FileUtils.chmod(0755, "script.sh")    # rwxr-xr-x
FileUtils.chmod(0600, "private.key")  # rw-------

# chmod beberapa file sekaligus
FileUtils.chmod(0644, ["a.txt", "b.txt", "c.txt"])

# chmod_R — ubah permission secara rekursif
FileUtils.chmod_R(0755, "bin/")         # semua isi bin/ jadi executable
FileUtils.chmod_R(0644, "public/")      # semua file public world-readable

# chown — ubah kepemilikan (butuh privilege)
FileUtils.chown("www-data", "www-data", "public/")

# chown_R — ubah kepemilikan secara rekursif
FileUtils.chown_R("deploy", "deploy", "releases/")

# Pola umum setelah deploy
def set_permission_produksi(app_dir)
  # Direktori: 755, file: 644
  Find.find(app_dir) do |path|
    if File.directory?(path)
      FileUtils.chmod(0755, path)
    else
      FileUtils.chmod(0644, path)
    end
  end
  # Script khusus: 755
  FileUtils.chmod_R(0755, File.join(app_dir, "bin"))
end

touch dan install #

require "fileutils"

# touch — buat file kosong atau update timestamp (seperti Unix touch)
FileUtils.touch("file_baru.txt")          # buat file kosong
FileUtils.touch("sudah_ada.txt")          # update mtime ke sekarang
FileUtils.touch(["a.txt", "b.txt"])       # beberapa file sekaligus

# install — copy dengan set permission sekaligus
# Sangat berguna untuk script deployment
FileUtils.install("build/app", "/usr/local/bin/app", mode: 0755)
FileUtils.install("config/app.conf", "/etc/app/app.conf", mode: 0644)

Mode Verbose dan Dry Run #

Salah satu fitur FileUtils yang sering diabaikan adalah dukungan built-in untuk debugging dan dry run — kamu bisa melihat apa yang akan dilakukan tanpa benar-benar melakukan apa-apa.

require "fileutils"

# verbose: true — cetak setiap operasi ke STDOUT
FileUtils.cp_r("src/", "dst/", verbose: true)
# => cp -r src/ dst/

FileUtils.rm_rf("old_build/", verbose: true)
# => rm -rf old_build/

# noop: true — jangan eksekusi, hanya simulasi
FileUtils.rm_rf("/tmp/penting", noop: true, verbose: true)
# => rm -rf /tmp/penting
# (tidak ada yang benar-benar dihapus)

# Kombinasi keduanya = dry run yang informatif
def deploy(env)
  dry_run = env != "production"
  opts = { verbose: true, noop: dry_run }

  FileUtils.mkdir_p("releases/#{versi}", **opts)
  FileUtils.cp_r("dist/.", "releases/#{versi}", **opts)
  FileUtils.ln_sf("releases/#{versi}", "current", **opts)

  if dry_run
    puts "[DRY RUN] Tidak ada perubahan nyata"
  else
    puts "Deploy selesai!"
  end
end

# Modul khusus untuk mode berbeda
# FileUtils::Verbose — semua operasi verbose secara default
# FileUtils::NoWrite — semua operasi noop secara default
# FileUtils::DryRun  — verbose + noop

class DeployDryRun
  include FileUtils::DryRun   # semua operasi adalah dry run + verbose

  def jalankan
    mkdir_p("releases/latest")
    cp_r("dist/.", "releases/latest")
    # Semua ini hanya dicetak, tidak dieksekusi
  end
end

Pola Penggunaan Nyata #

Script Deploy Sederhana #

require "fileutils"
require "pathname"

class Deployer
  RELEASE_DIR = Pathname.new("releases")
  CURRENT_LINK = Pathname.new("current")
  MAX_RELEASES = 5

  def initialize(build_dir, verbose: false)
    @build = Pathname.new(build_dir)
    @verbose = verbose
    @opts = { verbose: @verbose }
  end

  def deploy
    versi = Time.now.strftime("%Y%m%d%H%M%S")
    release_path = RELEASE_DIR / versi

    puts "Deploying versi #{versi}..."

    buat_release(release_path)
    update_symlink(release_path)
    bersihkan_rilis_lama

    puts "Deploy berhasil: #{release_path}"
    release_path
  end

  private

  def buat_release(path)
    FileUtils.mkdir_p(path.to_s, **@opts)
    FileUtils.cp_r("#{@build}/.", path.to_s, **@opts)
    FileUtils.chmod_R(0755, (path / "bin").to_s, **@opts) if (path / "bin").directory?
  end

  def update_symlink(release_path)
    FileUtils.rm(CURRENT_LINK.to_s, **@opts) if CURRENT_LINK.symlink?
    FileUtils.ln_s(release_path.expand_path.to_s, CURRENT_LINK.to_s, **@opts)
  end

  def bersihkan_rilis_lama
    semua_rilis = RELEASE_DIR.children.select(&:directory?).sort
    rilis_lama = semua_rilis.first([semua_rilis.length - MAX_RELEASES, 0].max)
    rilis_lama.each do |r|
      puts "Hapus rilis lama: #{r}"
      FileUtils.rm_rf(r.to_s, **@opts)
    end
  end
end

# Penggunaan
deployer = Deployer.new("build/", verbose: true)
deployer.deploy

Sinkronisasi Direktori #

require "fileutils"
require "pathname"
require "digest"

# Sinkronisasi sederhana: copy file yang berubah, hapus yang sudah tidak ada
def sinkronisasi(sumber_dir, tujuan_dir, verbose: false)
  sumber = Pathname.new(sumber_dir)
  tujuan = Pathname.new(tujuan_dir)

  FileUtils.mkdir_p(tujuan.to_s)

  ditambah = 0
  diperbarui = 0
  dihapus = 0

  # Copy file baru atau yang berubah
  sumber.glob("**/*").select(&:file?).each do |src|
    rel = src.relative_path_from(sumber)
    dst = tujuan / rel

    if !dst.exist?
      FileUtils.mkdir_p(dst.dirname.to_s)
      FileUtils.cp(src.to_s, dst.to_s, verbose: verbose)
      ditambah += 1
    elsif Digest::MD5.file(src) != Digest::MD5.file(dst)
      FileUtils.cp(src.to_s, dst.to_s, verbose: verbose)
      diperbarui += 1
    end
  end

  # Hapus file di tujuan yang tidak ada di sumber
  tujuan.glob("**/*").select(&:file?).each do |dst|
    rel = dst.relative_path_from(tujuan)
    src = sumber / rel

    unless src.exist?
      FileUtils.rm(dst.to_s, verbose: verbose)
      dihapus += 1
    end
  end

  { ditambah: ditambah, diperbarui: diperbarui, dihapus: dihapus }
end

hasil = sinkronisasi("src/", "backup/", verbose: true)
puts "Sinkronisasi selesai: +#{hasil[:ditambah]} ~#{hasil[:diperbarui]} -#{hasil[:dihapus]}"

Perbandingan FileUtils vs Alternatif #

KebutuhanFileUtilsPathnameShell (backtick)
Copy rekursifcp_rperlu manual`cp -r src dst`
Buat dir + parentmkdir_pmkpath`mkdir -p dir`
Hapus rekursifrm_rfrmtree`rm -rf dir`
Pindah/renamemvrename (satu fs)`mv src dst`
Permission rekursifchmod_Rtidak ada`chmod -R 755 dir`
Dry runnoop: truetidak adatidak ada
Cross-platform✗ Windows
Error handlingExceptionExceptionReturn code
# ANTI-PATTERN: menggunakan shell command dari Ruby
system("cp -r src/ dst/")          # ✗ tidak cross-platform, tidak ada error handling
`rm -rf #{user_input}`             # ✗ shell injection vulnerability!
system("mkdir -p #{dir}")          # ✗ bisa kena shell injection

# BENAR: gunakan FileUtils
FileUtils.cp_r("src/", "dst/")    # ✓ cross-platform, raise exception on error
FileUtils.rm_rf(sanitized_dir)    # ✓ aman dari injection
FileUtils.mkdir_p(dir)            # ✓ proper error handling

Ringkasan #

  • mkdir_p untuk membuat direktori — selalu gunakan mkdir_p bukan mkdir; ia membuat semua parent yang belum ada dan tidak error jika direktori sudah ada.
  • cp_r untuk copy direktori — menyalin direktori beserta seluruh isinya secara rekursif; gunakan "src/." sebagai sumber untuk menyalin isi tanpa membuat subdirektori baru.
  • rm_rf harus divalidasi — selalu pastikan path yang akan dihapus benar sebelum memanggil rm_rf; hapus yang salah tidak bisa diundo.
  • verbose: true dan noop: true — gunakan untuk debugging dan dry run; kombinasikan keduanya untuk melihat apa yang akan terjadi sebelum benar-benar dieksekusi.
  • Jangan gunakan shell command — backtick (`) dan system() untuk operasi file berbahaya terhadap shell injection dan tidak cross-platform; FileUtils selalu lebih aman.
  • FileUtils::DryRun untuk testing script — include modul ini ke kelas deploy/script-mu selama development agar semua operasi menjadi dry run secara default.
  • Kombinasikan dengan Pathname — gunakan Pathname untuk membangun dan memanipulasi path, lalu teruskan .to_s ke FileUtils untuk operasi yang sebenarnya.
  • install untuk deploymentFileUtils.install lebih ekspresif dari cp + chmod terpisah ketika kamu perlu menyalin file sekaligus mengatur permission-nya.

← Sebelumnya: Pathname   Berikutnya: JSON →

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