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

291 lines
11 KiB
Swift

//
// FileOperationManager.swift
// MNNLLMiOS
// Created by 游薪渝(揽清) on 2025/7/10.
//
import Foundation
import UIKit
/// FileOperationManager is a singleton utility class that handles various file operations
/// including image processing, audio processing, directory size calculation, and file cleanup.
final class FileOperationManager {
/// Shared singleton instance
static let shared = FileOperationManager()
/// Private initializer to enforce singleton pattern
private init() {}
// MARK: - Image Processing
/// Processes image files by copying to temporary directory and performing HEIC conversion if needed
///
/// - Parameters:
/// - url: The original image URL
/// - fileName: The desired file name for the processed image
/// - Returns: The processed image URL, or nil if processing fails
///
/// Usage:
/// ```swift
/// let imageURL = URL(fileURLWithPath: "/path/to/image.heic")
/// if let processedURL = FileOperationManager.shared.processImageFile(from: imageURL, fileName: "converted.jpg") {
/// // Use the processed image URL
/// }
/// ```
func processImageFile(from url: URL, fileName: String) -> URL? {
let isInTempDirectory = url.path.contains("/tmp/")
if !isInTempDirectory {
guard let fileUrl = AssetExtractor.copyFileToTmpDirectory(from: url, fileName: fileName) else {
return nil
}
return convertHEICImage(from: fileUrl)
} else {
return convertHEICImage(from: url)
}
}
/// Converts HEIC images to JPG format using AssetExtractor utility
///
/// - Parameter url: The HEIC image URL to convert
/// - Returns: The converted JPG image URL, or nil if conversion fails or format not supported
private func convertHEICImage(from url: URL) -> URL? {
var fileUrl = url
// Convert HEIC to JPG if needed
if fileUrl.isHEICImage() {
if let convertedUrl = AssetExtractor.convertHEICToJPG(heicUrl: fileUrl) {
fileUrl = convertedUrl
} else {
// Return nil - HEIC conversion failed
print("Failed to convert HEIC to JPG")
return nil
}
}
// Verify final format is JPG (MNN Diffusion only supports JPG)
let pathExtension = fileUrl.pathExtension.lowercased()
if pathExtension != "jpg" && pathExtension != "jpeg" {
print("Unsupported image format: \(pathExtension). Only JPG/JPEG is supported for Diffusion.")
return nil
}
return fileUrl
}
// MARK: - Video Processing
/// Copies a video to the tmp directory if needed and returns the accessible URL.
func prepareVideoFileURL(from url: URL, fileName: String) -> URL? {
if url.path.contains("/tmp/") {
return url
}
return AssetExtractor.copyFileToTmpDirectory(from: url, fileName: fileName)
}
/// Checks whether a file exists at the specified URL.
func fileExists(at url: URL?) -> Bool {
guard let url else { return false }
return FileManager.default.fileExists(atPath: url.path)
}
// MARK: - Directory Size Calculation
/// Formats byte count into human-readable string using ByteCountFormatter
///
/// - Parameter bytes: The number of bytes to format
/// - Returns: Formatted string (e.g., "1.5 GB")
///
/// Usage:
/// ```swift
/// let size: Int64 = 1073741824 // 1 GB
/// let formatted = FileOperationManager.shared.formatBytes(size)
/// print(formatted) // "1.0 GB"
/// ```
func formatBytes(_ bytes: Int64) -> String {
let formatter = ByteCountFormatter()
formatter.allowedUnits = [.useGB]
formatter.countStyle = .file
return formatter.string(fromByteCount: bytes)
}
/// Calculates the size of a local directory and returns a formatted string
///
/// - Parameter path: The directory path to calculate size for
/// - Returns: Formatted size string or "Unknown" if calculation fails
///
/// Usage:
/// ```swift
/// let directoryPath = "/path/to/directory"
/// let sizeString = FileOperationManager.shared.formatLocalDirectorySize(at: directoryPath)
/// print("Directory size: \(sizeString)")
/// ```
func formatLocalDirectorySize(at path: String) -> String {
guard FileManager.default.fileExists(atPath: path) else { return "Unknown" }
do {
let totalSize = try calculateDirectorySize(at: path)
return formatBytes(totalSize)
} catch {
return "Unknown"
}
}
/// Calculates the total size of a directory by traversing all files recursively
/// Uses actual disk allocated size when available, falls back to logical file size
///
/// - Parameter path: The directory path to calculate size for
/// - Returns: Total directory size in bytes
/// - Throws: FileSystem errors during directory traversal
///
/// Usage:
/// ```swift
/// do {
/// let size = try FileOperationManager.shared.calculateDirectorySize(at: "/path/to/directory")
/// print("Directory size: \(size) bytes")
/// } catch {
/// print("Failed to calculate directory size: \(error)")
/// }
/// ```
func calculateDirectorySize(at path: String) throws -> Int64 {
let fileManager = FileManager.default
var totalSize: Int64 = 0
// print("Calculating directory size for path: \(path)")
let directoryURL = URL(fileURLWithPath: path)
guard fileManager.fileExists(atPath: path) else {
// print("Path does not exist: \(path)")
return 0
}
let resourceKeys: [URLResourceKey] = [.isRegularFileKey, .totalFileAllocatedSizeKey, .fileSizeKey, .nameKey]
let enumerator = fileManager.enumerator(
at: directoryURL,
includingPropertiesForKeys: resourceKeys,
options: [.skipsHiddenFiles, .skipsPackageDescendants],
errorHandler: { url, error -> Bool in
print("Error accessing \(url): \(error)")
return true
}
)
guard let fileEnumerator = enumerator else {
throw NSError(domain: "FileEnumerationError", code: -1,
userInfo: [NSLocalizedDescriptionKey: "Failed to create file enumerator"])
}
var fileCount = 0
for case let fileURL as URL in fileEnumerator {
do {
let resourceValues = try fileURL.resourceValues(forKeys: Set(resourceKeys))
guard let isRegularFile = resourceValues.isRegularFile, isRegularFile else { continue }
let fileName = resourceValues.name ?? "Unknown"
fileCount += 1
// Use actual disk allocated size, fallback to logical size if not available
if let actualSize = resourceValues.totalFileAllocatedSize {
totalSize += Int64(actualSize)
if fileCount <= 10 {
let actualSizeGB = Double(actualSize) / (1024 * 1024 * 1024)
let logicalSizeGB = Double(resourceValues.fileSize ?? 0) / (1024 * 1024 * 1024)
// print("File \(fileCount): \(fileName) - Logical: \(String(format: "%.3f", logicalSizeGB)) GB, Actual: \(String(format: "%.3f", actualSizeGB)) GB")
}
} else if let logicalSize = resourceValues.fileSize {
totalSize += Int64(logicalSize)
if fileCount <= 10 {
let logicalSizeGB = Double(logicalSize) / (1024 * 1024 * 1024)
// print("File \(fileCount): \(fileName) - Size: \(String(format: "%.3f", logicalSizeGB)) GB (fallback)")
}
}
} catch {
print("Error getting resource values for \(fileURL): \(error)")
continue
}
}
let totalSizeGB = Double(totalSize) / (1024 * 1024 * 1024)
print("Total files: \(fileCount), Total actual disk usage: \(String(format: "%.2f", totalSizeGB)) GB")
return totalSize
}
// MARK: - Directory Cleaning
/// Cleans temporary directories based on memory mapping usage
/// Cleans system temporary directory and optionally model temporary directories
///
///
/// Usage:
/// ```swift
/// // Clean temporary directories
/// FileOperationManager.shared.cleanTempDirectories()
/// ```
func cleanTempDirectories() {
let fileManager = FileManager.default
let tmpDirectoryURL = fileManager.temporaryDirectory
cleanFolder(at: tmpDirectoryURL)
}
/// Cleans the temporary folder for a specific model
///
/// - Parameter modelPath: The path to the model directory
///
/// Usage:
/// ```swift
/// let modelPath = "/path/to/model"
/// FileOperationManager.shared.cleanModelTempFolder(modelPath: modelPath)
/// ```
func cleanModelTempFolder(modelPath: String) {
let tmpFolderURL = URL(fileURLWithPath: modelPath).appendingPathComponent("temp")
cleanFolder(at: tmpFolderURL)
}
/// Recursively cleans all files in the specified folder
/// Preserves files containing "networkdownload" in their path
///
/// - Parameter folderURL: The folder URL to clean
private func cleanFolder(at folderURL: URL) {
let fileManager = FileManager.default
do {
let files = try fileManager.contentsOfDirectory(at: folderURL, includingPropertiesForKeys: nil)
for file in files {
if !file.absoluteString.lowercased().contains("networkdownload") {
do {
try fileManager.removeItem(at: file)
print("Deleted file: \(file.path)")
} catch {
print("Error deleting file: \(file.path), \(error.localizedDescription)")
}
}
}
} catch {
print("Error accessing directory: \(error.localizedDescription)")
}
}
// MARK: - Diffusion Image Generation
/// Generates a unique temporary file path for Diffusion model image output
/// Creates a unique JPG filename in the system temporary directory
///
/// - Returns: A unique temporary image file URL
///
/// Usage:
/// ```swift
/// let tempImageURL = FileOperationManager.shared.generateTempImagePath()
/// // Use tempImageURL for image generation output
/// ```
func generateTempImagePath() -> URL {
let tempDir = FileManager.default.temporaryDirectory
let imageName = UUID().uuidString + ".jpg"
return tempDir.appendingPathComponent(imageName)
}
}