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_rftidak bisa dibatalkan. File yang dihapus denganrm_rftidak masuk recycle bin — langsung hilang dari disk. Selalu validasi path sebelum menjalankanrm_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
Symlink #
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 #
| Kebutuhan | FileUtils | Pathname | Shell (backtick) |
|---|---|---|---|
| Copy rekursif | cp_r | perlu manual | `cp -r src dst` |
| Buat dir + parent | mkdir_p | mkpath | `mkdir -p dir` |
| Hapus rekursif | rm_rf | rmtree | `rm -rf dir` |
| Pindah/rename | mv | rename (satu fs) | `mv src dst` |
| Permission rekursif | chmod_R | tidak ada | `chmod -R 755 dir` |
| Dry run | noop: true | tidak ada | tidak ada |
| Cross-platform | ✓ | ✓ | ✗ Windows |
| Error handling | Exception | Exception | Return 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_puntuk membuat direktori — selalu gunakanmkdir_pbukanmkdir; ia membuat semua parent yang belum ada dan tidak error jika direktori sudah ada.cp_runtuk copy direktori — menyalin direktori beserta seluruh isinya secara rekursif; gunakan"src/."sebagai sumber untuk menyalin isi tanpa membuat subdirektori baru.rm_rfharus divalidasi — selalu pastikan path yang akan dihapus benar sebelum memanggilrm_rf; hapus yang salah tidak bisa diundo.verbose: truedannoop: true— gunakan untuk debugging dan dry run; kombinasikan keduanya untuk melihat apa yang akan terjadi sebelum benar-benar dieksekusi.- Jangan gunakan shell command — backtick (
`) dansystem()untuk operasi file berbahaya terhadap shell injection dan tidak cross-platform; FileUtils selalu lebih aman.FileUtils::DryRununtuk 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_ske FileUtils untuk operasi yang sebenarnya.installuntuk deployment —FileUtils.installlebih ekspresif daricp + chmodterpisah ketika kamu perlu menyalin file sekaligus mengatur permission-nya.