Skip to content

✨ Add recursive delete for non-empty directories - #641

Merged
vinceglb merged 4 commits into
vinceglb:mainfrom
anggrayudi:fix/640-delete-non-empty-folders
Sep 7, 2026
Merged

vinceglb merged 4 commits into
vinceglb:mainfrom
anggrayudi:fix/640-delete-non-empty-folders

Conversation

@anggrayudi

@anggrayudi anggrayudi commented Aug 15, 2026 •

Copy link
Copy Markdown

Closes #640.

Adds opt-in recursive deletion of non-empty directories:

public expect suspend fun PlatformFile.delete(
    mustExist: Boolean = true,
    recursively: Boolean = false,
)

For filesystem paths, recursively = false keeps the existing non-empty-directory failure. With true, FileKit deletes the contents before removing the directory. Android document URIs continue to delegate deletion to the provider.

Links are inspected and unlinked before directory metadata is queried. This preserves outside targets and handles dangling and self-referencing links. Windows JVM uses reparse-point attributes so directory junctions are also recognized. Link removal bypasses kotlinx-io's target-existence check, which otherwise leaves dangling links behind.

Regression coverage

  • Shared tests cover nested directories, ordinary files, missing paths, and the non-recursive default.
  • JVM tests cover dangling links and outside-target preservation, plus a Windows-only junction test.
  • Apple native tests cover dangling, cyclic, and outside-target links.
  • Android host tests cover links at API 23 and 36. Their shadow supplies no-follow filesystem behavior because Robolectric's default lstat follows directory links.
  • Updated the deletion documentation and AGENTS.md to keep local validation targeted; broad builds belong in CI because they overload the maintainer's Mac.

Validation

  • JVM: 68 tests, 0 failures, 1 Windows-only skip on macOS. The dangling-link regressions failed before the fix and pass afterward.
  • Android host: 62 tests, 0 failures.
  • Linux and Windows native implementations compile from macOS.
  • Repository-wide ktlint passed; changed Kotlin files were checked again after the final edits.
  • Apple native runtime validation and Windows junction execution are pending CI. The local full build was stopped at the maintainer's request; it is not claimed as passing.

Manual native verification

  1. In a disposable temporary directory, create outside/treasure.txt with known contents and doomed/link pointing to outside. On Windows, also repeat with a junction created using mklink /J.
  2. Call PlatformFile(doomedPath).delete(recursively = true) in the target app. Verify doomed is gone and the sentinel's contents are unchanged.
  3. Repeat with a dangling link and a self-referencing link inside doomed. The directory and links should all be removed.
  4. Repeat with an ordinary non-empty directory and recursively = false; deletion should fail and its contents should remain.

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>
Inspect and unlink filesystem links before querying directory metadata.
Use Windows reparse-point attributes on JVM to avoid traversing junctions,
and direct deletion primitives to remove dangling and cyclic links.

Add JVM, native Apple, and Android host regressions, including a Windows
junction test. Run shared file tests under Robolectric, and supply no-follow
host filesystem behavior for its link tests because the default lstat shadow
follows directory links. Document recursive deletion and link behavior.

Manual native verification: create a directory containing a link to an
outside directory with a sentinel file, call delete(recursively = true),
and verify the directory is gone while the sentinel is unchanged. Repeat
with a dangling link and a self-referencing link. On Windows, repeat using
a directory junction created with mklink /J.
@vinceglb
vinceglb merged commit 9f34bef into vinceglb:main Sep 7, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Handle deletion of non-empty folders.

2 participants