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

  1. Point dest at a path that either does not exist or is an existing directory, so the copier can mkdir/create it correctly.
  2. Before copying, delete or rename the conflicting file at the destination path named in the error message.
  3. If you intended a file-to-file copy, ensure src is actually a regular file, not a directory.
  4. Pre-clean the destination tree (or enable isOverride / remove stale files) so no directory maps onto an existing file.
  5. 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

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


AI-assisted analysis of chinabugotech/hutool@8870454b2a (2026-08-14). Data as JSON: /api/errors/38f89de01022e0ab. Report an issue: GitHub.