chinabugotech/hutool · error · IORuntimeException
Src [{}] is a directory but dest [{}] is a file!
Error message
Src [{}] is a directory but dest [{}] is a file! What it means
Thrown by FileCopier during recursive directory copying (internalCopyDirContent) when a source subdirectory must be copied but the corresponding destination path already exists and is a regular file rather than a directory. Hutool's FileCopier defines file-to-file, file-to-dir, and dir-to-dir as legal, but copying a directory onto a file is undefined, so it aborts. The message formats both the source and destination paths to aid diagnosis. Note the top-level copy() (line 185) has its own equivalent check with an unformatted message; this 219 variant is the recursive one that fires mid-traversal.
Source
Thrown at hutool-core/src/main/java/cn/hutool/core/io/file/FileCopier.java:219
* 拷贝目录内容,只用于内部,不做任何安全检查<br>
* 拷贝内容的意思为源目录下的所有文件和目录拷贝到另一个目录下,而不拷贝源目录本身
*
* @param src 源目录
* @param dest 目标目录
* @throws IORuntimeException IO异常
*/
private void internalCopyDirContent(File src, File dest) throws IORuntimeException {
if (null != copyFilter && false == copyFilter.accept(src)) {
//被过滤的目录跳过
return;
}
if (false == dest.exists()) {
//目标为不存在路径,创建为目录
//noinspection ResultOfMethodCallIgnored
dest.mkdirs();
} else if (false == dest.isDirectory()) {
throw new IORuntimeException(StrUtil.format("Src [{}] is a directory but dest [{}] is a file!", src.getPath(), dest.getPath()));
}
final String[] files = src.list();
if(ArrayUtil.isNotEmpty(files)){
File srcFile;
File destFile;
for (String file : files) {
srcFile = new File(src, file);
destFile = this.isOnlyCopyFile ? dest : new File(dest, file);
// 递归复制
if (srcFile.isDirectory()) {
internalCopyDirContent(srcFile, destFile);
} else {
internalCopyFile(srcFile, destFile);
}
}
}
}View on GitHub (pinned to 8870454b2a)
Solutions
- Point dest at a path that either does not exist or is an existing directory, so the copier can mkdir/create it correctly.
- Before copying, delete or rename the conflicting file at the destination path named in the error message.
- If you intended a file-to-file copy, ensure src is actually a regular file, not a directory.
- Pre-clean the destination tree (or enable isOverride / remove stale files) so no directory maps onto an existing file.
- Make the source and destination layout deterministic and free of name collisions before invoking copy().
Example fix
// before: dest 'logs.bak' is an existing FILE, src 'logs' is a directory
FileCopier.create(new File("/data/logs"), new File("/data/logs.bak")).copy();
// -> IORuntimeException: Src [/data/logs] is a directory but dest [/data/logs.bak] is a file!
// after: target a fresh or existing directory
File dest = new File("/data/logs.bak");
if (dest.exists() && !dest.isDirectory()) {
FileUtil.del(dest); // remove the conflicting file first
}
FileCopier.create(new File("/data/logs"), dest).copy(); Defensive patterns
Strategy: validation
Validate before calling
File src = new File(srcPath);
File dest = new File(destPath);
if (!src.exists()) throw new IllegalArgumentException("src missing: " + src);
if (src.isDirectory() && dest.exists() && !dest.isDirectory()) {
throw new IllegalStateException(
"dest is an existing file but src is a directory: " + dest);
}
// For recursive safety, also scan src subdirs vs existing dest files:
if (src.isDirectory()) {
Path srcRoot = src.toPath();
Path destRoot = dest.toPath();
Files.walk(srcRoot).forEach(p -> {
if (Files.isDirectory(p)) {
Path rel = srcRoot.relativize(p);
Path candidate = destRoot.resolve(rel);
if (Files.exists(candidate) && !Files.isDirectory(candidate)) {
throw new IllegalStateException(
"Conflict: dest file blocks src dir at " + candidate);
}
}
});
}
FileCopier.create(src, dest).copy(); Type guard
boolean canCopyDirToDir(File src, File dest) {
if (src == null || dest == null) return false;
if (!src.isDirectory()) return false; // this error is dir->file
return !dest.exists() || dest.isDirectory(); // absent or a dir is OK
} Try / catch
try {
FileCopier.create(srcDir, dest).copy();
} catch (IORuntimeException e) {
if (e.getMessage() != null && e.getMessage().contains("is a directory but dest")) {
// resolve the conflicting file, then optionally retry
log.warn("dest file blocks dir copy: {}", dest);
} else {
throw e;
}
} Prevention
- Always pass a dest that is either non-existent or a directory when src is a directory.
- Pre-clean or version the destination directory before copying.
- Avoid setOnlyCopyFile(true) when the destination may contain files whose names collide with source subdirectories.
- Validate src.isDirectory()/dest.isDirectory() before calling copy().
- Run copies against a fresh staging directory, then atomically rename into place.
When it happens
Trigger: Calling FileCopier.create(srcDir, dest).copy() where srcDir is a directory and dest (or a child path resolved during recursion via new File(dest, childName)) is an existing regular file. Fires when a source subdirectory name collides with an existing file name in the destination tree, or when setOnlyCopyFile(true) is combined with a destination layout where a subdir maps onto a file. Also reachable if dest was created as a file by a prior partial copy run or another process between the top-level check and the recursive call.
Common situations: Re-running a directory copy into a target where a previous interrupted run left a file where a directory is expected; a backup/extract target path mistakenly pointing at an existing file; concurrent writers creating a file at the destination path mid-copy; extracting an archive whose entries include a directory that conflicts with a pre-existing file of the same name; misusing isOnlyCopyFile so subdirectories collapse onto existing files.
Related errors
- Not a file: {}
- File not exist: {}
- Path [{}] is not directory!
- File is larger then max array size
- File length is [{}] but read [{}]!
AI-assisted analysis of chinabugotech/hutool@8870454b2a (2026-08-14).
Data as JSON: /api/errors/38f89de01022e0ab.
Report an issue: GitHub.