JAVA Contents

NIO.2, safe file operations

Use NIO.2 with atomic writes, safe path handling, and explicit permissions to avoid partial files, path traversal, and environment-specific filesystem failures in production.

On this page

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.