291 lines
11 KiB
Swift
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)
|
|
}
|
|
}
|