diff --git a/Sources/LocalLLMClientCore/LLMSession.swift b/Sources/LocalLLMClientCore/LLMSession.swift index f14fe52..5937c9e 100644 --- a/Sources/LocalLLMClientCore/LLMSession.swift +++ b/Sources/LocalLLMClientCore/LLMSession.swift @@ -302,6 +302,21 @@ public extension LLMSession { public func downloadModel(onProgress: @Sendable @escaping (Double) async -> Void = { _ in }) async throws { try await downloader.download(onProgress: onProgress) } + + public static func removeAllModels( + in url: URL = FileDownloader.defaultRootDestination, + excludingModels: [any Model] = [] + ) throws { + let excludedURLs: [URL] = excludingModels.compactMap { model -> URL? in + switch model { + case let model as LLMSession.DownloadModel: model.modelPath + case let model as LLMSession.LocalModel: model.modelPath + default: nil + } + } + + try FileManager.default.removeAllItems(in: url, excludingURLs: excludedURLs) + } } struct LocalModel: Model { diff --git a/Sources/LocalLLMClientUtility/FileManager+.swift b/Sources/LocalLLMClientUtility/FileManager+.swift new file mode 100644 index 0000000..c4295d9 --- /dev/null +++ b/Sources/LocalLLMClientUtility/FileManager+.swift @@ -0,0 +1,36 @@ +import Foundation + +package extension FileManager { + func removeEmptyDirectories(in url: URL) throws { + guard url.isFileURL else { return } + + let contents = try contentsOfDirectory(at: url, includingPropertiesForKeys: [.isDirectoryKey]) + let subdirectories = try contents.filter { try $0.resourceValues(forKeys: [.isDirectoryKey]).isDirectory ?? false } + + for subdirectory in subdirectories { try removeEmptyDirectories(in: subdirectory) } + + guard try contentsOfDirectory(at: url, includingPropertiesForKeys: nil).isEmpty else { return } + + try removeItem(at: url) + } + + func removeAllItems(in url: URL, excludingURLs: [URL] = [], removingEmptyDirectories: Bool = true) throws { + guard let enumerator = enumerator(at: url, includingPropertiesForKeys: [.isDirectoryKey], options: [.skipsPackageDescendants]) else { return } + + let excludingURLsNormalized: Set = Set(excludingURLs.map { $0.resolvingSymlinksInPath().standardizedFileURL + }) + + for case let fileURL as URL in enumerator { + if excludingURLsNormalized.contains(fileURL.resolvingSymlinksInPath().standardizedFileURL) { + enumerator.skipDescendants() + continue + } + + if (try fileURL.resourceValues(forKeys: [.isDirectoryKey])).isDirectory == false { + try removeItem(at: fileURL) + } + } + + if removingEmptyDirectories { try removeEmptyDirectories(in: url) } + } +}