Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
45 changes: 45 additions & 0 deletions Sources/FileSystem/FileSystem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,17 @@ public protocol FileSysteming: Sendable {
/// - Returns: The file metadata.
func fileMetadata(at path: AbsolutePath) async throws -> FileMetadata?

/// Sets the last access and modification times of a file or directory.
/// - Parameters:
/// - path: The absolute path to the file or directory.
/// - lastAccessDate: The last access date. Pass `nil` to leave unchanged.
/// - lastModificationDate: The last modification date. Pass `nil` to leave unchanged.
func setFileTimes(
of path: AbsolutePath,
lastAccessDate: Date?,
lastModificationDate: Date?
) async throws

/// Given a path, it replaces it with the file or directory at the other path.
/// - Parameters:
/// - to: The path to be replaced.
Expand Down Expand Up @@ -802,6 +813,40 @@ public struct FileSystem: FileSysteming, Sendable {
#endif
}

public func setFileTimes(
of path: AbsolutePath,
lastAccessDate: Date?,
lastModificationDate: Date?
) async throws {
logger?.debug("Setting file times at path \(path.pathString).")

#if os(Windows)
var attributes: [FileAttributeKey: Any] = [:]
if let lastModificationDate {
attributes[.modificationDate] = lastModificationDate
}
if !attributes.isEmpty {
try FileManager.default.setAttributes(attributes, ofItemAtPath: path.pathString)
}
#else
let lastAccess = lastAccessDate.map { Self.dateToTimespec($0) }
let lastModification = lastModificationDate.map { Self.dateToTimespec($0) }
try await _NIOFileSystem.FileSystem.shared.withFileHandle(
forReadingAt: .init(path.pathString)
) { handle in
try await handle.setTimes(lastAccess: lastAccess, lastDataModification: lastModification)
}
#endif
}

#if !os(Windows)
private static func dateToTimespec(_ date: Date) -> _NIOFileSystem.FileInfo.Timespec {
let seconds = Int(date.timeIntervalSince1970)
let nanoseconds = Int((date.timeIntervalSince1970 - Double(seconds)) * 1_000_000_000)
return _NIOFileSystem.FileInfo.Timespec(seconds: seconds, nanoseconds: nanoseconds)
}
#endif

public func locateTraversingUp(from: AbsolutePath, relativePath: RelativePath) async throws -> AbsolutePath? {
logger?.debug("Locating the relative path \(relativePath.pathString) by traversing up from \(from.pathString).")
let path = from.appending(relativePath)
Expand Down
20 changes: 20 additions & 0 deletions Tests/FileSystemTests/FileSystemTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,26 @@ private struct TestError: Error, Equatable {}
}
}

func test_setFileTimes_modificationDate() async throws {
try await subject.runInTemporaryDirectory(prefix: "FileSystem") { temporaryDirectory in
// Given
let path = temporaryDirectory.appending(component: "file")
try await subject.touch(path)
let pastDate = Date(timeIntervalSince1970: 1_000_000)

// When
try await subject.setFileTimes(of: path, lastAccessDate: nil, lastModificationDate: pastDate)

// Then
let metadata = try await subject.fileMetadata(at: path)
XCTAssertEqual(
metadata?.lastModificationDate.timeIntervalSince1970 ?? 0,
pastDate.timeIntervalSince1970,
accuracy: 1.0
)
}
}

func test_replace_replaces_when_replacingPathIsADirectory_and_targetDirectoryIsAbsent() async throws {
try await subject.runInTemporaryDirectory(prefix: "FileSystem") { temporaryDirectory in
// Given
Expand Down
Loading