Files
yattee/Yattee/Services/PlayerControls/PlayerControlsBackupService.swift
2026-02-08 18:33:56 +01:00

139 lines
4.9 KiB
Swift

//
// PlayerControlsBackupService.swift
// Yattee
//
// Manages backup and recovery of player controls layout presets.
//
import Foundation
/// Manages backup and recovery of player controls layout presets.
actor PlayerControlsBackupService {
// MARK: - Storage
/// URL for the backup file in Application Support.
private var backupFileURL: URL {
let appSupport = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
let yatteeDir = appSupport.appendingPathComponent("Yattee", isDirectory: true)
return yatteeDir.appendingPathComponent("PlayerControlsBackup.json")
}
private let fileManager = FileManager.default
private let encoder = JSONEncoder()
private let decoder = JSONDecoder()
// MARK: - Initialization
init() {
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
encoder.dateEncodingStrategy = .iso8601
decoder.dateDecodingStrategy = .iso8601
}
// MARK: - Backup Operations
/// Creates a backup of the given presets.
/// - Parameter presets: The presets to back up.
func createBackup(presets: [LayoutPreset]) async throws {
// Ensure directory exists
let directory = backupFileURL.deletingLastPathComponent()
if !fileManager.fileExists(atPath: directory.path) {
try fileManager.createDirectory(at: directory, withIntermediateDirectories: true)
}
// Encode and write
let data = try encoder.encode(presets)
try data.write(to: backupFileURL, options: .atomic)
LoggingService.shared.info("Created player controls backup with \(presets.count) presets")
}
/// Restores presets from backup.
/// - Returns: The restored presets, or nil if no valid backup exists.
func restoreFromBackup() async throws -> [LayoutPreset]? {
guard fileManager.fileExists(atPath: backupFileURL.path) else {
LoggingService.shared.info("No player controls backup file found")
return nil
}
let data = try Data(contentsOf: backupFileURL)
let presets = try decoder.decode([LayoutPreset].self, from: data)
LoggingService.shared.info("Restored \(presets.count) presets from backup")
return presets
}
/// Checks if a valid backup exists.
/// - Returns: True if a valid backup file exists.
func hasValidBackup() async -> Bool {
guard fileManager.fileExists(atPath: backupFileURL.path) else {
return false
}
// Try to decode to verify it's valid
do {
let data = try Data(contentsOf: backupFileURL)
_ = try decoder.decode([LayoutPreset].self, from: data)
return true
} catch {
LoggingService.shared.error("Backup file exists but is invalid: \(error.localizedDescription)")
return false
}
}
/// Attempts to create a recovered preset from corrupted data.
/// - Parameter data: The corrupted data.
/// - Returns: A recovered preset if partial recovery is possible.
func createRecoveredPreset(from data: Data) async -> LayoutPreset? {
// Try to extract any valid layout from the data
// This is a best-effort recovery attempt
// First, try decoding as a single preset
if let preset = try? decoder.decode(LayoutPreset.self, from: data) {
let recovered = preset.duplicate(name: "Recovered \(formattedDate())")
LoggingService.shared.info("Recovered single preset from corrupted data")
return recovered
}
// Try decoding as an array and take the first valid one
if let presets = try? decoder.decode([LayoutPreset].self, from: data),
let first = presets.first {
let recovered = first.duplicate(name: "Recovered \(formattedDate())")
LoggingService.shared.info("Recovered preset from corrupted array data")
return recovered
}
// Try decoding just the layout
if let layout = try? decoder.decode(PlayerControlsLayout.self, from: data) {
let recovered = LayoutPreset(
name: "Recovered \(formattedDate())",
layout: layout
)
LoggingService.shared.info("Recovered layout from corrupted data")
return recovered
}
LoggingService.shared.error("Failed to recover any preset from corrupted data")
return nil
}
/// Deletes the backup file.
func deleteBackup() async throws {
guard fileManager.fileExists(atPath: backupFileURL.path) else {
return
}
try fileManager.removeItem(at: backupFileURL)
LoggingService.shared.info("Deleted player controls backup")
}
// MARK: - Helpers
private func formattedDate() -> String {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .short
return formatter.string(from: Date())
}
}