Files
firebase-ios-sdk/FirebaseMLModelDownloader/Sources/ModelFileManager.swift
T
manjanac 9597ffe51b Removes saved path on device (#7439)
* Restores ML Pods after M77.

* Fix Package.swift

* Re-add catalyst to GHA workflow.

* Add tests for list models.

* Failable emptying of model directory in tests.

* Remove path from local model info and user defaults.

* Clean up user defaults on delete + add interpreter to test app.

* Replace .absoluteString with .path. This makes model.path work directly with TFLite interpreter.

* Fix paths in tests.

* Unit test for successful get model.

* Rename modelPath to modelURL.

* Use defaults.integer instead of casting to Int.

* Remove force casting.

* Mostly minor fixes that came out of the bugbash (#7462)

* Use appropriate errors for network offline and api disabled.

* Error + test for resourceExhausted.

* Surface backend error message in logs whenever available.

* Add disclaimer to empty directories for testing only.

* Add Firelog events for model info retrieval.

* Add documentation + other minor fixes. (#7465)

* Add docs + clean up ModelDownloader.swift.

* Add docs + clean up ModelDownloadTask.swift.

* Add docs + clean up FileDownloader.swift.

* Add docs + clean up LocalModelInfo.swift. Handle TODO for userdefaults init failure.

* Add docs + clean up RemoteModelInfo.swift, DownloaderUserDefaults.swift.

* Add docs + clean up ModelInfoRetriever.swift.

* Add docs + clean up ModelDownloadConditions.swift.

* Add docs + clean up DeviceLogger.swift.

* Add docs + clean up TelemetryLogger.swift.

* Add docs + clean up ModelFileManager.swift.

* Remove resolved TODOs + other minor fixes.

* Update model download conditions init.

* Fix comment formatting.

* Minor newline formatting.

* Minor comment formatting.

* Add docs for ModelDownloadConditions.

* Basic fix for user defaults race condition.

* Minor updates to tests.

* Minor updates to tests.

* Final fixes (#7471)

* Refactor error parsing in model info response.

* Add Firelog for delete event + minor fixes.

* Temporary - test to check telemetry for delete event.

* Disable backup of model files; handle inability to locate model storage dir.

* Minor fixes.

* Add tflite path extension to model file.
2021-02-10 09:47:07 -08:00

192 lines
6.0 KiB
Swift

// 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? {
#if os(tvOS)
return fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first
#else
return fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first ?? nil
#endif
}
/// 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)"
}
}
}