Summary: - Sources: update Audit, Helpers, Migrations (+2 more) Stats: - 29 files changed, 471 insertions(+), 25 deletions(-)
94 lines
3.7 KiB
Swift
94 lines
3.7 KiB
Swift
import Foundation
|
|
|
|
/// Simple 1:1 legacy migration from a single source key.
|
|
///
|
|
/// Use this migration when the source and destination value types are identical
|
|
/// and no transformation is required.
|
|
public struct SimpleLegacyMigration<Value: Codable & Sendable>: StorageMigration {
|
|
/// Destination key for migrated data.
|
|
public let destinationKey: StorageKey<Value>
|
|
/// Source key providing legacy data.
|
|
public let sourceKey: AnyStorageKey
|
|
|
|
/// Creates a migration from a legacy key to a destination key.
|
|
///
|
|
/// - Parameters:
|
|
/// - destinationKey: The key that receives migrated data.
|
|
/// - sourceKey: The legacy key to read and remove.
|
|
public init(destinationKey: StorageKey<Value>, sourceKey: AnyStorageKey) {
|
|
self.destinationKey = destinationKey
|
|
self.sourceKey = sourceKey
|
|
}
|
|
|
|
/// Determines whether the migration should run.
|
|
///
|
|
/// The migration runs when:
|
|
/// - the destination is allowed on the current platform
|
|
/// - the destination does not already exist
|
|
/// - the source key contains data
|
|
///
|
|
/// - Parameters:
|
|
/// - router: The storage router used to query state.
|
|
/// - context: Migration context for conditional checks.
|
|
/// - Returns: `true` when migration should proceed.
|
|
public func shouldMigrate(using router: StorageRouter, context: MigrationContext) async throws -> Bool {
|
|
guard try await router.shouldAllowMigration(for: destinationKey, context: context) else {
|
|
return false
|
|
}
|
|
if try await router.exists(destinationKey) {
|
|
return false
|
|
}
|
|
|
|
return try await router.exists(descriptor: sourceKey.descriptor)
|
|
}
|
|
|
|
/// Executes the migration and returns a result.
|
|
///
|
|
/// The migration:
|
|
/// 1. Reads raw source data.
|
|
/// 2. Removes legacy security (if needed).
|
|
/// 3. Decodes into the destination value type.
|
|
/// 4. Writes to the destination and deletes the source.
|
|
///
|
|
/// - Parameters:
|
|
/// - router: The storage router used to read and write values.
|
|
/// - context: Migration context for conditional checks.
|
|
/// - Returns: A ``MigrationResult`` describing success or failure.
|
|
public func migrate(using router: StorageRouter, context: MigrationContext) async throws -> MigrationResult {
|
|
let startTime = Date()
|
|
var errors: [MigrationError] = []
|
|
var migratedCount = 0
|
|
|
|
do {
|
|
guard let sourceData = try await router.retrieve(for: sourceKey.descriptor) else {
|
|
return MigrationResult(success: false, errors: [.sourceDataNotFound])
|
|
}
|
|
|
|
let unsecuredData = try await router.applySecurity(
|
|
sourceData,
|
|
for: sourceKey.descriptor,
|
|
isEncrypt: false
|
|
)
|
|
let value = try router.deserialize(unsecuredData, with: destinationKey.serializer)
|
|
|
|
try await router.set(value, for: destinationKey)
|
|
try await router.delete(for: sourceKey.descriptor)
|
|
|
|
migratedCount = 1
|
|
Logger.info("!!! [MIGRATION] Successfully migrated from '\(sourceKey.descriptor.name)' to '\(destinationKey.name)'")
|
|
} catch {
|
|
let storageError = error as? StorageError ?? .fileError(error.localizedDescription)
|
|
errors.append(.storageFailed(storageError))
|
|
Logger.error("!!! [MIGRATION] Failed to migrate from '\(sourceKey.descriptor.name)': \(error)")
|
|
}
|
|
|
|
let duration = Date().timeIntervalSince(startTime)
|
|
return MigrationResult(
|
|
success: errors.isEmpty,
|
|
migratedCount: migratedCount,
|
|
errors: errors,
|
|
duration: duration
|
|
)
|
|
}
|
|
}
|