or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

file-io.mdfile-system.mdindex.mdpath-operations.mdrecursive-operations.mdtree-traversal.md

recursive-operations.mddocs/

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

```