CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-org-jetbrains-kotlin--kotlin-stdlib-jdk7

Kotlin Standard Library JDK 7 extension providing Path utilities and platform-specific implementations for Java NIO and JDK 7+ features

Pending
Overview
Eval results
Files

recursive-operations.mddocs/

Recursive Operations

Experimental APIs for recursive file operations providing comprehensive error handling, customizable behaviors, and robust directory tree manipulation for complex file system operations.

Capabilities

Recursive Copying

Copy entire directory trees with comprehensive options for handling conflicts, errors, and symbolic links.

/**
 * Recursively copies this directory and its content to the specified destination target path.
 * 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 function.
 * 
 * @param target the destination path to copy recursively this entry to
 * @param onError the function that determines further actions if an error occurs
 * @param followLinks false to copy a symbolic link itself, true to recursively copy the target
 * @param overwrite false to throw if a destination entry already exists, true to overwrite existing entries
 */
@ExperimentalPathApi
@SinceKotlin("1.8")
fun Path.copyToRecursively(
    target: Path,
    onError: (source: Path, target: Path, exception: Exception) -> OnErrorResult = { _, _, exception -> throw exception },
    followLinks: Boolean,
    overwrite: Boolean
): Path

/**
 * Recursively copies this directory and its content to the specified destination target path.
 * Copy operation is performed using copyAction for custom copy behavior.
 * 
 * @param target the destination path to copy recursively this entry to
 * @param onError the function that determines further actions if an error occurs
 * @param followLinks false to copy a symbolic link itself, true to recursively copy the target
 * @param copyAction the function to call for copying source entries to their destination path
 */
@ExperimentalPathApi
@SinceKotlin("1.8")
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

Usage Examples:

import kotlin.io.path.*
import java.nio.file.Paths

val sourceDir = Paths.get("/home/user/project")
val backupDir = Paths.get("/backup/project-backup")
val syncDir = Paths.get("/sync/project-mirror")

// Simple recursive copy with overwrite
sourceDir.copyToRecursively(
    target = backupDir,
    followLinks = false,
    overwrite = true
)
println("Backup completed to: $backupDir")

// Recursive copy with error handling
var errorCount = 0
sourceDir.copyToRecursively(
    target = syncDir,
    onError = { source, target, exception ->
        errorCount++
        println("Error copying $source to $target: ${exception.message}")
        OnErrorResult.SKIP_SUBTREE // Skip this entry and continue
    },
    followLinks = false,
    overwrite = false
)

if (errorCount > 0) {
    println("Copy completed with $errorCount errors")
} else {
    println("Copy completed successfully")
}

// Copy with following symbolic links (be careful of loops)
val linkAwareBackup = Paths.get("/backup/link-aware-backup")
try {
    sourceDir.copyToRecursively(
        target = linkAwareBackup,
        followLinks = true,
        overwrite = true
    )
} catch (e: java.nio.file.FileSystemLoopException) {
    println("Detected symbolic link loop: ${e.message}")
}

Custom Copy Actions

Implement custom copy behaviors using the copy action interface for advanced scenarios.

/**
 * Context for the copyAction function passed to Path.copyToRecursively.
 */
@ExperimentalPathApi
@SinceKotlin("1.8")
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.
     */
    fun Path.copyToIgnoringExistingDirectory(target: Path, followLinks: Boolean): CopyActionResult
}

/**
 * The result of the copyAction function passed to Path.copyToRecursively 
 * that specifies further actions when copying an entry.
 */
@ExperimentalPathApi
@SinceKotlin("1.8")
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 */
    SKIP_SUBTREE,
    /** Stop the recursive copy function */
    TERMINATE
}

Usage Examples:

import kotlin.io.path.*
import java.nio.file.Paths
import java.time.Instant
import java.time.temporal.ChronoUnit

val sourceDir = Paths.get("/home/user/large-project")
val selectiveBackup = Paths.get("/backup/selective-backup")

// Custom copy action - only copy recent files and exclude certain directories
val oneWeekAgo = Instant.now().minus(7, ChronoUnit.DAYS)
var copiedFiles = 0
var skippedFiles = 0

sourceDir.copyToRecursively(
    target = selectiveBackup,
    followLinks = false,
    copyAction = { source, target ->
        when {
            // Skip build directories entirely
            source.isDirectory() && source.name in setOf("build", "target", ".gradle", "node_modules") -> {
                println("Skipping directory: ${source.relativeTo(sourceDir)}")
                CopyActionResult.SKIP_SUBTREE
            }
            
            // Only copy recent files
            source.isRegularFile() -> {
                val lastModified = source.getLastModifiedTime().toInstant()
                if (lastModified.isAfter(oneWeekAgo)) {
                    source.copyToIgnoringExistingDirectory(target, followLinks = false)
                    copiedFiles++
                    println("Copied: ${source.relativeTo(sourceDir)}")
                } else {
                    skippedFiles++
                }
                CopyActionResult.CONTINUE
            }
            
            // Create directories as needed
            source.isDirectory() -> {
                target.createDirectories()
                CopyActionResult.CONTINUE
            }
            
            else -> CopyActionResult.CONTINUE
        }
    }
)

println("Selective backup completed:")
println("- Files copied: $copiedFiles")
println("- Files skipped: $skippedFiles")

// Copy with file filtering and transformation
val filteredCopy = Paths.get("/backup/filtered-copy")

sourceDir.copyToRecursively(
    target = filteredCopy,
    followLinks = false,
    copyAction = { source, target ->
        when {
            source.isDirectory() -> {
                target.createDirectories()
                CopyActionResult.CONTINUE
            }
            
            source.extension == "txt" -> {
                // Transform text files during copy
                val content = source.readText()
                val transformedContent = content.uppercase()
                target.writeText(transformedContent)
                println("Transformed: ${source.name}")
                CopyActionResult.CONTINUE
            }
            
            source.extension in setOf("kt", "java", "xml") -> {
                // Copy source files normally
                source.copyToIgnoringExistingDirectory(target, followLinks = false)
                CopyActionResult.CONTINUE
            }
            
            else -> {
                // Skip other file types
                CopyActionResult.CONTINUE
            }
        }
    }
)

Error Handling

Comprehensive error handling strategies for robust recursive operations.

/**
 * The result of the onError function passed to Path.copyToRecursively 
 * that specifies further actions when an exception occurs.
 */
@ExperimentalPathApi
@SinceKotlin("1.8")
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.
     */
    SKIP_SUBTREE,
    
    /**
     * Stop the recursive copy function. The function will return without throwing exception.
     * To terminate the function with an exception rethrow instead.
     */
    TERMINATE
}

Usage Examples:

import kotlin.io.path.*
import java.nio.file.*

val sourceDir = Paths.get("/home/user/problematic-source")
val targetDir = Paths.get("/backup/recovered-backup")
val errorLog = Paths.get("/logs/copy-errors.log")

// Comprehensive error handling and logging
val errors = mutableListOf<String>()

sourceDir.copyToRecursively(
    target = targetDir,
    onError = { source, target, exception ->
        val errorMessage = "Failed to copy '$source' to '$target': ${exception::class.simpleName} - ${exception.message}"
        errors.add(errorMessage)
        println("ERROR: $errorMessage")
        
        when (exception) {
            is NoSuchFileException -> {
                // Source file disappeared during copy
                println("Source file no longer exists, skipping...")
                OnErrorResult.SKIP_SUBTREE
            }
            
            is FileAlreadyExistsException -> {
                // Destination exists and we're not overwriting
                println("Destination exists, skipping...")
                OnErrorResult.SKIP_SUBTREE
            }
            
            is AccessDeniedException -> {
                // Permission denied
                println("Access denied, skipping...")
                OnErrorResult.SKIP_SUBTREE
            }
            
            is DirectoryNotEmptyException -> {
                // Can't overwrite non-empty directory
                println("Directory not empty, skipping...")
                OnErrorResult.SKIP_SUBTREE
            }
            
            is IOException -> {
                // Other I/O errors
                if (errors.size > 10) {
                    println("Too many errors, terminating copy operation")
                    OnErrorResult.TERMINATE
                } else {
                    OnErrorResult.SKIP_SUBTREE
                }
            }
            
            else -> {
                // Unexpected errors - terminate
                println("Unexpected error, terminating")
                OnErrorResult.TERMINATE
            }
        }
    },
    followLinks = false,
    overwrite = false
)

// Write error log
if (errors.isNotEmpty()) {
    errorLog.createParentDirectories()
    errorLog.writeLines(listOf("Copy Error Report - ${java.time.LocalDateTime.now()}") + errors)
    println("Copy completed with ${errors.size} errors. See: $errorLog")
} else {
    println("Copy completed successfully with no errors")
}

Recursive Deletion

Delete entire directory trees with comprehensive error collection and security considerations.

/**
 * 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.
 * This function does nothing if the entry located by this path does not exist.
 * 
 * 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.
 */
@ExperimentalPathApi
@SinceKotlin("1.8")
fun Path.deleteRecursively()

Usage Examples:

import kotlin.io.path.*
import java.nio.file.Paths
import java.io.IOException

val tempDir = Paths.get("/tmp/my-app-temp")
val oldBackups = Paths.get("/backup/old-backups")
val buildDirs = listOf("build", "target", ".gradle", "node_modules")

// Simple recursive deletion
if (tempDir.exists()) {
    try {
        tempDir.deleteRecursively()
        println("Temporary directory cleaned: $tempDir")
    } catch (e: IOException) {
        println("Failed to delete temp directory: ${e.message}")
        e.suppressedExceptions.forEach { suppressed ->
            println("  - ${suppressed.message}")
        }
    }
}

// Clean multiple build directories
val projectRoot = Paths.get("/home/user/projects")

projectRoot.walk(PathWalkOption.INCLUDE_DIRECTORIES)
    .filter { it.isDirectory() && it.name in buildDirs }
    .forEach { buildDir ->
        try {
            println("Cleaning build directory: ${buildDir.relativeTo(projectRoot)}")
            buildDir.deleteRecursively()
        } catch (e: IOException) {
            println("Failed to clean ${buildDir.name}: ${e.message}")
        }
    }

// Safe deletion with confirmation and backup
fun safeDeleteRecursively(path: Path, createBackup: Boolean = true) {
    if (!path.exists()) {
        println("Path does not exist: $path")
        return
    }
    
    // Calculate size for confirmation
    var totalSize = 0L
    var fileCount = 0
    
    if (path.isDirectory()) {
        path.walk().forEach { file ->
            if (file.isRegularFile()) {
                totalSize += file.fileSize()
                fileCount++
            }
        }
        println("About to delete directory with $fileCount files (${totalSize / 1024 / 1024} MB)")
    } else {
        totalSize = path.fileSize()
        println("About to delete file (${totalSize / 1024} KB)")
    }
    
    // Create backup if requested
    if (createBackup && totalSize > 0) {
        val backupPath = Paths.get("${path.absolutePathString()}.backup.${System.currentTimeMillis()}")
        try {
            if (path.isDirectory()) {
                path.copyToRecursively(backupPath, followLinks = false, overwrite = false)
            } else {
                path.copyTo(backupPath)
            }
            println("Backup created: $backupPath")
        } catch (e: Exception) {
            println("Failed to create backup: ${e.message}")
            println("Deletion cancelled for safety")
            return
        }
    }
    
    // Perform deletion
    try {
        path.deleteRecursively()
        println("Successfully deleted: $path")
    } catch (e: IOException) {
        println("Deletion completed with errors: ${e.message}")
        println("Suppressed exceptions:")
        e.suppressedExceptions.forEachIndexed { index, suppressed ->
            println("  ${index + 1}. ${suppressed.message}")
        }
    }
}

// Usage of safe deletion
val oldProjectDir = Paths.get("/home/user/old-project")
safeDeleteRecursively(oldProjectDir, createBackup = true)

// Conditional deletion based on age
val cacheDir = Paths.get("/home/user/.cache/myapp") 
val oneMonthAgo = java.time.Instant.now().minus(30, java.time.temporal.ChronoUnit.DAYS)

if (cacheDir.exists()) {
    val lastModified = cacheDir.getLastModifiedTime().toInstant()
    if (lastModified.isBefore(oneMonthAgo)) {
        println("Cache directory is old, cleaning...")
        try {
            cacheDir.deleteRecursively()
            cacheDir.createDirectories() // Recreate empty cache dir
        } catch (e: IOException) {
            println("Failed to clean cache: ${e.message}")
        }
    } else {
        println("Cache directory is recent, keeping")
    }
}

Advanced Recursive Patterns

Complex patterns combining recursive operations with other file system operations.

Usage Examples:

import kotlin.io.path.*
import java.nio.file.Paths
import java.time.temporal.ChronoUnit

// Archive and cleanup pattern
fun archiveAndCleanup(sourceDir: Path, archiveDir: Path, maxAge: Long, unit: ChronoUnit) {
    val cutoffTime = java.time.Instant.now().minus(maxAge, unit)
    
    // Find old directories
    val oldDirs = sourceDir.walk(PathWalkOption.INCLUDE_DIRECTORIES)
        .filter { it.isDirectory() && it != sourceDir }
        .filter { dir ->
            val lastModified = dir.walk()
                .filter { it.isRegularFile() }
                .map { it.getLastModifiedTime().toInstant() }
                .maxOrNull() ?: java.time.Instant.MIN
            lastModified.isBefore(cutoffTime)
        }
        .toList()
    
    println("Found ${oldDirs.size} directories older than $maxAge ${unit.name.lowercase()}")
    
    // Archive old directories
    oldDirs.forEach { oldDir ->
        val relativePath = oldDir.relativeTo(sourceDir)
        val archivePath = archiveDir / relativePath
        
        try {
            println("Archiving: $relativePath")
            oldDir.copyToRecursively(
                target = archivePath,
                followLinks = false,
                overwrite = true
            )
            
            // Verify archive was created successfully
            if (archivePath.exists()) {
                oldDir.deleteRecursively()
                println("Successfully archived and removed: $relativePath")
            } else {
                println("Archive verification failed for: $relativePath")
            }
        } catch (e: Exception) {
            println("Failed to archive $relativePath: ${e.message}")
        }
    }
}

// Usage
val logsDir = Paths.get("/var/log/myapp")
val archiveLogsDir = Paths.get("/archive/logs")
archiveAndCleanup(logsDir, archiveLogsDir, 90, ChronoUnit.DAYS)

// Synchronization with differential copy
fun synchronizeDirs(source: Path, target: Path) {
    println("Synchronizing $source -> $target")
    
    // First pass: copy new and modified files
    var copiedCount = 0
    var updatedCount = 0
    
    source.copyToRecursively(
        target = target,
        followLinks = false,
        copyAction = { src, dst ->
            when {
                src.isDirectory() -> {
                    dst.createDirectories()
                    CopyActionResult.CONTINUE
                }
                
                src.isRegularFile() -> {
                    val shouldCopy = !dst.exists() || 
                        src.getLastModifiedTime() > dst.getLastModifiedTime() ||
                        src.fileSize() != dst.fileSize()
                    
                    if (shouldCopy) {
                        src.copyToIgnoringExistingDirectory(dst, followLinks = false)
                        if (dst.exists()) updatedCount++ else copiedCount++
                    }
                    CopyActionResult.CONTINUE
                }
                
                else -> CopyActionResult.CONTINUE
            }
        }
    )
    
    // Second pass: remove files that no longer exist in source
    var deletedCount = 0
    
    target.walk().forEach { targetFile ->
        if (targetFile.isRegularFile()) {
            val relativePath = targetFile.relativeTo(target)
            val sourceFile = source / relativePath.pathString
            
            if (!sourceFile.exists()) {
                targetFile.deleteIfExists()
                deletedCount++
                println("Removed obsolete file: $relativePath")
            }
        }
    }
    
    println("Synchronization completed:")
    println("- New files: $copiedCount")
    println("- Updated files: $updatedCount")  
    println("- Removed files: $deletedCount")
}

// Usage
val masterDir = Paths.get("/home/user/master-project")
val mirrorDir = Paths.get("/backup/mirror-project")
synchronizeDirs(masterDir, mirrorDir)

Install with Tessl CLI

npx tessl i tessl/maven-org-jetbrains-kotlin--kotlin-stdlib-jdk7

docs

file-io.md

file-system.md

index.md

path-operations.md

recursive-operations.md

tree-traversal.md

tile.json