Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
979f8fe
Introduce `overwrite` parameter, update the Darwin platform atomic co…
stepan-ulyanin Dec 29, 2025
2b8f11a
Add implementation to NIOFS, add tests for file copy with overwrite
stepan-ulyanin Dec 29, 2025
10942dc
Add implementations for Glibc, Musl, and Bionic
stepan-ulyanin Dec 29, 2025
098e2ac
Update the _NIOFileSystem implementation to be in sync with NIOFS imp…
stepan-ulyanin Dec 29, 2025
cf3e311
Update some variable names in _copyRegularFile
stepan-ulyanin Dec 29, 2025
9434fc3
Remove the test involving testing the sendfile(2) chunking for large …
stepan-ulyanin Dec 29, 2025
512c183
Remove an incorrect comment inlcuding Darwin platform from the `testC…
stepan-ulyanin Dec 29, 2025
3246eb8
Add some comments and docstrings
stepan-ulyanin Dec 30, 2025
52b70de
Add the symlink copy, using the temp link and `remame(2)`
stepan-ulyanin Dec 30, 2025
0c6b04f
Update the protocol docstring
stepan-ulyanin Dec 30, 2025
66b7338
Merge branch 'main' into su/add-overwriting-file-copy
stepan-ulyanin Dec 30, 2025
5a52992
Revert back the error messages when destinations can't be opened
stepan-ulyanin Dec 30, 2025
596cfb0
Update the `overwriting` docstring in the protocol
stepan-ulyanin Dec 30, 2025
8ba010a
Lean out the `overwriting` description even more
stepan-ulyanin Dec 30, 2025
c3ef217
Update the test `testCopyFileOverwritingExistingDestination` naming
stepan-ulyanin Dec 30, 2025
7d36c81
Update the `testCopyFileOverwritingNonExistingDestination` test name
stepan-ulyanin Dec 30, 2025
2bb758d
Clean up the tests and comments
stepan-ulyanin Dec 30, 2025
b8d2a7d
Remove trailing commas
stepan-ulyanin Dec 30, 2025
1cc569a
Apply formatting
stepan-ulyanin Dec 30, 2025
365ad86
Remove more trailing commas
stepan-ulyanin Dec 30, 2025
84a1f71
Move `overwriting` to the last position in the paramter list
stepan-ulyanin Dec 30, 2025
8c26bcc
Remove trailing commas
stepan-ulyanin Dec 30, 2025
42705fc
Fix the position of the parameter in the test
stepan-ulyanin Dec 30, 2025
2dd6ad5
Update the Linux tests to have a discardable result
stepan-ulyanin Dec 30, 2025
aad6784
Fix the `removingLastComponent` call in Linux tests
stepan-ulyanin Dec 30, 2025
4392131
Add tests for the cases where we would like to overwrite a symlink wi…
stepan-ulyanin Dec 30, 2025
e43e583
Clean up tests
stepan-ulyanin Dec 30, 2025
6091818
Merge branch 'main' into su/add-overwriting-file-copy
stepan-ulyanin Jan 7, 2026
789eb93
build trigger
stepan-ulyanin Jan 7, 2026
2cd3faa
Merge branch 'su/add-overwriting-file-copy' of https://github.com/ste…
stepan-ulyanin Jan 7, 2026
cf95658
Merge branch 'apple:main' into su/add-overwriting-file-copy
stepan-ulyanin Jan 21, 2026
031e1d2
Merge branch 'main' of https://github.com/apple/swift-nio into su/add…
stepan-ulyanin Jan 21, 2026
89409eb
Move the `overwriting` to before the closures in `copyItem` to allow …
stepan-ulyanin Jan 21, 2026
051da31
Merge branch 'su/add-overwriting-file-copy' of https://github.com/ste…
stepan-ulyanin Jan 21, 2026
b322156
Move the `overwriting` parameter in the `testCopyFileOverwritingClean…
stepan-ulyanin Jan 21, 2026
326bfa7
Move the `overwriting` parameter in `_NIOFileSystem`
stepan-ulyanin Jan 21, 2026
3ca3013
Update the documentation references
stepan-ulyanin Jan 21, 2026
2d67d0e
build trigger
stepan-ulyanin Jan 21, 2026
66115d4
Use renameat2 for copying the files on non-Darwin systems
stepan-ulyanin Jan 22, 2026
146f4f0
Use renameat2 for symlinks as well
stepan-ulyanin Jan 22, 2026
0a8a94c
Apply formatting
stepan-ulyanin Jan 22, 2026
cb7206f
Add `symlinkat` system call
stepan-ulyanin Jan 22, 2026
071ae15
Add `renameatx_np` system call
stepan-ulyanin Jan 22, 2026
9f0fa91
Update the copy-to-temp-rename to be executed against file descriptors
stepan-ulyanin Jan 22, 2026
61bdc9e
Apply formatting
stepan-ulyanin Jan 22, 2026
1a39b0c
Line split the deferred file handle closes
stepan-ulyanin Jan 22, 2026
2903e7b
Remove a redundant `COPYFILE_UNLINK` comments
stepan-ulyanin Jan 22, 2026
c695450
Use functions instead of closures
stepan-ulyanin Jan 22, 2026
2bac123
Update the _NIOFileSystem to match NIOFS
stepan-ulyanin Jan 22, 2026
d6f0fe8
Unnest some error definitions
stepan-ulyanin Jan 22, 2026
2b6355e
Update the `SystemFileHandle` logic
stepan-ulyanin Jan 22, 2026
306daae
Update error handling
stepan-ulyanin Jan 22, 2026
7c318bf
Remove the test that requires root permissions
stepan-ulyanin Jan 22, 2026
cf670bf
Merge branch 'main' into su/add-overwriting-file-copy
stepan-ulyanin Jan 22, 2026
4618f35
Merge branch 'main' into su/add-overwriting-file-copy
stepan-ulyanin Jan 24, 2026
672cee6
Update the expectations for the `renameatx_np` sys call test
stepan-ulyanin Jan 24, 2026
de4253a
Merge branch 'su/add-overwriting-file-copy' of https://github.com/ste…
stepan-ulyanin Jan 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
357 changes: 334 additions & 23 deletions Sources/NIOFS/FileSystem.swift

Large diffs are not rendered by default.

28 changes: 19 additions & 9 deletions Sources/NIOFS/FileSystemProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ public protocol FileSystemProtocol: Sendable {
/// The following error codes may be thrown:
/// - ``FileSystemError/Code-swift.struct/notFound`` if the item at `sourcePath` does not exist,
/// - ``FileSystemError/Code-swift.struct/invalidArgument`` if an item at `destinationPath`
/// exists prior to the copy or its parent directory does not exist.
/// exists prior to the copy (when `overwriting` is `false`) or its parent directory does not exist.
///
/// Note that other errors may also be thrown.
///
Expand All @@ -186,6 +186,7 @@ public protocol FileSystemProtocol: Sendable {
/// - sourcePath: The path to the item to copy.
/// - destinationPath: The path at which to place the copy.
/// - copyStrategy: How to deal with concurrent aspects of the copy, only relevant to directories.
/// - overwriting: Whether to overwrite an existing file or symlink at `destinationPath`.
/// - shouldProceedAfterError: A closure which is executed to determine whether to continue
/// copying files if an error is encountered during the operation. See Errors section for full details.
/// - shouldCopyItem: A closure which is executed before each copy to determine whether each
Expand All @@ -195,7 +196,7 @@ public protocol FileSystemProtocol: Sendable {
///
/// No errors should be throw by implementors without first calling `shouldProceedAfterError`,
/// if that returns without throwing this is taken as permission to continue and the error is swallowed.
/// If instead the closure throws then ``copyItem(at:to:strategy:shouldProceedAfterError:shouldCopyItem:)``
/// If instead the closure throws then ``copyItem(at:to:strategy:overwriting:shouldProceedAfterError:shouldCopyItem:)``
/// will throw and copying will stop, though the precise semantics of this can depend on the `strategy`.
///
/// if using ``CopyStrategy/parallel(maxDescriptors:)``
Expand Down Expand Up @@ -236,6 +237,7 @@ public protocol FileSystemProtocol: Sendable {
at sourcePath: NIOFilePath,
to destinationPath: NIOFilePath,
strategy copyStrategy: CopyStrategy,
overwriting: Bool,
shouldProceedAfterError:
@escaping @Sendable (
_ source: DirectoryEntry,
Expand Down Expand Up @@ -472,7 +474,7 @@ extension FileSystemProtocol {
///
/// Note that other errors may also be thrown. If any error is encountered during the copy
/// then the copy is aborted. You can modify the behaviour with the `shouldProceedAfterError`
/// parameter of ``FileSystemProtocol/copyItem(at:to:strategy:shouldProceedAfterError:shouldCopyItem:)``.
/// parameter of ``FileSystemProtocol/copyItem(at:to:strategy:overwriting:shouldProceedAfterError:shouldCopyItem:)``.
///
/// If the file at `sourcePath` is a symbolic link then only the link is copied to the new path.
///
Expand All @@ -485,11 +487,18 @@ extension FileSystemProtocol {
to destinationPath: NIOFilePath,
strategy copyStrategy: CopyStrategy = .platformDefault
) async throws {
try await self.copyItem(at: sourcePath, to: destinationPath, strategy: copyStrategy) { path, error in
throw error
} shouldCopyItem: { source, destination in
true
}
try await self.copyItem(
at: sourcePath,
to: destinationPath,
strategy: copyStrategy,
overwriting: false,
shouldProceedAfterError: { path, error in
throw error
},
shouldCopyItem: { source, destination in
true
}
)
}

/// Copies the item at the specified path to a new location.
Expand All @@ -516,7 +525,7 @@ extension FileSystemProtocol {
///
/// This overload uses ``CopyStrategy/platformDefault`` which is likely to result in multiple concurrency domains being used
/// in the event of copying a directory.
/// See the detailed description on ``copyItem(at:to:strategy:shouldProceedAfterError:shouldCopyItem:)``
/// See the detailed description on ``copyItem(at:to:strategy:overwriting:shouldProceedAfterError:shouldCopyItem:)``
/// for the implications of this with respect to the `shouldProceedAfterError` and `shouldCopyItem` callbacks
public func copyItem(
at sourcePath: NIOFilePath,
Expand All @@ -536,6 +545,7 @@ extension FileSystemProtocol {
at: sourcePath,
to: destinationPath,
strategy: .platformDefault,
overwriting: false,
shouldProceedAfterError: shouldProceedAfterError,
shouldCopyItem: shouldCopyItem
)
Expand Down
2 changes: 1 addition & 1 deletion Sources/NIOFS/IOStrategy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ enum IOStrategy: Hashable, Sendable {
}

/// How to perform copies. Currently only relevant to directory level copies when using
/// ``FileSystemProtocol/copyItem(at:to:strategy:shouldProceedAfterError:shouldCopyItem:)`` or other
/// ``FileSystemProtocol/copyItem(at:to:strategy:overwriting:shouldProceedAfterError:shouldCopyItem:)`` or other
/// overloads that use the default behaviour.
public struct CopyStrategy: Hashable, Sendable {
internal let wrapped: IOStrategy
Expand Down
51 changes: 51 additions & 0 deletions Sources/NIOFS/Internal/System Calls/Syscall.swift
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,29 @@ public enum Syscall: Sendable {
Self(rawValue: UInt32(bitPattern: RENAME_SWAP))
}
}

@_spi(Testing)
public static func rename(
from old: FilePath,
relativeTo oldFD: FileDescriptor,
to new: FilePath,
relativeTo newFD: FileDescriptor,
options: RenameOptions
) -> Result<Void, Errno> {
nothingOrErrno(retryOnInterrupt: false) {
old.withPlatformString { oldPath in
new.withPlatformString { newPath in
system_renameatx_np(
oldFD.rawValue,
oldPath,
newFD.rawValue,
newPath,
options.rawValue
)
}
}
}
}
#endif

#if canImport(Glibc) || canImport(Musl) || canImport(Bionic)
Expand Down Expand Up @@ -219,6 +242,19 @@ public enum Syscall: Sendable {
}
}

@_spi(Testing)
public static func unlinkat(
path: FilePath,
relativeTo directoryDescriptor: FileDescriptor,
flags: CInt = 0
) -> Result<Void, Errno> {
nothingOrErrno(retryOnInterrupt: false) {
path.withPlatformString { ptr in
system_unlinkat(directoryDescriptor.rawValue, ptr, flags)
}
}
}

@_spi(Testing)
public static func symlink(
to destination: FilePath,
Expand All @@ -233,6 +269,21 @@ public enum Syscall: Sendable {
}
}

@_spi(Testing)
public static func symlinkat(
to destination: FilePath,
in directoryDescriptor: FileDescriptor,
from source: FilePath
) -> Result<Void, Errno> {
nothingOrErrno(retryOnInterrupt: false) {
source.withPlatformString { src in
destination.withPlatformString { dst in
system_symlinkat(dst, directoryDescriptor.rawValue, src)
}
}
}
}

@_spi(Testing)
public static func readlink(at path: FilePath) -> Result<FilePath, Errno> {
do {
Expand Down
43 changes: 43 additions & 0 deletions Sources/NIOFS/Internal/System Calls/Syscalls.swift
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,20 @@ internal func system_symlink(
return symlink(destination, source)
}

/// symlinkat(2): Make symbolic link to a file relative to directory file descriptor
internal func system_symlinkat(
_ destination: UnsafePointer<CInterop.PlatformChar>,
_ dirfd: FileDescriptor.RawValue,
_ source: UnsafePointer<CInterop.PlatformChar>
) -> CInt {
#if ENABLE_MOCKING
if mockingEnabled {
return mock(destination, dirfd, source)
}
#endif
return symlinkat(destination, dirfd, source)
}

/// readlink(2): Read value of a symolic link
internal func system_readlink(
_ path: UnsafePointer<CInterop.PlatformChar>,
Expand Down Expand Up @@ -271,6 +285,21 @@ internal func system_renamex_np(
#endif
return renamex_np(old, new, flags)
}

internal func system_renameatx_np(
_ oldFD: FileDescriptor.RawValue,
_ old: UnsafePointer<CInterop.PlatformChar>,
_ newFD: FileDescriptor.RawValue,
_ new: UnsafePointer<CInterop.PlatformChar>,
_ flags: CUnsignedInt
) -> CInt {
#if ENABLE_MOCKING
if mockingEnabled {
return mock(oldFD, old, newFD, new, flags)
}
#endif
return renameatx_np(oldFD, old, newFD, new, flags)
}
#endif

#if canImport(Glibc) || canImport(Musl) || canImport(Android)
Expand Down Expand Up @@ -333,6 +362,20 @@ internal func system_unlink(
return unlink(path)
}

/// unlinkat(2): Remove a directory entry relative to a directory file descriptor.
internal func system_unlinkat(
_ fd: FileDescriptor.RawValue,
_ path: UnsafePointer<CInterop.PlatformChar>,
_ flags: CInt
) -> CInt {
#if ENABLE_MOCKING
if mockingEnabled {
return mock(fd, path, flags)
}
#endif
return unlinkat(fd, path, flags)
}

#if canImport(Glibc) || canImport(Musl) || canImport(Android)
/// sendfile(2): Transfer data between descriptors
internal func system_sendfile(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ closing it to avoid leaking resources.

### Managing files

- ``copyItem(at:to:strategy:shouldProceedAfterError:shouldCopyItem:)``
- ``copyItem(at:to:strategy:overwriting:shouldProceedAfterError:shouldCopyItem:)``
- ``removeItem(at:)``
- ``moveItem(at:to:)``
- ``replaceItem(at:withItemAt:)``
Expand Down
Loading
Loading