Aplikasi yang membaca dan menulis file harus mengantisipasi berbagai kegagalan: file tidak ada, disk penuh, izin ditolak, atau serangan path traversal dari input pengguna. Tanpa pola yang konsisten, kode file menjadi rapuh dan sulit di-debug. Bab ini menggabungkan teknik-teknik yang sudah kita pelajari menjadi satu pipeline yang robust.

Enam Pilar File Processing yang Andal #

PolaTujuanAPI Kotlin
use { }Auto-close resource (Reader, Writer, Stream)Extension function pada Closeable
useLines { }Streaming baca baris-per-baris, hemat memoriFile.useLines { }
Validasi pathCegah path traversal (../etc/passwd)Path.normalize(), startsWith()
try-catch IOExceptionTangani kegagalan I/O secara eksplisitcatch (e: IOException)
Atomic writeTulis ke temp file, lalu move atomikFiles.move(..., ATOMIC_MOVE)
deleteIfExistsCleanup idempoten, tidak melempar jika file tidak adaFiles.deleteIfExists()

Pipeline Lengkap #

Mari kita rangkai semua pola di atas menjadi satu program:

kotlin
import java.io.IOException
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardOpenOption

fun main() {
    // === 1. Pattern use { } untuk resource auto-close ===
    val tmp = Files.createTempFile("robust-", ".txt")
    try {
        Files.newBufferedWriter(tmp).use { writer ->
            writer.write("baris pertama\n")
            writer.write("baris kedua\n")
        }

        // === 2. Baca dengan useLines (streaming, hemat memori) ===
        Files.newBufferedReader(tmp).use { reader ->
            reader.forEachLine { line -> println("Dibaca: $line") }
        }

        // === 3. Validasi path (cegah path traversal) ===
        val userInput = "../etc/passwd"
        val safeBase = Path.of("/tmp/app-data").also { Files.createDirectories(it) }
        val resolved = safeBase.resolve(userInput).normalize()
        if (!resolved.startsWith(safeBase)) {
            println("[BLOCK] Path traversal terdeteksi: $userInput")
        } else {
            println("[OK] Path aman: $resolved")
        }

        // === 4. Penanganan IOException eksplisit ===
        try {
            val content = Files.readString(Path.of("/file/tidak/ada"))
        } catch (e: IOException) {
            println("[ERROR] Tidak bisa membaca file: ${e.message}")
        }

        // === 5. Atomic write (write to temp, then move) ===
        val target = Path.of("/tmp/atomic-target.txt")
        val tempWrite = Files.createTempFile("atomic-", ".tmp")
        try {
            Files.write(tempWrite, "konten baru".toByteArray())
            // Atomic move (POSIX) — jika target ada, akan ditimpa
            Files.move(tempWrite, target, java.nio.file.StandardCopyOption.ATOMIC_MOVE, java.nio.file.StandardCopyOption.REPLACE_EXISTING)
            println("Atomic write sukses: ${Files.readString(target)}")
        } catch (e: Exception) {
            Files.deleteIfExists(tempWrite)
            println("Atomic write gagal: ${e.message}")
        } finally {
            Files.deleteIfExists(target)
        }

    } finally {
        // === 6. Cleanup idempoten ===
        Files.deleteIfExists(tmp)
    }

    println("Pipeline robust selesai.")
}

Output yang Dihasilkan:

text
Dibaca: baris pertama
Dibaca: baris kedua
[BLOCK] Path traversal terdeteksi: ../etc/passwd
[ERROR] Tidak bisa membaca file: /file/tidak/ada
Atomic write sukses: konten baru
Pipeline robust selesai.

Mari kita bedah setiap blok kode di atas:

  1. Files.newBufferedWriter(tmp).use { }: membuka writer, menulis, lalu menutup secara otomatis. Jika exception terjadi di dalam block, resource tetap ditutup. Inilah padanan try-with-resources Java.
  2. Files.newBufferedReader(tmp).use { reader -> reader.forEachLine { } }: membaca baris-per-baris tanpa memuat seluruh file ke memori. Penting untuk file besar (>100MB).
  3. safeBase.resolve(userInput).normalize(): menggabungkan base directory dengan input pengguna, lalu .normalize() menyelesaikan .. dan . dalam path. Hasilnya diverifikasi harus tetap di bawah safeBase — jika tidak, ini path traversal.
  4. catch (e: IOException): menangkap exception I/O secara spesifik. Jangan pakai catch (e: Exception) umum karena menyembunyikan bug logic.
  5. Files.move(tempWrite, target, ATOMIC_MOVE, REPLACE_EXISTING): memindahkan file secara atomik di POSIX. Operasi ini tidak dapat di-intercept oleh proses lain, sehingga target file selalu dalam keadaan konsisten — file lama atau file baru, tidak pernah setengah-ditulis.
  6. Files.deleteIfExists(tmp): cleanup yang aman dipanggil berulang. Tidak melempar exception jika file sudah hilang.

Pattern use untuk Resource #

use { } adalah extension function pada Closeable dan AutoCloseable. Setara dengan try-with-resources di Java:

kotlin
// Tanpa use { } — perlu finally manual
val reader = Files.newBufferedReader(path)
try {
    reader.forEachLine { println(it) }
} finally {
    reader.close()
}

// Dengan use { } — otomatis close
Files.newBufferedReader(path).use { reader ->
    reader.forEachLine { println(it) }
}

Kedua versi equivalent secara semantik: jika exception terjadi di dalam block, close() tetap dipanggil.

Anti-Pattern yang Sering Ditemui #

Anti-PatternMasalahSolusi
Files.readString(hugeFile)Memuat seluruh file ke memoriuseLines { } atau BufferedReader
Lupa use { }Resource leak (file descriptor)Selalu bungkus dengan use { }
path = base + "/" + userInputTidak ada validasi traversalGunakan resolve().normalize().startsWith()
Files.write(target, data) langsungBisa tinggalkan file setengah-ditulis saat crashTulis ke temp file, lalu move(ATOMIC_MOVE)
Files.delete(path)Melempar jika tidak adaFiles.deleteIfExists(path)

Best Practice Ringkas #

  1. Selalu use { } untuk setiap Reader, Writer, InputStream, OutputStream.
  2. Validasi path dari input pengguna dengan normalize().startsWith(baseDir).
  3. Tangkap IOException secara spesifik, bukan Exception umum.
  4. Atomic write untuk file penting: tulis ke temp, lalu move.
  5. Cleanup idempoten dengan deleteIfExists di finally block.
  6. Streaming untuk file besar dengan useLines { } atau BufferedReader.
  7. Logging untuk setiap kegagalan — file operation adalah sumber bug produksi yang sering.

Referensi Lanjutan #

Selanjutnya #

Dengan file I/O yang robust, kita siap melangkah ke topik berikutnya: pengelolaan tanggal dan waktu dengan java.time dan kotlinx-datetime, yang merupakan bagian penting dari aplikasi nyata.