From f349309a56644f0670f0e5febc577062b370fff4 Mon Sep 17 00:00:00 2001 From: Brandon Sneed Date: Fri, 1 Aug 2025 10:48:56 -0700 Subject: [PATCH 1/3] Use proper directory for storage + migrate --- Sources/Segment/Utilities/Utils.swift | 43 ++++++++++++++++++----- Tests/Segment-Tests/Storage_Tests.swift | 46 ++++++++++++++++++++++++- 2 files changed, 80 insertions(+), 9 deletions(-) diff --git a/Sources/Segment/Utilities/Utils.swift b/Sources/Segment/Utilities/Utils.swift index 72c20b9..d60bedd 100644 --- a/Sources/Segment/Utilities/Utils.swift +++ b/Sources/Segment/Utilities/Utils.swift @@ -75,17 +75,44 @@ extension Optional: Flattenable { } internal func eventStorageDirectory(writeKey: String) -> URL { - #if (os(iOS) || os(watchOS)) && !targetEnvironment(macCatalyst) - let searchPathDirectory = FileManager.SearchPathDirectory.documentDirectory - #else - let searchPathDirectory = FileManager.SearchPathDirectory.cachesDirectory - #endif + let urls = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask) + let appSupportURL = urls[0] + let segmentURL = appSupportURL.appendingPathComponent("segment/\(writeKey)/") + + // Handle one-time migration from old locations + migrateFromOldLocations(writeKey: writeKey, to: segmentURL) - let urls = FileManager.default.urls(for: searchPathDirectory, in: .userDomainMask) - let docURL = urls[0] - let segmentURL = docURL.appendingPathComponent("segment/\(writeKey)/") // try to create it, will fail if already exists, nbd. // tvOS, watchOS regularly clear out data. try? FileManager.default.createDirectory(at: segmentURL, withIntermediateDirectories: true, attributes: nil) return segmentURL } + +private func migrateFromOldLocations(writeKey: String, to newLocation: URL) { + let fm = FileManager.default + + // Get the parent of where our new segment directory should live + let appSupportURL = fm.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0] + let newSegmentDir = appSupportURL.appendingPathComponent("segment") + + // If segment dir already exists in app support, we're done + guard !fm.fileExists(atPath: newSegmentDir.path) else { return } + + // Look for old segment directories to move + let oldLocations = [ + fm.urls(for: .documentDirectory, in: .userDomainMask).first?.appendingPathComponent("segment"), + fm.urls(for: .cachesDirectory, in: .userDomainMask).first?.appendingPathComponent("segment") + ].compactMap { $0 } + + for oldSegmentDir in oldLocations { + guard fm.fileExists(atPath: oldSegmentDir.path) else { continue } + + do { + try fm.moveItem(at: oldSegmentDir, to: newSegmentDir) + Analytics.segmentLog(message: "Migrated analytics data from \(oldSegmentDir.path)", kind: .debug) + return // Success! + } catch { + Analytics.segmentLog(message: "Failed to migrate from \(oldSegmentDir.path): \(error)", kind: .error) + } + } +} diff --git a/Tests/Segment-Tests/Storage_Tests.swift b/Tests/Segment-Tests/Storage_Tests.swift index 116fdc7..d931fc3 100644 --- a/Tests/Segment-Tests/Storage_Tests.swift +++ b/Tests/Segment-Tests/Storage_Tests.swift @@ -137,7 +137,10 @@ class StorageTests: XCTestCase { } func testFilePrepAndFinish() { - let analytics = Analytics(configuration: Configuration(writeKey: "test")) + let config = Configuration(writeKey: "test") + .storageMode(.diskAtURL(URL(fileURLWithPath: NSTemporaryDirectory()))) + let analytics = Analytics(configuration: config) + analytics.storage.hardReset(doYouKnowHowToUseThis: true) analytics.waitUntilStarted() @@ -302,4 +305,45 @@ class StorageTests: XCTestCase { let remaining = analytics.storage.read(.events) XCTAssertNil(remaining) } + + func testMigrationFromOldLocation() { + let writeKey = "test-migration" + let fm = FileManager.default + + // Clean slate + let appSupportURL = fm.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0] + let newSegmentDir = appSupportURL.appendingPathComponent("segment") + try? fm.removeItem(at: newSegmentDir) + + // Create fake old data in the platform-specific old location + #if (os(iOS) || os(watchOS)) && !targetEnvironment(macCatalyst) + let oldSearchPath = FileManager.SearchPathDirectory.documentDirectory + #else + let oldSearchPath = FileManager.SearchPathDirectory.cachesDirectory + #endif + + let oldBaseURL = fm.urls(for: oldSearchPath, in: .userDomainMask)[0] + let oldSegmentDir = oldBaseURL.appendingPathComponent("segment/\(writeKey)") + try! fm.createDirectory(at: oldSegmentDir, withIntermediateDirectories: true, attributes: nil) + + // Write some fake event files + let testFile1 = oldSegmentDir.appendingPathComponent("0-segment-events.temp") + let testFile2 = oldSegmentDir.appendingPathComponent("1-segment-events.temp") + try! "fake event data 1".write(to: testFile1, atomically: true, encoding: .utf8) + try! "fake event data 2".write(to: testFile2, atomically: true, encoding: .utf8) + + // Trigger migration + let resultURL = eventStorageDirectory(writeKey: writeKey) + + // Verify migration worked + XCTAssertTrue(fm.fileExists(atPath: resultURL.path)) + XCTAssertTrue(fm.fileExists(atPath: resultURL.appendingPathComponent("0-segment-events.temp").path)) + XCTAssertTrue(fm.fileExists(atPath: resultURL.appendingPathComponent("1-segment-events.temp").path)) + + // Verify old directory is gone + XCTAssertFalse(fm.fileExists(atPath: oldSegmentDir.path)) + + // Clean up + try? fm.removeItem(at: newSegmentDir) + } } From cf8947c1f1fbb2d154723f38f6ab80c726dcee95 Mon Sep 17 00:00:00 2001 From: Brandon Sneed Date: Fri, 1 Aug 2025 10:49:03 -0700 Subject: [PATCH 2/3] Remove dead code. --- Sources/Segment/State.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/Segment/State.swift b/Sources/Segment/State.swift index 5fba332..3b0e53e 100644 --- a/Sources/Segment/State.swift +++ b/Sources/Segment/State.swift @@ -149,7 +149,6 @@ struct System: State { func reduce(state: System) -> System { var waitingPlugins = state.waitingPlugins - let countBefore = waitingPlugins.count waitingPlugins.removeAll { p in return plugin === p } From 3701acc40d8bdc3504787c89ab33f8f624df2667 Mon Sep 17 00:00:00 2001 From: Brandon Sneed Date: Fri, 1 Aug 2025 12:00:59 -0700 Subject: [PATCH 3/3] tightened up migration code --- Sources/Segment/Utilities/Utils.swift | 31 ++++++++++++++------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/Sources/Segment/Utilities/Utils.swift b/Sources/Segment/Utilities/Utils.swift index d60bedd..feba819 100644 --- a/Sources/Segment/Utilities/Utils.swift +++ b/Sources/Segment/Utilities/Utils.swift @@ -98,21 +98,22 @@ private func migrateFromOldLocations(writeKey: String, to newLocation: URL) { // If segment dir already exists in app support, we're done guard !fm.fileExists(atPath: newSegmentDir.path) else { return } - // Look for old segment directories to move - let oldLocations = [ - fm.urls(for: .documentDirectory, in: .userDomainMask).first?.appendingPathComponent("segment"), - fm.urls(for: .cachesDirectory, in: .userDomainMask).first?.appendingPathComponent("segment") - ].compactMap { $0 } + // Only check the old location that was actually used on this platform + #if (os(iOS) || os(watchOS)) && !targetEnvironment(macCatalyst) + let oldSearchPath = FileManager.SearchPathDirectory.documentDirectory + #else + let oldSearchPath = FileManager.SearchPathDirectory.cachesDirectory + #endif - for oldSegmentDir in oldLocations { - guard fm.fileExists(atPath: oldSegmentDir.path) else { continue } - - do { - try fm.moveItem(at: oldSegmentDir, to: newSegmentDir) - Analytics.segmentLog(message: "Migrated analytics data from \(oldSegmentDir.path)", kind: .debug) - return // Success! - } catch { - Analytics.segmentLog(message: "Failed to migrate from \(oldSegmentDir.path): \(error)", kind: .error) - } + guard let oldBaseURL = fm.urls(for: oldSearchPath, in: .userDomainMask).first else { return } + let oldSegmentDir = oldBaseURL.appendingPathComponent("segment") + + guard fm.fileExists(atPath: oldSegmentDir.path) else { return } + + do { + try fm.moveItem(at: oldSegmentDir, to: newSegmentDir) + Analytics.segmentLog(message: "Migrated analytics data from \(oldSegmentDir.path)", kind: .debug) + } catch { + Analytics.segmentLog(message: "Failed to migrate from \(oldSegmentDir.path): \(error)", kind: .error) } }