Skip to content

Commit 50d0419

Browse files
authored
Ensure cache writes create parent directory (#5986)
* Ensure cache writes create parent directory * camel case * added missing test
1 parent bcc72e4 commit 50d0419

File tree

2 files changed

+82
-19
lines changed

2 files changed

+82
-19
lines changed

Sources/Caching/LargeItemCacheType.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ extension FileManager: LargeItemCacheType {
5151

5252
/// Store data to a url
5353
func saveData(_ data: Data, to url: URL) throws {
54+
let directoryURL = url.deletingLastPathComponent()
55+
try createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil)
5456
try data.write(to: url)
5557
}
5658

@@ -111,6 +113,8 @@ extension FileManager: LargeItemCacheType {
111113

112114
// If all succeeds, move the temporary file to the more permanant storage location
113115
// effectively a "save" operation
116+
let directoryURL = url.deletingLastPathComponent()
117+
try createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil)
114118
try moveItem(at: tempFileURL, to: url)
115119
}
116120

Tests/UnitTests/Caching/LargeItemCacheTypeTests.swift

Lines changed: 78 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import XCTest
1717

1818
@MainActor
1919
@available(iOS 15.0, macOS 12.0, tvOS 15.0, visionOS 1.0, watchOS 8.0, *)
20-
class LargeItemCacheTypeTests: TestCase {
20+
final class LargeItemCacheTypeTests: TestCase {
2121

2222
private var fileManager = FileManager.default
2323
private lazy var testDirectory = fileManager
@@ -26,7 +26,7 @@ class LargeItemCacheTypeTests: TestCase {
2626

2727
// MARK: - saveData Tests
2828

29-
func test_saveData_writesDataToFile() async throws {
29+
func testSaveDataWritesDataToFile() async throws {
3030
let testData = "Hello, World!"
3131
let url = self.testDirectory.appendingPathComponent("test.txt")
3232
let stream = createAsyncStream(from: testData)
@@ -40,7 +40,47 @@ class LargeItemCacheTypeTests: TestCase {
4040
try fileManager.removeItem(at: url)
4141
}
4242

43-
func test_saveData_withValidChecksum_succeeds() async throws {
43+
func testSaveDataCreatesParentDirectoryIfNeeded() throws {
44+
let nonExistentSubdir = UUID().uuidString
45+
let url = self.testDirectory
46+
.appendingPathComponent(nonExistentSubdir)
47+
.appendingPathComponent("nested_file.txt")
48+
let testData = Data("test content".utf8)
49+
50+
// Ensure the directory doesn't exist
51+
expect(self.fileManager.fileExists(atPath: url.deletingLastPathComponent().path)) == false
52+
53+
try self.fileManager.saveData(testData, to: url)
54+
55+
let savedData = try Data(contentsOf: url)
56+
expect(savedData) == testData
57+
58+
// Cleanup
59+
try fileManager.removeItem(at: self.testDirectory.appendingPathComponent(nonExistentSubdir))
60+
}
61+
62+
func testSaveDataAsyncCreatesParentDirectoryIfNeeded() async throws {
63+
let nonExistentSubdir = UUID().uuidString
64+
let url = self.testDirectory
65+
.appendingPathComponent(nonExistentSubdir)
66+
.appendingPathComponent("nested_async_file.txt")
67+
let testData = "async test content"
68+
let stream = createAsyncStream(from: testData)
69+
70+
// Ensure the directory doesn't exist
71+
expect(self.fileManager.fileExists(atPath: url.deletingLastPathComponent().path)) == false
72+
73+
try await self.fileManager.saveData(stream, to: url, checksum: nil)
74+
75+
let savedData = try Data(contentsOf: url)
76+
let savedString = String(data: savedData, encoding: .utf8)
77+
expect(savedString) == testData
78+
79+
// Cleanup
80+
try fileManager.removeItem(at: self.testDirectory.appendingPathComponent(nonExistentSubdir))
81+
}
82+
83+
func testSaveDataWithValidChecksumSucceeds() async throws {
4484
let testData = "Test data for checksum"
4585
let data = Data(testData.utf8)
4686
let checksum = Checksum.generate(from: data, with: .sha256)
@@ -54,7 +94,7 @@ class LargeItemCacheTypeTests: TestCase {
5494
try fileManager.removeItem(at: url)
5595
}
5696

57-
func test_saveData_withInvalidChecksum_throws() async throws {
97+
func testSaveDataWithInvalidChecksumThrows() async throws {
5898
let testData = "Test data"
5999
let wrongChecksum = Checksum.generate(from: Data("Different data".utf8), with: .sha256)
60100
let url = self.testDirectory.appendingPathComponent("invalid_checksum.txt")
@@ -71,7 +111,7 @@ class LargeItemCacheTypeTests: TestCase {
71111
expect(self.fileManager.fileExists(atPath: url.path)) == false
72112
}
73113

74-
func test_saveData_withLargeData_succeeds() async throws {
114+
func testSaveDataWithLargeDataSucceeds() async throws {
75115
// Create data larger than the buffer size (256KB)
76116
let largeData = String(repeating: "A", count: 500_000)
77117
let url = self.testDirectory.appendingPathComponent("large_file.txt")
@@ -88,7 +128,7 @@ class LargeItemCacheTypeTests: TestCase {
88128

89129
}
90130

91-
func test_saveData_withMultipleChunks_writesCorrectly() async throws {
131+
func testSaveDataWithMultipleChunksWritesCorrectly() async throws {
92132
let testData = String(repeating: "X", count: 1_000_000) // 1MB
93133
let url = self.testDirectory.appendingPathComponent("chunked_file.txt")
94134
let stream = createAsyncStream(from: testData)
@@ -101,7 +141,7 @@ class LargeItemCacheTypeTests: TestCase {
101141

102142
}
103143

104-
func test_saveData_withChecksumAlgorithms_succeeds() async throws {
144+
func testSaveDataWithChecksumAlgorithmsSucceeds() async throws {
105145
let testData = "Algorithm test"
106146
let data = Data(testData.utf8)
107147
let algorithms: [Checksum.Algorithm] = [.sha256, .sha384, .sha512, .md5]
@@ -122,7 +162,7 @@ class LargeItemCacheTypeTests: TestCase {
122162

123163
// MARK: - cachedContentExists Tests
124164

125-
func test_cachedContentExists_returnsTrueForExistingFile() throws {
165+
func testCachedContentExistsReturnsTrueForExistingFile() throws {
126166
let url = self.testDirectory.appendingPathComponent("existing.txt")
127167
let testData = "Existing content"
128168
try testData.write(to: url, atomically: true, encoding: .utf8)
@@ -134,15 +174,15 @@ class LargeItemCacheTypeTests: TestCase {
134174

135175
}
136176

137-
func test_cachedContentExists_returnsFalseForNonExistentFile() {
177+
func testCachedContentExistsReturnsFalseForNonExistentFile() {
138178
let url = self.testDirectory.appendingPathComponent("nonexistent.txt")
139179

140180
let exists = self.fileManager.cachedContentExists(at: url)
141181

142182
expect(exists) == false
143183
}
144184

145-
func test_cachedContentExists_returnsFalseForEmptyFile() throws {
185+
func testCachedContentExistsReturnsFalseForEmptyFile() throws {
146186
let url = self.testDirectory.appendingPathComponent("empty.txt")
147187
try Data().write(to: url)
148188

@@ -153,7 +193,7 @@ class LargeItemCacheTypeTests: TestCase {
153193

154194
}
155195

156-
func test_cachedContentExists_returnsTrueForNonEmptyFile() throws {
196+
func testCachedContentExistsReturnsTrueForNonEmptyFile() throws {
157197
let url = self.testDirectory.appendingPathComponent("nonempty.txt")
158198
try "A".write(to: url, atomically: true, encoding: .utf8)
159199

@@ -166,7 +206,7 @@ class LargeItemCacheTypeTests: TestCase {
166206

167207
// MARK: - loadFile Tests
168208

169-
func test_loadFile_returnsCorrectData() throws {
209+
func testLoadFileReturnsCorrectData() throws {
170210
let url = self.testDirectory.appendingPathComponent("load_test.txt")
171211
let testData = "Data to load"
172212
try testData.write(to: url, atomically: true, encoding: .utf8)
@@ -179,15 +219,15 @@ class LargeItemCacheTypeTests: TestCase {
179219

180220
}
181221

182-
func test_loadFile_throwsForNonExistentFile() {
222+
func testLoadFileThrowsForNonExistentFile() {
183223
let url = self.testDirectory.appendingPathComponent("missing.txt")
184224

185225
expect {
186226
try self.fileManager.loadFile(at: url)
187227
}.to(throwError())
188228
}
189229

190-
func test_loadFile_returnsEmptyDataForEmptyFile() throws {
230+
func testLoadFileReturnsEmptyDataForEmptyFile() throws {
191231
let url = self.testDirectory.appendingPathComponent("empty_load.txt")
192232
try Data().write(to: url)
193233

@@ -200,7 +240,7 @@ class LargeItemCacheTypeTests: TestCase {
200240

201241
// MARK: - createCacheDirectoryIfNeeded Tests
202242

203-
func test_createCacheDirectoryIfNeeded_createsDirectory() {
243+
func testCreateCacheDirectoryIfNeededCreatesDirectory() {
204244
let basePath = "TestCache/Subdirectory"
205245

206246
let createdURL = self.fileManager.createCacheDirectoryIfNeeded(basePath: basePath)
@@ -215,7 +255,7 @@ class LargeItemCacheTypeTests: TestCase {
215255

216256
}
217257

218-
func test_createCacheDirectoryIfNeeded_createsIntermediateDirectories() {
258+
func testCreateCacheDirectoryIfNeededCreatesIntermediateDirectories() {
219259
let basePath = "TestCache/Level1/Level2/Level3"
220260

221261
let createdURL = self.fileManager.createCacheDirectoryIfNeeded(basePath: basePath)
@@ -230,9 +270,28 @@ class LargeItemCacheTypeTests: TestCase {
230270

231271
}
232272

273+
func testCreateCacheDirectoryIfNeededDoesNotFailWhenDirectoryExists() throws {
274+
let basePath = "TestCache/\(UUID().uuidString)"
275+
276+
let firstURL = self.fileManager.createCacheDirectoryIfNeeded(basePath: basePath)
277+
let secondURL = self.fileManager.createCacheDirectoryIfNeeded(basePath: basePath)
278+
279+
expect(firstURL).toNot(beNil())
280+
expect(secondURL).toNot(beNil())
281+
expect(secondURL?.standardizedFileURL.path) == firstURL?.standardizedFileURL.path
282+
283+
if let url = firstURL {
284+
var isDirectory: ObjCBool = false
285+
let exists = self.fileManager.fileExists(atPath: url.path, isDirectory: &isDirectory)
286+
expect(exists) == true
287+
expect(isDirectory.boolValue) == true
288+
try self.fileManager.removeItem(at: url)
289+
}
290+
}
291+
233292
// MARK: - Integration Tests
234293

235-
func test_saveAndLoad_roundTrip() async throws {
294+
func testSaveAndLoadRoundTrip() async throws {
236295
let testData = "Round trip test"
237296
let url = self.testDirectory.appendingPathComponent("roundtrip.txt")
238297
let stream = createAsyncStream(from: testData)
@@ -246,7 +305,7 @@ class LargeItemCacheTypeTests: TestCase {
246305

247306
}
248307

249-
func test_saveData_thenCheckExists() async throws {
308+
func testSaveDataThenCheckExists() async throws {
250309
let testData = "Existence test"
251310
let url = self.testDirectory.appendingPathComponent("exists_test.txt")
252311
let stream = createAsyncStream(from: testData)
@@ -261,7 +320,7 @@ class LargeItemCacheTypeTests: TestCase {
261320

262321
}
263322

264-
func test_saveData_withChecksum_thenLoad() async throws {
323+
func testSaveDataWithChecksumThenLoad() async throws {
265324
let testData = "Checksum round trip"
266325
let data = Data(testData.utf8)
267326
let checksum = Checksum.generate(from: data, with: .sha256)

0 commit comments

Comments
 (0)