| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212 |
- // Copyright 2021 Google LLC
- //
- // Licensed under the Apache License, Version 2.0 (the "License");
- // you may not use this file except in compliance with the License.
- // You may obtain a copy of the License at
- //
- // http://www.apache.org/licenses/LICENSE-2.0
- //
- // Unless required by applicable law or agreed to in writing, software
- // distributed under the License is distributed on an "AS IS" BASIS,
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- // See the License for the specific language governing permissions and
- // limitations under the License.
- import Foundation
- /// Manager for common file operations.
- enum ModelFileManager {
- /// Separator in model file name components.
- private static let nameSeparator = "@@"
- private static let modelNamePrefix = "fbml_model"
- private static let fileExtension = "tflite"
- private static let fileManager = FileManager.default
- /// Root directory of model file storage on device.
- static var modelsDirectory: URL? {
- let rootDirOptional: URL? = {
- #if os(tvOS)
- return fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first
- #else
- return fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first ?? nil
- #endif
- }()
- guard let rootDirURL = rootDirOptional else {
- return nil
- }
- let modelDirURL = rootDirURL.appendingPathComponent(
- "com.firebase.FirebaseMLModelDownloader",
- isDirectory: true
- )
- do {
- if !fileManager.fileExists(atPath: modelDirURL.absoluteString) {
- try fileManager.createDirectory(at: modelDirURL, withIntermediateDirectories: true)
- }
- } catch {
- return nil
- }
- return modelDirURL
- }
- /// Name for model file stored on device.
- static func getDownloadedModelFileName(appName: String, modelName: String) -> String {
- return [modelNamePrefix, appName, modelName].joined(separator: nameSeparator)
- }
- /// Model name from file path.
- static func getModelNameFromFilePath(_ path: URL) -> String? {
- let components = path.deletingPathExtension().lastPathComponent
- .components(separatedBy: nameSeparator)
- // The file name should have prefix, app name, and model name.
- if components.count == 3 {
- return components.last
- }
- return nil
- }
- /// Full path of model file stored on device.
- static func getDownloadedModelFileURL(appName: String, modelName: String) -> URL? {
- let modelFileName = ModelFileManager.getDownloadedModelFileName(
- appName: appName,
- modelName: modelName
- )
- guard let modelsDir = ModelFileManager.modelsDirectory else { return nil }
- return modelsDir.appendingPathComponent(modelFileName).appendingPathExtension(fileExtension)
- }
- /// Check if file is available at URL.
- static func isFileReachable(at fileURL: URL) -> Bool {
- do {
- return try fileURL.checkResourceIsReachable()
- } catch {
- // File unreachable.
- return false
- }
- }
- /// Move file at a location to another location.
- static func moveFile(at sourceURL: URL, to destinationURL: URL, size: Int64) throws {
- if isFileReachable(at: destinationURL) {
- do {
- try fileManager.removeItem(at: destinationURL)
- } catch {
- // TODO: Handle this - new model file downloaded but not saved due to FileManager error.
- throw DownloadError
- .internalError(
- description: ModelFileManager.ErrorDescription
- .replaceFile(error.localizedDescription)
- )
- }
- }
- do {
- try fileManager.moveItem(at: sourceURL, to: destinationURL)
- } catch CocoaError.fileWriteOutOfSpace {
- throw DownloadError.notEnoughSpace
- } catch {
- throw DownloadError
- .internalError(description: ModelFileManager
- .ErrorDescription.saveFile(error.localizedDescription))
- }
- }
- /// Remove model file at a specific location.
- static func removeFile(at url: URL) throws {
- do {
- try fileManager.removeItem(at: url)
- } catch {
- throw DownloadedModelError
- .internalError(
- description: ModelFileManager.ErrorDescription.deleteFile(error.localizedDescription)
- )
- }
- }
- /// Get all model files in models directory.
- static func contentsOfModelsDirectory() throws -> [URL] {
- guard let modelsDir = modelsDirectory else {
- throw DownloadedModelError
- .internalError(
- description: ModelFileManager.ErrorDescription.noApplicationSupportDirectory
- )
- }
- do {
- let directoryContents = try ModelFileManager.fileManager.contentsOfDirectory(
- at: modelsDir,
- includingPropertiesForKeys: nil,
- options: .skipsHiddenFiles
- )
- return directoryContents.filter { directoryItem in
- directoryItem.path.contains(ModelFileManager.modelNamePrefix) && !directoryItem
- .hasDirectoryPath
- }
- } catch {
- throw DownloadedModelError
- .internalError(
- description: ModelFileManager.ErrorDescription
- .retrieveFile(error.localizedDescription)
- )
- }
- }
- }
- /// NOTE: Use for testing only.
- extension ModelFileManager {
- static func emptyModelsDirectory() throws {
- do {
- let directoryContents = try ModelFileManager.contentsOfModelsDirectory()
- for path in directoryContents {
- try ModelFileManager.removeFile(at: path)
- }
- } catch {
- throw DownloadedModelError
- .internalError(
- description: ModelFileManager.ErrorDescription
- .emptyDirectory(error.localizedDescription)
- )
- }
- }
- }
- /// Possible error messages during file management.
- extension ModelFileManager {
- /// Error descriptions.
- private enum ErrorDescription {
- static let retrieveFile = { (error: String) in
- "Could not retrieve model files in directory: \(error)"
- }
- static let deleteFile = { (error: String) in
- "Could not delete old model file: \(error)"
- }
- static let saveFile = { (error: String) in
- "Unable to save model file: \(error)"
- }
- static let replaceFile = { (error: String) in
- "Could not replace existing model file: \(error)"
- }
- static let availableStorage = { (error: String?) -> String in
- if let error = error {
- return "Failed to check storage capacity on device: \(error)"
- } else {
- return "Failed to check storage capacity on device."
- }
- }
- static let noApplicationSupportDirectory =
- "Could not locate Application Support directory for model storage."
- static let emptyDirectory = { (error: String) in
- "Could not empty models directory: \(error)"
- }
- }
- }
|