GBA003/External/Harmony/Backends/Dropbox/Harmony-Dropbox/DropboxService.swift
2024-05-30 10:22:15 +08:00

476 lines
18 KiB
Swift

//
// DropboxService.swift
// Harmony-Dropbox
//
// Created by Riley Testut on 3/4/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
import CoreData
import Harmony
import SwiftyDropbox
extension DropboxService
{
enum DropboxError: LocalizedError
{
case nilDirectoryName
var errorDescription: String? {
switch self
{
case .nilDirectoryName: return NSLocalizedString("There is no provided Dropbox directory name.", comment: "")
}
}
}
private struct OAuthError: LocalizedError
{
var oAuthError: OAuth2Error
var errorDescription: String?
init(error: OAuth2Error, description: String)
{
self.oAuthError = error
self.errorDescription = description
}
}
internal enum CallError<T>: Error
{
case error(SwiftyDropbox.CallError<T>)
init(_ callError: SwiftyDropbox.CallError<T>)
{
self = .error(callError)
}
init?(_ callError: SwiftyDropbox.CallError<T>?)
{
guard let callError = callError else { return nil }
self = .error(callError)
}
}
}
public class DropboxService: NSObject, Service
{
public static let shared = DropboxService()
public let localizedName = NSLocalizedString("Dropbox", comment: "")
public let identifier = "com.rileytestut.Harmony.Dropbox"
public var clientID: String? {
didSet {
guard let clientID = self.clientID else { return }
DropboxClientsManager.setupWithAppKey(clientID)
}
}
public var preferredDirectoryName: String?
internal private(set) var dropboxClient: DropboxClient?
internal let responseQueue = DispatchQueue(label: "com.rileytestut.Harmony.Dropbox.responseQueue")
private var authorizationCompletionHandlers = [(Result<Account, AuthenticationError>) -> Void]()
private var accountID: String? {
get {
return UserDefaults.standard.string(forKey: "harmony-dropbox_accountID")
}
set {
UserDefaults.standard.set(newValue, forKey: "harmony-dropbox_accountID")
}
}
private(set) var propertyGroupTemplate: (String, FileProperties.PropertyGroupTemplate)?
private override init()
{
super.init()
}
}
public extension DropboxService
{
func authenticate(withPresentingViewController viewController: UIViewController, completionHandler: @escaping (Result<Account, AuthenticationError>) -> Void)
{
self.authorizationCompletionHandlers.append(completionHandler)
DropboxClientsManager.authorizeFromController(UIApplication.shared, controller: viewController) { (url) in
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
}
func authenticateInBackground(completionHandler: @escaping (Result<Account, AuthenticationError>) -> Void)
{
guard let accountID = self.accountID else { return completionHandler(.failure(.noSavedCredentials)) }
self.authorizationCompletionHandlers.append(completionHandler)
DropboxClientsManager.reauthorizeClient(accountID)
self.finishAuthentication()
}
func deauthenticate(completionHandler: @escaping (Result<Void, DeauthenticationError>) -> Void)
{
DropboxClientsManager.unlinkClients()
self.accountID = nil
completionHandler(.success)
}
func handleDropboxURL(_ url: URL) -> Bool
{
guard let result = DropboxClientsManager.handleRedirectURL(url) else { return false }
switch result
{
case .cancel:
self.authorizationCompletionHandlers.forEach { $0(.failure(.other(GeneralError.cancelled))) }
self.authorizationCompletionHandlers.removeAll()
case .success:
self.finishAuthentication()
case .error(let error, let description):
print("Error authorizing with Dropbox.", error, description)
let oAuthError = OAuthError(error: error, description: description)
self.authorizationCompletionHandlers.forEach { $0(.failure(.other(oAuthError))) }
self.authorizationCompletionHandlers.removeAll()
}
return true
}
}
private extension DropboxService
{
func finishAuthentication()
{
func finish(_ result: Result<Account, AuthenticationError>)
{
// Reset self.authorizationCompletionHandlers _before_ calling all the completion handlers.
// This stops us from accidentally calling completion handlers twice in some instances.
let completionHandlers = self.authorizationCompletionHandlers
self.authorizationCompletionHandlers.removeAll()
completionHandlers.forEach { $0(result) }
}
guard let dropboxClient = DropboxClientsManager.authorizedClient else { return finish(.failure(.notAuthenticated)) }
dropboxClient.users.getCurrentAccount().response { (account, error) in
do
{
let account = try self.process(Result(account, error))
self.createSyncDirectoryIfNeeded() { (result) in
switch result
{
case .success:
// Validate metadata first so we can also retrieve property group template ID.
let dummyMetadata = HarmonyMetadataKey.allHarmonyKeys.reduce(into: [:], { $0[$1] = $1.rawValue as Any })
self.validateMetadata(dummyMetadata) { (result) in
switch result
{
case .success:
// We could just always use DropboxClientsManager.authorizedClient,
// but this way dropboxClient is nil until _all_ authentication steps are finished.
self.dropboxClient = dropboxClient
self.accountID = account.accountId
let account = Account(name: account.name.displayName, emailAddress: account.email)
finish(.success(account))
case .failure(let error):
finish(.failure(AuthenticationError(error)))
}
}
case .failure(let error):
finish(.failure(AuthenticationError(error)))
}
}
}
catch
{
finish(.failure(AuthenticationError(error)))
}
}
}
func createSyncDirectoryIfNeeded(completionHandler: @escaping (Result<Void, Error>) -> Void)
{
do
{
guard let dropboxClient = DropboxClientsManager.authorizedClient else { throw AuthenticationError.notAuthenticated }
let path = try self.remotePath(filename: nil)
dropboxClient.files.getMetadata(path: path).response(queue: self.responseQueue) { (metadata, error) in
// Retrieved metadata successfully, which means folder exists, so no need to do anything else.
guard let error = error else { return completionHandler(.success) }
if case .routeError(let error, _, _, _) = error, case .path(.notFound) = error.unboxed
{
dropboxClient.files.createFolderV2(path: path).response(queue: self.responseQueue) { (result, error) in
do
{
try self.process(Result(error))
completionHandler(.success)
}
catch
{
completionHandler(.failure(error))
}
}
}
else
{
completionHandler(.failure(CallError(error)))
}
}
}
catch
{
completionHandler(.failure(error))
}
}
}
extension DropboxService
{
func process<T, E>(_ result: Result<T, CallError<E>>) throws -> T
{
do
{
do
{
let value = try result.get()
return value
}
catch let error
{
do
{
throw error
}
catch CallError<E>.error(.authError(let authError, _, _, _))
{
switch authError
{
case .invalidAccessToken: throw AuthenticationError.notAuthenticated
case .expiredAccessToken: throw AuthenticationError.tokenExpired
default: break
}
}
catch CallError<E>.error(.rateLimitError)
{
throw ServiceError.rateLimitExceeded
}
catch CallError<E>.error(.clientError(let error as URLError))
{
throw ServiceError.connectionFailed(error)
}
catch CallError<E>.error(.routeError(let boxedError, _, _, _))
{
switch boxedError.unboxed
{
case let error as Files.DownloadError:
if case .path(.notFound) = error
{
throw ServiceError.itemDoesNotExist
}
if case .path(.restrictedContent) = error
{
throw ServiceError.restrictedContent
}
case let error as Files.UploadError:
if case .path(let failure) = error
{
switch failure.reason
{
case .insufficientSpace: throw ServiceError.insufficentSpace
default: break
}
}
case let error as Files.GetMetadataError:
if case .path(.notFound) = error
{
throw ServiceError.itemDoesNotExist
}
case let error as Files.DeleteError:
if case .pathLookup(.notFound) = error
{
throw ServiceError.itemDoesNotExist
}
case let error as Files.ListRevisionsError:
if case .path(.notFound) = error
{
throw ServiceError.itemDoesNotExist
}
default: break
}
}
catch
{
// Ignore, just here to prevent propagating to outer do-catch.
}
// If we haven't re-thrown the error as a HarmonyError by now, throw it now.
throw ServiceError(error)
}
}
catch let error as HarmonyError
{
throw error
}
catch
{
assertionFailure("Non-HarmonyError thrown from DropboxService.process(_:)")
throw error
}
}
func validateMetadata<T>(_ metadata: [HarmonyMetadataKey: T], completionHandler: @escaping (Result<String, Error>) -> Void)
{
let fields = metadata.keys.map { FileProperties.PropertyFieldTemplate(name: $0.rawValue, description_: $0.rawValue, type: .string_) }
do
{
guard let dropboxClient = DropboxClientsManager.authorizedClient else { throw AuthenticationError.notAuthenticated }
if let (templateID, propertyGroupTemplate) = self.propertyGroupTemplate
{
let existingFields = Set(propertyGroupTemplate.fields.map { $0.name })
let addedFields = fields.filter { !existingFields.contains($0.name) }
guard !addedFields.isEmpty else { return completionHandler(.success(templateID)) }
dropboxClient.file_properties.templatesUpdateForUser(templateId: templateID, name: nil, description_: nil, addFields: addedFields).response(queue: self.responseQueue) { (result, error) in
do
{
let result = try self.process(Result(result, error))
let templateID = result.templateId
self.fetchPropertyGroupTemplate(forTemplateID: templateID) { (result) in
switch result
{
case .success: completionHandler(.success(templateID))
case .failure(let error): completionHandler(.failure(error))
}
}
}
catch
{
completionHandler(.failure(error))
}
}
}
else
{
dropboxClient.file_properties.templatesListForUser().response(queue: self.responseQueue) { (result, error) in
do
{
let result = try self.process(Result(result, error))
if let templateID = result.templateIds.first
{
self.fetchPropertyGroupTemplate(forTemplateID: templateID) { (result) in
switch result
{
case .success: self.validateMetadata(metadata, completionHandler: completionHandler)
case .failure(let error): completionHandler(.failure(error))
}
}
}
else
{
dropboxClient.file_properties.templatesAddForUser(name: "Harmony", description_: "Harmony syncing metadata.", fields: fields).response(queue: self.responseQueue) { (result, error) in
do
{
let result = try self.process(Result(result, error))
let templateID = result.templateId
self.fetchPropertyGroupTemplate(forTemplateID: templateID) { (result) in
switch result
{
case .success: completionHandler(.success(templateID))
case .failure(let error): completionHandler(.failure(error))
}
}
}
catch
{
completionHandler(.failure(error))
}
}
}
}
catch
{
completionHandler(.failure(error))
}
}
}
}
catch
{
completionHandler(.failure(error))
}
}
func fetchPropertyGroupTemplate(forTemplateID templateID: String, completionHandler: @escaping (Result<FileProperties.PropertyGroupTemplate, Error>) -> Void)
{
do
{
guard let dropboxClient = DropboxClientsManager.authorizedClient else { throw AuthenticationError.notAuthenticated }
dropboxClient.file_properties.templatesGetForUser(templateId: templateID).response(queue: self.responseQueue) { (result, error) in
do
{
let result = try self.process(Result(result, error))
self.propertyGroupTemplate = (templateID, result)
completionHandler(.success(result))
}
catch
{
completionHandler(.failure(error))
}
}
}
catch
{
completionHandler(.failure(error))
}
}
func remotePath(filename: String?) throws -> String
{
guard let directoryName = self.preferredDirectoryName else { throw DropboxError.nilDirectoryName }
var remotePath = "/" + directoryName
if let filename = filename
{
remotePath += "/" + filename
}
return remotePath
}
}