0
# Recursive Operations
1
2
Experimental APIs for recursive file operations providing comprehensive error handling, customizable behaviors, and robust directory tree manipulation for complex file system operations.
3
4
## Capabilities
5
6
### Recursive Copying
7
8
Copy entire directory trees with comprehensive options for handling conflicts, errors, and symbolic links.
9
10
```kotlin { .api }
11
/**
12
* Recursively copies this directory and its content to the specified destination target path.
13
* If an exception occurs attempting to read, open or copy any entry under the source subtree,
14
* further actions will depend on the result of the onError function.
15
*
16
* @param target the destination path to copy recursively this entry to
17
* @param onError the function that determines further actions if an error occurs
18
* @param followLinks false to copy a symbolic link itself, true to recursively copy the target
19
* @param overwrite false to throw if a destination entry already exists, true to overwrite existing entries
20
*/
21
@ExperimentalPathApi
22
@SinceKotlin("1.8")
23
fun Path.copyToRecursively(
24
target: Path,
25
onError: (source: Path, target: Path, exception: Exception) -> OnErrorResult = { _, _, exception -> throw exception },
26
followLinks: Boolean,
27
overwrite: Boolean
28
): Path
29
30
/**
31
* Recursively copies this directory and its content to the specified destination target path.
32
* Copy operation is performed using copyAction for custom copy behavior.
33
*
34
* @param target the destination path to copy recursively this entry to
35
* @param onError the function that determines further actions if an error occurs
36
* @param followLinks false to copy a symbolic link itself, true to recursively copy the target
37
* @param copyAction the function to call for copying source entries to their destination path
38
*/
39
@ExperimentalPathApi
40
@SinceKotlin("1.8")
41
fun Path.copyToRecursively(
42
target: Path,
43
onError: (source: Path, target: Path, exception: Exception) -> OnErrorResult = { _, _, exception -> throw exception },
44
followLinks: Boolean,
45
copyAction: CopyActionContext.(source: Path, target: Path) -> CopyActionResult = { src, dst ->
46
src.copyToIgnoringExistingDirectory(dst, followLinks)
47
}
48
): Path
49
```
50
51
**Usage Examples:**
52
53
```kotlin
54
import kotlin.io.path.*
55
import java.nio.file.Paths
56
57
val sourceDir = Paths.get("/home/user/project")
58
val backupDir = Paths.get("/backup/project-backup")
59
val syncDir = Paths.get("/sync/project-mirror")
60
61
// Simple recursive copy with overwrite
62
sourceDir.copyToRecursively(
63
target = backupDir,
64
followLinks = false,
65
overwrite = true
66
)
67
println("Backup completed to: $backupDir")
68
69
// Recursive copy with error handling
70
var errorCount = 0
71
sourceDir.copyToRecursively(
72
target = syncDir,
73
onError = { source, target, exception ->
74
errorCount++
75
println("Error copying $source to $target: ${exception.message}")
76
OnErrorResult.SKIP_SUBTREE // Skip this entry and continue
77
},
78
followLinks = false,
79
overwrite = false
80
)
81
82
if (errorCount > 0) {
83
println("Copy completed with $errorCount errors")
84
} else {
85
println("Copy completed successfully")
86
}
87
88
// Copy with following symbolic links (be careful of loops)
89
val linkAwareBackup = Paths.get("/backup/link-aware-backup")
90
try {
91
sourceDir.copyToRecursively(
92
target = linkAwareBackup,
93
followLinks = true,
94
overwrite = true
95
)
96
} catch (e: java.nio.file.FileSystemLoopException) {
97
println("Detected symbolic link loop: ${e.message}")
98
}
99
```
100
101
### Custom Copy Actions
102
103
Implement custom copy behaviors using the copy action interface for advanced scenarios.
104
105
```kotlin { .api }
106
/**
107
* Context for the copyAction function passed to Path.copyToRecursively.
108
*/
109
@ExperimentalPathApi
110
@SinceKotlin("1.8")
111
interface CopyActionContext {
112
/**
113
* Copies the entry located by this path to the specified target path,
114
* except if both this and target entries are directories,
115
* in which case the method completes without copying the entry.
116
*/
117
fun Path.copyToIgnoringExistingDirectory(target: Path, followLinks: Boolean): CopyActionResult
118
}
119
120
/**
121
* The result of the copyAction function passed to Path.copyToRecursively
122
* that specifies further actions when copying an entry.
123
*/
124
@ExperimentalPathApi
125
@SinceKotlin("1.8")
126
enum class CopyActionResult {
127
/** Continue with the next entry in the traversal order */
128
CONTINUE,
129
/** Skip the directory content, continue with the next entry outside the directory */
130
SKIP_SUBTREE,
131
/** Stop the recursive copy function */
132
TERMINATE
133
}
134
```
135
136
**Usage Examples:**
137
138
```kotlin
139
import kotlin.io.path.*
140
import java.nio.file.Paths
141
import java.time.Instant
142
import java.time.temporal.ChronoUnit
143
144
val sourceDir = Paths.get("/home/user/large-project")
145
val selectiveBackup = Paths.get("/backup/selective-backup")
146
147
// Custom copy action - only copy recent files and exclude certain directories
148
val oneWeekAgo = Instant.now().minus(7, ChronoUnit.DAYS)
149
var copiedFiles = 0
150
var skippedFiles = 0
151
152
sourceDir.copyToRecursively(
153
target = selectiveBackup,
154
followLinks = false,
155
copyAction = { source, target ->
156
when {
157
// Skip build directories entirely
158
source.isDirectory() && source.name in setOf("build", "target", ".gradle", "node_modules") -> {
159
println("Skipping directory: ${source.relativeTo(sourceDir)}")
160
CopyActionResult.SKIP_SUBTREE
161
}
162
163
// Only copy recent files
164
source.isRegularFile() -> {
165
val lastModified = source.getLastModifiedTime().toInstant()
166
if (lastModified.isAfter(oneWeekAgo)) {
167
source.copyToIgnoringExistingDirectory(target, followLinks = false)
168
copiedFiles++
169
println("Copied: ${source.relativeTo(sourceDir)}")
170
} else {
171
skippedFiles++
172
}
173
CopyActionResult.CONTINUE
174
}
175
176
// Create directories as needed
177
source.isDirectory() -> {
178
target.createDirectories()
179
CopyActionResult.CONTINUE
180
}
181
182
else -> CopyActionResult.CONTINUE
183
}
184
}
185
)
186
187
println("Selective backup completed:")
188
println("- Files copied: $copiedFiles")
189
println("- Files skipped: $skippedFiles")
190
191
// Copy with file filtering and transformation
192
val filteredCopy = Paths.get("/backup/filtered-copy")
193
194
sourceDir.copyToRecursively(
195
target = filteredCopy,
196
followLinks = false,
197
copyAction = { source, target ->
198
when {
199
source.isDirectory() -> {
200
target.createDirectories()
201
CopyActionResult.CONTINUE
202
}
203
204
source.extension == "txt" -> {
205
// Transform text files during copy
206
val content = source.readText()
207
val transformedContent = content.uppercase()
208
target.writeText(transformedContent)
209
println("Transformed: ${source.name}")
210
CopyActionResult.CONTINUE
211
}
212
213
source.extension in setOf("kt", "java", "xml") -> {
214
// Copy source files normally
215
source.copyToIgnoringExistingDirectory(target, followLinks = false)
216
CopyActionResult.CONTINUE
217
}
218
219
else -> {
220
// Skip other file types
221
CopyActionResult.CONTINUE
222
}
223
}
224
}
225
)
226
```
227
228
### Error Handling
229
230
Comprehensive error handling strategies for robust recursive operations.
231
232
```kotlin { .api }
233
/**
234
* The result of the onError function passed to Path.copyToRecursively
235
* that specifies further actions when an exception occurs.
236
*/
237
@ExperimentalPathApi
238
@SinceKotlin("1.8")
239
enum class OnErrorResult {
240
/**
241
* If the entry that caused the error is a directory, skip the directory and its content,
242
* and continue with the next entry outside this directory in the traversal order.
243
* Otherwise, skip this entry and continue with the next entry.
244
*/
245
SKIP_SUBTREE,
246
247
/**
248
* Stop the recursive copy function. The function will return without throwing exception.
249
* To terminate the function with an exception rethrow instead.
250
*/
251
TERMINATE
252
}
253
```
254
255
**Usage Examples:**
256
257
```kotlin
258
import kotlin.io.path.*
259
import java.nio.file.*
260
261
val sourceDir = Paths.get("/home/user/problematic-source")
262
val targetDir = Paths.get("/backup/recovered-backup")
263
val errorLog = Paths.get("/logs/copy-errors.log")
264
265
// Comprehensive error handling and logging
266
val errors = mutableListOf<String>()
267
268
sourceDir.copyToRecursively(
269
target = targetDir,
270
onError = { source, target, exception ->
271
val errorMessage = "Failed to copy '$source' to '$target': ${exception::class.simpleName} - ${exception.message}"
272
errors.add(errorMessage)
273
println("ERROR: $errorMessage")
274
275
when (exception) {
276
is NoSuchFileException -> {
277
// Source file disappeared during copy
278
println("Source file no longer exists, skipping...")
279
OnErrorResult.SKIP_SUBTREE
280
}
281
282
is FileAlreadyExistsException -> {
283
// Destination exists and we're not overwriting
284
println("Destination exists, skipping...")
285
OnErrorResult.SKIP_SUBTREE
286
}
287
288
is AccessDeniedException -> {
289
// Permission denied
290
println("Access denied, skipping...")
291
OnErrorResult.SKIP_SUBTREE
292
}
293
294
is DirectoryNotEmptyException -> {
295
// Can't overwrite non-empty directory
296
println("Directory not empty, skipping...")
297
OnErrorResult.SKIP_SUBTREE
298
}
299
300
is IOException -> {
301
// Other I/O errors
302
if (errors.size > 10) {
303
println("Too many errors, terminating copy operation")
304
OnErrorResult.TERMINATE
305
} else {
306
OnErrorResult.SKIP_SUBTREE
307
}
308
}
309
310
else -> {
311
// Unexpected errors - terminate
312
println("Unexpected error, terminating")
313
OnErrorResult.TERMINATE
314
}
315
}
316
},
317
followLinks = false,
318
overwrite = false
319
)
320
321
// Write error log
322
if (errors.isNotEmpty()) {
323
errorLog.createParentDirectories()
324
errorLog.writeLines(listOf("Copy Error Report - ${java.time.LocalDateTime.now()}") + errors)
325
println("Copy completed with ${errors.size} errors. See: $errorLog")
326
} else {
327
println("Copy completed successfully with no errors")
328
}
329
```
330
331
### Recursive Deletion
332
333
Delete entire directory trees with comprehensive error collection and security considerations.
334
335
```kotlin { .api }
336
/**
337
* Recursively deletes this directory and its content.
338
* Note that if this function throws, partial deletion may have taken place.
339
*
340
* If the entry located by this path is a directory, this function recursively deletes its content and the directory itself.
341
* Otherwise, this function deletes only the entry.
342
* This function does nothing if the entry located by this path does not exist.
343
*
344
* If an exception occurs attempting to read, open or delete any entry under the given file tree,
345
* this method skips that entry and continues. Such exceptions are collected and, after attempting to delete all entries,
346
* an IOException is thrown containing those exceptions as suppressed exceptions.
347
*/
348
@ExperimentalPathApi
349
@SinceKotlin("1.8")
350
fun Path.deleteRecursively()
351
```
352
353
**Usage Examples:**
354
355
```kotlin
356
import kotlin.io.path.*
357
import java.nio.file.Paths
358
import java.io.IOException
359
360
val tempDir = Paths.get("/tmp/my-app-temp")
361
val oldBackups = Paths.get("/backup/old-backups")
362
val buildDirs = listOf("build", "target", ".gradle", "node_modules")
363
364
// Simple recursive deletion
365
if (tempDir.exists()) {
366
try {
367
tempDir.deleteRecursively()
368
println("Temporary directory cleaned: $tempDir")
369
} catch (e: IOException) {
370
println("Failed to delete temp directory: ${e.message}")
371
e.suppressedExceptions.forEach { suppressed ->
372
println(" - ${suppressed.message}")
373
}
374
}
375
}
376
377
// Clean multiple build directories
378
val projectRoot = Paths.get("/home/user/projects")
379
380
projectRoot.walk(PathWalkOption.INCLUDE_DIRECTORIES)
381
.filter { it.isDirectory() && it.name in buildDirs }
382
.forEach { buildDir ->
383
try {
384
println("Cleaning build directory: ${buildDir.relativeTo(projectRoot)}")
385
buildDir.deleteRecursively()
386
} catch (e: IOException) {
387
println("Failed to clean ${buildDir.name}: ${e.message}")
388
}
389
}
390
391
// Safe deletion with confirmation and backup
392
fun safeDeleteRecursively(path: Path, createBackup: Boolean = true) {
393
if (!path.exists()) {
394
println("Path does not exist: $path")
395
return
396
}
397
398
// Calculate size for confirmation
399
var totalSize = 0L
400
var fileCount = 0
401
402
if (path.isDirectory()) {
403
path.walk().forEach { file ->
404
if (file.isRegularFile()) {
405
totalSize += file.fileSize()
406
fileCount++
407
}
408
}
409
println("About to delete directory with $fileCount files (${totalSize / 1024 / 1024} MB)")
410
} else {
411
totalSize = path.fileSize()
412
println("About to delete file (${totalSize / 1024} KB)")
413
}
414
415
// Create backup if requested
416
if (createBackup && totalSize > 0) {
417
val backupPath = Paths.get("${path.absolutePathString()}.backup.${System.currentTimeMillis()}")
418
try {
419
if (path.isDirectory()) {
420
path.copyToRecursively(backupPath, followLinks = false, overwrite = false)
421
} else {
422
path.copyTo(backupPath)
423
}
424
println("Backup created: $backupPath")
425
} catch (e: Exception) {
426
println("Failed to create backup: ${e.message}")
427
println("Deletion cancelled for safety")
428
return
429
}
430
}
431
432
// Perform deletion
433
try {
434
path.deleteRecursively()
435
println("Successfully deleted: $path")
436
} catch (e: IOException) {
437
println("Deletion completed with errors: ${e.message}")
438
println("Suppressed exceptions:")
439
e.suppressedExceptions.forEachIndexed { index, suppressed ->
440
println(" ${index + 1}. ${suppressed.message}")
441
}
442
}
443
}
444
445
// Usage of safe deletion
446
val oldProjectDir = Paths.get("/home/user/old-project")
447
safeDeleteRecursively(oldProjectDir, createBackup = true)
448
449
// Conditional deletion based on age
450
val cacheDir = Paths.get("/home/user/.cache/myapp")
451
val oneMonthAgo = java.time.Instant.now().minus(30, java.time.temporal.ChronoUnit.DAYS)
452
453
if (cacheDir.exists()) {
454
val lastModified = cacheDir.getLastModifiedTime().toInstant()
455
if (lastModified.isBefore(oneMonthAgo)) {
456
println("Cache directory is old, cleaning...")
457
try {
458
cacheDir.deleteRecursively()
459
cacheDir.createDirectories() // Recreate empty cache dir
460
} catch (e: IOException) {
461
println("Failed to clean cache: ${e.message}")
462
}
463
} else {
464
println("Cache directory is recent, keeping")
465
}
466
}
467
```
468
469
### Advanced Recursive Patterns
470
471
Complex patterns combining recursive operations with other file system operations.
472
473
**Usage Examples:**
474
475
```kotlin
476
import kotlin.io.path.*
477
import java.nio.file.Paths
478
import java.time.temporal.ChronoUnit
479
480
// Archive and cleanup pattern
481
fun archiveAndCleanup(sourceDir: Path, archiveDir: Path, maxAge: Long, unit: ChronoUnit) {
482
val cutoffTime = java.time.Instant.now().minus(maxAge, unit)
483
484
// Find old directories
485
val oldDirs = sourceDir.walk(PathWalkOption.INCLUDE_DIRECTORIES)
486
.filter { it.isDirectory() && it != sourceDir }
487
.filter { dir ->
488
val lastModified = dir.walk()
489
.filter { it.isRegularFile() }
490
.map { it.getLastModifiedTime().toInstant() }
491
.maxOrNull() ?: java.time.Instant.MIN
492
lastModified.isBefore(cutoffTime)
493
}
494
.toList()
495
496
println("Found ${oldDirs.size} directories older than $maxAge ${unit.name.lowercase()}")
497
498
// Archive old directories
499
oldDirs.forEach { oldDir ->
500
val relativePath = oldDir.relativeTo(sourceDir)
501
val archivePath = archiveDir / relativePath
502
503
try {
504
println("Archiving: $relativePath")
505
oldDir.copyToRecursively(
506
target = archivePath,
507
followLinks = false,
508
overwrite = true
509
)
510
511
// Verify archive was created successfully
512
if (archivePath.exists()) {
513
oldDir.deleteRecursively()
514
println("Successfully archived and removed: $relativePath")
515
} else {
516
println("Archive verification failed for: $relativePath")
517
}
518
} catch (e: Exception) {
519
println("Failed to archive $relativePath: ${e.message}")
520
}
521
}
522
}
523
524
// Usage
525
val logsDir = Paths.get("/var/log/myapp")
526
val archiveLogsDir = Paths.get("/archive/logs")
527
archiveAndCleanup(logsDir, archiveLogsDir, 90, ChronoUnit.DAYS)
528
529
// Synchronization with differential copy
530
fun synchronizeDirs(source: Path, target: Path) {
531
println("Synchronizing $source -> $target")
532
533
// First pass: copy new and modified files
534
var copiedCount = 0
535
var updatedCount = 0
536
537
source.copyToRecursively(
538
target = target,
539
followLinks = false,
540
copyAction = { src, dst ->
541
when {
542
src.isDirectory() -> {
543
dst.createDirectories()
544
CopyActionResult.CONTINUE
545
}
546
547
src.isRegularFile() -> {
548
val shouldCopy = !dst.exists() ||
549
src.getLastModifiedTime() > dst.getLastModifiedTime() ||
550
src.fileSize() != dst.fileSize()
551
552
if (shouldCopy) {
553
src.copyToIgnoringExistingDirectory(dst, followLinks = false)
554
if (dst.exists()) updatedCount++ else copiedCount++
555
}
556
CopyActionResult.CONTINUE
557
}
558
559
else -> CopyActionResult.CONTINUE
560
}
561
}
562
)
563
564
// Second pass: remove files that no longer exist in source
565
var deletedCount = 0
566
567
target.walk().forEach { targetFile ->
568
if (targetFile.isRegularFile()) {
569
val relativePath = targetFile.relativeTo(target)
570
val sourceFile = source / relativePath.pathString
571
572
if (!sourceFile.exists()) {
573
targetFile.deleteIfExists()
574
deletedCount++
575
println("Removed obsolete file: $relativePath")
576
}
577
}
578
}
579
580
println("Synchronization completed:")
581
println("- New files: $copiedCount")
582
println("- Updated files: $updatedCount")
583
println("- Removed files: $deletedCount")
584
}
585
586
// Usage
587
val masterDir = Paths.get("/home/user/master-project")
588
val mirrorDir = Paths.get("/backup/mirror-project")
589
synchronizeDirs(masterDir, mirrorDir)
590
```