✨ Add recursive delete for non-empty directories - #641
Open
anggrayudi wants to merge 1 commit into
Open
Conversation
delete() gains a `recursively` flag, defaulting to false, so existing calls behave exactly as before and a non-empty directory still fails. Symlinks are unlinked rather than followed. kotlinx-io's FileMetadata carries no link information and isDirectory() resolves the link, so a naive recursion would delete the contents of whatever the link points at. A new internal isSymbolicLink() answers that per platform: Files.isSymbolicLink on JVM, Os.lstat on Android (java.nio needs API 26 and this library supports 21), attributesOfItemAtPath on Apple, lstat on Linux, and the reparse point attribute on Windows. The SAF branch on Android is left alone: removing a document is the provider's job and there is no empty-directory rule to work around. Closes vinceglb#640 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #640.
delete()currently fails on a directory that still has contents, so callers end up writing their own recursive extension — which is exactly what the issue reports.What changed
delete()gains arecursivelyflag:It defaults to
false, so every existing call behaves exactly as before and a non-empty directory still fails. Nothing starts deleting trees by surprise.Symlinks
The issue asks for links to be unlinked rather than followed, and this turned out to be the part that actually needs care. kotlinx-io's
FileMetadatacarries no link information, andisDirectory()resolves the link and reports on the target — so a naive recursion walks into a symlink and deletes the contents of whatever it points at, outside the tree the caller asked to remove.So there is a new internal
isSymbolicLink(), answered with the primitive that does not resolve the link:Files.isSymbolicLinkOs.lstat+OsConstants.S_ISLNK—java.nio.file.Filesneeds API 26 and this library supports 21attributesOfItemAtPath(does not resolve the link)lstatGetFileAttributesW+FILE_ATTRIBUTE_REPARSE_POINTThe SAF branch on Android is deliberately untouched: there is no empty-directory rule to work around there, and removing a document is the provider's job.
Tests
Four cases in
nonWebTest, so they run on JVM, iOS simulator, macOS and Android host: a non-empty directory still fails without the flag, a nested tree is removed with it, a plain file is fine either way, and a missing path withmustExist = falsestays a no-op.The symlink case is in
jvmTest, since creating a link needs a platform API: it builds a directory containing a symlink to a folder outside it, deletes recursively, and asserts the outside folder's contents survive.I checked that test is load-bearing rather than trusting it — removing the
isSymbolicLink()guard makes it fail on exactly the right assertion ("the symlink target lives outside that tree and is untouched"), and it passes again once restored.Verification
./gradlew assemble— all four published modules assemble.jvmTest,iosSimulatorArm64Test,macosArm64TestandtestAndroidHostTest(209 tests, 0 failures).linuxX64andmingwX64compile, cross-compiled from macOS. I could not run tests on those two, so theirisSymbolicLink()is compile-verified and reasoned about, not executed — worth a look from someone with those machines.AGENTS.md: ktlint is not on my PATH andktlint-compose-0.4.28-all.jaris not in the repo, so style is matched by hand. Happy to fix anything CI flags.Two notes on the OOM and JS failures I hit while running
assemble: both were local environment problems (a full disk, then an 8 GB Kotlin daemon heap on the sample's release framework link), not related to this change.🤖 Generated with Claude Code