feat: meeting data migration and backup

This commit is contained in:
Owen Gretzinger
2025-07-11 14:44:45 -04:00
parent 3a183cb5e8
commit 858f934fb0
3 changed files with 102 additions and 6 deletions
@@ -0,0 +1,54 @@
// DataMigrationManager.swift
// Handles data migration between different app versions
import Foundation
/// Manages data migration between different app versions
class DataMigrationManager {
static let shared = DataMigrationManager()
private init() {}
/// Migrates a meeting from an older version to the current version
/// - Parameter meeting: The meeting to migrate
/// - Returns: The migrated meeting, or nil if migration failed
func migrateMeeting(_ meeting: Meeting) -> Meeting? {
// No releases prior to version 1 any older file is considered unsupported.
guard meeting.dataVersion >= 1 else {
print("🚫 Cannot migrate meeting \(meeting.id) unsupported data version \(meeting.dataVersion)")
return nil
}
// Future migrations can be added here as `switch` cases.
if meeting.dataVersion < Meeting.currentDataVersion {
print("⚠️ No migration path for versions \(meeting.dataVersion + 1)...\(Meeting.currentDataVersion)")
return nil
}
return meeting
}
// Future migrateXToVersionY helpers will go here as needed
/// Performs a backup of the meetings directory before migration
/// - Returns: The backup directory URL, or nil if backup failed
func backupMeetingsDirectory() -> URL? {
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let meetingsDirectory = documentsDirectory.appendingPathComponent("Meetings")
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd_HH-mm-ss"
let timestamp = formatter.string(from: Date())
let backupDirectory = documentsDirectory.appendingPathComponent("Meetings_Backup_\(timestamp)")
do {
try FileManager.default.copyItem(at: meetingsDirectory, to: backupDirectory)
print("✅ Created backup at: \(backupDirectory)")
return backupDirectory
} catch {
print("❌ Failed to create backup: \(error)")
return nil
}
}
}
+38 -5
View File
@@ -30,15 +30,19 @@ class LocalStorageManager {
/// - Returns: True if successful, false otherwise
func saveMeeting(_ meeting: Meeting) -> Bool {
let fileURL = meetingsDirectory.appendingPathComponent("\(meeting.id.uuidString).json")
do {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
encoder.outputFormatting = [.prettyPrinted]
encoder.dateEncodingStrategy = .iso8601
let data = try encoder.encode(meeting)
try data.write(to: fileURL)
// Write atomically using a temp file then replace
let tmpURL = fileURL.appendingPathExtension("tmp")
try data.write(to: tmpURL, options: .atomic)
try FileManager.default.replaceItem(at: fileURL, withItemAt: tmpURL, backupItemName: nil, options: [], resultingItemURL: nil)
print("✅ Saved meeting: \(meeting.id)")
return true
} catch {
@@ -58,12 +62,41 @@ class LocalStorageManager {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
var didCreateBackup = false
let meetings = fileURLs.compactMap { url -> Meeting? in
guard let data = try? Data(contentsOf: url),
let meeting = try? decoder.decode(Meeting.self, from: data) else {
print("⚠️ Failed to decode meeting at: \(url)")
return nil
}
// Forward-compatibility guard skip if file was written by a newer build
if meeting.dataVersion > Meeting.currentDataVersion {
print("🚫 Meeting \(meeting.id) written by newer app version (\(meeting.dataVersion)). Skipping load.")
return nil
}
// Check if migration is needed
if meeting.dataVersion < Meeting.currentDataVersion {
// Create backup **once** before we start mutating anything
if !didCreateBackup {
_ = DataMigrationManager.shared.backupMeetingsDirectory()
didCreateBackup = true
}
if let migratedMeeting = DataMigrationManager.shared.migrateMeeting(meeting) {
if saveMeeting(migratedMeeting) {
print("✅ Migrated and saved meeting: \(migratedMeeting.id)")
return migratedMeeting
}
print("❌ Failed to save migrated meeting: \(migratedMeeting.id)")
} else {
print("❌ Failed to migrate meeting: \(meeting.id)")
}
// Return original if anything failed
return meeting
}
return meeting
}
+10 -1
View File
@@ -69,21 +69,30 @@ struct Meeting: Codable, Identifiable, Hashable {
var transcriptChunks: [TranscriptChunk]
var userNotes: String
var generatedNotes: String
// MARK: - Data versioning
/// Version of this Meeting record on disk. Useful for migration.
var dataVersion: Int
/// Current app data version. Increment whenever you make a breaking change to `Meeting` that requires migration.
static let currentDataVersion = 1
init(id: UUID = UUID(),
date: Date = Date(),
title: String = "",
transcriptChunks: [TranscriptChunk] = [],
userNotes: String = "",
generatedNotes: String = "") {
generatedNotes: String = "",
dataVersion: Int = Meeting.currentDataVersion) {
self.id = id
self.date = date
self.title = title
self.transcriptChunks = transcriptChunks
self.userNotes = userNotes
self.generatedNotes = generatedNotes
self.dataVersion = dataVersion
}
// `Codable` conformance now uses the compiler-synthesised implementation.
// Computed property for backward compatibility with existing code
var transcript: String {
return transcriptChunks