Skip to content

Commit 32b8d90

Browse files
Honza Dvorskyaciidgh
authored andcommitted
Add atomic writes to FileSystem and implement it for LocalFileSystem (#1617)
* Add atomic writes to FileSystem and implement it for LocalFileSystem * Add header doc * Cleanup temp file if write/rename fails, verify with test * "Make" in-memory file system atomic * Add an option to not delete TemporaryFile on dealloc * Adapted to Ankit's feedback * Cleanup previous commit
1 parent 971fece commit 32b8d90

File tree

2 files changed

+56
-1
lines changed

2 files changed

+56
-1
lines changed

Sources/Basic/FileSystem.swift

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,11 @@ public protocol FileSystem: class {
166166
// FIXME: This is obviously not a very efficient or flexible API.
167167
func writeFileContents(_ path: AbsolutePath, bytes: ByteString) throws
168168

169+
/// Write the contents of a file.
170+
//
171+
// FIXME: This is obviously not a very efficient or flexible API.
172+
func writeFileContents(_ path: AbsolutePath, bytes: ByteString, atomically: Bool) throws
173+
169174
/// Recursively deletes the file system entity at `path`.
170175
///
171176
/// If there is no file system entity at `path`, this function does nothing (in particular, this is not considered
@@ -193,6 +198,15 @@ public extension FileSystem {
193198
func chmod(_ mode: FileMode, path: AbsolutePath) throws {
194199
try chmod(mode, path: path, options: [])
195200
}
201+
202+
// Unless the file system type provides an override for this method, throw
203+
// if `atomically` is `true`, otherwise fall back to whatever implementation already exists.
204+
func writeFileContents(_ path: AbsolutePath, bytes: ByteString, atomically: Bool) throws {
205+
guard !atomically else {
206+
throw FileSystemError.unsupported
207+
}
208+
try writeFileContents(path, bytes: bytes)
209+
}
196210

197211
/// Write to a file from a stream producer.
198212
func writeFileContents(_ path: AbsolutePath, body: (OutputByteStream) -> Void) throws {
@@ -351,6 +365,24 @@ private class LocalFileSystem: FileSystem {
351365
break
352366
}
353367
}
368+
369+
func writeFileContents(_ path: AbsolutePath, bytes: ByteString, atomically: Bool) throws {
370+
// Perform non-atomic writes using the fast path.
371+
if !atomically {
372+
return try writeFileContents(path, bytes: bytes)
373+
}
374+
let temp = try TemporaryFile(dir: path.parentDirectory, deleteOnClose: false)
375+
do {
376+
try writeFileContents(temp.path, bytes: bytes)
377+
try rename(temp.path, to: path)
378+
} catch {
379+
// Write or rename failed, delete the temporary file.
380+
// Rethrow the original error, however, as that's the
381+
// root cause of the failure.
382+
_ = try? self.removeFileTree(temp.path)
383+
throw error
384+
}
385+
}
354386

355387
func removeFileTree(_ path: AbsolutePath) throws {
356388
if self.exists(path, followSymlink: false) {
@@ -681,6 +713,12 @@ public class InMemoryFileSystem: FileSystem {
681713
// Write the file.
682714
contents.entries[path.basename] = Node(.file(bytes))
683715
}
716+
717+
public func writeFileContents(_ path: AbsolutePath, bytes: ByteString, atomically: Bool) throws {
718+
// In memory file system's writeFileContents is already atomic, so ignore the parameter here
719+
// and just call the base implementation.
720+
try writeFileContents(path, bytes: bytes)
721+
}
684722

685723
public func removeFileTree(_ path: AbsolutePath) throws {
686724
// Ignore root and get the parent node's content if its a directory.

Tests/BasicTests/FileSystemTests.swift

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import XCTest
1313
import Basic
1414
import TestSupport
1515
import SPMLibc
16+
import POSIX
1617

1718
class FileSystemTests: XCTestCase {
1819

@@ -134,7 +135,23 @@ class FileSystemTests: XCTestCase {
134135
XCTAssertTrue(fs.isFile(filePath))
135136
let data = try! fs.readFileContents(filePath)
136137
XCTAssertEqual(data, ByteString(testData))
137-
138+
139+
// Atomic writes
140+
let inMemoryFilePath = AbsolutePath("/file.text")
141+
XCTAssertNoThrow(try Basic.InMemoryFileSystem(files: [:]).writeFileContents(inMemoryFilePath, bytes: ByteString(testData), atomically: true))
142+
XCTAssertNoThrow(try Basic.InMemoryFileSystem(files: [:]).writeFileContents(inMemoryFilePath, bytes: ByteString(testData), atomically: false))
143+
// Local file system does support atomic writes, so it doesn't throw.
144+
let byteString = ByteString(testData)
145+
let filePath1 = tmpDir.path.appending(components: "test-data-1.txt")
146+
XCTAssertNoThrow(try fs.writeFileContents(filePath1, bytes: byteString, atomically: false))
147+
let read1 = try fs.readFileContents(filePath1)
148+
XCTAssertEqual(read1, byteString)
149+
150+
let filePath2 = tmpDir.path.appending(components: "test-data-2.txt")
151+
XCTAssertNoThrow(try fs.writeFileContents(filePath2, bytes: byteString, atomically: true))
152+
let read2 = try fs.readFileContents(filePath2)
153+
XCTAssertEqual(read2, byteString)
154+
138155
// Check overwrite of a file.
139156
try! fs.writeFileContents(filePath, bytes: "Hello, new world!")
140157
XCTAssertEqual(try! fs.readFileContents(filePath), "Hello, new world!")

0 commit comments

Comments
 (0)