mirror of
https://github.com/yattee/yattee.git
synced 2026-02-19 17:29:45 +00:00
139 lines
4.9 KiB
Swift
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())
|
|
}
|
|
}
|