Provide Path.copyToRecursively() and Path.deleteRecursively() #KT-52928
This commit is contained in:
committed by
Space
parent
7271de2642
commit
90189f9c39
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||
*/
|
||||
|
||||
package kotlin.io.path
|
||||
|
||||
import java.nio.file.Path
|
||||
|
||||
/**
|
||||
* Context for the `copyAction` function passed to [Path.copyToRecursively].
|
||||
*/
|
||||
@ExperimentalPathApi
|
||||
@SinceKotlin("1.8")
|
||||
public interface CopyActionContext {
|
||||
/**
|
||||
* Copies the entry located by this path to the specified [target] path,
|
||||
* except if both this and [target] entries are directories,
|
||||
* in which case the method completes without copying the entry.
|
||||
*
|
||||
* The entry is copied using `this.copyTo(target, *followLinksOption)`. See [kotlin.io.path.copyTo].
|
||||
*
|
||||
* @param target the path to copy this entry to.
|
||||
* @param followLinks `false` to copy the entry itself even if it's a symbolic link.
|
||||
* `true` to copy its target if this entry is a symbolic link.
|
||||
* If this entry is not a symbolic link, the value of this parameter doesn't make any difference.
|
||||
* @return [CopyActionResult.CONTINUE]
|
||||
*/
|
||||
public fun Path.copyToIgnoringExistingDirectory(target: Path, followLinks: Boolean): CopyActionResult
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||
*/
|
||||
|
||||
package kotlin.io.path
|
||||
|
||||
import java.nio.file.Path
|
||||
|
||||
/**
|
||||
* The result of the `copyAction` function passed to [Path.copyToRecursively] that specifies further actions when copying an entry.
|
||||
*/
|
||||
@ExperimentalPathApi
|
||||
@SinceKotlin("1.8")
|
||||
public enum class CopyActionResult {
|
||||
/**
|
||||
* Continue with the next entry in the traversal order.
|
||||
*/
|
||||
CONTINUE,
|
||||
|
||||
/**
|
||||
* Skip the directory content, continue with the next entry outside the directory in the traversal order.
|
||||
* For files this option is equivalent to [CONTINUE].
|
||||
*/
|
||||
SKIP_SUBTREE,
|
||||
|
||||
/**
|
||||
* Stop the recursive copy function. The function will return without throwing exception.
|
||||
*/
|
||||
TERMINATE
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||
*/
|
||||
|
||||
package kotlin.io.path
|
||||
|
||||
import java.nio.file.Path
|
||||
|
||||
/**
|
||||
* The result of the `onError` function passed to [Path.copyToRecursively] that specifies further actions when an exception occurs.
|
||||
*/
|
||||
@ExperimentalPathApi
|
||||
@SinceKotlin("1.8")
|
||||
public enum class OnErrorResult {
|
||||
/**
|
||||
* If the entry that caused the error is a directory, skip the directory and its content, and
|
||||
* continue with the next entry outside this directory in the traversal order.
|
||||
* Otherwise, skip this entry and continue with the next entry in the traversal order.
|
||||
*/
|
||||
SKIP_SUBTREE,
|
||||
|
||||
/**
|
||||
* Stop the recursive copy function. The function will return without throwing exception.
|
||||
* To terminate the function with an exception rethrow instead.
|
||||
*/
|
||||
TERMINATE
|
||||
}
|
||||
@@ -0,0 +1,417 @@
|
||||
/*
|
||||
* Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||
*/
|
||||
|
||||
@file:JvmMultifileClass
|
||||
@file:JvmName("PathsKt")
|
||||
|
||||
package kotlin.io.path
|
||||
|
||||
import java.io.IOException
|
||||
import java.nio.file.*
|
||||
import java.nio.file.FileSystemException
|
||||
import java.nio.file.attribute.BasicFileAttributeView
|
||||
import java.nio.file.attribute.BasicFileAttributes
|
||||
|
||||
/**
|
||||
* Recursively copies this directory and its content to the specified destination [target] path.
|
||||
* Note that if this function throws, partial copying may have taken place.
|
||||
*
|
||||
* Unlike `File.copyRecursively`, if some directories on the way to the [target] are missing, then they won't be created automatically.
|
||||
* You can use the following approach to ensure that required intermediate directories are created:
|
||||
* ```
|
||||
* sourcePath.copyToRecursively(
|
||||
* destinationPath.apply { parent?.createDirectories() },
|
||||
* followLinks = false
|
||||
* )
|
||||
* ```
|
||||
*
|
||||
* If the entry located by this path is a directory, this function recursively copies the directory itself and its content.
|
||||
* Otherwise, this function copies only the entry.
|
||||
*
|
||||
* If an exception occurs attempting to read, open or copy any entry under the source subtree,
|
||||
* further actions will depend on the result of the [onError] invoked with
|
||||
* the source and destination paths, that caused the error, and the exception itself as arguments.
|
||||
* If [onError] throws, this function ends immediately with the exception.
|
||||
* By default [onError] rethrows the exception. See [OnErrorResult] for available options.
|
||||
*
|
||||
* This function performs "directory merge" operation. If an entry in the source subtree is a directory
|
||||
* and the corresponding entry in the target subtree already exists and is also a directory, it does nothing.
|
||||
* Otherwise, [overwrite] determines whether to overwrite existing destination entries.
|
||||
* Attributes of a source entry, such as creation/modification date, are not copied.
|
||||
* [followLinks] determines whether to copy a symbolic link itself or its target.
|
||||
* Symbolic links in the target subtree are not followed.
|
||||
*
|
||||
* To provide a custom logic for copying use the overload that takes a `copyAction` lambda.
|
||||
*
|
||||
* @param target the destination path to copy recursively this entry to.
|
||||
* @param onError the function that determines further actions if an error occurs. By default, rethrows the exception.
|
||||
* @param followLinks `false` to copy a symbolic link itself, not its target.
|
||||
* `true` to recursively copy the target of a symbolic link.
|
||||
* @param overwrite `false` to throw if a destination entry already exists.
|
||||
* `true` to overwrite existing destination entries.
|
||||
* @throws NoSuchFileException if the entry located by this path does not exist.
|
||||
* @throws FileSystemException if [target] is an entry inside the source subtree.
|
||||
* @throws FileAlreadyExistsException if a destination entry already exists and [overwrite] is `false`.
|
||||
* This exception is passed to [onError] for handling.
|
||||
* @throws IOException if any errors occur while copying.
|
||||
* This exception is passed to [onError] for handling.
|
||||
* @throws SecurityException if a security manager is installed and access is not permitted to an entry in the source or target subtree.
|
||||
* This exception is passed to [onError] for handling.
|
||||
*/
|
||||
@ExperimentalPathApi
|
||||
@SinceKotlin("1.8")
|
||||
public fun Path.copyToRecursively(
|
||||
target: Path,
|
||||
onError: (source: Path, target: Path, exception: Exception) -> OnErrorResult = { _, _, exception -> throw exception },
|
||||
followLinks: Boolean,
|
||||
overwrite: Boolean
|
||||
): Path {
|
||||
return if (overwrite) {
|
||||
copyToRecursively(target, onError, followLinks) { src, dst ->
|
||||
val options = LinkFollowing.toLinkOptions(followLinks)
|
||||
val dstIsDirectory = dst.isDirectory(LinkOption.NOFOLLOW_LINKS)
|
||||
val srcIsDirectory = src.isDirectory(*options)
|
||||
if ((srcIsDirectory && dstIsDirectory).not()) {
|
||||
if (dstIsDirectory)
|
||||
dst.deleteRecursively()
|
||||
|
||||
src.copyTo(dst, *options, StandardCopyOption.REPLACE_EXISTING)
|
||||
}
|
||||
|
||||
// else: do nothing, the destination directory already exists
|
||||
CopyActionResult.CONTINUE
|
||||
}
|
||||
} else {
|
||||
copyToRecursively(target, onError, followLinks)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively copies this directory and its content to the specified destination [target] path.
|
||||
* Note that if this function throws, partial copying may have taken place.
|
||||
*
|
||||
* Unlike `File.copyRecursively`, if some directories on the way to the [target] are missing, then they won't be created automatically.
|
||||
* You can use the following approach to ensure that required intermediate directories are created:
|
||||
* ```
|
||||
* sourcePath.copyToRecursively(
|
||||
* destinationPath.apply { parent?.createDirectories() },
|
||||
* followLinks = false
|
||||
* )
|
||||
* ```
|
||||
*
|
||||
* If the entry located by this path is a directory, this function recursively copies the directory itself and its content.
|
||||
* Otherwise, this function copies only the entry.
|
||||
*
|
||||
* If an exception occurs attempting to read, open or copy any entry under the source subtree,
|
||||
* further actions will depend on the result of the [onError] invoked with
|
||||
* the source and destination paths, that caused the error, and the exception itself as arguments.
|
||||
* If [onError] throws, this function ends immediately with the exception.
|
||||
* By default [onError] rethrows the exception. See [OnErrorResult] for available options.
|
||||
*
|
||||
* Copy operation is performed using [copyAction].
|
||||
* By default [copyAction] performs "directory merge" operation. If an entry in the source subtree is a directory
|
||||
* and the corresponding entry in the target subtree already exists and is also a directory, it does nothing.
|
||||
* Otherwise, the entry is copied using `sourcePath.copyTo(destinationPath, *followLinksOption)`,
|
||||
* which doesn't copy attributes of the source entry and throws if the destination entry already exists.
|
||||
* [followLinks] determines whether to copy a symbolic link itself or its target.
|
||||
* Symbolic links in the target subtree are not followed.
|
||||
*
|
||||
* If a custom implementation of [copyAction] is provided, consider making it consistent with [followLinks] value.
|
||||
* See [CopyActionResult] for available options.
|
||||
*
|
||||
* If [copyAction] throws an exception, it is passed to [onError] for handling.
|
||||
*
|
||||
* @param target the destination path to copy recursively this entry to.
|
||||
* @param onError the function that determines further actions if an error occurs. By default, rethrows the exception.
|
||||
* @param followLinks `false` to copy a symbolic link itself, not its target.
|
||||
* `true` to recursively copy the target of a symbolic link.
|
||||
* @param copyAction the function to call for copying source entries to their destination path rooted in [target].
|
||||
* By default, performs "directory merge" operation.
|
||||
* @throws NoSuchFileException if the entry located by this path does not exist.
|
||||
* @throws FileSystemException if [target] is an entry inside the source subtree.
|
||||
* @throws IOException if any errors occur while copying.
|
||||
* This exception is passed to [onError] for handling.
|
||||
* @throws SecurityException if a security manager is installed and access is not permitted to an entry in the source or target subtree.
|
||||
* This exception is passed to [onError] for handling.
|
||||
*/
|
||||
@ExperimentalPathApi
|
||||
@SinceKotlin("1.8")
|
||||
public fun Path.copyToRecursively(
|
||||
target: Path,
|
||||
onError: (source: Path, target: Path, exception: Exception) -> OnErrorResult = { _, _, exception -> throw exception },
|
||||
followLinks: Boolean,
|
||||
copyAction: CopyActionContext.(source: Path, target: Path) -> CopyActionResult = { src, dst ->
|
||||
src.copyToIgnoringExistingDirectory(dst, followLinks)
|
||||
}
|
||||
): Path {
|
||||
if (!this.exists(*LinkFollowing.toLinkOptions(followLinks)))
|
||||
throw NoSuchFileException(this.toString(), target.toString(), "The source file doesn't exist.")
|
||||
|
||||
if (this.exists() && (followLinks || !this.isSymbolicLink())) {
|
||||
// Here the checks are conducted without followLinks option, because:
|
||||
// * isSameFileAs doesn't take LinkOption and throws if `this` or `other` file doesn't exist
|
||||
// * toRealPath takes LinkOption, but the option also applies to the directories on the way to the file
|
||||
// Thus links are always followed in isSameFileAs and toRealPath
|
||||
|
||||
val targetExistsAndNotSymlink = target.exists() && !target.isSymbolicLink()
|
||||
|
||||
if (targetExistsAndNotSymlink && this.isSameFileAs(target)) {
|
||||
// TODO: KT-38678
|
||||
// source and target files are the same entry, continue recursive copy operation
|
||||
} else {
|
||||
val realPath = this.toRealPath()
|
||||
val isSubdirectory = if (targetExistsAndNotSymlink) {
|
||||
target.toRealPath().startsWith(realPath)
|
||||
} else {
|
||||
target.parent?.let { it.exists() && it.toRealPath().startsWith(realPath) } ?: false
|
||||
}
|
||||
if (isSubdirectory)
|
||||
throw FileSystemException(
|
||||
this.toString(),
|
||||
target.toString(),
|
||||
"Recursively copying a directory into its subdirectory is prohibited."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun destination(source: Path): Path {
|
||||
val relativePath = source.relativeTo(this@copyToRecursively)
|
||||
return target.resolve(relativePath)
|
||||
}
|
||||
|
||||
fun error(source: Path, exception: Exception): FileVisitResult {
|
||||
return onError(source, destination(source), exception).toFileVisitResult()
|
||||
}
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
fun copy(source: Path, attributes: BasicFileAttributes): FileVisitResult {
|
||||
return try {
|
||||
DefaultCopyActionContext.copyAction(source, destination(source)).toFileVisitResult()
|
||||
} catch (exception: Exception) {
|
||||
error(source, exception)
|
||||
}
|
||||
}
|
||||
|
||||
visitFileTree(followLinks = followLinks) {
|
||||
onPreVisitDirectory(::copy)
|
||||
onVisitFile(::copy)
|
||||
onVisitFileFailed(::error)
|
||||
onPostVisitDirectory { directory, exception ->
|
||||
if (exception == null) {
|
||||
FileVisitResult.CONTINUE
|
||||
} else {
|
||||
error(directory, exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return target
|
||||
}
|
||||
|
||||
|
||||
@ExperimentalPathApi
|
||||
private object DefaultCopyActionContext : CopyActionContext {
|
||||
override fun Path.copyToIgnoringExistingDirectory(target: Path, followLinks: Boolean): CopyActionResult {
|
||||
val options = LinkFollowing.toLinkOptions(followLinks)
|
||||
if (this.isDirectory(*options) && target.isDirectory(LinkOption.NOFOLLOW_LINKS)) {
|
||||
// do nothing, the destination directory already exists
|
||||
} else {
|
||||
this.copyTo(target, *options)
|
||||
}
|
||||
|
||||
return CopyActionResult.CONTINUE
|
||||
}
|
||||
}
|
||||
|
||||
@ExperimentalPathApi
|
||||
private fun CopyActionResult.toFileVisitResult() = when (this) {
|
||||
CopyActionResult.CONTINUE -> FileVisitResult.CONTINUE
|
||||
CopyActionResult.TERMINATE -> FileVisitResult.TERMINATE
|
||||
CopyActionResult.SKIP_SUBTREE -> FileVisitResult.SKIP_SUBTREE
|
||||
}
|
||||
|
||||
@ExperimentalPathApi
|
||||
private fun OnErrorResult.toFileVisitResult() = when (this) {
|
||||
OnErrorResult.TERMINATE -> FileVisitResult.TERMINATE
|
||||
OnErrorResult.SKIP_SUBTREE -> FileVisitResult.SKIP_SUBTREE
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively deletes this directory and its content.
|
||||
* Note that if this function throws, partial deletion may have taken place.
|
||||
*
|
||||
* If the entry located by this path is a directory, this function recursively deletes its content and the directory itself.
|
||||
* Otherwise, this function deletes only the entry.
|
||||
* Symbolic links are not followed to their targets.
|
||||
* This function does nothing if the entry located by this path does not exist.
|
||||
*
|
||||
* If the underlying platform supports [SecureDirectoryStream],
|
||||
* traversal of the file tree and removal of entries are performed using it.
|
||||
* Otherwise, directories in the file tree are opened with the less secure [Files.newDirectoryStream].
|
||||
* Note that on a platform that supports symbolic links and does not support [SecureDirectoryStream],
|
||||
* it is possible for a recursive delete to delete files and directories that are outside the directory being deleted.
|
||||
* This can happen if, after checking that an entry is a directory (and not a symbolic link), that directory is replaced
|
||||
* by a symbolic link to an outside directory before the call that opens the directory to read its entries.
|
||||
*
|
||||
* If an exception occurs attempting to read, open or delete any entry under the given file tree,
|
||||
* this method skips that entry and continues. Such exceptions are collected and, after attempting to delete all entries,
|
||||
* an [IOException] is thrown containing those exceptions as suppressed exceptions.
|
||||
* Maximum of `64` exceptions are collected. After reaching that amount, thrown exceptions are ignored and not collected.
|
||||
*
|
||||
* @throws IOException if any entry in the file tree can't be deleted for any reason.
|
||||
*/
|
||||
@ExperimentalPathApi
|
||||
@SinceKotlin("1.8")
|
||||
public fun Path.deleteRecursively(): Unit {
|
||||
val suppressedExceptions = this.deleteRecursivelyImpl()
|
||||
|
||||
if (suppressedExceptions.isNotEmpty()) {
|
||||
throw FileSystemException("Failed to delete one or more files. See suppressed exceptions for details.").apply {
|
||||
suppressedExceptions.forEach { addSuppressed(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ExceptionsCollector(private val limit: Int = 64) {
|
||||
var totalExceptions: Int = 0
|
||||
private set
|
||||
|
||||
val collectedExceptions = mutableListOf<Exception>()
|
||||
|
||||
var path: Path? = null
|
||||
|
||||
fun enterEntry(name: Path) {
|
||||
path = path?.resolve(name)
|
||||
}
|
||||
|
||||
fun exitEntry(name: Path) {
|
||||
require(name == path?.fileName)
|
||||
path = path?.parent
|
||||
}
|
||||
|
||||
fun collect(exception: Exception) {
|
||||
totalExceptions += 1
|
||||
val shouldCollect = collectedExceptions.size < limit
|
||||
if (shouldCollect) {
|
||||
val restoredException = if (path != null) {
|
||||
// When SecureDirectoryStream is used, only entry name gets reported in exception message.
|
||||
// Thus, wrap such exceptions in FileSystemException with restored path.
|
||||
FileSystemException(path.toString()).initCause(exception) as FileSystemException
|
||||
} else {
|
||||
exception
|
||||
}
|
||||
collectedExceptions.add(restoredException)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Path.deleteRecursivelyImpl(): List<Exception> {
|
||||
val collector = ExceptionsCollector()
|
||||
var useInsecure = true
|
||||
|
||||
// TODO: KT-54077
|
||||
this.parent?.let { parent ->
|
||||
val directoryStream = try { Files.newDirectoryStream(parent) } catch (_: Throwable) { null }
|
||||
directoryStream?.use { stream ->
|
||||
if (stream is SecureDirectoryStream<Path>) {
|
||||
useInsecure = false
|
||||
collector.path = parent
|
||||
stream.handleEntry(this.fileName, collector)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (useInsecure) {
|
||||
insecureHandleEntry(this, collector)
|
||||
}
|
||||
|
||||
return collector.collectedExceptions
|
||||
}
|
||||
|
||||
private inline fun collectIfThrows(collector: ExceptionsCollector, function: () -> Unit) {
|
||||
try {
|
||||
function()
|
||||
} catch (exception: Exception) {
|
||||
collector.collect(exception)
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <R> tryIgnoreNoSuchFileException(function: () -> R): R? {
|
||||
return try { function() } catch (_: NoSuchFileException) { null }
|
||||
}
|
||||
|
||||
// secure walk
|
||||
|
||||
private fun SecureDirectoryStream<Path>.handleEntry(name: Path, collector: ExceptionsCollector) {
|
||||
collector.enterEntry(name)
|
||||
|
||||
collectIfThrows(collector) {
|
||||
if (this.isDirectory(name, LinkOption.NOFOLLOW_LINKS)) {
|
||||
val preEnterTotalExceptions = collector.totalExceptions
|
||||
|
||||
this.enterDirectory(name, collector)
|
||||
|
||||
// If something went wrong trying to delete the contents of the
|
||||
// directory, don't try to delete the directory as it will probably fail.
|
||||
if (preEnterTotalExceptions == collector.totalExceptions) {
|
||||
tryIgnoreNoSuchFileException { this.deleteDirectory(name) }
|
||||
}
|
||||
} else {
|
||||
tryIgnoreNoSuchFileException { this.deleteFile(name) } // deletes symlink itself, not its target
|
||||
}
|
||||
}
|
||||
|
||||
collector.exitEntry(name)
|
||||
}
|
||||
|
||||
private fun SecureDirectoryStream<Path>.enterDirectory(name: Path, collector: ExceptionsCollector) {
|
||||
collectIfThrows(collector) {
|
||||
tryIgnoreNoSuchFileException {
|
||||
this.newDirectoryStream(name, LinkOption.NOFOLLOW_LINKS)
|
||||
}?.use { directoryStream ->
|
||||
for (entry in directoryStream) {
|
||||
directoryStream.handleEntry(entry.fileName, collector)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun SecureDirectoryStream<Path>.isDirectory(entryName: Path, vararg options: LinkOption): Boolean {
|
||||
return tryIgnoreNoSuchFileException {
|
||||
this.getFileAttributeView(entryName, BasicFileAttributeView::class.java, *options).readAttributes().isDirectory
|
||||
} ?: false
|
||||
}
|
||||
|
||||
// insecure walk
|
||||
|
||||
private fun insecureHandleEntry(entry: Path, collector: ExceptionsCollector) {
|
||||
collectIfThrows(collector) {
|
||||
if (entry.isDirectory(LinkOption.NOFOLLOW_LINKS)) {
|
||||
val preEnterTotalExceptions = collector.totalExceptions
|
||||
|
||||
insecureEnterDirectory(entry, collector)
|
||||
|
||||
// If something went wrong trying to delete the contents of the
|
||||
// directory, don't try to delete the directory as it will probably fail.
|
||||
if (preEnterTotalExceptions == collector.totalExceptions) {
|
||||
entry.deleteIfExists()
|
||||
}
|
||||
} else {
|
||||
entry.deleteIfExists() // deletes symlink itself, not its target
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun insecureEnterDirectory(path: Path, collector: ExceptionsCollector) {
|
||||
collectIfThrows(collector) {
|
||||
tryIgnoreNoSuchFileException {
|
||||
Files.newDirectoryStream(path)
|
||||
}?.use { directoryStream ->
|
||||
for (entry in directoryStream) {
|
||||
insecureHandleEntry(entry, collector)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -137,7 +137,7 @@ private fun PathNode.createsCycle(): Boolean {
|
||||
}
|
||||
|
||||
|
||||
private object LinkFollowing {
|
||||
internal object LinkFollowing {
|
||||
private val nofollowLinkOption = arrayOf(LinkOption.NOFOLLOW_LINKS)
|
||||
private val followLinkOption = emptyArray<LinkOption>()
|
||||
|
||||
|
||||
@@ -199,7 +199,7 @@ private object PathRelativizer {
|
||||
* When [overwrite] is `true` and [target] is a directory, it is replaced only if it is empty.
|
||||
*
|
||||
* If this path is a directory, it is copied without its content, i.e. an empty [target] directory is created.
|
||||
* If you want to copy directory including its contents, use [copyRecursively].
|
||||
* If you want to copy directory including its contents, use [copyToRecursively].
|
||||
*
|
||||
* The operation doesn't preserve copied file attributes such as creation/modification date, permissions, etc.
|
||||
*
|
||||
@@ -238,7 +238,7 @@ public inline fun Path.copyTo(target: Path, overwrite: Boolean = false): Path {
|
||||
* it is replaced only if it is empty.
|
||||
*
|
||||
* If this path is a directory, it is copied *without* its content, i.e. an empty [target] directory is created.
|
||||
* If you want to copy a directory including its contents, use [copyRecursively].
|
||||
* If you want to copy a directory including its contents, use [copyToRecursively].
|
||||
*
|
||||
* The operation doesn't preserve copied file attributes such as creation/modification date,
|
||||
* permissions, etc. unless [COPY_ATTRIBUTES][StandardCopyOption.COPY_ATTRIBUTES] is used.
|
||||
|
||||
@@ -0,0 +1,973 @@
|
||||
/*
|
||||
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||
*/
|
||||
|
||||
package kotlin.jdk7.test
|
||||
|
||||
import java.nio.file.*
|
||||
import kotlin.io.path.*
|
||||
import kotlin.jdk7.test.PathTreeWalkTest.Companion.createTestFiles
|
||||
import kotlin.jdk7.test.PathTreeWalkTest.Companion.referenceFilenames
|
||||
import kotlin.jdk7.test.PathTreeWalkTest.Companion.referenceFilesOnly
|
||||
import kotlin.jdk7.test.PathTreeWalkTest.Companion.testVisitedFiles
|
||||
import kotlin.test.*
|
||||
|
||||
class PathRecursiveFunctionsTest : AbstractPathTest() {
|
||||
@Test
|
||||
fun deleteFile() {
|
||||
val file = createTempFile()
|
||||
|
||||
assertTrue(file.exists())
|
||||
file.deleteRecursively()
|
||||
assertFalse(file.exists())
|
||||
|
||||
file.createFile().writeText("non-empty file")
|
||||
|
||||
assertTrue(file.exists())
|
||||
file.deleteRecursively()
|
||||
assertFalse(file.exists())
|
||||
file.deleteRecursively() // successfully deletes recursively a non-existent file
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteDirectory() {
|
||||
val dir = createTestFiles()
|
||||
|
||||
assertTrue(dir.exists())
|
||||
dir.deleteRecursively()
|
||||
assertFalse(dir.exists())
|
||||
dir.deleteRecursively() // successfully deletes recursively a non-existent directory
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteNotExistingParent() {
|
||||
val basedir = createTempDirectory().cleanupRecursively()
|
||||
basedir.resolve("a/b").deleteRecursively()
|
||||
basedir.resolve("a/b/c").deleteRecursively()
|
||||
}
|
||||
|
||||
private fun Path.walkIncludeDirectories(): Sequence<Path> =
|
||||
this.walk(PathWalkOption.INCLUDE_DIRECTORIES)
|
||||
|
||||
@Test
|
||||
fun deleteRestrictedRead() {
|
||||
val basedir = createTestFiles().cleanupRecursively()
|
||||
val restrictedEmptyDir = basedir.resolve("6")
|
||||
val restrictedDir = basedir.resolve("1")
|
||||
val restrictedFile = basedir.resolve("7.txt")
|
||||
|
||||
withRestrictedRead(restrictedEmptyDir, restrictedDir, restrictedFile) {
|
||||
val error = assertFailsWith<java.nio.file.FileSystemException>("Expected incomplete recursive deletion") {
|
||||
basedir.deleteRecursively()
|
||||
}
|
||||
|
||||
// AccessDeniedException when opening restrictedEmptyDir and restrictedDir, wrapped in FileSystemException if SecureDirectoryStream was used
|
||||
// DirectoryNotEmptyException is not thrown from parent directory
|
||||
assertEquals(2, error.suppressedExceptions.size)
|
||||
assertIs<java.nio.file.AccessDeniedException>(error.suppressedExceptions[0].let { it.cause ?: it })
|
||||
assertIs<java.nio.file.AccessDeniedException>(error.suppressedExceptions[1].let { it.cause ?: it })
|
||||
|
||||
// Couldn't read directory entries.
|
||||
// No attempt to delete even when empty directories can be removed without write permission
|
||||
assertTrue(restrictedEmptyDir.exists())
|
||||
assertTrue(restrictedDir.exists()) // couldn't read directory entries
|
||||
assertFalse(restrictedFile.exists()) // restricted read allows removal of file
|
||||
|
||||
restrictedEmptyDir.toFile().setReadable(true)
|
||||
restrictedDir.toFile().setReadable(true)
|
||||
testVisitedFiles(listOf("", "1", "1/2", "1/3", "1/3/4.txt", "1/3/5.txt", "6"), basedir.walkIncludeDirectories(), basedir)
|
||||
basedir.deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteRestrictedWrite() {
|
||||
val basedir = createTestFiles().cleanupRecursively()
|
||||
val restrictedEmptyDir = basedir.resolve("6")
|
||||
val restrictedDir = basedir.resolve("8")
|
||||
val restrictedFile = basedir.resolve("1/3/5.txt")
|
||||
|
||||
withRestrictedWrite(restrictedEmptyDir, restrictedDir, restrictedFile) {
|
||||
val error = assertFailsWith<java.nio.file.FileSystemException>("Expected incomplete recursive deletion") {
|
||||
basedir.deleteRecursively()
|
||||
}
|
||||
|
||||
// AccessDeniedException when deleting "8/9.txt", wrapped in FileSystemException if SecureDirectoryStream was used
|
||||
// DirectoryNotEmptyException is not thrown from parent directories
|
||||
when (val accessDenied = error.suppressedExceptions.single()) {
|
||||
is java.nio.file.AccessDeniedException -> {
|
||||
assertEquals(restrictedDir.resolve("9.txt").toString(), accessDenied.file)
|
||||
}
|
||||
is java.nio.file.FileSystemException -> {
|
||||
assertEquals(restrictedDir.resolve("9.txt").toString(), accessDenied.file)
|
||||
assertIs<java.nio.file.AccessDeniedException>(accessDenied.cause)
|
||||
}
|
||||
else -> {
|
||||
fail("Unexpected exception $accessDenied")
|
||||
}
|
||||
}
|
||||
|
||||
assertFalse(restrictedEmptyDir.exists()) // empty directories can be removed without write permission
|
||||
assertTrue(restrictedDir.exists())
|
||||
assertTrue(restrictedDir.resolve("9.txt").exists())
|
||||
assertFalse(restrictedFile.exists()) // plain files can be removed without write permission
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteBaseSymlinkToFile() {
|
||||
val file = createTempFile().cleanup()
|
||||
val link = createTempDirectory().cleanupRecursively().resolve("link").tryCreateSymbolicLinkTo(file) ?: return
|
||||
|
||||
link.deleteRecursively()
|
||||
assertFalse(link.exists(LinkOption.NOFOLLOW_LINKS))
|
||||
assertTrue(file.exists())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteBaseSymlinkToDirectory() {
|
||||
val dir = createTestFiles().cleanupRecursively()
|
||||
val link = createTempDirectory().cleanupRecursively().resolve("link").tryCreateSymbolicLinkTo(dir) ?: return
|
||||
|
||||
link.deleteRecursively()
|
||||
assertFalse(link.exists(LinkOption.NOFOLLOW_LINKS))
|
||||
testVisitedFiles(listOf("") + referenceFilenames, dir.walkIncludeDirectories(), dir)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteSymlinkToFile() {
|
||||
val file = createTempFile().cleanup()
|
||||
val dir = createTestFiles().cleanupRecursively().also { it.resolve("8/link").tryCreateSymbolicLinkTo(file) ?: return }
|
||||
|
||||
dir.deleteRecursively()
|
||||
assertFalse(dir.exists())
|
||||
assertTrue(file.exists())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteSymlinkToDirectory() {
|
||||
val dir1 = createTestFiles().cleanupRecursively()
|
||||
val dir2 = createTestFiles().cleanupRecursively().also { it.resolve("8/link").tryCreateSymbolicLinkTo(dir1) ?: return }
|
||||
|
||||
dir2.deleteRecursively()
|
||||
assertFalse(dir2.exists())
|
||||
testVisitedFiles(listOf("") + referenceFilenames, dir1.walkIncludeDirectories(), dir1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteParentSymlink() {
|
||||
val dir1 = createTestFiles().cleanupRecursively()
|
||||
val dir2 = createTempDirectory().cleanupRecursively().also { it.resolve("link").tryCreateSymbolicLinkTo(dir1) ?: return }
|
||||
|
||||
dir2.resolve("link/8").deleteRecursively()
|
||||
assertFalse(dir1.resolve("8").exists())
|
||||
|
||||
dir2.resolve("link/1/3").deleteRecursively()
|
||||
assertFalse(dir1.resolve("1/3").exists())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteSymlinkToSymlink() {
|
||||
val dir = createTestFiles().cleanupRecursively()
|
||||
val link = createTempDirectory().cleanupRecursively().resolve("link").tryCreateSymbolicLinkTo(dir) ?: return
|
||||
val linkToLink = createTempDirectory().cleanupRecursively().resolve("linkToLink").tryCreateSymbolicLinkTo(link) ?: return
|
||||
|
||||
linkToLink.deleteRecursively()
|
||||
assertFalse(linkToLink.exists(LinkOption.NOFOLLOW_LINKS))
|
||||
assertTrue(link.exists(LinkOption.NOFOLLOW_LINKS))
|
||||
testVisitedFiles(listOf("") + referenceFilenames, dir.walkIncludeDirectories(), dir)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteSymlinkCyclic() {
|
||||
val basedir = createTestFiles().cleanupRecursively()
|
||||
val original = basedir.resolve("1")
|
||||
original.resolve("2/link").tryCreateSymbolicLinkTo(original) ?: return
|
||||
|
||||
basedir.deleteRecursively()
|
||||
assertFalse(basedir.exists())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteSymlinkCyclicWithTwo() {
|
||||
val basedir = createTestFiles().cleanupRecursively()
|
||||
val dir8 = basedir.resolve("8")
|
||||
val dir2 = basedir.resolve("1/2")
|
||||
dir8.resolve("linkTo2").tryCreateSymbolicLinkTo(dir2) ?: return
|
||||
dir2.resolve("linkTo8").tryCreateSymbolicLinkTo(dir8) ?: return
|
||||
|
||||
basedir.deleteRecursively()
|
||||
assertFalse(basedir.exists())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteSymlinkPointingToItself() {
|
||||
val basedir = createTempDirectory().cleanupRecursively()
|
||||
val link = basedir.resolve("link")
|
||||
link.tryCreateSymbolicLinkTo(link) ?: return
|
||||
|
||||
basedir.deleteRecursively()
|
||||
assertFalse(basedir.exists())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteSymlinkTwoPointingToEachOther() {
|
||||
val basedir = createTempDirectory().cleanupRecursively()
|
||||
val link1 = basedir.resolve("link1")
|
||||
val link2 = basedir.resolve("link2").tryCreateSymbolicLinkTo(link1) ?: return
|
||||
link1.tryCreateSymbolicLinkTo(link2) ?: return
|
||||
|
||||
basedir.deleteRecursively()
|
||||
assertFalse(basedir.exists())
|
||||
}
|
||||
|
||||
private fun compareFiles(src: Path, dst: Path, message: String? = null) {
|
||||
assertTrue(dst.exists())
|
||||
assertEquals(src.isRegularFile(), dst.isRegularFile(), message)
|
||||
assertEquals(src.isDirectory(), dst.isDirectory(), message)
|
||||
if (dst.isRegularFile()) {
|
||||
assertTrue(src.readBytes().contentEquals(dst.readBytes()), message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun compareDirectories(src: Path, dst: Path) {
|
||||
for (srcFile in src.walkIncludeDirectories()) {
|
||||
val dstFile = dst.resolve(srcFile.relativeTo(src))
|
||||
compareFiles(srcFile, dstFile)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun copyFileToFile() {
|
||||
val src = createTempFile().cleanup().also { it.writeText("hello") }
|
||||
val dst = createTempDirectory().cleanupRecursively().resolve("dst")
|
||||
|
||||
val copyResult = src.copyToRecursively(dst, followLinks = false)
|
||||
assertEquals(dst, copyResult)
|
||||
compareFiles(src, dst)
|
||||
|
||||
dst.writeText("bye")
|
||||
assertFailsWith<java.nio.file.FileAlreadyExistsException> {
|
||||
src.copyToRecursively(dst, followLinks = false)
|
||||
}
|
||||
assertEquals("bye", dst.readText())
|
||||
|
||||
src.copyToRecursively(dst, followLinks = false, overwrite = true)
|
||||
compareFiles(src, dst)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun copyFileToDirectory() {
|
||||
val src = createTempFile().cleanup().also { it.writeText("hello") }
|
||||
val dst = createTestFiles().cleanupRecursively()
|
||||
|
||||
assertFailsWith<java.nio.file.FileAlreadyExistsException> {
|
||||
src.copyToRecursively(dst, followLinks = false)
|
||||
}
|
||||
assertTrue(dst.isDirectory())
|
||||
|
||||
assertFailsWith<java.nio.file.DirectoryNotEmptyException> {
|
||||
src.copyToRecursively(dst, followLinks = false) { source, target ->
|
||||
source.copyTo(target, overwrite = true)
|
||||
CopyActionResult.CONTINUE
|
||||
}
|
||||
}
|
||||
assertTrue(dst.isDirectory())
|
||||
|
||||
val copyResult = src.copyToRecursively(dst, followLinks = false, overwrite = true)
|
||||
assertEquals(dst, copyResult)
|
||||
compareFiles(src, dst)
|
||||
}
|
||||
|
||||
private fun Path.relativePathString(base: Path): String {
|
||||
return relativeToOrSelf(base).invariantSeparatorsPathString
|
||||
}
|
||||
|
||||
@Test
|
||||
fun copyDirectoryToDirectory() {
|
||||
val src = createTestFiles().cleanupRecursively()
|
||||
val dst = createTempDirectory().cleanupRecursively().resolve("dst")
|
||||
|
||||
val copyResult = src.copyToRecursively(dst, followLinks = false)
|
||||
assertEquals(dst, copyResult)
|
||||
compareDirectories(src, dst)
|
||||
|
||||
src.resolve("1/3/4.txt").writeText("hello")
|
||||
dst.resolve("10").createDirectory()
|
||||
|
||||
val conflictingFiles = mutableListOf<String>()
|
||||
src.copyToRecursively(dst, followLinks = false, onError = { source, _, exception ->
|
||||
assertIs<java.nio.file.FileAlreadyExistsException>(exception)
|
||||
conflictingFiles.add(source.relativePathString(src))
|
||||
OnErrorResult.SKIP_SUBTREE
|
||||
})
|
||||
assertEquals(referenceFilesOnly.sorted(), conflictingFiles.sorted())
|
||||
assertTrue(dst.resolve("1/3/4.txt").readText().isEmpty())
|
||||
|
||||
src.copyToRecursively(dst, followLinks = false, overwrite = true)
|
||||
compareDirectories(src, dst)
|
||||
assertTrue(dst.resolve("10").exists())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun copyDirectoryToFile() {
|
||||
val src = createTestFiles().cleanupRecursively()
|
||||
val dst = createTempFile().cleanupRecursively().also { it.writeText("hello") }
|
||||
|
||||
val existsException = assertFailsWith<java.nio.file.FileAlreadyExistsException> {
|
||||
src.copyToRecursively(dst, followLinks = false)
|
||||
}
|
||||
// attempted to copy only the root directory(src)
|
||||
assertEquals(dst.toString(), existsException.file)
|
||||
assertTrue(dst.isRegularFile())
|
||||
|
||||
src.copyToRecursively(dst, followLinks = false, overwrite = true)
|
||||
compareDirectories(src, dst)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun copyNonExistentSource() {
|
||||
val src = createTempDirectory().also { it.deleteExisting() }
|
||||
val dst = createTempDirectory()
|
||||
|
||||
assertFailsWith<java.nio.file.NoSuchFileException> {
|
||||
src.copyToRecursively(dst, followLinks = false)
|
||||
}
|
||||
|
||||
dst.deleteExisting()
|
||||
assertFailsWith<java.nio.file.NoSuchFileException> {
|
||||
src.copyToRecursively(dst, followLinks = false)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun copyNonExistentDestinationParent() {
|
||||
val src = createTempDirectory().cleanupRecursively()
|
||||
val dst = createTempDirectory().cleanupRecursively().resolve("parent/dst")
|
||||
|
||||
assertFalse(dst.parent.exists())
|
||||
|
||||
src.copyToRecursively(dst, followLinks = false, onError = { source, target, exception ->
|
||||
assertIs<java.nio.file.NoSuchFileException>(exception)
|
||||
assertEquals(src, source)
|
||||
assertEquals(dst, target)
|
||||
assertEquals(dst.toString(), exception.file)
|
||||
OnErrorResult.SKIP_SUBTREE
|
||||
})
|
||||
|
||||
src.copyToRecursively(dst.apply { parent?.createDirectories() }, followLinks = false)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun copyRestrictedReadInSource() {
|
||||
val src = createTestFiles().cleanupRecursively()
|
||||
val dst = createTempDirectory().cleanupRecursively()
|
||||
|
||||
val restrictedDir = src.resolve("1/3")
|
||||
val restrictedFile = src.resolve("7.txt")
|
||||
|
||||
withRestrictedRead(restrictedDir, restrictedFile, alsoReset = listOf(dst.resolve("1/3"), dst.resolve("7.txt"))) {
|
||||
// Restricted directories fail during traversal, while files fail when copied.
|
||||
// Because Files.walkFileTree opens a directory before calling FileVisitor.onPreVisitDirectory with it.
|
||||
src.copyToRecursively(dst, followLinks = false, onError = { source, _, exception ->
|
||||
assertIs<java.nio.file.AccessDeniedException>(exception)
|
||||
assertEquals(source.toString(), exception.file)
|
||||
assertEquals("1/3", source.relativePathString(src))
|
||||
OnErrorResult.SKIP_SUBTREE
|
||||
}) { source, target ->
|
||||
try {
|
||||
source.copyToIgnoringExistingDirectory(target, followLinks = false)
|
||||
} catch (exception: Throwable) {
|
||||
assertIs<java.nio.file.AccessDeniedException>(exception)
|
||||
assertEquals(source.toString(), exception.file)
|
||||
assertEquals("7.txt", source.relativePathString(src))
|
||||
}
|
||||
CopyActionResult.CONTINUE
|
||||
}
|
||||
|
||||
assertFalse(dst.resolve("1/3").exists()) // restricted directory is not copied
|
||||
assertFalse(dst.resolve("7.txt").exists()) // restricted file is not copied
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun copyRestrictedWriteInSource() {
|
||||
val src = createTestFiles().cleanupRecursively()
|
||||
val dst = createTempDirectory().cleanupRecursively()
|
||||
|
||||
val restrictedDir = src.resolve("1/3")
|
||||
val restrictedFile = src.resolve("7.txt")
|
||||
|
||||
withRestrictedWrite(restrictedDir, restrictedFile, alsoReset = listOf(dst.resolve("1/3"), dst.resolve("7.txt"))) {
|
||||
val accessDeniedFiles = mutableListOf<String>()
|
||||
src.copyToRecursively(dst, followLinks = false, onError = { _, target, exception ->
|
||||
assertIs<java.nio.file.AccessDeniedException>(exception)
|
||||
assertEquals(target.toString(), exception.file)
|
||||
accessDeniedFiles.add(target.relativePathString(dst))
|
||||
OnErrorResult.SKIP_SUBTREE
|
||||
})
|
||||
assertEquals(listOf("1/3/4.txt", "1/3/5.txt"), accessDeniedFiles.sorted())
|
||||
|
||||
assertTrue(dst.resolve("1/3").exists()) // restricted directory is copied
|
||||
assertFalse(dst.resolve("1/3").isWritable()) // access permissions are copied
|
||||
assertTrue(dst.resolve("7.txt").exists()) // restricted file is copied
|
||||
assertFalse(dst.resolve("7.txt").isWritable()) // access permissions are copied
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun copyRestrictedWriteInDestination() {
|
||||
val src = createTestFiles().cleanupRecursively()
|
||||
val dst = createTestFiles().cleanupRecursively()
|
||||
|
||||
src.resolve("1/3/4.txt").writeText("hello")
|
||||
src.resolve("7.txt").writeText("hi")
|
||||
|
||||
val restrictedDir = dst.resolve("1/3")
|
||||
val restrictedFile = dst.resolve("7.txt")
|
||||
|
||||
withRestrictedWrite(restrictedDir, restrictedFile) {
|
||||
val accessDeniedFiles = mutableListOf<String>()
|
||||
src.copyToRecursively(dst, followLinks = false, overwrite = true, onError = { _, target, exception ->
|
||||
assertIs<java.nio.file.AccessDeniedException>(exception)
|
||||
assertEquals(target.toString(), exception.file)
|
||||
accessDeniedFiles.add(target.relativePathString(dst))
|
||||
OnErrorResult.SKIP_SUBTREE
|
||||
})
|
||||
assertEquals(listOf("1/3/4.txt", "1/3/5.txt"), accessDeniedFiles.sorted())
|
||||
|
||||
assertNotEquals(src.resolve("1/3/4.txt").readText(), dst.resolve("1/3/4.txt").readText())
|
||||
assertEquals(src.resolve("7.txt").readText(), dst.resolve("7.txt").readText())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun copyBrokenBaseSymlink() {
|
||||
val basedir = createTempDirectory().cleanupRecursively()
|
||||
val target = basedir.resolve("target")
|
||||
val link = basedir.resolve("link").tryCreateSymbolicLinkTo(target) ?: return
|
||||
val dst = basedir.resolve("dst")
|
||||
|
||||
// the same behavior as link.copyTo(dst, LinkOption.NOFOLLOW_LINKS)
|
||||
link.copyToRecursively(dst, followLinks = false)
|
||||
assertTrue(dst.isSymbolicLink())
|
||||
assertTrue(dst.exists(LinkOption.NOFOLLOW_LINKS))
|
||||
assertFalse(dst.exists())
|
||||
|
||||
assertFailsWith<java.nio.file.FileAlreadyExistsException> {
|
||||
link.copyToRecursively(dst, followLinks = false)
|
||||
}
|
||||
|
||||
// the same behavior as link.copyTo(dst)
|
||||
dst.deleteExisting()
|
||||
assertFailsWith<java.nio.file.NoSuchFileException> {
|
||||
link.copyToRecursively(dst, followLinks = true)
|
||||
}
|
||||
assertFalse(dst.exists(LinkOption.NOFOLLOW_LINKS))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun copyBrokenSymlink() {
|
||||
val src = createTestFiles().cleanupRecursively()
|
||||
val dst = createTempDirectory().cleanupRecursively().resolve("dst")
|
||||
val target = createTempDirectory().cleanupRecursively().resolve("target")
|
||||
src.resolve("8/link").tryCreateSymbolicLinkTo(target) ?: return
|
||||
val dstLink = dst.resolve("8/link")
|
||||
|
||||
// the same behavior as link.copyTo(dst, LinkOption.NOFOLLOW_LINKS)
|
||||
src.copyToRecursively(dst, followLinks = false)
|
||||
assertTrue(dstLink.isSymbolicLink())
|
||||
assertTrue(dstLink.exists(LinkOption.NOFOLLOW_LINKS))
|
||||
assertFalse(dstLink.exists())
|
||||
|
||||
// the same behavior as link.copyTo(dst)
|
||||
dst.deleteRecursively()
|
||||
assertFailsWith<java.nio.file.NoSuchFileException> {
|
||||
src.copyToRecursively(dst, followLinks = true)
|
||||
}
|
||||
assertFalse(dstLink.exists(LinkOption.NOFOLLOW_LINKS))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun copyBaseSymlinkPointingToFile() {
|
||||
val src = createTempFile().cleanup().also { it.writeText("hello") }
|
||||
val link = createTempDirectory().cleanupRecursively().resolve("link").tryCreateSymbolicLinkTo(src) ?: return
|
||||
val dst = createTempDirectory().cleanupRecursively().resolve("dst")
|
||||
|
||||
link.copyToRecursively(dst, followLinks = false)
|
||||
compareFiles(link, dst)
|
||||
|
||||
dst.deleteExisting()
|
||||
|
||||
link.copyToRecursively(dst, followLinks = true)
|
||||
compareFiles(src, dst)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun copyBaseSymlinkPointingToDirectory() {
|
||||
val src = createTestFiles().cleanupRecursively()
|
||||
val link = createTempDirectory().cleanupRecursively().resolve("link").tryCreateSymbolicLinkTo(src) ?: return
|
||||
val dst = createTempDirectory().cleanupRecursively().resolve("dst")
|
||||
|
||||
link.copyToRecursively(dst, followLinks = false)
|
||||
compareFiles(link, dst)
|
||||
|
||||
dst.deleteExisting()
|
||||
|
||||
link.copyToRecursively(dst, followLinks = true)
|
||||
compareDirectories(src, dst)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun copySymlinkPointingToDirectory() {
|
||||
val symlinkTarget = createTestFiles().cleanupRecursively()
|
||||
val src = createTestFiles().cleanupRecursively().also { it.resolve("8/link").tryCreateSymbolicLinkTo(symlinkTarget) ?: return }
|
||||
val dst = createTempDirectory().cleanupRecursively().resolve("dst")
|
||||
|
||||
src.copyToRecursively(dst, followLinks = false)
|
||||
val srcContent = listOf("", "8/link") + referenceFilenames
|
||||
testVisitedFiles(srcContent, dst.walkIncludeDirectories(), dst)
|
||||
|
||||
dst.deleteRecursively()
|
||||
|
||||
src.copyToRecursively(dst, followLinks = true)
|
||||
val expectedDstContent = srcContent + referenceFilenames.map { "8/link/$it" }
|
||||
testVisitedFiles(expectedDstContent, dst.walkIncludeDirectories(), dst)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun copyIgnoreExistingDirectoriesFollowLinks() {
|
||||
val src = createTestFiles().cleanupRecursively()
|
||||
val symlinkTarget = createTempDirectory().cleanupRecursively()
|
||||
val dst = createTempDirectory().cleanupRecursively().also {
|
||||
it.resolve("1").createDirectory()
|
||||
it.resolve("1/3").tryCreateSymbolicLinkTo(symlinkTarget) ?: return
|
||||
}
|
||||
|
||||
src.copyToRecursively(dst, followLinks = true, onError = { source, target, exception ->
|
||||
assertIs<java.nio.file.FileAlreadyExistsException>(exception)
|
||||
assertEquals(src.resolve("1/3"), source)
|
||||
assertEquals(dst.resolve("1/3"), target)
|
||||
assertEquals(target.toString(), exception.file)
|
||||
OnErrorResult.SKIP_SUBTREE
|
||||
})
|
||||
assertTrue(dst.resolve("1/3").isSymbolicLink())
|
||||
assertTrue(symlinkTarget.listDirectoryEntries().isEmpty())
|
||||
|
||||
src.copyToRecursively(dst, followLinks = true, overwrite = true)
|
||||
assertFalse(dst.resolve("1/3").isSymbolicLink())
|
||||
assertTrue(symlinkTarget.listDirectoryEntries().isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun copyIgnoreExistingDirectoriesNoFollowLinks() {
|
||||
val src = createTestFiles().cleanupRecursively()
|
||||
val symlinkTarget = createTempDirectory().cleanupRecursively()
|
||||
val dst = createTempDirectory().cleanupRecursively().also {
|
||||
it.resolve("1").createDirectory()
|
||||
it.resolve("1/3").tryCreateSymbolicLinkTo(symlinkTarget) ?: return
|
||||
}
|
||||
|
||||
src.copyToRecursively(dst, followLinks = false, onError = { source, target, exception ->
|
||||
assertIs<java.nio.file.FileAlreadyExistsException>(exception)
|
||||
assertEquals(src.resolve("1/3"), source)
|
||||
assertEquals(dst.resolve("1/3"), target)
|
||||
assertEquals(target.toString(), exception.file)
|
||||
OnErrorResult.SKIP_SUBTREE
|
||||
})
|
||||
assertTrue(dst.resolve("1/3").isSymbolicLink())
|
||||
assertTrue(symlinkTarget.listDirectoryEntries().isEmpty())
|
||||
|
||||
src.copyToRecursively(dst, followLinks = false, overwrite = true)
|
||||
assertFalse(dst.resolve("1/3").isSymbolicLink())
|
||||
assertTrue(symlinkTarget.listDirectoryEntries().isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun copyParentSymlink() {
|
||||
val source = createTestFiles().cleanupRecursively()
|
||||
val linkToSource = createTempDirectory().cleanupRecursively().resolve("link").tryCreateSymbolicLinkTo(source) ?: return
|
||||
val sources = listOf(
|
||||
source to referenceFilenames,
|
||||
linkToSource.resolve("8") to listOf("9.txt"),
|
||||
linkToSource.resolve("1/3") to listOf("4.txt", "5.txt")
|
||||
)
|
||||
|
||||
for ((src, srcContent) in sources) {
|
||||
for (followLinks in listOf(false, true)) {
|
||||
val target = createTempDirectory().cleanupRecursively().also { it.resolve("a/b").createDirectories() }
|
||||
val linkToTarget = createTempDirectory().cleanupRecursively().resolve("link").tryCreateSymbolicLinkTo(target) ?: return
|
||||
val targets = listOf(
|
||||
target to listOf("a", "a/b"),
|
||||
linkToTarget.resolve("a") to listOf("b"),
|
||||
linkToTarget.resolve("a/b") to listOf()
|
||||
)
|
||||
|
||||
for ((dst, dstContent) in targets) {
|
||||
src.copyToRecursively(dst, followLinks = followLinks)
|
||||
val expectedDstContent = listOf("") + dstContent + srcContent
|
||||
testVisitedFiles(expectedDstContent, dst.walkIncludeDirectories(), dst)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun copySymlinkToSymlink() {
|
||||
val src = createTestFiles().cleanupRecursively()
|
||||
val link = createTempDirectory().cleanupRecursively().resolve("link").tryCreateSymbolicLinkTo(src) ?: return
|
||||
val linkToLink = createTempDirectory().cleanupRecursively().resolve("linkToLink").tryCreateSymbolicLinkTo(link) ?: return
|
||||
val dst = createTempDirectory().cleanupRecursively().resolve("dst")
|
||||
|
||||
linkToLink.copyToRecursively(dst, followLinks = true)
|
||||
testVisitedFiles(listOf("") + referenceFilenames, dst.walkIncludeDirectories(), dst)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun copySymlinkCyclic() {
|
||||
val src = createTestFiles().cleanupRecursively()
|
||||
val original = src.resolve("1")
|
||||
original.resolve("2/link").tryCreateSymbolicLinkTo(original) ?: return
|
||||
val dst = createTempDirectory().cleanupRecursively().resolve("dst")
|
||||
|
||||
src.copyToRecursively(dst, followLinks = true, onError = { source, _, exception ->
|
||||
assertIs<java.nio.file.FileSystemLoopException>(exception)
|
||||
assertEquals(src.resolve("1/2/link"), source)
|
||||
assertEquals(source.toString(), exception.file)
|
||||
OnErrorResult.SKIP_SUBTREE
|
||||
})
|
||||
|
||||
// partial copy, only "1/2/link" is not copied
|
||||
testVisitedFiles(listOf("") + referenceFilenames, dst.walkIncludeDirectories(), dst)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun copySymlinkCyclicWithTwo() {
|
||||
val src = createTestFiles().cleanupRecursively()
|
||||
val dir8 = src.resolve("8")
|
||||
val dir2 = src.resolve("1/2")
|
||||
dir8.resolve("linkTo2").tryCreateSymbolicLinkTo(dir2) ?: return
|
||||
dir2.resolve("linkTo8").tryCreateSymbolicLinkTo(dir8) ?: return
|
||||
val dst = createTempDirectory().cleanupRecursively().resolve("dst")
|
||||
|
||||
val loops = mutableListOf<String>()
|
||||
src.copyToRecursively(dst, followLinks = true, onError = { source, _, exception ->
|
||||
assertIs<java.nio.file.FileSystemLoopException>(exception)
|
||||
assertEquals(source.toString(), exception.file)
|
||||
loops.add(source.relativePathString(src))
|
||||
OnErrorResult.SKIP_SUBTREE
|
||||
})
|
||||
assertEquals(listOf("1/2/linkTo8/linkTo2", "8/linkTo2/linkTo8"), loops.sorted())
|
||||
|
||||
// partial copy, only "1/2/linkTo8/linkTo2" and "8/linkTo2/linkTo8" are not copied
|
||||
val expected = listOf("", "1/2/linkTo8", "1/2/linkTo8/9.txt", "8/linkTo2") + referenceFilenames
|
||||
testVisitedFiles(expected, dst.walkIncludeDirectories(), dst)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun copySymlinkPointingToItself() {
|
||||
val src = createTempDirectory().cleanupRecursively()
|
||||
val link = src.resolve("link")
|
||||
link.tryCreateSymbolicLinkTo(link) ?: return
|
||||
val dst = createTempDirectory().cleanupRecursively().resolve("dst")
|
||||
|
||||
assertFailsWith<java.nio.file.FileSystemException> {
|
||||
// throws with message "Too many levels of symbolic links"
|
||||
src.copyToRecursively(dst, followLinks = true)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun copySymlinkTwoPointingToEachOther() {
|
||||
val src = createTempDirectory().cleanupRecursively()
|
||||
val link1 = src.resolve("link1")
|
||||
val link2 = src.resolve("link2").tryCreateSymbolicLinkTo(link1) ?: return
|
||||
link1.tryCreateSymbolicLinkTo(link2) ?: return
|
||||
val dst = createTempDirectory().cleanupRecursively().resolve("dst")
|
||||
|
||||
assertFailsWith<java.nio.file.FileSystemException> {
|
||||
// throws with message "Too many levels of symbolic links"
|
||||
src.copyToRecursively(dst, followLinks = true)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun copyWithNestedCopyToRecursively() {
|
||||
val src = createTestFiles().cleanupRecursively()
|
||||
val dst = createTempDirectory().cleanupRecursively().resolve("dst")
|
||||
val nested = createTestFiles().cleanupRecursively()
|
||||
|
||||
src.copyToRecursively(dst, followLinks = false) { source, target ->
|
||||
if (source.name == "2") {
|
||||
nested.copyToRecursively(target, followLinks = false)
|
||||
} else {
|
||||
source.copyToIgnoringExistingDirectory(target, followLinks = false)
|
||||
}
|
||||
CopyActionResult.CONTINUE
|
||||
}
|
||||
|
||||
val expected = listOf("") + referenceFilenames + referenceFilenames.map { "1/2/$it" }
|
||||
testVisitedFiles(expected, dst.walkIncludeDirectories(), dst)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun copyWithSkipSubtree() {
|
||||
val src = createTestFiles().cleanupRecursively()
|
||||
val dst = createTempDirectory().cleanupRecursively().resolve("dst")
|
||||
|
||||
src.copyToRecursively(dst, followLinks = false) { source, target ->
|
||||
source.copyToIgnoringExistingDirectory(target, followLinks = false)
|
||||
if (source.name == "3" || source.name == "9.txt") {
|
||||
CopyActionResult.SKIP_SUBTREE
|
||||
} else {
|
||||
CopyActionResult.CONTINUE
|
||||
}
|
||||
}
|
||||
|
||||
// both "3" and "9.txt" are copied
|
||||
val copied3 = dst.resolve("1/3").exists()
|
||||
val copied9 = dst.resolve("8/9.txt").exists()
|
||||
assertTrue(copied3 && copied9)
|
||||
|
||||
// content of "3" is not copied
|
||||
assertTrue(dst.resolve("1/3").listDirectoryEntries().isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun copyWithTerminate() {
|
||||
val src = createTestFiles().cleanupRecursively()
|
||||
val dst = createTempDirectory().cleanupRecursively().resolve("dst")
|
||||
|
||||
src.copyToRecursively(dst, followLinks = false) { source, target ->
|
||||
source.copyToIgnoringExistingDirectory(target, followLinks = false)
|
||||
if (source.name == "3" || source.name == "9.txt") {
|
||||
CopyActionResult.TERMINATE
|
||||
} else {
|
||||
CopyActionResult.CONTINUE
|
||||
}
|
||||
}
|
||||
|
||||
// either "3" or "9.txt" is not copied
|
||||
val copied3 = dst.resolve("1/3").exists()
|
||||
val copied9 = dst.resolve("8/9.txt").exists()
|
||||
assertTrue(copied3 || copied9)
|
||||
assertFalse(copied3 && copied9)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun copyFailureWithTerminate() {
|
||||
val src = createTestFiles().cleanupRecursively()
|
||||
val dst = createTempDirectory().cleanupRecursively().resolve("dst")
|
||||
|
||||
src.copyToRecursively(dst, followLinks = false, onError = { source, _, exception ->
|
||||
assertIs<IllegalArgumentException>(exception)
|
||||
assertTrue(source.name == "3" || source.name == "9.txt")
|
||||
OnErrorResult.TERMINATE
|
||||
}) { source, target ->
|
||||
source.copyToIgnoringExistingDirectory(target, followLinks = false)
|
||||
if (source.name == "3" || source.name == "9.txt") throw IllegalArgumentException()
|
||||
CopyActionResult.CONTINUE
|
||||
}
|
||||
|
||||
// either "3" or "9.txt" is not copied
|
||||
val copied3 = dst.resolve("1/3").exists()
|
||||
val copied9 = dst.resolve("8/9.txt").exists()
|
||||
assertTrue(copied3 || copied9)
|
||||
assertFalse(copied3 && copied9)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun copyIntoSourceDirectory() {
|
||||
val source = createTestFiles().cleanupRecursively()
|
||||
val linkToSource = createTempDirectory().cleanupRecursively().resolve("link").tryCreateSymbolicLinkTo(source) ?: return
|
||||
val sources = listOf(
|
||||
source to source,
|
||||
linkToSource.resolve("8") to source.resolve("8"),
|
||||
linkToSource.resolve("1/3") to source.resolve("1/3")
|
||||
)
|
||||
|
||||
for ((src, resolvedSrc) in sources) {
|
||||
val linkToSrc = createTempDirectory().cleanupRecursively().resolve("linkToSrc").tryCreateSymbolicLinkTo(resolvedSrc) ?: return
|
||||
val targets = listOf(
|
||||
linkToSrc.resolve("a").createDirectory(),
|
||||
linkToSrc.resolve("a/b").createDirectories()
|
||||
)
|
||||
|
||||
for (followLinks in listOf(false, true)) {
|
||||
assertFailsWith<java.nio.file.FileAlreadyExistsException> {
|
||||
src.copyToRecursively(linkToSrc, followLinks = followLinks)
|
||||
}
|
||||
for (dst in targets) {
|
||||
val error = assertFailsWith<java.nio.file.FileSystemException> {
|
||||
src.copyToRecursively(dst, followLinks = followLinks)
|
||||
}
|
||||
assertEquals("Recursively copying a directory into its subdirectory is prohibited.", error.reason)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun kt38678() {
|
||||
val src = createTempDirectory().cleanupRecursively()
|
||||
src.resolve("test.txt").writeText("plain text file")
|
||||
|
||||
val dst = src.resolve("x")
|
||||
|
||||
val error = assertFailsWith<java.nio.file.FileSystemException> {
|
||||
src.copyToRecursively(dst, followLinks = false)
|
||||
}
|
||||
assertEquals("Recursively copying a directory into its subdirectory is prohibited.", error.reason)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun copyToTheSameFile() {
|
||||
for (src in listOf(createTempFile().cleanupRecursively(), createTestFiles().cleanupRecursively())) {
|
||||
src.copyToRecursively(src, followLinks = false)
|
||||
|
||||
val link = createTempDirectory().cleanupRecursively().resolve("link").tryCreateSymbolicLinkTo(src) ?: return
|
||||
|
||||
val error = assertFailsWith<java.nio.file.FileAlreadyExistsException> {
|
||||
link.copyToRecursively(src, followLinks = false)
|
||||
}
|
||||
assertEquals(src.toString(), error.file)
|
||||
link.copyToRecursively(src, followLinks = true)
|
||||
|
||||
for (followLinks in listOf(false, true)) {
|
||||
assertFailsWith<java.nio.file.FileAlreadyExistsException> {
|
||||
src.copyToRecursively(link, followLinks = followLinks)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun copyDstLinkPointingToSrc() {
|
||||
for (followLinks in listOf(false, true)) {
|
||||
val root = createTempDirectory().cleanupRecursively()
|
||||
val src = root.resolve("src").createFile()
|
||||
val dstLink = root.resolve("dstLink").tryCreateSymbolicLinkTo(src) ?: return
|
||||
|
||||
assertTrue(src.isSameFileAs(dstLink))
|
||||
assertTrue(dstLink.isSameFileAs(src))
|
||||
assertFailsWith<FileAlreadyExistsException> {
|
||||
src.copyToRecursively(dstLink, followLinks = followLinks)
|
||||
}
|
||||
assertTrue(dstLink.isSymbolicLink())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun copyDstLinkPointingToSrcOverwrite() {
|
||||
for (followLinks in listOf(false, true)) {
|
||||
val root = createTempDirectory().cleanupRecursively()
|
||||
val src = root.resolve("src").createFile()
|
||||
val dstLink = root.resolve("dstLink").tryCreateSymbolicLinkTo(src) ?: return
|
||||
|
||||
src.copyToRecursively(dstLink, followLinks = followLinks, overwrite = true)
|
||||
assertFalse(dstLink.isSymbolicLink())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun copySrcLinkAndDstLinkPointingToSameFile() {
|
||||
for (followLinks in listOf(false, true)) {
|
||||
val root = createTempDirectory().cleanupRecursively()
|
||||
val original = root.resolve("original").createFile()
|
||||
val srcLink = root.resolve("srcLink").tryCreateSymbolicLinkTo(original) ?: return
|
||||
val dstLink = root.resolve("dstLink").tryCreateSymbolicLinkTo(original) ?: return
|
||||
|
||||
assertTrue(srcLink.isSameFileAs(dstLink))
|
||||
assertTrue(dstLink.isSameFileAs(srcLink))
|
||||
assertFailsWith<FileAlreadyExistsException> {
|
||||
srcLink.copyToRecursively(dstLink, followLinks = followLinks)
|
||||
}
|
||||
assertTrue(dstLink.isSymbolicLink())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun copySrcLinkAndDstLinkPointingToSameFileOverwrite() {
|
||||
for (followLinks in listOf(false, true)) {
|
||||
val root = createTempDirectory().cleanupRecursively()
|
||||
val original = root.resolve("original").createFile()
|
||||
val srcLink = root.resolve("srcLink").tryCreateSymbolicLinkTo(original) ?: return
|
||||
val dstLink = root.resolve("dstLink").tryCreateSymbolicLinkTo(original) ?: return
|
||||
|
||||
srcLink.copyToRecursively(dstLink, followLinks = followLinks, overwrite = true)
|
||||
|
||||
if (!followLinks) {
|
||||
assertTrue(dstLink.isSymbolicLink()) // src symlink was copied
|
||||
} else {
|
||||
assertFalse(dstLink.isSymbolicLink()) // target of src symlink was copied
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun copySameLinkDifferentRoute() {
|
||||
for (followLinks in listOf(false, true)) {
|
||||
val root = createTempDirectory().cleanupRecursively()
|
||||
val original = root.resolve("original").createFile()
|
||||
val srcLink = root.resolve("srcLink").tryCreateSymbolicLinkTo(original) ?: return
|
||||
val dstLink = root.resolve("dstLink").tryCreateSymbolicLinkTo(root)?.resolve("srcLink") ?: return
|
||||
|
||||
assertTrue(srcLink.isSameFileAs(dstLink))
|
||||
assertTrue(dstLink.isSameFileAs(srcLink))
|
||||
|
||||
if (!followLinks) {
|
||||
srcLink.copyToRecursively(dstLink, followLinks = followLinks) // same file
|
||||
} else {
|
||||
assertFailsWith<FileAlreadyExistsException> {
|
||||
srcLink.copyToRecursively(dstLink, followLinks = followLinks) // target of srcLink copied to srcLink location
|
||||
}
|
||||
}
|
||||
|
||||
assertTrue(dstLink.isSymbolicLink())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun copySameLinkDifferentRouteOverwrite() {
|
||||
for (followLinks in listOf(false, true)) {
|
||||
val root = createTempDirectory().cleanupRecursively()
|
||||
val original = root.resolve("original").createFile()
|
||||
val srcLink = root.resolve("srcLink").tryCreateSymbolicLinkTo(original) ?: return
|
||||
val dstLink = root.resolve("dstLink").tryCreateSymbolicLinkTo(root)?.resolve("srcLink") ?: return
|
||||
|
||||
if (!followLinks) {
|
||||
srcLink.copyToRecursively(dstLink, followLinks = followLinks, overwrite = true) // same file
|
||||
} else {
|
||||
// dstLink is deleted before srcLink gets copied.
|
||||
// Actually srcLink gets removed because dstLink is srcLink with different path.
|
||||
val error = assertFailsWith<NoSuchFileException> {
|
||||
srcLink.copyToRecursively(dstLink, followLinks = followLinks, overwrite = true)
|
||||
}
|
||||
assertEquals(srcLink.toString(), error.file)
|
||||
assertFalse(srcLink.exists(LinkOption.NOFOLLOW_LINKS))
|
||||
assertFalse(dstLink.exists(LinkOption.NOFOLLOW_LINKS))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun copySameFileDifferentRoute() {
|
||||
val root = createTempDirectory().cleanupRecursively()
|
||||
val src = root.resolve("src").createFile()
|
||||
val dst = root.resolve("dstLink").tryCreateSymbolicLinkTo(root)?.resolve("src") ?: return
|
||||
|
||||
assertTrue(src.isSameFileAs(dst))
|
||||
assertTrue(dst.isSameFileAs(src))
|
||||
src.copyToRecursively(dst, followLinks = false)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun copyToSameFileDifferentRouteOverwrite() {
|
||||
val root = createTempDirectory().cleanupRecursively()
|
||||
val src = root.resolve("src").createFile()
|
||||
val dst = root.resolve("dstLink").tryCreateSymbolicLinkTo(root)?.resolve("src") ?: return
|
||||
|
||||
src.copyToRecursively(dst, followLinks = false, overwrite = true)
|
||||
}
|
||||
}
|
||||
+24
@@ -3267,6 +3267,18 @@ public final class kotlin/io/TextStreamsKt {
|
||||
public static final fun useLines (Ljava/io/Reader;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object;
|
||||
}
|
||||
|
||||
public abstract interface class kotlin/io/path/CopyActionContext {
|
||||
public abstract fun copyToIgnoringExistingDirectory (Ljava/nio/file/Path;Ljava/nio/file/Path;Z)Lkotlin/io/path/CopyActionResult;
|
||||
}
|
||||
|
||||
public final class kotlin/io/path/CopyActionResult : java/lang/Enum {
|
||||
public static final field CONTINUE Lkotlin/io/path/CopyActionResult;
|
||||
public static final field SKIP_SUBTREE Lkotlin/io/path/CopyActionResult;
|
||||
public static final field TERMINATE Lkotlin/io/path/CopyActionResult;
|
||||
public static fun valueOf (Ljava/lang/String;)Lkotlin/io/path/CopyActionResult;
|
||||
public static fun values ()[Lkotlin/io/path/CopyActionResult;
|
||||
}
|
||||
|
||||
public abstract interface annotation class kotlin/io/path/ExperimentalPathApi : java/lang/annotation/Annotation {
|
||||
}
|
||||
|
||||
@@ -3277,6 +3289,13 @@ public abstract interface class kotlin/io/path/FileVisitorBuilder {
|
||||
public abstract fun onVisitFileFailed (Lkotlin/jvm/functions/Function2;)V
|
||||
}
|
||||
|
||||
public final class kotlin/io/path/OnErrorResult : java/lang/Enum {
|
||||
public static final field SKIP_SUBTREE Lkotlin/io/path/OnErrorResult;
|
||||
public static final field TERMINATE Lkotlin/io/path/OnErrorResult;
|
||||
public static fun valueOf (Ljava/lang/String;)Lkotlin/io/path/OnErrorResult;
|
||||
public static fun values ()[Lkotlin/io/path/OnErrorResult;
|
||||
}
|
||||
|
||||
public final class kotlin/io/path/PathWalkOption : java/lang/Enum {
|
||||
public static final field BREADTH_FIRST Lkotlin/io/path/PathWalkOption;
|
||||
public static final field FOLLOW_LINKS Lkotlin/io/path/PathWalkOption;
|
||||
@@ -3288,10 +3307,15 @@ public final class kotlin/io/path/PathWalkOption : java/lang/Enum {
|
||||
public final class kotlin/io/path/PathsKt {
|
||||
public static final fun appendText (Ljava/nio/file/Path;Ljava/lang/CharSequence;Ljava/nio/charset/Charset;)V
|
||||
public static synthetic fun appendText$default (Ljava/nio/file/Path;Ljava/lang/CharSequence;Ljava/nio/charset/Charset;ILjava/lang/Object;)V
|
||||
public static final fun copyToRecursively (Ljava/nio/file/Path;Ljava/nio/file/Path;Lkotlin/jvm/functions/Function3;ZLkotlin/jvm/functions/Function3;)Ljava/nio/file/Path;
|
||||
public static final fun copyToRecursively (Ljava/nio/file/Path;Ljava/nio/file/Path;Lkotlin/jvm/functions/Function3;ZZ)Ljava/nio/file/Path;
|
||||
public static synthetic fun copyToRecursively$default (Ljava/nio/file/Path;Ljava/nio/file/Path;Lkotlin/jvm/functions/Function3;ZLkotlin/jvm/functions/Function3;ILjava/lang/Object;)Ljava/nio/file/Path;
|
||||
public static synthetic fun copyToRecursively$default (Ljava/nio/file/Path;Ljava/nio/file/Path;Lkotlin/jvm/functions/Function3;ZZILjava/lang/Object;)Ljava/nio/file/Path;
|
||||
public static final fun createTempDirectory (Ljava/nio/file/Path;Ljava/lang/String;[Ljava/nio/file/attribute/FileAttribute;)Ljava/nio/file/Path;
|
||||
public static synthetic fun createTempDirectory$default (Ljava/nio/file/Path;Ljava/lang/String;[Ljava/nio/file/attribute/FileAttribute;ILjava/lang/Object;)Ljava/nio/file/Path;
|
||||
public static final fun createTempFile (Ljava/nio/file/Path;Ljava/lang/String;Ljava/lang/String;[Ljava/nio/file/attribute/FileAttribute;)Ljava/nio/file/Path;
|
||||
public static synthetic fun createTempFile$default (Ljava/nio/file/Path;Ljava/lang/String;Ljava/lang/String;[Ljava/nio/file/attribute/FileAttribute;ILjava/lang/Object;)Ljava/nio/file/Path;
|
||||
public static final fun deleteRecursively (Ljava/nio/file/Path;)V
|
||||
public static final fun fileAttributeViewNotAvailable (Ljava/nio/file/Path;Ljava/lang/Class;)Ljava/lang/Void;
|
||||
public static final fun fileVisitor (Lkotlin/jvm/functions/Function1;)Ljava/nio/file/FileVisitor;
|
||||
public static final fun getExtension (Ljava/nio/file/Path;)Ljava/lang/String;
|
||||
|
||||
Reference in New Issue
Block a user