Files
2026-07-13 13:33:03 +08:00

164 lines
6.2 KiB
Swift

//
// ModelDownloadStorage.swift
// MNNLLMiOS
//
// Created by 游薪渝(揽清) on 2025/2/20.
//
import Foundation
/// ModelDownloadStorage - Persistent storage manager for download state tracking
///
/// This class provides comprehensive storage management for tracking downloaded model files,
/// their metadata, and download completion status. It uses UserDefaults for persistence
/// and maintains file integrity through size and revision validation.
///
/// Key Features:
/// - Persistent download state tracking across app sessions
/// - File integrity validation using size and revision checks
/// - Efficient storage using relative path mapping
/// - Thread-safe operations with immediate persistence
/// - Automatic cleanup and state management
///
/// Architecture:
/// - Uses UserDefaults as the underlying storage mechanism
/// - Maintains a dictionary mapping relative paths to FileStatus objects
/// - Provides atomic operations for state updates
/// - Supports both individual file and batch operations
///
/// Storage Format:
/// - Files are indexed by relative paths from Documents directory
/// - Each entry contains size, revision, and timestamp information
/// - JSON encoding ensures cross-session compatibility
///
/// Usage:
/// ```swift
/// let storage = ModelDownloadStorage()
///
/// // Check if file is already downloaded
/// if storage.isFileDownloaded(file, at: destinationPath) {
/// print("File already exists and is up to date")
/// }
///
/// // Save download completion status
/// storage.saveFileStatus(file, at: destinationPath)
/// ```
final class ModelDownloadStorage {
// MARK: - Properties
private let userDefaults: UserDefaults
private let downloadedFilesKey = "ModelScope.DownloadedFiles"
// MARK: - Initialization
/// Initializes the storage manager with configurable UserDefaults
///
/// - Parameter userDefaults: UserDefaults instance for persistence (defaults to .standard)
init(userDefaults: UserDefaults = .standard) {
self.userDefaults = userDefaults
}
// MARK: - Public Methods
/// Checks if a file has been completely downloaded and is up to date
///
/// Validates both file existence on disk and metadata consistency including
/// file size and revision to ensure the downloaded file is current and complete.
///
/// - Parameters:
/// - file: ModelFile object containing expected metadata
/// - path: Destination path where the file should be located
/// - Returns: true if file exists and matches expected metadata, false otherwise
func isFileDownloaded(_ file: ModelFile, at path: String) -> Bool {
let filePath = (path as NSString).appendingPathComponent(file.name.sanitizedPath)
let relativePath = getRelativePath(from: filePath)
guard FileManager.default.fileExists(atPath: filePath),
let downloadedFiles = getDownloadedFiles(),
let fileStatus = downloadedFiles[relativePath]
else {
return false
}
return fileStatus.size == file.size && fileStatus.revision == file.revision
}
/// Saves the download completion status for a file
///
/// Records file metadata including size, revision, and download timestamp
/// to persistent storage. This enables resume functionality and prevents
/// unnecessary re-downloads of unchanged files.
///
/// - Parameters:
/// - file: ModelFile object containing file metadata
/// - path: Destination path where the file was downloaded
func saveFileStatus(_ file: ModelFile, at path: String) {
let filePath = (path as NSString).appendingPathComponent(file.name.sanitizedPath)
let relativePath = getRelativePath(from: filePath)
let fileStatus = FileStatus(
path: relativePath,
size: file.size,
revision: file.revision,
lastModified: Date()
)
var downloadedFiles = getDownloadedFiles() ?? [:]
downloadedFiles[relativePath] = fileStatus
// Save to UserDefaults and ensure immediate write
if let encoded = try? JSONEncoder().encode(downloadedFiles) {
userDefaults.set(encoded, forKey: downloadedFilesKey)
userDefaults.synchronize()
}
}
/// Retrieves all downloaded file statuses from persistent storage
///
/// - Returns: Dictionary mapping relative file paths to FileStatus objects,
/// or nil if no download history exists
func getDownloadedFiles() -> [String: FileStatus]? {
guard let data = userDefaults.data(forKey: downloadedFilesKey),
let downloadedFiles = try? JSONDecoder().decode([String: FileStatus].self, from: data)
else {
return nil
}
return downloadedFiles
}
/// Removes download status for a specific file
///
/// Cleans up storage by removing the file's download record, typically
/// used when files are deleted or need to be re-downloaded.
///
/// - Parameter path: Full path to the file whose status should be cleared
func clearFileStatus(at path: String) {
let relativePath = getRelativePath(from: path)
var downloadedFiles = getDownloadedFiles() ?? [:]
downloadedFiles.removeValue(forKey: relativePath)
if let encoded = try? JSONEncoder().encode(downloadedFiles) {
userDefaults.set(encoded, forKey: downloadedFilesKey)
userDefaults.synchronize()
}
}
// MARK: - Private Methods
/// Converts absolute file paths to relative paths from Documents directory
///
/// This normalization ensures consistent path handling across different
/// app installations and device configurations.
///
/// - Parameter fullPath: Absolute file path to convert
/// - Returns: Relative path from Documents directory, or original path if conversion fails
private func getRelativePath(from fullPath: String) -> String {
guard let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.path else {
return fullPath
}
// Convert absolute path to relative path from Documents directory
return fullPath.replacingOccurrences(of: documentsPath + "/", with: "")
}
}