You're building a service that ingests user-uploaded files of unknown, possibly huge size and writes processed output that must be durable. Critique using File.readText/writeText here and describe what you'd reach for instead and why.
answer
- readText/writeText = small, trusted, best-effort only
- Unbounded input -> OOM/DoS: stream + size cap
- writeText doesn't fsync and truncates in place
- Durable write = temp + fsync + ATOMIC_MOVE rename
- Untrusted: validate path, cap size, pin charset
basics
~20 sreadText/writeText load or write the whole file at once and don't guarantee the data is safely saved to disk. For untrusted, possibly huge files you should stream and use atomic, flushed writes so you don't run out of memory or leave a half-written file.
solid answer
~50 sThe whole-content helpers are wrong here on three axes. Memory: `readText`/`readLines` are O(file); an attacker or accident can OOM the service — stream with `useLines`/`bufferedReader().use` or chunked byte reads so memory is bounded. Durability: `writeText` writes and closes but doesn't `fsync`; a crash can leave a truncated or empty file, and because it truncates first, a mid-write failure can also destroy the previous good version. For durable output, write to a temp file in the same directory, flush + force to disk (`FileOutputStream.fd.sync()` / `FileChannel.force(true)`), then `Files.move(tmp, target, ATOMIC_MOVE, REPLACE_EXISTING)` so readers only ever see the old or the new file, never a partial one. Safety: untrusted input also means validating the path (no traversal), capping size, and choosing charset explicitly. So the stdlib helpers stay for small, trusted, internal files; this workload calls for `java.nio.file` (`Files`, `FileChannel`, `StandardCopyOption.ATOMIC_MOVE`) plus streaming.
code
kotlin · 13 linesimport java.nio.file.*
import java.io.FileOutputStream
fun writeDurable(target: Path, bytes: ByteArray) {
val tmp = Files.createTempFile(target.parent, "out", ".tmp")
FileOutputStream(tmp.toFile()).use { fos ->
fos.write(bytes)
fos.fd.sync() // fsync to stable storage
}
Files.move(tmp, target,
StandardCopyOption.ATOMIC_MOVE, // readers never see a partial file
StandardCopyOption.REPLACE_EXISTING)
}go deeper
Recognizes readText could run out of memory on a huge file.
Suggests streaming for memory and notes writeText overwrites the existing file.
Adds the temp-then-atomic-rename durability pattern and fsync, plus size caps and charset handling.
Frames the full trade-off: memory/DoS, crash-safety semantics, atomic visibility, untrusted-path defense, and articulates exactly where the stdlib helpers stop being appropriate.
## Why the convenience helpers don't fit `File.readText`/`readLines`/`writeText` are excellent for small, trusted, internal files. This scenario violates every assumption behind them. ### 1. Memory: unbounded input `readText()`/`readLines()` materialize the entire file (O(file) heap). With *unknown, possibly huge* uploads, a single large file — or a malicious one — triggers `OutOfMemoryError` and can take down the whole service (a denial-of-service vector). Fix: **stream** with bounded memory: ```kotlin File(upload).useLines { lines -> lines.forEach(::process) } // O(one line) // or chunked binary: File(upload).inputStream().buffered().use { input -> val buf = ByteArray(8 * 1024) while (true) { val n = input.read(buf); if (n < 0) break; consume(buf, n) } } ``` Also **cap the size** up front (reject above a threshold) and validate the path to prevent traversal (`../../etc/passwd`). ### 2. Durability: writeText is not crash-safe `writeText` opens the target, **truncates** it, writes, and closes. Two failure modes: - It does **not** `fsync`, so after `close()` returns the bytes may still sit in the OS page cache. A power loss/crash can leave an **empty or truncated** file even though the call returned successfully. - Because it truncates the existing file first, a crash mid-write **destroys the previous good version** — you lose both old and new. The durable pattern is **write-temp-then-atomic-rename**: ```kotlin import java.nio.file.* import java.io.FileOutputStream fun writeDurable(target: Path, bytes: ByteArray) { val tmp = Files.createTempFile(target.parent, "out", ".tmp") // same filesystem! FileOutputStream(tmp.toFile()).use { fos -> fos.write(bytes) fos.fd.sync() // force bytes to disk (fsync) } Files.move(tmp, target, StandardCopyOption.ATOMIC_MOVE, // rename is atomic on same FS StandardCopyOption.REPLACE_EXISTING) } ``` - The temp file must be on the **same filesystem/directory** as the target so the rename is a metadata-only atomic operation. - `fd.sync()` (or `FileChannel.force(true)`) flushes to stable storage. - `ATOMIC_MOVE` means concurrent readers see either the complete old file or the complete new one — **never a half-written file**. For full durability you'd also fsync the parent directory. ### 3. Safety with untrusted input - **Path validation:** canonicalize and confine within an allowed root to block directory traversal. - **Size limits + timeouts:** bound resource use. - **Explicit charset:** don't assume UTF-8 for foreign text; decode strictly or as bytes. ## The decision | Concern | stdlib helper | What this workload needs | |---------|---------------|--------------------------| | Memory | O(file), OOM risk | streaming, size cap | | Crash safety | no fsync, truncate-in-place | temp + fsync + ATOMIC_MOVE | | Concurrency | readers can see partial | atomic rename hides partial | | Untrusted path | none | canonicalize + confine | **Reach for `java.nio.file`** (`Files`, `Path`, `FileChannel`, `StandardCopyOption.ATOMIC_MOVE`) plus streaming readers. Keep `readText`/`writeText` for the 90% case: small, trusted, internal files where the simplicity wins. ## Knowing the boundary is the skill The point isn't that the stdlib helpers are bad — it's recognizing the exact assumptions they encode (small, trusted, best-effort durability) and escalating to nio only when the workload breaks them.
- Why must the temp file be in the same directory as the target?ATOMIC_MOVE is only guaranteed atomic within the same filesystem. A rename across filesystems falls back to copy+delete, which isn't atomic and can leave a partial file.
- writeText returned without error — can the file still be empty after a crash?Yes. Without fsync, bytes may remain in the OS page cache after close(); a crash before flush can leave a truncated or empty file despite the successful return.
writeText is saving over your only copy of a document with no backup; the temp-then-rename pattern is editing a draft and only swapping it in once it's fully saved.
saying these in an interview costs you the question
- Claiming writeText is crash-safe/durable as-is
- Reading untrusted, unbounded uploads with readText (OOM/DoS)
- Renaming a temp file across filesystems and calling it atomic
- Ignoring path traversal and size limits for user uploads
- Insisting nio is always needed even for small trusted internal files