From 0fb3d7b34f4347ee267486725fab2538c0ed1a35 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Wed, 5 Jun 2024 17:02:42 +0100 Subject: [PATCH 1/5] Add API for setting last accessed and last modified file times --- Sources/NIOFileSystem/FileHandle.swift | 20 +++ .../NIOFileSystem/FileHandleProtocol.swift | 36 ++++++ .../FileSystemError+Syscall.swift | 44 +++++++ .../Internal/SystemFileHandle.swift | 70 ++++++++++- .../FileHandleTests.swift | 115 ++++++++++++++++++ 5 files changed, 282 insertions(+), 3 deletions(-) diff --git a/Sources/NIOFileSystem/FileHandle.swift b/Sources/NIOFileSystem/FileHandle.swift index a59436dc5b..92a3d08341 100644 --- a/Sources/NIOFileSystem/FileHandle.swift +++ b/Sources/NIOFileSystem/FileHandle.swift @@ -203,6 +203,16 @@ public struct WriteFileHandle: WritableFileHandleProtocol, _HasFileHandle { public func close(makeChangesVisible: Bool) async throws { try await self.fileHandle.systemFileHandle.close(makeChangesVisible: makeChangesVisible) } + + public func setTimes( + lastAccessTime: FileInfo.Timespec?, + lastDataModificationTime: FileInfo.Timespec? + ) async throws { + try await self.fileHandle.systemFileHandle.setTimes( + lastAccessTime: lastAccessTime, + lastDataModificationTime: lastDataModificationTime + ) + } } /// Implements ``ReadableAndWritableFileHandleProtocol`` by making system calls to interact with the @@ -247,6 +257,16 @@ public struct ReadWriteFileHandle: ReadableAndWritableFileHandleProtocol, _HasFi public func close(makeChangesVisible: Bool) async throws { try await self.fileHandle.systemFileHandle.close(makeChangesVisible: makeChangesVisible) } + + public func setTimes( + lastAccessTime: FileInfo.Timespec?, + lastDataModificationTime: FileInfo.Timespec? + ) async throws { + try await self.fileHandle.systemFileHandle.setTimes( + lastAccessTime: lastAccessTime, + lastDataModificationTime: lastDataModificationTime + ) + } } /// Implements ``DirectoryFileHandleProtocol`` by making system calls to interact with the local diff --git a/Sources/NIOFileSystem/FileHandleProtocol.swift b/Sources/NIOFileSystem/FileHandleProtocol.swift index 372bf0b01c..5e51a9fa6a 100644 --- a/Sources/NIOFileSystem/FileHandleProtocol.swift +++ b/Sources/NIOFileSystem/FileHandleProtocol.swift @@ -15,6 +15,15 @@ #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(Linux) || os(Android) import NIOCore import SystemPackage +#if canImport(Darwin) +import Darwin +#elseif canImport(Glibc) +import Glibc +#elseif canImport(Musl) +import Musl +#else +#error("The File Handle Protocol module was unable to identify your C library.") +#endif /// A handle for a file system object. /// @@ -463,6 +472,26 @@ public protocol WritableFileHandleProtocol: FileHandleProtocol { /// filesystem with the expected name, otherwise no file will be created or the original /// file won't be modified (if one existed). func close(makeChangesVisible: Bool) async throws + + /// Sets the file's last access and last data modification times to the given values. + /// + /// If **either** time is `nil`, the current value will not be changed. + /// If **both** times are `nil`, then both times will be set to the current time. + /// + /// > Important: Times are only considered valid if their nanoseconds components are one of the following: + /// > - `UTIME_NOW`, + /// > - `UTIME_OMIT`, + /// > - Greater than zero and no larger than 1000 million + /// + /// - Parameters: + /// - lastAccessTime: The new value of the file's last access time, as time elapsed since the Epoch. + /// - lastDataModificationTime: The new value of the file's last data modification time, as time elapsed since the Epoch. + /// + /// - Throws: If there's an error updating the times. If this happens, the original values won't be modified. + func setTimes( + lastAccessTime: FileInfo.Timespec?, + lastDataModificationTime: FileInfo.Timespec? + ) async throws } @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) @@ -486,6 +515,13 @@ extension WritableFileHandleProtocol { ) async throws -> Int64 { try await self.write(contentsOf: buffer.readableBytesView, toAbsoluteOffset: offset) } + + /// Sets the file's last access and last data modification times to the current time. + /// + /// - Throws: If there's an error updating the times. If this happens, the original values won't be modified. + public func touch() async throws { + try await self.setTimes(lastAccessTime: nil, lastDataModificationTime: nil) + } } /// A file handle which is suitable for reading and writing. diff --git a/Sources/NIOFileSystem/FileSystemError+Syscall.swift b/Sources/NIOFileSystem/FileSystemError+Syscall.swift index 8f9808c005..96aee327eb 100644 --- a/Sources/NIOFileSystem/FileSystemError+Syscall.swift +++ b/Sources/NIOFileSystem/FileSystemError+Syscall.swift @@ -1066,6 +1066,50 @@ extension FileSystemError { location: location ) } + + public static func futimens( + errno: Errno, + path: FilePath, + lastAccessTime: FileInfo.Timespec?, + lastDataModificationTime: FileInfo.Timespec?, + location: SourceLocation + ) -> FileSystemError { + let code: FileSystemError.Code + let message: String + + switch errno { + case .permissionDenied, .notPermitted: + code = .permissionDenied + message = "Not permited to change last access or last data modification times for \(path)." + + case .invalidArgument: + code = .invalidArgument + message = """ + Invalid times value: nanoseconds for both timespecs must be UTIME_NOW, UTIME_OMIT, or a number between 0 and 1000 million + (last access time: \(String(describing: lastAccessTime)), last data modification time: \(String(describing: lastDataModificationTime))). + """ + + case .readOnlyFileSystem: + code = .unsupported + message = "Not permited to change last access or last data modification times for \(path): this is a read-only file system." + + case .badFileDescriptor: + code = .closed + message = "Could not change last access or last data modification dates for \(path): file is closed." + + default: + code = .unknown + message = "Could not change last access or last data modification dates for \(path)." + } + + return FileSystemError( + code: code, + message: message, + systemCall: "futimens", + errno: errno, + location: location + ) + } } #endif diff --git a/Sources/NIOFileSystem/Internal/SystemFileHandle.swift b/Sources/NIOFileSystem/Internal/SystemFileHandle.swift index 54496673d3..12508cf662 100644 --- a/Sources/NIOFileSystem/Internal/SystemFileHandle.swift +++ b/Sources/NIOFileSystem/Internal/SystemFileHandle.swift @@ -153,7 +153,7 @@ extension SystemFileHandle.SendableView { } } - /// Executes a closure with the file descriptor it it's available otherwise throws the result + /// Executes a closure with the file descriptor if it's available otherwise throws the result /// of `onUnavailable`. internal func _withUnsafeDescriptor( _ execute: (FileDescriptor) throws -> R, @@ -166,8 +166,8 @@ extension SystemFileHandle.SendableView { } } - /// Executes a closure with the file descriptor it it's available otherwise throws the result - /// of `onUnavailable`. + /// Executes a closure with the file descriptor if it's available otherwise returns the result + /// of `onUnavailable` as a `Result` Error. internal func _withUnsafeDescriptorResult( _ execute: (FileDescriptor) -> Result, onUnavailable: () -> FileSystemError @@ -1046,6 +1046,70 @@ extension SystemFileHandle: WritableFileHandleProtocol { try sendableView._resize(to: size).get() } } + + public func setTimes( + lastAccessTime: FileInfo.Timespec?, + lastDataModificationTime: FileInfo.Timespec? + ) async throws { + try await self.threadPool.runIfActive { [sendableView] in + try sendableView._withUnsafeDescriptor { descriptor in + if lastAccessTime == nil, lastDataModificationTime == nil { + // If the timespec array is nil, as per the `futimens` docs, + // both the last accessed and last modification times + // will be set to now. + futimens(descriptor.rawValue, nil) + } else { + let lastAccessTimespec: timespec + if let lastAccessTime = lastAccessTime { + lastAccessTimespec = timespec( + tv_sec: lastAccessTime.seconds, + tv_nsec: lastAccessTime.nanoseconds + ) + } else { + // Don't modify the last access time. + // Note: tv_sec will be ignored. + lastAccessTimespec = timespec( + tv_sec: 0, + tv_nsec: Int(UTIME_OMIT) + ) + } + + let lastDataModificationTimespec: timespec + if let lastDataModificationTime = lastDataModificationTime { + lastDataModificationTimespec = timespec( + tv_sec: lastDataModificationTime.seconds, + tv_nsec: lastDataModificationTime.nanoseconds + ) + } else { + // Don't modify the last modification time. + // Note: tv_sec will be ignored. + lastDataModificationTimespec = timespec( + tv_sec: 0, + tv_nsec: Int(UTIME_OMIT) + ) + } + + let result = futimens(descriptor.rawValue, [lastAccessTimespec, lastDataModificationTimespec]) + guard result == 0 else { + throw FileSystemError.futimens( + errno: Errno(rawValue: result), + path: self.path, + lastAccessTime: lastAccessTime, + lastDataModificationTime: lastDataModificationTime, + location: .here() + ) + } + } + } onUnavailable: { + FileSystemError( + code: .closed, + message: "Couldn't modify file dates, the file '\(sendableView.path)' is closed.", + cause: nil, + location: .here() + ) + } + } + } } @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) diff --git a/Tests/NIOFileSystemIntegrationTests/FileHandleTests.swift b/Tests/NIOFileSystemIntegrationTests/FileHandleTests.swift index 7356a9f12d..39b5da9cab 100644 --- a/Tests/NIOFileSystemIntegrationTests/FileHandleTests.swift +++ b/Tests/NIOFileSystemIntegrationTests/FileHandleTests.swift @@ -1178,6 +1178,121 @@ final class FileHandleTests: XCTestCase { } } } + + func testSetLastAccesTime() async throws { + try await self.withTemporaryFile { handle in + let originalLastDataModificationTime = try await handle.info().lastDataModificationTime + let originalLastAccessTime = try await handle.info().lastAccessTime + + try await handle.setTimes( + lastAccessTime: FileInfo.Timespec(seconds: 10, nanoseconds: 5), + lastModificationTime: nil + ) + + let actualLastAccessTime = try await handle.info().lastAccessTime + XCTAssertEqual(actualLastAccessTime, FileInfo.Timespec(seconds: 10, nanoseconds: 5)) + XCTAssertNotEqual(actualLastAccessTime, originalLastAccessTime) + + let actualLastDataModificationTime = try await handle.info().lastDataModificationTime + XCTAssertEqual(actualLastDataModificationTime, originalLastDataModificationTime) + } + } + + func testSetLastDataModificationTime() async throws { + try await self.withTemporaryFile { handle in + let originalLastDataModificationTime = try await handle.info().lastDataModificationTime + let originalLastAccessTime = try await handle.info().lastAccessTime + + try await handle.setTimes( + lastAccessTime: nil, + lastModificationTime: FileInfo.Timespec(seconds: 10, nanoseconds: 5) + ) + + let actualLastDataModificationTime = try await handle.info().lastDataModificationTime + XCTAssertEqual(actualLastDataModificationTime, FileInfo.Timespec(seconds: 10, nanoseconds: 5)) + XCTAssertNotEqual(actualLastDataModificationTime, originalLastDataModificationTime) + + let actualLastAccessTime = try await handle.info().lastAccessTime + XCTAssertEqual(actualLastAccessTime, originalLastAccessTime) + } + } + + func testSetLastAccessAndLastDataModificationTimes() async throws { + try await self.withTemporaryFile { handle in + let originalLastDataModificationTime = try await handle.info().lastDataModificationTime + let originalLastAccessTime = try await handle.info().lastAccessTime + + try await handle.setTimes( + lastAccessTime: FileInfo.Timespec(seconds: 20, nanoseconds: 25), + lastModificationTime: FileInfo.Timespec(seconds: 10, nanoseconds: 5) + ) + + let actualLastAccessTime = try await handle.info().lastAccessTime + XCTAssertEqual(actualLastAccessTime, FileInfo.Timespec(seconds: 20, nanoseconds: 25)) + XCTAssertNotEqual(actualLastAccessTime, originalLastAccessTime) + + let actualLastDataModificationTime = try await handle.info().lastDataModificationTime + XCTAssertEqual(actualLastDataModificationTime, FileInfo.Timespec(seconds: 10, nanoseconds: 5)) + XCTAssertNotEqual(actualLastDataModificationTime, originalLastDataModificationTime) + } + } + + func testSetLastAccessAndLastDataModificationTimesToNil() async throws { + try await self.withTemporaryFile { handle in + // Set some random value for both times, only to be overwritten by the current time + // right after. + try await handle.setTimes( + lastAccessTime: FileInfo.Timespec(seconds: 1, nanoseconds: 0), + lastModificationTime: FileInfo.Timespec(seconds: 1, nanoseconds: 0) + ) + + var actualLastAccessTime = try await handle.info().lastAccessTime + XCTAssertEqual(actualLastAccessTime, FileInfo.Timespec(seconds: 1, nanoseconds: 0)) + + var actualLastDataModificationTime = try await handle.info().lastDataModificationTime + XCTAssertEqual(actualLastDataModificationTime, FileInfo.Timespec(seconds: 1, nanoseconds: 0)) + + try await handle.setTimes( + lastAccessTime: nil, + lastModificationTime: nil + ) + let estimatedCurrentTime = Date.now.timeIntervalSince1970 + + // Assert that the times are equal to the current time, with up to a second difference (to avoid timing flakiness). + actualLastAccessTime = try await handle.info().lastAccessTime + XCTAssertEqual(Float(actualLastAccessTime.seconds), Float(estimatedCurrentTime), accuracy: 1) + + actualLastDataModificationTime = try await handle.info().lastDataModificationTime + XCTAssertEqual(Float(actualLastDataModificationTime.seconds), Float(estimatedCurrentTime), accuracy: 1) + } + } + + func testTouchFile() async throws { + try await self.withTemporaryFile { handle in + // Set some random value for both times, only to be overwritten by the current time + // right after. + try await handle.setTimes( + lastAccessTime: FileInfo.Timespec(seconds: 1, nanoseconds: 0), + lastModificationTime: FileInfo.Timespec(seconds: 1, nanoseconds: 0) + ) + + var actualLastAccessTime = try await handle.info().lastAccessTime + XCTAssertEqual(actualLastAccessTime, FileInfo.Timespec(seconds: 1, nanoseconds: 0)) + + var actualLastDataModificationTime = try await handle.info().lastDataModificationTime + XCTAssertEqual(actualLastDataModificationTime, FileInfo.Timespec(seconds: 1, nanoseconds: 0)) + + try await handle.touch() + let estimatedCurrentTime = Date.now.timeIntervalSince1970 + + // Assert that the times are equal to the current time, with up to a second difference (to avoid timing flakiness). + actualLastAccessTime = try await handle.info().lastAccessTime + XCTAssertEqual(Float(actualLastAccessTime.seconds), Float(estimatedCurrentTime), accuracy: 1) + + actualLastDataModificationTime = try await handle.info().lastDataModificationTime + XCTAssertEqual(Float(actualLastDataModificationTime.seconds), Float(estimatedCurrentTime), accuracy: 1) + } + } } @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) From 8222158a110c16d705f767fd3e53caeca910aa56 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Thu, 6 Jun 2024 11:14:53 +0100 Subject: [PATCH 2/5] Fix Linux build --- Sources/CNIOLinux/include/CNIOLinux.h | 2 ++ Sources/CNIOLinux/shim.c | 3 +++ .../NIOFileSystem/Internal/SystemFileHandle.swift | 12 ++++++++++-- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/Sources/CNIOLinux/include/CNIOLinux.h b/Sources/CNIOLinux/include/CNIOLinux.h index a75e07f63a..cda1bd5650 100644 --- a/Sources/CNIOLinux/include/CNIOLinux.h +++ b/Sources/CNIOLinux/include/CNIOLinux.h @@ -133,5 +133,7 @@ extern const int CNIOLinux_AT_EMPTY_PATH; extern const unsigned int CNIOLinux_RENAME_NOREPLACE; extern const unsigned int CNIOLinux_RENAME_EXCHANGE; +extern const unsigned long CNIOLinux_UTIME_OMIT; + #endif #endif diff --git a/Sources/CNIOLinux/shim.c b/Sources/CNIOLinux/shim.c index dfe0076abb..20603c40f3 100644 --- a/Sources/CNIOLinux/shim.c +++ b/Sources/CNIOLinux/shim.c @@ -33,6 +33,7 @@ void CNIOLinux_i_do_nothing_just_working_around_a_darwin_toolchain_bug(void) {} #include #include #include +#include _Static_assert(sizeof(CNIOLinux_mmsghdr) == sizeof(struct mmsghdr), "sizes of CNIOLinux_mmsghdr and struct mmsghdr differ"); @@ -211,4 +212,6 @@ const unsigned int CNIOLinux_RENAME_NOREPLACE = RENAME_NOREPLACE; const unsigned int CNIOLinux_RENAME_EXCHANGE = RENAME_EXCHANGE; const int CNIOLinux_AT_EMPTY_PATH = AT_EMPTY_PATH; +const unsigned long CNIOLinux_UTIME_OMIT = UTIME_OMIT; + #endif diff --git a/Sources/NIOFileSystem/Internal/SystemFileHandle.swift b/Sources/NIOFileSystem/Internal/SystemFileHandle.swift index 12508cf662..ced54ef56f 100644 --- a/Sources/NIOFileSystem/Internal/SystemFileHandle.swift +++ b/Sources/NIOFileSystem/Internal/SystemFileHandle.swift @@ -22,8 +22,10 @@ import NIOPosix import Darwin #elseif canImport(Glibc) import Glibc +import CNIOLinux #elseif canImport(Musl) import Musl +import CNIOLinux #endif /// An implementation of ``FileHandleProtocol`` which is backed by system calls and a file @@ -1059,6 +1061,12 @@ extension SystemFileHandle: WritableFileHandleProtocol { // will be set to now. futimens(descriptor.rawValue, nil) } else { + #if canImport(Darwin) + let OMIT_TIME_CHANGE = Int(UTIME_OMIT) + #elseif canImport(Glibc) || canImport(Musl) + let OMIT_TIME_CHANGE = Int(CNIOLinux_UTIME_OMIT) + #endif + let lastAccessTimespec: timespec if let lastAccessTime = lastAccessTime { lastAccessTimespec = timespec( @@ -1070,7 +1078,7 @@ extension SystemFileHandle: WritableFileHandleProtocol { // Note: tv_sec will be ignored. lastAccessTimespec = timespec( tv_sec: 0, - tv_nsec: Int(UTIME_OMIT) + tv_nsec: OMIT_TIME_CHANGE ) } @@ -1085,7 +1093,7 @@ extension SystemFileHandle: WritableFileHandleProtocol { // Note: tv_sec will be ignored. lastDataModificationTimespec = timespec( tv_sec: 0, - tv_nsec: Int(UTIME_OMIT) + tv_nsec: OMIT_TIME_CHANGE ) } From d3cf9efe57bff7a541f0edf24dcda48dd0d4962a Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Fri, 7 Jun 2024 14:02:44 +0100 Subject: [PATCH 3/5] PR changes --- Sources/CNIOLinux/include/CNIOLinux.h | 1 + Sources/CNIOLinux/shim.c | 1 + Sources/NIOFileSystem/FileHandle.swift | 46 ++++- .../NIOFileSystem/FileHandleProtocol.swift | 51 +++--- Sources/NIOFileSystem/FileInfo.swift | 25 +++ .../FileSystemError+Syscall.swift | 10 +- .../Internal/System Calls/Syscall.swift | 10 ++ .../Internal/System Calls/Syscalls.swift | 12 ++ .../Internal/SystemFileHandle.swift | 160 ++++++++++-------- .../FileHandleTests.swift | 24 +-- .../FileSystemErrorTests.swift | 13 ++ .../Internal/SyscallTests.swift | 16 ++ 12 files changed, 241 insertions(+), 128 deletions(-) diff --git a/Sources/CNIOLinux/include/CNIOLinux.h b/Sources/CNIOLinux/include/CNIOLinux.h index cda1bd5650..cde2942fde 100644 --- a/Sources/CNIOLinux/include/CNIOLinux.h +++ b/Sources/CNIOLinux/include/CNIOLinux.h @@ -134,6 +134,7 @@ extern const unsigned int CNIOLinux_RENAME_NOREPLACE; extern const unsigned int CNIOLinux_RENAME_EXCHANGE; extern const unsigned long CNIOLinux_UTIME_OMIT; +extern const unsigned long CNIOLinux_UTIME_NOW; #endif #endif diff --git a/Sources/CNIOLinux/shim.c b/Sources/CNIOLinux/shim.c index 20603c40f3..832cef739e 100644 --- a/Sources/CNIOLinux/shim.c +++ b/Sources/CNIOLinux/shim.c @@ -213,5 +213,6 @@ const unsigned int CNIOLinux_RENAME_EXCHANGE = RENAME_EXCHANGE; const int CNIOLinux_AT_EMPTY_PATH = AT_EMPTY_PATH; const unsigned long CNIOLinux_UTIME_OMIT = UTIME_OMIT; +const unsigned long CNIOLinux_UTIME_NOW = UTIME_NOW; #endif diff --git a/Sources/NIOFileSystem/FileHandle.swift b/Sources/NIOFileSystem/FileHandle.swift index 92a3d08341..2f6f0ed47f 100644 --- a/Sources/NIOFileSystem/FileHandle.swift +++ b/Sources/NIOFileSystem/FileHandle.swift @@ -83,6 +83,16 @@ extension _HasFileHandle { public func close() async throws { try await self.fileHandle.close() } + + public func setTimes( + lastAccess: FileInfo.Timespec?, + lastDataModification: FileInfo.Timespec? + ) async throws { + try await self.fileHandle.setTimes( + lastAccess: lastAccess, + lastDataModification: lastDataModification + ) + } } /// Implements ``FileHandleProtocol`` by making system calls to interact with the local file system. @@ -148,6 +158,16 @@ public struct FileHandle: FileHandleProtocol { public func close() async throws { try await self.systemFileHandle.close() } + + public func setTimes( + lastAccess: FileInfo.Timespec?, + lastDataModification: FileInfo.Timespec? + ) async throws { + try await self.systemFileHandle.setTimes( + lastAccess: lastAccess, + lastDataModification: lastDataModification + ) + } } /// Implements ``ReadableFileHandleProtocol`` by making system calls to interact with the local @@ -173,6 +193,16 @@ public struct ReadFileHandle: ReadableFileHandleProtocol, _HasFileHandle { public func readChunks(in range: Range, chunkLength: ByteCount) -> FileChunks { self.fileHandle.systemFileHandle.readChunks(in: range, chunkLength: chunkLength) } + + public func setTimes( + lastAccess: FileInfo.Timespec?, + lastDataModification: FileInfo.Timespec? + ) async throws { + try await self.fileHandle.systemFileHandle.setTimes( + lastAccess: lastAccess, + lastDataModification: lastDataModification + ) + } } /// Implements ``WritableFileHandleProtocol`` by making system calls to interact with the local @@ -205,12 +235,12 @@ public struct WriteFileHandle: WritableFileHandleProtocol, _HasFileHandle { } public func setTimes( - lastAccessTime: FileInfo.Timespec?, - lastDataModificationTime: FileInfo.Timespec? + lastAccess: FileInfo.Timespec?, + lastDataModification: FileInfo.Timespec? ) async throws { try await self.fileHandle.systemFileHandle.setTimes( - lastAccessTime: lastAccessTime, - lastDataModificationTime: lastDataModificationTime + lastAccess: lastAccess, + lastDataModification: lastDataModification ) } } @@ -259,12 +289,12 @@ public struct ReadWriteFileHandle: ReadableAndWritableFileHandleProtocol, _HasFi } public func setTimes( - lastAccessTime: FileInfo.Timespec?, - lastDataModificationTime: FileInfo.Timespec? + lastAccess: FileInfo.Timespec?, + lastDataModification: FileInfo.Timespec? ) async throws { try await self.fileHandle.systemFileHandle.setTimes( - lastAccessTime: lastAccessTime, - lastDataModificationTime: lastDataModificationTime + lastAccess: lastAccess, + lastDataModification: lastDataModification ) } } diff --git a/Sources/NIOFileSystem/FileHandleProtocol.swift b/Sources/NIOFileSystem/FileHandleProtocol.swift index 5e51a9fa6a..56c0f9ca00 100644 --- a/Sources/NIOFileSystem/FileHandleProtocol.swift +++ b/Sources/NIOFileSystem/FileHandleProtocol.swift @@ -15,15 +15,6 @@ #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(Linux) || os(Android) import NIOCore import SystemPackage -#if canImport(Darwin) -import Darwin -#elseif canImport(Glibc) -import Glibc -#elseif canImport(Musl) -import Musl -#else -#error("The File Handle Protocol module was unable to identify your C library.") -#endif /// A handle for a file system object. /// @@ -152,6 +143,26 @@ public protocol FileHandleProtocol { /// /// After closing the handle calls to other functions will throw an appropriate error. func close() async throws + + /// Sets the file's last access and last data modification times to the given values. + /// + /// If **either** time is `nil`, the current value will not be changed. + /// If **both** times are `nil`, then both times will be set to the current time. + /// + /// > Important: Times are only considered valid if their nanoseconds components are one of the following: + /// > - `UTIME_NOW` (you can use ``FileInfo/Timespec/now`` to get a Timespec set to this value), + /// > - `UTIME_OMIT` (you can use ``FileInfo/Timespec/omit`` to get a Timespec set to this value),, + /// > - Greater than zero and no larger than 1000 million + /// + /// - Parameters: + /// - lastAccessTime: The new value of the file's last access time, as time elapsed since the Epoch. + /// - lastDataModificationTime: The new value of the file's last data modification time, as time elapsed since the Epoch. + /// + /// - Throws: If there's an error updating the times. If this happens, the original values won't be modified. + func setTimes( + lastAccess: FileInfo.Timespec?, + lastDataModification: FileInfo.Timespec? + ) async throws } // MARK: - Readable @@ -472,26 +483,6 @@ public protocol WritableFileHandleProtocol: FileHandleProtocol { /// filesystem with the expected name, otherwise no file will be created or the original /// file won't be modified (if one existed). func close(makeChangesVisible: Bool) async throws - - /// Sets the file's last access and last data modification times to the given values. - /// - /// If **either** time is `nil`, the current value will not be changed. - /// If **both** times are `nil`, then both times will be set to the current time. - /// - /// > Important: Times are only considered valid if their nanoseconds components are one of the following: - /// > - `UTIME_NOW`, - /// > - `UTIME_OMIT`, - /// > - Greater than zero and no larger than 1000 million - /// - /// - Parameters: - /// - lastAccessTime: The new value of the file's last access time, as time elapsed since the Epoch. - /// - lastDataModificationTime: The new value of the file's last data modification time, as time elapsed since the Epoch. - /// - /// - Throws: If there's an error updating the times. If this happens, the original values won't be modified. - func setTimes( - lastAccessTime: FileInfo.Timespec?, - lastDataModificationTime: FileInfo.Timespec? - ) async throws } @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) @@ -520,7 +511,7 @@ extension WritableFileHandleProtocol { /// /// - Throws: If there's an error updating the times. If this happens, the original values won't be modified. public func touch() async throws { - try await self.setTimes(lastAccessTime: nil, lastDataModificationTime: nil) + try await self.setTimes(lastAccess: nil, lastDataModification: nil) } } diff --git a/Sources/NIOFileSystem/FileInfo.swift b/Sources/NIOFileSystem/FileInfo.swift index 7f174bf6d7..836dc43b9c 100644 --- a/Sources/NIOFileSystem/FileInfo.swift +++ b/Sources/NIOFileSystem/FileInfo.swift @@ -19,8 +19,10 @@ import SystemPackage import Darwin #elseif canImport(Glibc) import Glibc +import CNIOLinux #elseif canImport(Musl) import Musl +import CNIOLinux #endif /// Information about a file system object. @@ -145,6 +147,29 @@ extension FileInfo { /// A time interval consisting of whole seconds and nanoseconds. public struct Timespec: Hashable, Sendable { + #if canImport(Darwin) + private static let UTIME_OMIT_INT = Int(UTIME_OMIT) + private static let UTIME_NOW_INT = Int(UTIME_NOW) + #elseif canImport(Glibc) || canImport(Musl) + private static let UTIME_OMIT_INT = Int(CNIOLinux_UTIME_OMIT) + private static let UTIME_NOW_INT = Int(CNIOLinux_UTIME_NOW) + #endif + + /// A timespec where the seconds are set to zero and the nanoseconds set to `UTIME_OMIT`. + /// In syscalls such as `futimens`, this means the time component set to this value will be ignored. + public static var omit = Self.init( + seconds: 0, + nanoseconds: Self.UTIME_OMIT_INT + ) + + /// A timespec where the seconds are set to zero and the nanoseconds set to `UTIME_NOW`. + /// In syscalls such as `futimens`, this means the time component set to this value will be + /// be set to the current time or the largest value supported by the platform, whichever is smaller. + public static var now = Self.init( + seconds: 0, + nanoseconds: Self.UTIME_NOW_INT + ) + /// The number of seconds. public var seconds: Int diff --git a/Sources/NIOFileSystem/FileSystemError+Syscall.swift b/Sources/NIOFileSystem/FileSystemError+Syscall.swift index 96aee327eb..418bfd0701 100644 --- a/Sources/NIOFileSystem/FileSystemError+Syscall.swift +++ b/Sources/NIOFileSystem/FileSystemError+Syscall.swift @@ -1067,6 +1067,7 @@ extension FileSystemError { ) } + @_spi(Testing) public static func futimens( errno: Errno, path: FilePath, @@ -1082,16 +1083,9 @@ extension FileSystemError { code = .permissionDenied message = "Not permited to change last access or last data modification times for \(path)." - case .invalidArgument: - code = .invalidArgument - message = """ - Invalid times value: nanoseconds for both timespecs must be UTIME_NOW, UTIME_OMIT, or a number between 0 and 1000 million - (last access time: \(String(describing: lastAccessTime)), last data modification time: \(String(describing: lastDataModificationTime))). - """ - case .readOnlyFileSystem: code = .unsupported - message = "Not permited to change last access or last data modification times for \(path): this is a read-only file system." + message = "Not permitted to change last access or last data modification times for \(path): this is a read-only file system." case .badFileDescriptor: code = .closed diff --git a/Sources/NIOFileSystem/Internal/System Calls/Syscall.swift b/Sources/NIOFileSystem/Internal/System Calls/Syscall.swift index 06b2689165..308c239c0f 100644 --- a/Sources/NIOFileSystem/Internal/System Calls/Syscall.swift +++ b/Sources/NIOFileSystem/Internal/System Calls/Syscall.swift @@ -266,6 +266,16 @@ public enum Syscall { } } #endif + + @_spi(Testing) + public static func futimens( + fileDescriptor fd: FileDescriptor, + times: UnsafePointer? + ) -> Result { + nothingOrErrno(retryOnInterrupt: false) { + system_futimens(fd.rawValue, times) + } + } } @_spi(Testing) diff --git a/Sources/NIOFileSystem/Internal/System Calls/Syscalls.swift b/Sources/NIOFileSystem/Internal/System Calls/Syscalls.swift index 120500c27f..284d59d779 100644 --- a/Sources/NIOFileSystem/Internal/System Calls/Syscalls.swift +++ b/Sources/NIOFileSystem/Internal/System Calls/Syscalls.swift @@ -349,6 +349,18 @@ internal func system_sendfile( } #endif +internal func system_futimens( + _ fd: CInt, + _ times: UnsafePointer? +) -> CInt { + #if ENABLE_MOCKING + if mockingEnabled { + return mock(fd, times) + } + #endif + return futimens(fd, times) +} + // MARK: - libc /// fdopendir(3): Opens a directory stream for the file descriptor diff --git a/Sources/NIOFileSystem/Internal/SystemFileHandle.swift b/Sources/NIOFileSystem/Internal/SystemFileHandle.swift index ced54ef56f..7c6a6f3d10 100644 --- a/Sources/NIOFileSystem/Internal/SystemFileHandle.swift +++ b/Sources/NIOFileSystem/Internal/SystemFileHandle.swift @@ -357,6 +357,18 @@ extension SystemFileHandle: FileHandleProtocol { } } + public func setTimes( + lastAccess: FileInfo.Timespec?, + lastDataModification: FileInfo.Timespec? + ) async throws { + try await self.threadPool.runIfActive { [sendableView] in + try sendableView._setTimes( + lastAccess: lastAccess, + lastDataModification: lastDataModification + ) + } + } + @_spi(Testing) public enum UpdatePermissionsOperation { case set, add, remove } } @@ -911,6 +923,84 @@ extension SystemFileHandle.SendableView { return result } + + func _setTimes( + lastAccess: FileInfo.Timespec?, + lastDataModification: FileInfo.Timespec? + ) throws { + try self._withUnsafeDescriptor { descriptor in + let syscallResult: Result + switch (lastAccess, lastDataModification) { + case (.none, .none): + // If the timespec array is nil, as per the `futimens` docs, + // both the last accessed and last modification times + // will be set to now. + syscallResult = Syscall.futimens( + fileDescriptor: descriptor, + times: nil + ) + + case (.some(let lastAccess), .none): + // Don't modify the last modification time. + syscallResult = Syscall.futimens( + fileDescriptor: descriptor, + times: [timespec(lastAccess), timespec(.omit)] + ) + + case (.none, .some(let lastDataModification)): + // Don't modify the last access time. + syscallResult = Syscall.futimens( + fileDescriptor: descriptor, + times: [timespec(.omit), timespec(lastDataModification)] + ) + + case (.some(let lastAccess), .some(let lastDataModification)): + syscallResult = Syscall.futimens( + fileDescriptor: descriptor, + times: [timespec(lastAccess), timespec(lastDataModification)] + ) + } + + try syscallResult.mapError { errno in + FileSystemError.futimens( + errno: errno, + path: self.path, + lastAccessTime: lastAccess, + lastDataModificationTime: lastDataModification, + location: .here() + ) + }.get() + } onUnavailable: { + FileSystemError( + code: .closed, + message: "Couldn't modify file dates, the file '\(self.path)' is closed.", + cause: nil, + location: .here() + ) + } + } +} + +fileprivate extension timespec { + init(_ fileinfoTimespec: FileInfo.Timespec) { + // Clamp seconds to be positive + let seconds = max(0, fileinfoTimespec.seconds) + + // If nanoseconds are not UTIME_NOW or UTIME_OMIT, clamp to be between + // 0 and 1,000 million. + let nanoseconds: Int + switch fileinfoTimespec { + case .now, .omit: + nanoseconds = fileinfoTimespec.nanoseconds + default: + nanoseconds = min(1_000_000_000, max(0, fileinfoTimespec.nanoseconds)) + } + + self.init( + tv_sec: seconds, + tv_nsec: nanoseconds + ) + } } // MARK: - Readable File Handle @@ -1048,76 +1138,6 @@ extension SystemFileHandle: WritableFileHandleProtocol { try sendableView._resize(to: size).get() } } - - public func setTimes( - lastAccessTime: FileInfo.Timespec?, - lastDataModificationTime: FileInfo.Timespec? - ) async throws { - try await self.threadPool.runIfActive { [sendableView] in - try sendableView._withUnsafeDescriptor { descriptor in - if lastAccessTime == nil, lastDataModificationTime == nil { - // If the timespec array is nil, as per the `futimens` docs, - // both the last accessed and last modification times - // will be set to now. - futimens(descriptor.rawValue, nil) - } else { - #if canImport(Darwin) - let OMIT_TIME_CHANGE = Int(UTIME_OMIT) - #elseif canImport(Glibc) || canImport(Musl) - let OMIT_TIME_CHANGE = Int(CNIOLinux_UTIME_OMIT) - #endif - - let lastAccessTimespec: timespec - if let lastAccessTime = lastAccessTime { - lastAccessTimespec = timespec( - tv_sec: lastAccessTime.seconds, - tv_nsec: lastAccessTime.nanoseconds - ) - } else { - // Don't modify the last access time. - // Note: tv_sec will be ignored. - lastAccessTimespec = timespec( - tv_sec: 0, - tv_nsec: OMIT_TIME_CHANGE - ) - } - - let lastDataModificationTimespec: timespec - if let lastDataModificationTime = lastDataModificationTime { - lastDataModificationTimespec = timespec( - tv_sec: lastDataModificationTime.seconds, - tv_nsec: lastDataModificationTime.nanoseconds - ) - } else { - // Don't modify the last modification time. - // Note: tv_sec will be ignored. - lastDataModificationTimespec = timespec( - tv_sec: 0, - tv_nsec: OMIT_TIME_CHANGE - ) - } - - let result = futimens(descriptor.rawValue, [lastAccessTimespec, lastDataModificationTimespec]) - guard result == 0 else { - throw FileSystemError.futimens( - errno: Errno(rawValue: result), - path: self.path, - lastAccessTime: lastAccessTime, - lastDataModificationTime: lastDataModificationTime, - location: .here() - ) - } - } - } onUnavailable: { - FileSystemError( - code: .closed, - message: "Couldn't modify file dates, the file '\(sendableView.path)' is closed.", - cause: nil, - location: .here() - ) - } - } - } } @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) diff --git a/Tests/NIOFileSystemIntegrationTests/FileHandleTests.swift b/Tests/NIOFileSystemIntegrationTests/FileHandleTests.swift index 39b5da9cab..ffcd85af17 100644 --- a/Tests/NIOFileSystemIntegrationTests/FileHandleTests.swift +++ b/Tests/NIOFileSystemIntegrationTests/FileHandleTests.swift @@ -1185,8 +1185,8 @@ final class FileHandleTests: XCTestCase { let originalLastAccessTime = try await handle.info().lastAccessTime try await handle.setTimes( - lastAccessTime: FileInfo.Timespec(seconds: 10, nanoseconds: 5), - lastModificationTime: nil + lastAccess: FileInfo.Timespec(seconds: 10, nanoseconds: 5), + lastDataModification: nil ) let actualLastAccessTime = try await handle.info().lastAccessTime @@ -1204,8 +1204,8 @@ final class FileHandleTests: XCTestCase { let originalLastAccessTime = try await handle.info().lastAccessTime try await handle.setTimes( - lastAccessTime: nil, - lastModificationTime: FileInfo.Timespec(seconds: 10, nanoseconds: 5) + lastAccess: nil, + lastDataModification: FileInfo.Timespec(seconds: 10, nanoseconds: 5) ) let actualLastDataModificationTime = try await handle.info().lastDataModificationTime @@ -1223,8 +1223,8 @@ final class FileHandleTests: XCTestCase { let originalLastAccessTime = try await handle.info().lastAccessTime try await handle.setTimes( - lastAccessTime: FileInfo.Timespec(seconds: 20, nanoseconds: 25), - lastModificationTime: FileInfo.Timespec(seconds: 10, nanoseconds: 5) + lastAccess: FileInfo.Timespec(seconds: 20, nanoseconds: 25), + lastDataModification: FileInfo.Timespec(seconds: 10, nanoseconds: 5) ) let actualLastAccessTime = try await handle.info().lastAccessTime @@ -1242,8 +1242,8 @@ final class FileHandleTests: XCTestCase { // Set some random value for both times, only to be overwritten by the current time // right after. try await handle.setTimes( - lastAccessTime: FileInfo.Timespec(seconds: 1, nanoseconds: 0), - lastModificationTime: FileInfo.Timespec(seconds: 1, nanoseconds: 0) + lastAccess: FileInfo.Timespec(seconds: 1, nanoseconds: 0), + lastDataModification: FileInfo.Timespec(seconds: 1, nanoseconds: 0) ) var actualLastAccessTime = try await handle.info().lastAccessTime @@ -1253,8 +1253,8 @@ final class FileHandleTests: XCTestCase { XCTAssertEqual(actualLastDataModificationTime, FileInfo.Timespec(seconds: 1, nanoseconds: 0)) try await handle.setTimes( - lastAccessTime: nil, - lastModificationTime: nil + lastAccess: nil, + lastDataModification: nil ) let estimatedCurrentTime = Date.now.timeIntervalSince1970 @@ -1272,8 +1272,8 @@ final class FileHandleTests: XCTestCase { // Set some random value for both times, only to be overwritten by the current time // right after. try await handle.setTimes( - lastAccessTime: FileInfo.Timespec(seconds: 1, nanoseconds: 0), - lastModificationTime: FileInfo.Timespec(seconds: 1, nanoseconds: 0) + lastAccess: FileInfo.Timespec(seconds: 1, nanoseconds: 0), + lastDataModification: FileInfo.Timespec(seconds: 1, nanoseconds: 0) ) var actualLastAccessTime = try await handle.info().lastAccessTime diff --git a/Tests/NIOFileSystemTests/FileSystemErrorTests.swift b/Tests/NIOFileSystemTests/FileSystemErrorTests.swift index 308b21da80..c48744704e 100644 --- a/Tests/NIOFileSystemTests/FileSystemErrorTests.swift +++ b/Tests/NIOFileSystemTests/FileSystemErrorTests.swift @@ -558,6 +558,19 @@ final class FileSystemErrorTests: XCTestCase { } } + func testErrnoMapping_futimens() { + self.testErrnoToErrorCode( + expected: [ + .permissionDenied: .permissionDenied, + .notPermitted: .permissionDenied, + .readOnlyFileSystem: .unsupported, + .badFileDescriptor: .closed, + ] + ) { errno in + .futimens(errno: errno, path: "", lastAccessTime: nil, lastDataModificationTime: nil, location: .fixed) + } + } + private func testErrnoToErrorCode( expected mapping: [Errno: FileSystemError.Code], _ makeError: (Errno) -> FileSystemError diff --git a/Tests/NIOFileSystemTests/Internal/SyscallTests.swift b/Tests/NIOFileSystemTests/Internal/SyscallTests.swift index 2c22d87a19..abd7209c95 100644 --- a/Tests/NIOFileSystemTests/Internal/SyscallTests.swift +++ b/Tests/NIOFileSystemTests/Internal/SyscallTests.swift @@ -398,6 +398,22 @@ final class SyscallTests: XCTestCase { testCases.run() } + func test_futimens() throws { + let fd = FileDescriptor(rawValue: 42) + let times = timespec(tv_sec: 1, tv_nsec: 1) + withUnsafePointer(to: times) { unsafeTimesPointer in + let testCases = [ + MockTestCase(name: "futimens", .noInterrupt, 42, unsafeTimesPointer) { _ in + try Syscall.futimens( + fileDescriptor: fd, + times: unsafeTimesPointer + ).get() + } + ] + testCases.run() + } + } + func testValueOrErrno() throws { let r1: Result = valueOrErrno(retryOnInterrupt: false) { Errno._current = .addressInUse From 62afbbb58e337d79629a76bd25ad1ca52b3c2f00 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Fri, 7 Jun 2024 16:52:24 +0100 Subject: [PATCH 4/5] PR changes --- Sources/NIOFileSystem/FileHandleProtocol.swift | 5 +++-- Sources/NIOFileSystem/FileInfo.swift | 16 ++++++++-------- .../NIOFileSystem/FileSystemError+Syscall.swift | 2 +- .../Internal/SystemFileHandle.swift | 4 ++-- .../FileSystemErrorTests.swift | 2 +- 5 files changed, 15 insertions(+), 14 deletions(-) diff --git a/Sources/NIOFileSystem/FileHandleProtocol.swift b/Sources/NIOFileSystem/FileHandleProtocol.swift index 56c0f9ca00..2eed2e2998 100644 --- a/Sources/NIOFileSystem/FileHandleProtocol.swift +++ b/Sources/NIOFileSystem/FileHandleProtocol.swift @@ -151,8 +151,9 @@ public protocol FileHandleProtocol { /// /// > Important: Times are only considered valid if their nanoseconds components are one of the following: /// > - `UTIME_NOW` (you can use ``FileInfo/Timespec/now`` to get a Timespec set to this value), - /// > - `UTIME_OMIT` (you can use ``FileInfo/Timespec/omit`` to get a Timespec set to this value),, - /// > - Greater than zero and no larger than 1000 million + /// > - `UTIME_OMIT` (you can use ``FileInfo/Timespec/omit`` to get a Timespec set to this value), + /// > - Greater than zero and no larger than 1000 million: if outside of this range, the value will be clamped to the closest valid value. + /// > The seconds component must also be positive: if it's not, zero will be used as the value instead. /// /// - Parameters: /// - lastAccessTime: The new value of the file's last access time, as time elapsed since the Epoch. diff --git a/Sources/NIOFileSystem/FileInfo.swift b/Sources/NIOFileSystem/FileInfo.swift index 836dc43b9c..3df14db1c1 100644 --- a/Sources/NIOFileSystem/FileInfo.swift +++ b/Sources/NIOFileSystem/FileInfo.swift @@ -148,26 +148,26 @@ extension FileInfo { /// A time interval consisting of whole seconds and nanoseconds. public struct Timespec: Hashable, Sendable { #if canImport(Darwin) - private static let UTIME_OMIT_INT = Int(UTIME_OMIT) - private static let UTIME_NOW_INT = Int(UTIME_NOW) + private static let utimeOmit = Int(UTIME_OMIT) + private static let utimeNow = Int(UTIME_NOW) #elseif canImport(Glibc) || canImport(Musl) - private static let UTIME_OMIT_INT = Int(CNIOLinux_UTIME_OMIT) - private static let UTIME_NOW_INT = Int(CNIOLinux_UTIME_NOW) + private static let utimeOmit = Int(CNIOLinux_UTIME_OMIT) + private static let utimeNow = Int(CNIOLinux_UTIME_NOW) #endif /// A timespec where the seconds are set to zero and the nanoseconds set to `UTIME_OMIT`. /// In syscalls such as `futimens`, this means the time component set to this value will be ignored. - public static var omit = Self.init( + public static let omit = Self( seconds: 0, - nanoseconds: Self.UTIME_OMIT_INT + nanoseconds: Self.utimeOmit ) /// A timespec where the seconds are set to zero and the nanoseconds set to `UTIME_NOW`. /// In syscalls such as `futimens`, this means the time component set to this value will be /// be set to the current time or the largest value supported by the platform, whichever is smaller. - public static var now = Self.init( + public static let now = Self( seconds: 0, - nanoseconds: Self.UTIME_NOW_INT + nanoseconds: Self.utimeNow ) /// The number of seconds. diff --git a/Sources/NIOFileSystem/FileSystemError+Syscall.swift b/Sources/NIOFileSystem/FileSystemError+Syscall.swift index 418bfd0701..a59185bb25 100644 --- a/Sources/NIOFileSystem/FileSystemError+Syscall.swift +++ b/Sources/NIOFileSystem/FileSystemError+Syscall.swift @@ -1081,7 +1081,7 @@ extension FileSystemError { switch errno { case .permissionDenied, .notPermitted: code = .permissionDenied - message = "Not permited to change last access or last data modification times for \(path)." + message = "Not permitted to change last access or last data modification times for \(path)." case .readOnlyFileSystem: code = .unsupported diff --git a/Sources/NIOFileSystem/Internal/SystemFileHandle.swift b/Sources/NIOFileSystem/Internal/SystemFileHandle.swift index 7c6a6f3d10..34c8f62353 100644 --- a/Sources/NIOFileSystem/Internal/SystemFileHandle.swift +++ b/Sources/NIOFileSystem/Internal/SystemFileHandle.swift @@ -981,8 +981,8 @@ extension SystemFileHandle.SendableView { } } -fileprivate extension timespec { - init(_ fileinfoTimespec: FileInfo.Timespec) { +extension timespec { + fileprivate init(_ fileinfoTimespec: FileInfo.Timespec) { // Clamp seconds to be positive let seconds = max(0, fileinfoTimespec.seconds) diff --git a/Tests/NIOFileSystemTests/FileSystemErrorTests.swift b/Tests/NIOFileSystemTests/FileSystemErrorTests.swift index c48744704e..f32165f622 100644 --- a/Tests/NIOFileSystemTests/FileSystemErrorTests.swift +++ b/Tests/NIOFileSystemTests/FileSystemErrorTests.swift @@ -567,7 +567,7 @@ final class FileSystemErrorTests: XCTestCase { .badFileDescriptor: .closed, ] ) { errno in - .futimens(errno: errno, path: "", lastAccessTime: nil, lastDataModificationTime: nil, location: .fixed) + .futimens(errno: errno, path: "", lastAccessTime: nil, lastDataModificationTime: nil, location: .fixed) } } From 87c7c992c5b2816c2f3c62752c846c875eea1c25 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Mon, 10 Jun 2024 15:31:36 +0100 Subject: [PATCH 5/5] Add some new convenience APIs --- .../NIOFileSystem/FileHandleProtocol.swift | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/Sources/NIOFileSystem/FileHandleProtocol.swift b/Sources/NIOFileSystem/FileHandleProtocol.swift index 2eed2e2998..e7607f7829 100644 --- a/Sources/NIOFileSystem/FileHandleProtocol.swift +++ b/Sources/NIOFileSystem/FileHandleProtocol.swift @@ -507,7 +507,30 @@ extension WritableFileHandleProtocol { ) async throws -> Int64 { try await self.write(contentsOf: buffer.readableBytesView, toAbsoluteOffset: offset) } - +} + +// MARK: - File times modifiers + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +extension FileHandleProtocol { + /// Sets the file's last access time to the given time. + /// + /// - Parameter time: The time to which the file's last access time should be set. + /// + /// - Throws: If there's an error updating the time. If this happens, the original value won't be modified. + public func setLastAccessTime(to time: FileInfo.Timespec) async throws { + try await self.setTimes(lastAccess: time, lastDataModification: nil) + } + + /// Sets the file's last data modification time to the given time. + /// + /// - Parameter time: The time to which the file's last data modification time should be set. + /// + /// - Throws: If there's an error updating the time. If this happens, the original value won't be modified. + public func setLastDataModificationTime(to time: FileInfo.Timespec) async throws { + try await self.setTimes(lastAccess: nil, lastDataModification: time) + } + /// Sets the file's last access and last data modification times to the current time. /// /// - Throws: If there's an error updating the times. If this happens, the original values won't be modified.