Skip to content
CalliCoder

How to Copy a File or Directory in Java

Java 13 min read

Files.copy will not overwrite unless you ask, copying a directory copies only the directory, and COPY_ATTRIBUTES is the difference between a copy and a duplicate.

Files.copy is one call with three overloads and two options that change what “copy” means. The two surprises are that it refuses to overwrite by default, and that copying a directory creates an empty one rather than copying its contents.

Written against Java 17.

The basic call

Path source = Path.of("/var/data/report.csv");
Path target = Path.of("/var/backup/report.csv");

Files.copy(source, target);                                    // FileAlreadyExistsException if it exists
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);

Refusing to overwrite by default is the right choice, a copy that silently destroys the destination is a data loss waiting to happen, and it is the first thing people trip over.

The target must be the full destination path, not a directory:

Files.copy(source, Path.of("/var/backup"));                    // creates a FILE named "backup"
Files.copy(source, Path.of("/var/backup").resolve(source.getFileName()));   // correct

That is the second common mistake, and it does not throw anything, it quietly produces a file with the wrong name.

Copying attributes

Files.copy(source, target,
        StandardCopyOption.REPLACE_EXISTING,
        StandardCopyOption.COPY_ATTRIBUTES);

Without COPY_ATTRIBUTES, the copy gets a fresh modification time and default permissions. With it, the timestamps and, where the filesystem supports them, the POSIX permissions and ownership come across.

Which you want depends on the purpose. A backup or an archive wants the attributes preserved, so that “when was this last changed” survives, a copy that is a new document in its own right should start clean, and inheriting a source file’s rw------- can make it unreadable to the process that needs it next.

ATOMIC_MOVE is a move option and not available for copy. There is no atomic copy, because copying data is not a single filesystem operation the way rewriting a directory entry is.

Directories are not copied recursively

Files.copy(sourceDir, targetDir);      // creates an EMPTY directory

That is the documented behaviour and it surprises everyone. The recursive version is a walk:

static void copyDirectory(Path source, Path target) throws IOException {
    try (Stream<Path> walk = Files.walk(source)) {
        walk.forEach(from -> {
            Path to = target.resolve(source.relativize(from));
            try {
                Files.copy(from, to,
                        StandardCopyOption.REPLACE_EXISTING,
                        StandardCopyOption.COPY_ATTRIBUTES);
            } catch (IOException e) {
                throw new UncheckedIOException(e);
            }
        });
    }
}

source.relativize(from) gives the path of each entry relative to the source root, and resolving that against the target reproduces the structure. Getting this wrong, resolving the absolute path instead, produces a copy nested inside the target at the source’s full path.

Unlike a recursive delete, the order is natural: Files.walk yields parents before children, which is what a copy needs, so no sorting is required, the target root must exist before the walk starts, though, the first entry copied is the source directory itself, mapping to target.resolve(""), which is the target root, and Files.copy creates it. Every deeper entry then finds its parent already there.

The try-with-resources is mandatory for the same reason as always, the walk holds directory handles.

Copying to and from streams

try (InputStream in = url.openStream()) {
    Files.copy(in, target, StandardCopyOption.REPLACE_EXISTING);
}

try (OutputStream out = Files.newOutputStream(target)) {
    Files.copy(source, out);
}

The stream overloads are what make “download this URL to a file” a single line. Note the asymmetry: the InputStream version accepts copy options because it is writing to a path; the OutputStream version does not, because the destination is already open.

Files.copy(source, target) between two paths can use a platform-level copy (copy_file_range on Linux, for instance) which is faster than reading and writing through the JVM. The stream overloads cannot, so prefer the path form for a file-to-file copy.

Copying is not atomic

A copy interrupted halfway leaves a partial file at the destination, and any reader arriving in the meantime sees it. For anything another process watches, copy to a temporary name and rename:

Path temp = target.resolveSibling(target.getFileName() + ".tmp");
Files.copy(source, temp, StandardCopyOption.REPLACE_EXISTING);
Files.move(temp, target, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);

resolveSibling keeps the temporary file in the target’s own directory, which is required. An atomic move only works within one filesystem, so a temp file in /tmp degrades to a copy plus a delete and reintroduces the problem.

Readers then see either the old file or the complete new one, and never anything in between.

Copying a file onto itself

Files.copy(path, path, StandardCopyOption.REPLACE_EXISTING);

The specification leaves this unspecified, and implementations differ. Some detect it and do nothing, others truncate the file before reading it and leave an empty file. It arises more often than it sounds, because a normalised path and a symlinked one can name the same file without being equal.

Files.isSameFile(source, target) is the check, and it asks the filesystem rather than comparing text:

if (Files.exists(target) && Files.isSameFile(source, target)) {
    return;
}

Worth adding to any copy where either path came from configuration or user input.

Files.copy(link, target);                                   // copies the TARGET's contents
Files.copy(link, target, LinkOption.NOFOLLOW_LINKS);        // copies the LINK itself

The default follows the link, which is usually intended for a single file and usually wrong for a directory tree, a link pointing outside the tree pulls in data from elsewhere, and a link pointing back inside it makes Files.walk loop.

Files.walk does not follow links by default, so the recursive copy above is safe; adding FOLLOW_LINKS to it is the thing not to do.

Copying between filesystems and to object storage

Files.copy works across filesystems: a local disk to a mounted network share, or into a FileSystem provided by a ZIP filesystem provider:

try (FileSystem zip = FileSystems.newFileSystem(Path.of("archive.zip"), Map.of("create", "true"))) {
    Files.copy(source, zip.getPath("/report.csv"), StandardCopyOption.REPLACE_EXISTING);
}

That is the JDK’s built-in way to write into a ZIP without a third-party library, and the same Files calls work inside it.

Two things change once the destination is not a local disk. The platform-level copy optimisation disappears, so throughput is whatever the network gives. And ATOMIC_MOVE is unavailable across providers, which removes the copy-then-rename trick, a partial upload is visible, and the usual answer is to write to a temporary key and issue a provider-specific rename or a completion marker.

For a large copy it is also worth knowing that Files.copy gives no progress and cannot be cancelled. Where either matters, a manual loop over a ReadableByteChannel with a modest buffer lets you count bytes and check an interrupt flag between iterations, at the cost of the platform copy.

Verifying a copy

long copied = Files.copy(source, target, REPLACE_EXISTING);   // returns the byte count

The return value is the number of bytes, which is a cheap sanity check against Files.size(source). For anything where correctness matters, compare digests:

static String sha256(Path path) throws IOException, NoSuchAlgorithmException {
    MessageDigest digest = MessageDigest.getInstance("SHA-256");
    try (InputStream in = Files.newInputStream(path);
         DigestInputStream dis = new DigestInputStream(in, digest)) {
        dis.transferTo(OutputStream.nullOutputStream());
    }
    return HexFormat.of().formatHex(digest.digest());
}

Comparing sizes catches truncation; comparing digests catches corruption. Comparing modification times catches neither, which is worth remembering because it is the check most backup scripts make — and COPY_ATTRIBUTES makes the timestamps match by construction, so a comparison that relies on them is guaranteed to agree whether or not the bytes did.

Note also that Files.size on the destination is a second filesystem call and can disagree with the returned count if something else wrote to the file in between. For a verification that means anything, the destination should be in a location nothing else touches until the check has run.

Related: moving and renaming, deleting and reading. More in the Java guides.

Frequently asked questions

Why does Files.copy throw when the target exists?

Refusing to overwrite is the default. Pass StandardCopyOption.REPLACE_EXISTING when replacing is intended.

Can I pass a directory as the target?

No, the target is the full destination path. Passing a directory creates a file with that directory’s name, and it does not throw.

Why is my copied directory empty?

Files.copy on a directory copies only the directory itself. Walk the tree and copy each entry to reproduce the contents.

What does COPY_ATTRIBUTES do?

Preserves timestamps and, where supported, permissions and ownership. Without it the copy gets a current timestamp and default permissions.

Is there an atomic copy?

No. ATOMIC_MOVE is a move option only. To get atomic visibility, copy to a temporary name in the same directory and then move it into place.

Why must the temporary file be in the same directory?

An atomic move only works within one filesystem. Across filesystems it degrades to a copy and a delete, which is not atomic.

Yes by default, copying the target’s contents. Pass LinkOption.NOFOLLOW_LINKS to copy the link itself.

Which is faster, Files.copy or a stream loop?

The path-to-path form, which can use a platform-level copy. The stream overloads always go through the JVM.

How do I verify the copy succeeded?

Files.copy returns the byte count, compare it to Files.size(source). Compare SHA-256 digests when corruption, not just truncation, matters.

Does the recursive copy need sorting like the recursive delete does?

No. Files.walk yields parents before children, which is the order a copy needs. Only the delete needs it reversed.