// 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 /// The error domain for codes in the `StorageErrorCode` enum. public let StorageErrorDomain: String = "FIRStorageErrorDomain" /** * Adds wrappers for common Firebase Storage errors (including creating errors from GCS errors). * For more information on unwrapping GCS errors, see the GCS errors docs: * https://cloud.google.com/storage/docs/json_api/v1/status-codes * This is never publicly exposed to end developers (as they will simply see an NSError). */ @objc(FIRStorageErrorCode) public enum StorageErrorCode: Int, Swift.Error { case unknown = -13000 case objectNotFound = -13010 case bucketNotFound = -13011 case projectNotFound = -13012 case quotaExceeded = -13013 case unauthenticated = -13020 case unauthorized = -13021 case retryLimitExceeded = -13030 case nonMatchingChecksum = -13031 case downloadSizeExceeded = -13032 case cancelled = -13040 case invalidArgument = -13050 /** * Creates a Firebase Storage error from a specific GCS error and FIRIMPLStorageReference. * @param serverError Server error to wrap and return as a Firebase Storage error. * @param ref StorageReference which provides context about the request being made. * @return Returns a Firebase Storage error. */ static func error(withServerError serverError: NSError, ref: StorageReference) -> NSError { var errorCode: StorageErrorCode switch serverError.code { case 400: errorCode = .unknown case 401: errorCode = .unauthenticated case 402: errorCode = .quotaExceeded case 403: errorCode = .unauthorized case 404: errorCode = .objectNotFound default: errorCode = .unknown } var errorDictionary = serverError.userInfo errorDictionary["ResponseErrorDomain"] = serverError.domain errorDictionary["ResponseErrorCode"] = serverError.code errorDictionary["bucket"] = ref.path.bucket errorDictionary[NSUnderlyingErrorKey] = serverError if let object = ref.path.object { errorDictionary["object"] = object } if let data = (errorDictionary["data"] as? Data) { errorDictionary["ResponseBody"] = String(data: data, encoding: .utf8) } return error(withCode: errorCode, infoDictionary: errorDictionary) } /** Creates a Firebase Storage error from an invalid request. * * @param request The Data representation of the invalid user request. * @return Returns the corresponding Firebase Storage error. */ static func error(withInvalidRequest request: Data?) -> NSError { var requestString: String if let request { requestString = String(data: request, encoding: .utf8) ?? "" } else { requestString = "" } let invalidDataString = "Invalid data returned from the server:\(requestString)" var localizedFailureKey: String if #available(OSX 10.13, iOS 11.0, watchOS 4.0, tvOS 11.0, *) { localizedFailureKey = NSLocalizedFailureErrorKey } else { localizedFailureKey = "NSLocalizedFailure" } return error(withCode: .unknown, infoDictionary: [localizedFailureKey: invalidDataString]) } /** * Creates a Firebase Storage error from a specific FIRStorageErrorCode while adding * custom info from an optionally provided info dictionary. */ static func error(withCode code: StorageErrorCode, infoDictionary: [String: Any]? = nil) -> NSError { var dictionary = infoDictionary ?? [:] var errorMessage: String switch code { case .objectNotFound: let object = dictionary["object"] ?? "" errorMessage = "Object \(object) does not exist." case .bucketNotFound: let bucket = dictionary["bucket"] ?? "" errorMessage = "Bucket \(bucket) does not exist." case .projectNotFound: let project = dictionary["project"] ?? "" errorMessage = "Project \(project) does not exist." case .quotaExceeded: let bucket = dictionary["bucket"] ?? "" errorMessage = "Quota for bucket \(bucket) exceeded, please view quota on firebase.google.com." case .downloadSizeExceeded: let total = "\(dictionary["totalSize"] ?? "unknown")" let size = "\(dictionary["maxAllowedSize"] ?? "unknown")" errorMessage = "Attempted to download object with size of \(total) bytes, " + "which exceeds the maximum size of \(size) bytes. " + "Consider raising the maximum download size, or using StorageReference.write" case .unauthenticated: errorMessage = "User is not authenticated, please authenticate using Firebase " + "Authentication and try again." case .unauthorized: let bucket = dictionary["bucket"] ?? "" let object = dictionary["object"] ?? "" errorMessage = "User does not have permission to access gs://\(bucket)/\(object)." case .retryLimitExceeded: errorMessage = "Max retry time for operation exceeded, please try again." case .nonMatchingChecksum: // TODO: replace with actual checksum strings when we choose to implement. errorMessage = "Uploaded/downloaded object TODO has checksum: TODO " + "which does not match server checksum: TODO. Please retry the upload/download." case .cancelled: errorMessage = "User cancelled the upload/download." case .unknown, .invalidArgument: // invalidArgument fell through in the old Objective-C code. errorMessage = "An unknown error occurred, please check the server response." } dictionary[NSLocalizedDescriptionKey] = errorMessage return NSError(domain: StorageErrorDomain, code: code.rawValue, userInfo: dictionary) } } public enum StorageError: Error { case unknown(String) case objectNotFound(String) case bucketNotFound(String) case projectNotFound(String) case quotaExceeded(String) case unauthenticated case unauthorized(String, String) case retryLimitExceeded case nonMatchingChecksum case downloadSizeExceeded(Int64, Int64) case cancelled case invalidArgument(String) case internalError(String) case bucketMismatch(String) case pathError(String) static func swiftConvert(objcError: NSError) -> StorageError { let userInfo = objcError.userInfo switch objcError.code { case StorageErrorCode.unknown.rawValue: return StorageError.unknown(objcError.localizedDescription) case StorageErrorCode.objectNotFound.rawValue: guard let object = userInfo["object"] as? String else { return StorageError .internalError( "Failed to decode object not found error: \(objcError.localizedDescription)" ) } return StorageError.objectNotFound(object) case StorageErrorCode.bucketNotFound.rawValue: guard let bucket = userInfo["bucket"] as? String else { return StorageError .internalError( "Failed to decode bucket not found error: \(objcError.localizedDescription)" ) } return StorageError.bucketNotFound(bucket) case StorageErrorCode.projectNotFound.rawValue: guard let project = userInfo["project"] as? String else { return StorageError .internalError( "Failed to decode project not found error: \(objcError.localizedDescription)" ) } return StorageError.projectNotFound(project) case StorageErrorCode.quotaExceeded.rawValue: guard let bucket = userInfo["bucket"] as? String else { return StorageError .internalError("Failed to decode quota exceeded error: \(objcError.localizedDescription)") } return StorageError.quotaExceeded(bucket) case StorageErrorCode.unauthenticated.rawValue: return StorageError.unauthenticated case StorageErrorCode.unauthorized.rawValue: guard let bucket = userInfo["bucket"] as? String, let object = userInfo["object"] as? String else { return StorageError .internalError( "Failed to decode unauthorized error: \(objcError.localizedDescription)" ) } return StorageError.unauthorized(bucket, object) case StorageErrorCode.retryLimitExceeded.rawValue: return StorageError.retryLimitExceeded case StorageErrorCode.nonMatchingChecksum.rawValue: return StorageError .nonMatchingChecksum case StorageErrorCode.downloadSizeExceeded.rawValue: guard let total = userInfo["totalSize"] as? Int64, let maxSize = userInfo["maxAllowedSize"] as? Int64 else { return StorageError .internalError( "Failed to decode downloadSizeExceeded error: \(objcError.localizedDescription)" ) } return StorageError.downloadSizeExceeded(total, maxSize) case StorageErrorCode.cancelled.rawValue: return StorageError.cancelled case StorageErrorCode.invalidArgument.rawValue: return StorageError .invalidArgument(objcError.localizedDescription) default: return StorageError.internalError("Internal error converting ObjC Error to Swift") } } }