NIO.2, safe file operations
File operations fail differently in production
Local development hides most filesystem realities. In production you face:
- different mount options (noexec, nodev),
- permissions and ownership,
- disk full / inode exhaustion,
- NFS semantics and flaky IO,
- container filesystems and ephemeral storage.
Safe file operations must be designed defensively.
Prefer NIO.2 (java.nio.file) over legacy File
NIO.2 provides:
- Path and FileSystem abstractions
- Atomic move operations
- Better exceptions and metadata
- Streamed directory iteration
Always treat paths as untrusted input
If any part of a path comes from user input (filename, subdir), you must protect against path traversal.
Path traversal example
User provides filename: ../../etc/passwd
Safe path resolve pattern
import java.nio.file.*;
Path baseDir = Paths.get("/var/app/uploads").toAbsolutePath().normalize();
String userFile = requestFilename; // untrusted
Path resolved = baseDir.resolve(userFile).normalize();
if (!resolved.startsWith(baseDir)) {
throw new IllegalArgumentException("path traversal detected");
}
Normalization + startsWith check prevents escaping base directory.
Atomic write pattern (avoid partial files)
Production failures often create partial files:
- process crash mid-write
- disk full mid-write
- network filesystem glitch
Safer pattern: write to temp file, fsync if needed, then atomic move.
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
Path target = Paths.get("/var/app/config/generated.json");
Path dir = target.getParent();
Files.createDirectories(dir);
Path tmp = Files.createTempFile(dir, target.getFileName().toString(), ".tmp");
try {
Files.writeString(
tmp,
jsonPayload,
StandardCharsets.UTF_8,
StandardOpenOption.TRUNCATE_EXISTING
);
// Atomic replace if supported by filesystem
Files.move(tmp, target,
StandardCopyOption.ATOMIC_MOVE,
StandardCopyOption.REPLACE_EXISTING
);
} finally {
// best-effort cleanup if move failed
try { Files.deleteIfExists(tmp); } catch (Exception ignored) {}
}
This reduces the chance of leaving corrupt partial files behind.
ATOMIC_MOVE caveat
Atomic moves are only guaranteed on the same filesystem and when supported. If unsupported, an exception may be thrown. In production you should handle this:
- Keep temp file in the same directory (same filesystem)
- Fallback to non-atomic move if business can tolerate it
Explicit file permissions (Linux)
Default permissions depend on umask and environment. For sensitive files, set explicit permissions where supported.
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
Set<PosixFilePermission> perms =
PosixFilePermissions.fromString("rw-------");
Files.setPosixFilePermissions(target, perms);
Note: This only works on POSIX file systems.
Safe directory iteration (avoid loading everything)
Never do Files.list(...).toList() on huge directories in production.
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, "*.log")) {
for (Path p : stream) {
// process one by one
}
}
DirectoryStream iterates lazily and is safer for large dirs.
Handle disk full and permission errors explicitly
Disk full can present as:
- NoSuchFileException (tmp cannot be created)
- FileSystemException (No space left on device)
In production, treat these as operational failures, not generic exceptions. Ensure logs include path and action.
Symlink considerations
Symlinks can bypass path traversal checks if you only validate strings. For high-security contexts, consider:
- disallowing symlinks
- using toRealPath() and comparing canonical paths
Example: canonical path validation (more expensive)
Path realBase = baseDir.toRealPath();
Path realResolved = resolved.toRealPath(LinkOption.NOFOLLOW_LINKS);
if (!realResolved.startsWith(realBase)) {
throw new IllegalArgumentException("escaped base dir via symlink");
}
Cleanup discipline
Temp files accumulate. In production, implement:
- periodic cleanup jobs
- bounded tmp directories per request
- robust deleteIfExists on failures
Production incident scenario
A service writes a config file directly. During deployment, process is restarted mid-write. The file becomes empty. Next startup reads empty config and crashes repeatedly. Root cause: missing atomic write pattern.
Checklist
- Use NIO.2 Path APIs.
- Treat filenames/paths as untrusted; prevent traversal.
- Write temp + atomic move to avoid partial files.
- Keep temp file in same directory for atomic move.
- Set explicit permissions for sensitive files.
- Iterate directories lazily.
- Log filesystem errors with path context.
- Plan cleanup for temp artifacts.
Final principle
Filesystem code must assume failure. The safest file write is the one that cannot leave corrupted state behind.