From 8495070715948035dd268b718e04c9cc52b600f9 Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Tue, 3 May 2022 16:08:00 +0200 Subject: [PATCH 01/18] Added different alert when restoring purchase and user is eligible for upgrade, fixes #214 --- Cryptomator/Purchase/PurchaseAlert.swift | 14 ++++++++++---- Cryptomator/Purchase/PurchaseCoordinator.swift | 2 +- SharedResources/en.lproj/Localizable.strings | 2 ++ 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/Cryptomator/Purchase/PurchaseAlert.swift b/Cryptomator/Purchase/PurchaseAlert.swift index 80c268cde..66c7658b6 100644 --- a/Cryptomator/Purchase/PurchaseAlert.swift +++ b/Cryptomator/Purchase/PurchaseAlert.swift @@ -23,10 +23,16 @@ enum PurchaseAlert { return showAlert(title: title, message: message, on: presentingViewController) } - static func showForNoRestorablePurchases(on presentingViewController: UIViewController) -> Promise { - return showAlert(title: LocalizedString.getValue("purchase.restorePurchase.fullVersionNotFound.alert.title"), - message: LocalizedString.getValue("purchase.restorePurchase.fullVersionNotFound.alert.message"), - on: presentingViewController) + static func showForNoRestorablePurchases(on presentingViewController: UIViewController, eligibleForUpgrade: Bool) -> Promise { + if eligibleForUpgrade { + return showAlert(title: LocalizedString.getValue("purchase.restorePurchase.eligibleForUpgrade.alert.title"), + message: LocalizedString.getValue("purchase.restorePurchase.eligibleForUpgrade.alert.message"), + on: presentingViewController) + } else { + return showAlert(title: LocalizedString.getValue("purchase.restorePurchase.fullVersionNotFound.alert.title"), + message: LocalizedString.getValue("purchase.restorePurchase.fullVersionNotFound.alert.message"), + on: presentingViewController) + } } private static func showAlert(title: String, message: String, on presentingViewController: UIViewController) -> Promise { diff --git a/Cryptomator/Purchase/PurchaseCoordinator.swift b/Cryptomator/Purchase/PurchaseCoordinator.swift index 9c9db1297..306abdf5d 100644 --- a/Cryptomator/Purchase/PurchaseCoordinator.swift +++ b/Cryptomator/Purchase/PurchaseCoordinator.swift @@ -66,7 +66,7 @@ class PurchaseCoordinator: Coordinator { self.unlockedPro() } case .noRestorablePurchases: - _ = PurchaseAlert.showForNoRestorablePurchases(on: navigationController) + _ = PurchaseAlert.showForNoRestorablePurchases(on: navigationController, eligibleForUpgrade: UpgradeChecker.shared.isEligibleForUpgrade()) } } diff --git a/SharedResources/en.lproj/Localizable.strings b/SharedResources/en.lproj/Localizable.strings index fd10c1550..9a8313728 100644 --- a/SharedResources/en.lproj/Localizable.strings +++ b/SharedResources/en.lproj/Localizable.strings @@ -149,6 +149,8 @@ "purchase.restorePurchase.fullVersionFound.alert.title" = "Restore Successful"; "purchase.restorePurchase.fullVersionNotFound.alert.title" = "No Full Version"; "purchase.restorePurchase.fullVersionNotFound.alert.message" = "We were unable to find a previously purchased full version that could be restored. Please try another option."; +"purchase.restorePurchase.eligibleForUpgrade.alert.title" = "Eligible for Upgrade"; +"purchase.restorePurchase.eligibleForUpgrade.alert.message" = "It seems like that you are trying to upgrade from an older version of Cryptomator. In that case, please select the \"Upgrade Offer\" option instead."; "purchase.retry.button" = "Retry"; "purchase.retry.footer" = "Could not load the available products."; "purchase.title" = "Unlock Full Version"; From 6799b3ab340a92804866543386911072240b2a4a Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Fri, 6 May 2022 18:01:22 +0200 Subject: [PATCH 02/18] Refactored the FileProviderServiceSources --- Cryptomator.xcodeproj/project.pbxproj | 18 +++++- Cryptomator/Settings/SettingsViewModel.swift | 2 +- .../ChangePasswordViewModel.swift | 2 +- .../VaultKeepUnlockedViewModel.swift | 4 +- .../MoveVault/MoveVaultViewModel.swift | 2 +- .../VaultDetail/VaultDetailViewModel.swift | 4 +- .../VaultList/VaultCellViewModel.swift | 2 +- .../VaultList/VaultListViewModel.swift | 4 +- .../FileProviderXPC/LogLevelUpdating.swift | 6 +- .../FileProviderXPC/VaultLocking.swift | 6 +- .../FileProviderXPC/VaultUnlocking.swift | 6 +- .../LogLevelUpdatingServiceSource.swift | 58 ----------------- .../LogLevelUpdatingServiceSource.swift | 31 +++++++++ .../ServiceSource/ServiceSource.swift | 45 +++++++++++++ .../VaultLockingServiceSource.swift | 30 +-------- .../VaultUnlockingServiceSource.swift | 64 +++++++++++++++++++ CryptomatorTests/SettingsViewModelTests.swift | 2 +- .../VaultKeepUnlockedViewModelTests.swift | 2 +- .../UnlockVaultViewModel.swift | 4 +- 19 files changed, 178 insertions(+), 114 deletions(-) delete mode 100644 CryptomatorFileProvider/LogLevelUpdatingServiceSource.swift create mode 100644 CryptomatorFileProvider/ServiceSource/LogLevelUpdatingServiceSource.swift create mode 100644 CryptomatorFileProvider/ServiceSource/ServiceSource.swift rename CryptomatorFileProvider/{ => ServiceSource}/VaultLockingServiceSource.swift (57%) create mode 100644 CryptomatorFileProvider/ServiceSource/VaultUnlockingServiceSource.swift diff --git a/Cryptomator.xcodeproj/project.pbxproj b/Cryptomator.xcodeproj/project.pbxproj index a455db5a3..8cc332b49 100644 --- a/Cryptomator.xcodeproj/project.pbxproj +++ b/Cryptomator.xcodeproj/project.pbxproj @@ -221,6 +221,7 @@ 4AA22BFB261CA69F00A17486 /* WebDAVAuthenticationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA22BFA261CA69F00A17486 /* WebDAVAuthenticationViewController.swift */; }; 4AA22C16261CA8D800A17486 /* URLFieldCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA22C15261CA8D800A17486 /* URLFieldCell.swift */; }; 4AA22C1E261CA94700A17486 /* UsernameFieldCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA22C1D261CA94700A17486 /* UsernameFieldCell.swift */; }; + 4AA2531B28216E45003B45EE /* ServiceSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA2531A28216E45003B45EE /* ServiceSource.swift */; }; 4AA621D9249A6A8400A0BCBD /* FileProviderExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA621D8249A6A8400A0BCBD /* FileProviderExtension.swift */; }; 4AA8613725C19D4F002A59F5 /* DetectedMasterkeyViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA8613625C19D4F002A59F5 /* DetectedMasterkeyViewModel.swift */; }; 4AA8614825C1C670002A59F5 /* OpenExistingVaultChooseFolderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA8614725C1C670002A59F5 /* OpenExistingVaultChooseFolderViewController.swift */; }; @@ -697,6 +698,7 @@ 4AA22BFA261CA69F00A17486 /* WebDAVAuthenticationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebDAVAuthenticationViewController.swift; sourceTree = ""; }; 4AA22C15261CA8D800A17486 /* URLFieldCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLFieldCell.swift; sourceTree = ""; }; 4AA22C1D261CA94700A17486 /* UsernameFieldCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UsernameFieldCell.swift; sourceTree = ""; }; + 4AA2531A28216E45003B45EE /* ServiceSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceSource.swift; sourceTree = ""; }; 4AA621D6249A6A8400A0BCBD /* FileProviderExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = FileProviderExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 4AA621D8249A6A8400A0BCBD /* FileProviderExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileProviderExtension.swift; sourceTree = ""; }; 4AA621DE249A6A8400A0BCBD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -1436,6 +1438,17 @@ path = Cells; sourceTree = ""; }; + 4AA2531728216BD1003B45EE /* ServiceSource */ = { + isa = PBXGroup; + children = ( + 4AEFF7F127145ADD00D6CB99 /* LogLevelUpdatingServiceSource.swift */, + 4AA2531A28216E45003B45EE /* ServiceSource.swift */, + 4A24001926AE9F3A009DBC2E /* VaultLockingServiceSource.swift */, + 4A9BED63268F1DB000721BAA /* VaultUnlockingServiceSource.swift */, + ); + path = ServiceSource; + sourceTree = ""; + }; 4AA621D7249A6A8400A0BCBD /* FileProviderExtension */ = { isa = PBXGroup; children = ( @@ -1655,17 +1668,15 @@ 4A2060CC2799645300DA6C62 /* FileProviderNotificatorManager.swift */, 740375F42587AEB50023FF53 /* ItemStatus.swift */, 4AB1D4EB27D0E027009060AB /* LocalURLProviderType.swift */, - 4AEFF7F127145ADD00D6CB99 /* LogLevelUpdatingServiceSource.swift */, 4AC1157527F5BD890023F51B /* Promise+AllIgnoringResult.swift */, 4ADD233F26737CD400374E4E /* RootFileProviderItem.swift */, 4ADC66C027A7F426002E6CC7 /* UnlockMonitor.swift */, 740375F52587AEB50023FF53 /* URL+NameCollisionExtension.swift */, - 4A24001926AE9F3A009DBC2E /* VaultLockingServiceSource.swift */, - 4A9BED63268F1DB000721BAA /* VaultUnlockingServiceSource.swift */, 4AE0D8D82653D8F200DF5D22 /* CloudTask */, 740376022587AEB60023FF53 /* DB */, 740375FD2587AEB60023FF53 /* Locks */, 4AEBE8C326540D3A0031487F /* Middleware */, + 4AA2531728216BD1003B45EE /* ServiceSource */, 4A511D4F26610000000A0E01 /* Workflow */, ); path = CryptomatorFileProvider; @@ -2580,6 +2591,7 @@ 4AB1D4EC27D0E027009060AB /* LocalURLProviderType.swift in Sources */, 4A511D4E2660FF9E000A0E01 /* WorkflowScheduler.swift in Sources */, 4A2482352671110A002D9F59 /* DBManagerError.swift in Sources */, + 4AA2531B28216E45003B45EE /* ServiceSource.swift in Sources */, 4A2060D5279AF67C00DA6C62 /* WorkingSetObserver.swift in Sources */, 4A511D5B26668E0C000A0E01 /* UploadTaskRecord.swift in Sources */, 4A511D5F26668E68000A0E01 /* DeletionTaskRecord.swift in Sources */, diff --git a/Cryptomator/Settings/SettingsViewModel.swift b/Cryptomator/Settings/SettingsViewModel.swift index f2df2b429..2d4846ff0 100644 --- a/Cryptomator/Settings/SettingsViewModel.swift +++ b/Cryptomator/Settings/SettingsViewModel.swift @@ -152,7 +152,7 @@ class SettingsViewModel: TableViewModel { } private func notifyFileProviderAboutLogLevelUpdate() { - let getXPCPromise: Promise> = fileProviderConnector.getXPC(serviceName: LogLevelUpdatingService.name, domain: nil) + let getXPCPromise: Promise> = fileProviderConnector.getXPC(serviceName: .logLevelUpdating, domain: nil) getXPCPromise.then { xpc in xpc.proxy.logLevelUpdated() }.always { diff --git a/Cryptomator/VaultDetail/ChangePassword/ChangePasswordViewModel.swift b/Cryptomator/VaultDetail/ChangePassword/ChangePasswordViewModel.swift index a93fb79a8..7cc60abdd 100644 --- a/Cryptomator/VaultDetail/ChangePassword/ChangePasswordViewModel.swift +++ b/Cryptomator/VaultDetail/ChangePassword/ChangePasswordViewModel.swift @@ -169,7 +169,7 @@ class ChangePasswordViewModel: TableViewModel, ChangePass private func lockVault() -> Promise { let domainIdentifier = NSFileProviderDomainIdentifier(vaultAccount.vaultUID) - let getXPCPromise: Promise> = fileProviderConnector.getXPC(serviceName: VaultLockingService.name, domainIdentifier: domainIdentifier) + let getXPCPromise: Promise> = fileProviderConnector.getXPC(serviceName: .vaultLocking, domainIdentifier: domainIdentifier) return getXPCPromise.then { xpc -> Void in xpc.proxy.lockVault(domainIdentifier: domainIdentifier) }.always { diff --git a/Cryptomator/VaultDetail/KeepUnlocked/VaultKeepUnlockedViewModel.swift b/Cryptomator/VaultDetail/KeepUnlocked/VaultKeepUnlockedViewModel.swift index d1c442c60..08e1f36d3 100644 --- a/Cryptomator/VaultDetail/KeepUnlocked/VaultKeepUnlockedViewModel.swift +++ b/Cryptomator/VaultDetail/KeepUnlocked/VaultKeepUnlockedViewModel.swift @@ -82,7 +82,7 @@ class VaultKeepUnlockedViewModel: TableViewModel, Vaul func gracefulLockVault() -> Promise { let domainIdentifier = NSFileProviderDomainIdentifier(vaultUID) - let getXPCPromise: Promise> = fileProviderConnector.getXPC(serviceName: VaultLockingService.name, domainIdentifier: domainIdentifier) + let getXPCPromise: Promise> = fileProviderConnector.getXPC(serviceName: .vaultLocking, domainIdentifier: domainIdentifier) return getXPCPromise.then { xpc in xpc.proxy.gracefulLockVault(domainIdentifier: domainIdentifier) }.then { @@ -118,7 +118,7 @@ class VaultKeepUnlockedViewModel: TableViewModel, Vaul private func getVaultIsUnlocked() -> Promise { let domainIdentifier = NSFileProviderDomainIdentifier(vaultUID) - let getXPCPromise: Promise> = fileProviderConnector.getXPC(serviceName: VaultLockingService.name, domainIdentifier: domainIdentifier) + let getXPCPromise: Promise> = fileProviderConnector.getXPC(serviceName: .vaultLocking, domainIdentifier: domainIdentifier) return getXPCPromise.then { xpc in return xpc.proxy.getIsUnlockedVault(domainIdentifier: domainIdentifier) }.always { diff --git a/Cryptomator/VaultDetail/MoveVault/MoveVaultViewModel.swift b/Cryptomator/VaultDetail/MoveVault/MoveVaultViewModel.swift index 2f69b5eb5..d193f60e7 100644 --- a/Cryptomator/VaultDetail/MoveVault/MoveVaultViewModel.swift +++ b/Cryptomator/VaultDetail/MoveVault/MoveVaultViewModel.swift @@ -73,7 +73,7 @@ class MoveVaultViewModel: ChooseFolderViewModel, MoveVaultViewModelProtocol { private func lockVault() -> Promise { let domainIdentifier = NSFileProviderDomainIdentifier(vaultInfo.vaultUID) - let getXPCPromise: Promise> = fileProviderConnector.getXPC(serviceName: VaultLockingService.name, domainIdentifier: domainIdentifier) + let getXPCPromise: Promise> = fileProviderConnector.getXPC(serviceName: .vaultLocking, domainIdentifier: domainIdentifier) return getXPCPromise.then { xpc -> Void in xpc.proxy.lockVault(domainIdentifier: domainIdentifier) self.fileProviderConnector.invalidateXPC(xpc) diff --git a/Cryptomator/VaultDetail/VaultDetailViewModel.swift b/Cryptomator/VaultDetail/VaultDetailViewModel.swift index 239a5ab48..e46d333f8 100644 --- a/Cryptomator/VaultDetail/VaultDetailViewModel.swift +++ b/Cryptomator/VaultDetail/VaultDetailViewModel.swift @@ -215,7 +215,7 @@ class VaultDetailViewModel: VaultDetailViewModelProtocol { func lockVault() -> Promise { let domainIdentifier = NSFileProviderDomainIdentifier(vaultUID) - let getXPCPromise: Promise> = fileProviderConnector.getXPC(serviceName: VaultLockingService.name, domainIdentifier: domainIdentifier) + let getXPCPromise: Promise> = fileProviderConnector.getXPC(serviceName: .vaultLocking, domainIdentifier: domainIdentifier) return getXPCPromise.then { xpc -> Void in xpc.proxy.lockVault(domainIdentifier: domainIdentifier) @@ -225,7 +225,7 @@ class VaultDetailViewModel: VaultDetailViewModelProtocol { func refreshVaultStatus() -> Promise { let domainIdentifier = NSFileProviderDomainIdentifier(vaultUID) - let getXPCPromise: Promise> = fileProviderConnector.getXPC(serviceName: VaultLockingService.name, domainIdentifier: domainIdentifier) + let getXPCPromise: Promise> = fileProviderConnector.getXPC(serviceName: .vaultLocking, domainIdentifier: domainIdentifier) switchCellViewModel?.isOn.value = biometricalUnlockEnabled return getXPCPromise.then { xpc in return wrap { handler in diff --git a/Cryptomator/VaultList/VaultCellViewModel.swift b/Cryptomator/VaultList/VaultCellViewModel.swift index 66faeaba0..0119693f8 100644 --- a/Cryptomator/VaultList/VaultCellViewModel.swift +++ b/Cryptomator/VaultList/VaultCellViewModel.swift @@ -42,7 +42,7 @@ class VaultCellViewModel: TableViewCellViewModel, VaultCellViewModelProtocol { func lockVault() -> Promise { let domainIdentifier = NSFileProviderDomainIdentifier(vault.vaultUID) - let getXPCPromise: Promise> = fileProviderConnector.getXPC(serviceName: VaultLockingService.name, domainIdentifier: domainIdentifier) + let getXPCPromise: Promise> = fileProviderConnector.getXPC(serviceName: .vaultLocking, domainIdentifier: domainIdentifier) return getXPCPromise.then { xpc in xpc.proxy.lockVault(domainIdentifier: domainIdentifier) }.then { diff --git a/Cryptomator/VaultList/VaultListViewModel.swift b/Cryptomator/VaultList/VaultListViewModel.swift index 39886d7c8..73b467a27 100644 --- a/Cryptomator/VaultList/VaultListViewModel.swift +++ b/Cryptomator/VaultList/VaultListViewModel.swift @@ -93,7 +93,7 @@ class VaultListViewModel: ViewModel, VaultListViewModelProtocol { func lockVault(_ vaultInfo: VaultInfo) -> Promise { let domainIdentifier = NSFileProviderDomainIdentifier(vaultInfo.vaultUID) - let getXPCPromise: Promise> = fileProviderConnector.getXPC(serviceName: VaultLockingService.name, domainIdentifier: domainIdentifier) + let getXPCPromise: Promise> = fileProviderConnector.getXPC(serviceName: .vaultLocking, domainIdentifier: domainIdentifier) return getXPCPromise.then { xpc in xpc.proxy.lockVault(domainIdentifier: domainIdentifier) }.then { @@ -104,7 +104,7 @@ class VaultListViewModel: ViewModel, VaultListViewModelProtocol { } func refreshVaultLockStates() -> Promise { - let getXPCPromise: Promise> = fileProviderConnector.getXPC(serviceName: VaultLockingService.name, domain: nil) + let getXPCPromise: Promise> = fileProviderConnector.getXPC(serviceName: .vaultLocking, domain: nil) return getXPCPromise.then { xpc in return wrap { handler in diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/FileProviderXPC/LogLevelUpdating.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/FileProviderXPC/LogLevelUpdating.swift index 1e202e6e0..e83e018e5 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/FileProviderXPC/LogLevelUpdating.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/FileProviderXPC/LogLevelUpdating.swift @@ -12,8 +12,6 @@ import Foundation func logLevelUpdated() } -public enum LogLevelUpdatingService { - public static var name: NSFileProviderServiceName { - return NSFileProviderServiceName("org.cryptomator.ios.log-level-updating") - } +public extension NSFileProviderServiceName { + static let logLevelUpdating = NSFileProviderServiceName("org.cryptomator.ios.log-level-updating") } diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/FileProviderXPC/VaultLocking.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/FileProviderXPC/VaultLocking.swift index b51546aa0..efa769e1c 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/FileProviderXPC/VaultLocking.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/FileProviderXPC/VaultLocking.swift @@ -17,10 +17,8 @@ import Promises func getUnlockedVaultDomainIdentifiers(reply: @escaping ([NSFileProviderDomainIdentifier]) -> Void) } -public enum VaultLockingService { - public static var name: NSFileProviderServiceName { - return NSFileProviderServiceName("org.cryptomator.ios.vault-locking") - } +public extension NSFileProviderServiceName { + static let vaultLocking = NSFileProviderServiceName("org.cryptomator.ios.vault-locking") } // MARK: Convenience diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/FileProviderXPC/VaultUnlocking.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/FileProviderXPC/VaultUnlocking.swift index 1407a363a..783ebb863 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/FileProviderXPC/VaultUnlocking.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/FileProviderXPC/VaultUnlocking.swift @@ -15,8 +15,6 @@ import Foundation func endBiometricalUnlock() } -public enum VaultUnlockingService { - public static var name: NSFileProviderServiceName { - return NSFileProviderServiceName("org.cryptomator.ios.vault-unlocking") - } +public extension NSFileProviderServiceName { + static let vaultUnlocking = NSFileProviderServiceName("org.cryptomator.ios.vault-unlocking") } diff --git a/CryptomatorFileProvider/LogLevelUpdatingServiceSource.swift b/CryptomatorFileProvider/LogLevelUpdatingServiceSource.swift deleted file mode 100644 index 829c621d0..000000000 --- a/CryptomatorFileProvider/LogLevelUpdatingServiceSource.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// LogLevelUpdatingServiceSource.swift -// CryptomatorFileProvider -// -// Created by Philipp Schmid on 11.10.21. -// Copyright © 2021 Skymatic GmbH. All rights reserved. -// - -import CocoaLumberjackSwift -import CryptomatorCommonCore -import FileProvider -import Foundation - -public class LogLevelUpdatingServiceSource: NSObject, NSFileProviderServiceSource, NSXPCListenerDelegate, LogLevelUpdating { - public var serviceName: NSFileProviderServiceName { - LogLevelUpdatingService.name - } - - private lazy var listener: NSXPCListener = { - let listener = NSXPCListener.anonymous() - listener.delegate = self - listener.resume() - return listener - }() - - private let cryptomatorSettings: CryptomatorSettings - - init(cryptomatorSettings: CryptomatorSettings) { - self.cryptomatorSettings = cryptomatorSettings - } - - override public convenience init() { - self.init(cryptomatorSettings: CryptomatorUserDefaults.shared) - } - - public func makeListenerEndpoint() throws -> NSXPCListenerEndpoint { - return listener.endpoint - } - - // MARK: - NSXPCListenerDelegate - - public func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool { - newConnection.exportedInterface = NSXPCInterface(with: LogLevelUpdating.self) - newConnection.exportedObject = self - newConnection.resume() - weak var weakConnection = newConnection - newConnection.interruptionHandler = { - weakConnection?.invalidate() - } - return true - } - - // MARK: - LogLevelUpdating - - public func logLevelUpdated() { - LoggerSetup.setDynamicLogLevel(debugModeEnabled: cryptomatorSettings.debugModeEnabled) - } -} diff --git a/CryptomatorFileProvider/ServiceSource/LogLevelUpdatingServiceSource.swift b/CryptomatorFileProvider/ServiceSource/LogLevelUpdatingServiceSource.swift new file mode 100644 index 000000000..f554c79fc --- /dev/null +++ b/CryptomatorFileProvider/ServiceSource/LogLevelUpdatingServiceSource.swift @@ -0,0 +1,31 @@ +// +// LogLevelUpdatingServiceSource.swift +// CryptomatorFileProvider +// +// Created by Philipp Schmid on 11.10.21. +// Copyright © 2021 Skymatic GmbH. All rights reserved. +// + +import CocoaLumberjackSwift +import CryptomatorCommonCore +import FileProvider +import Foundation + +public class LogLevelUpdatingServiceSource: ServiceSource, LogLevelUpdating { + private let cryptomatorSettings: CryptomatorSettings + + init(cryptomatorSettings: CryptomatorSettings) { + self.cryptomatorSettings = cryptomatorSettings + super.init(serviceName: .logLevelUpdating, exportedInterface: NSXPCInterface(with: LogLevelUpdating.self)) + } + + public convenience init() { + self.init(cryptomatorSettings: CryptomatorUserDefaults.shared) + } + + // MARK: - LogLevelUpdating + + public func logLevelUpdated() { + LoggerSetup.setDynamicLogLevel(debugModeEnabled: cryptomatorSettings.debugModeEnabled) + } +} diff --git a/CryptomatorFileProvider/ServiceSource/ServiceSource.swift b/CryptomatorFileProvider/ServiceSource/ServiceSource.swift new file mode 100644 index 000000000..ec80ed430 --- /dev/null +++ b/CryptomatorFileProvider/ServiceSource/ServiceSource.swift @@ -0,0 +1,45 @@ +// +// ServiceSource.swift +// CryptomatorFileProvider +// +// Created by Philipp Schmid on 03.05.22. +// Copyright © 2022 Skymatic GmbH. All rights reserved. +// + +import FileProvider +import Foundation + +public class ServiceSource: NSObject, NSFileProviderServiceSource, NSXPCListenerDelegate { + public var serviceName: NSFileProviderServiceName + private let exportedInterface: NSXPCInterface + + private lazy var listener: NSXPCListener = { + let listener = NSXPCListener.anonymous() + listener.delegate = self + listener.resume() + return listener + }() + + init(serviceName: NSFileProviderServiceName, exportedInterface: NSXPCInterface) { + self.serviceName = serviceName + self.exportedInterface = exportedInterface + } + + public func makeListenerEndpoint() throws -> NSXPCListenerEndpoint { + return listener.endpoint + } + + // MARK: - NSXPCListenerDelegate + + public func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool { + newConnection.exportedInterface = exportedInterface + newConnection.exportedObject = self + newConnection.resume() + weak var weakConnection = newConnection + newConnection.interruptionHandler = { + #warning("TODO: investigate if we should set the invalidationHandler for the newConnection") + weakConnection?.invalidate() + } + return true + } +} diff --git a/CryptomatorFileProvider/VaultLockingServiceSource.swift b/CryptomatorFileProvider/ServiceSource/VaultLockingServiceSource.swift similarity index 57% rename from CryptomatorFileProvider/VaultLockingServiceSource.swift rename to CryptomatorFileProvider/ServiceSource/VaultLockingServiceSource.swift index d1ce01627..b6e5576f7 100644 --- a/CryptomatorFileProvider/VaultLockingServiceSource.swift +++ b/CryptomatorFileProvider/ServiceSource/VaultLockingServiceSource.swift @@ -11,33 +11,9 @@ import CryptomatorCommonCore import FileProvider import Foundation -public class VaultLockingServiceSource: NSObject, NSFileProviderServiceSource, NSXPCListenerDelegate, VaultLocking { - public var serviceName: NSFileProviderServiceName { - VaultLockingService.name - } - - private lazy var listener: NSXPCListener = { - let listener = NSXPCListener.anonymous() - listener.delegate = self - listener.resume() - return listener - }() - - public func makeListenerEndpoint() throws -> NSXPCListenerEndpoint { - return listener.endpoint - } - - // MARK: - NSXPCListenerDelegate - - public func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool { - newConnection.exportedInterface = NSXPCInterface(with: VaultLocking.self) - newConnection.exportedObject = self - newConnection.resume() - weak var weakConnection = newConnection - newConnection.interruptionHandler = { - weakConnection?.invalidate() - } - return true +public class VaultLockingServiceSource: ServiceSource, VaultLocking { + public init() { + super.init(serviceName: .vaultLocking, exportedInterface: NSXPCInterface(with: VaultLocking.self)) } // MARK: - VaultLocking diff --git a/CryptomatorFileProvider/ServiceSource/VaultUnlockingServiceSource.swift b/CryptomatorFileProvider/ServiceSource/VaultUnlockingServiceSource.swift new file mode 100644 index 000000000..0e0e51e37 --- /dev/null +++ b/CryptomatorFileProvider/ServiceSource/VaultUnlockingServiceSource.swift @@ -0,0 +1,64 @@ +// +// VaultUnlockingServiceSource.swift +// FileProviderExtension +// +// Created by Philipp Schmid on 02.07.21. +// Copyright © 2021 Skymatic GmbH. All rights reserved. +// + +import CocoaLumberjackSwift +import CryptomatorCommonCore +import FileProvider +import Foundation + +public class VaultUnlockingServiceSource: ServiceSource, VaultUnlocking { + private let domain: NSFileProviderDomain + private let notificator: FileProviderNotificatorType? + private let dbPath: URL? + private let localURLProvider: LocalURLProviderType + private var vaultUID: String { + return domain.identifier.rawValue + } + + public init(domain: NSFileProviderDomain, notificator: FileProviderNotificatorType?, dbPath: URL?, delegate: LocalURLProviderType) { + self.domain = domain + self.notificator = notificator + self.dbPath = dbPath + self.localURLProvider = delegate + super.init(serviceName: .vaultUnlocking, exportedInterface: NSXPCInterface(with: VaultUnlocking.self)) + } + + // MARK: - VaultUnlocking + + public func unlockVault(kek: [UInt8], reply: @escaping (NSError?) -> Void) { + let domain = self.domain + let vaultUID = vaultUID + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + guard let notificator = self.notificator else { + DDLogError("Unlocking vault failed, unable to find FileProviderDomain") + reply(VaultManagerError.fileProviderDomainNotFound as NSError) + return + } + do { + try FileProviderAdapterManager.shared.unlockVault(with: domain.identifier, kek: kek, dbPath: self.dbPath, delegate: self.localURLProvider, notificator: notificator) + FileProviderAdapterManager.shared.unlockMonitor.unlockSucceeded(forVaultUID: vaultUID) + DDLogInfo("Unlocked vault \"\(domain.displayName)\" (\(domain.identifier.rawValue))") + reply(nil) + } catch { + FileProviderAdapterManager.shared.unlockMonitor.unlockFailed(forVaultUID: vaultUID) + DDLogError("Unlocking vault \"\(domain.displayName)\" (\(domain.identifier.rawValue)) failed with error: \(error)") + reply(XPCErrorHelper.bridgeError(error)) + } + } + } + + public func startBiometricalUnlock() { + DDLogInfo("startBiometricalUnlock called for \(vaultUID)") + FileProviderAdapterManager.shared.unlockMonitor.startBiometricalUnlock(forVaultUID: vaultUID) + } + + public func endBiometricalUnlock() { + DDLogInfo("endBiometricalUnlock called for \(vaultUID)") + FileProviderAdapterManager.shared.unlockMonitor.endBiometricalUnlock(forVaultUID: vaultUID) + } +} diff --git a/CryptomatorTests/SettingsViewModelTests.swift b/CryptomatorTests/SettingsViewModelTests.swift index f16cf5d6e..5371c359b 100644 --- a/CryptomatorTests/SettingsViewModelTests.swift +++ b/CryptomatorTests/SettingsViewModelTests.swift @@ -246,7 +246,7 @@ class SettingsViewModelTests: XCTestCase { } private func checkLogLevelUpdatingServiceSourceCall() { - XCTAssertEqual(LogLevelUpdatingService.name, fileProviderConnectorMock.passedServiceName) + XCTAssertEqual(.logLevelUpdating, fileProviderConnectorMock.passedServiceName) XCTAssertNil(fileProviderConnectorMock.passedDomain) XCTAssertNil(fileProviderConnectorMock.passedDomainIdentifier) } diff --git a/CryptomatorTests/VaultKeepUnlockedViewModelTests.swift b/CryptomatorTests/VaultKeepUnlockedViewModelTests.swift index de9810bb6..d3284f401 100644 --- a/CryptomatorTests/VaultKeepUnlockedViewModelTests.swift +++ b/CryptomatorTests/VaultKeepUnlockedViewModelTests.swift @@ -229,7 +229,7 @@ class VaultKeepUnlockedViewModelTests: XCTestCase { private func assertFileProviderConnectorCalled() { XCTAssertEqual(vaultUID, fileProviderConnectorMock.passedDomainIdentifier?.rawValue) - XCTAssertEqual(VaultLockingService.name, fileProviderConnectorMock.passedServiceName) + XCTAssertEqual(.vaultLocking, fileProviderConnectorMock.passedServiceName) } private func assertFileProviderConnectorNotCalled() { diff --git a/FileProviderExtensionUI/UnlockVaultViewModel.swift b/FileProviderExtensionUI/UnlockVaultViewModel.swift index 2271c88d0..6af8e4794 100644 --- a/FileProviderExtensionUI/UnlockVaultViewModel.swift +++ b/FileProviderExtensionUI/UnlockVaultViewModel.swift @@ -187,7 +187,7 @@ class UnlockVaultViewModel { // MARK: Unlock Vault func unlock(withPassword password: String, storePasswordInKeychain: Bool) -> Promise { - let getXPCPromise: Promise> = fileProviderConnector.getXPC(serviceName: VaultUnlockingService.name, domain: domain) + let getXPCPromise: Promise> = fileProviderConnector.getXPC(serviceName: .vaultUnlocking, domain: domain) return getXPCPromise.then { xpc -> Promise in self.unlockVault(with: password, proxy: xpc.proxy) }.then { @@ -211,7 +211,7 @@ class UnlockVaultViewModel { */ func biometricalUnlock() -> Promise { let reason = LocalizedString.getValue("unlockVault.evaluatePolicy.reason") - let getXPCPromise: Promise> = fileProviderConnector.getXPC(serviceName: VaultUnlockingService.name, domain: domain) + let getXPCPromise: Promise> = fileProviderConnector.getXPC(serviceName: .vaultUnlocking, domain: domain) return getXPCPromise.then { xpc in xpc.proxy.startBiometricalUnlock() }.then { _ -> Void in From afb31c6d2224ea0f9b49e142367c93befb612fa1 Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Fri, 6 May 2022 18:31:33 +0200 Subject: [PATCH 03/18] Removed unused code from FileProviderConnector --- .../FileProviderConnector.swift | 41 ------------------- 1 file changed, 41 deletions(-) diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/FileProviderXPC/FileProviderConnector.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/FileProviderXPC/FileProviderConnector.swift index b60ec5ecb..2caa860c0 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/FileProviderXPC/FileProviderConnector.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/FileProviderXPC/FileProviderConnector.swift @@ -51,15 +51,6 @@ public class FileProviderXPCConnector: FileProviderConnector { public static let shared = FileProviderXPCConnector() - public func getProxy(serviceName: NSFileProviderServiceName, domainIdentifier: NSFileProviderDomainIdentifier) -> Promise { - return NSFileProviderManager.getDomains().then { domains in - guard let domain = domains.first(where: { $0.identifier == domainIdentifier }) else { - throw FileProviderXPCConnectorError.domainNotFound - } - return self.getProxy(serviceName: serviceName, domain: domain) - } - } - public func getXPC(serviceName: NSFileProviderServiceName, domain: NSFileProviderDomain?) -> Promise> { var url = NSFileProviderManager.default.documentStorageURL if let domain = domain { @@ -93,38 +84,6 @@ public class FileProviderXPCConnector: FileProviderConnector { }) } } - - public func getProxy(serviceName: NSFileProviderServiceName, domain: NSFileProviderDomain?) -> Promise { - var url = NSFileProviderManager.default.documentStorageURL - if let domain = domain { - url.appendPathComponent(domain.pathRelativeToDocumentStorage) - } - return wrap { handler in - FileManager.default.getFileProviderServicesForItem(at: url, completionHandler: handler) - }.then { services -> Promise in - if let desiredService = services?[serviceName] { - return desiredService.getFileProviderConnection() - } else { - return Promise(FileProviderXPCConnectorError.serviceNotSupported) - } - }.then { connection -> T in - guard let connection = connection else { - throw FileProviderXPCConnectorError.connectionIsNil - } - guard let type = T.self as AnyObject as? Protocol else { - throw FileProviderXPCConnectorError.typeMismatch - } - connection.remoteObjectInterface = NSXPCInterface(with: type) - connection.resume() - let rawProxy = connection.remoteObjectProxyWithErrorHandler { errorAccessingRemoteObject in - DDLogError("remoteObjectProxy failed with error: \(errorAccessingRemoteObject)") - } - guard let proxy = rawProxy as? T else { - throw FileProviderXPCConnectorError.rawProxyCastingFailed - } - return proxy - } - } } extension NSFileProviderService { From 9d6fb5b2b4e059397243297a24e74ba87c064b17 Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Fri, 6 May 2022 19:06:07 +0200 Subject: [PATCH 04/18] Added the corresponding `NSFileProviderDomainIdentifier` to the `NSFileProviderItemIdentifier` This is necessary because the `extensionContext.domainIdentifier` in the FileProviderExtensionUI is not guaranteed to be the `domainIdentifier` of the currently visible `NSFileProviderDomain` and thus a correct XPC connection could not be established. --- Cryptomator.xcodeproj/project.pbxproj | 4 ++ .../DB/WorkingSetObserver.swift | 6 +- .../FileProviderAdapter.swift | 32 +++++----- .../FileProviderAdapterManager.swift | 15 ++--- .../FileProviderItem.swift | 11 ++-- .../LocalURLProviderType.swift | 13 +++- .../TaskExecutor/DownloadTaskExecutor.swift | 6 +- .../FolderCreationTaskExecutor.swift | 7 ++- .../ItemEnumerationTaskExecutor.swift | 8 ++- .../TaskExecutor/ReparentTaskExecutor.swift | 7 ++- .../TaskExecutor/UploadTaskExecutor.swift | 8 ++- ...SFileProviderItemIdentifier+Database.swift | 63 +++++++++++++++++++ .../Workflow/WorkflowFactory.swift | 12 ++-- ...eProviderAdapterCreateDirectoryTests.swift | 8 +-- .../FileProviderAdapterDeleteItemTests.swift | 10 +-- ...ileProviderAdapterEnumerateItemTests.swift | 2 +- .../FileProviderAdapterGetItemTests.swift | 6 +- ...eProviderAdapterImportDirectoryTests.swift | 2 +- ...leProviderAdapterImportDocumentTests.swift | 8 +-- .../FileProviderAdapterMoveItemTests.swift | 16 ++--- ...eProviderAdapterSetFavoriteRankTests.swift | 2 +- .../FileProviderAdapterSetTagDataTests.swift | 4 +- ...oviderAdapterStartProvidingItemTests.swift | 4 +- .../FileProviderAdapterTestCase.swift | 10 ++- .../FileProviderEnumeratorTests.swift | 4 +- .../FileProviderItemTests.swift | 32 +++++----- .../FileProviderNotificatorTests.swift | 8 +-- .../DownloadTaskExecutorTests.swift | 10 +-- .../FolderCreationTaskExecutorTests.swift | 4 +- .../ItemEnumerationTaskTests.swift | 48 +++++++------- .../ReparentTaskExecutorTests.swift | 8 +-- .../UploadTaskExecutorTests.swift | 8 +-- .../Mocks/LocalURLProviderMock.swift | 9 +++ .../WorkingSetObserverTests.swift | 4 +- 34 files changed, 255 insertions(+), 144 deletions(-) create mode 100644 CryptomatorFileProvider/NSFileProviderItemIdentifier+Database.swift diff --git a/Cryptomator.xcodeproj/project.pbxproj b/Cryptomator.xcodeproj/project.pbxproj index 8cc332b49..469cc2692 100644 --- a/Cryptomator.xcodeproj/project.pbxproj +++ b/Cryptomator.xcodeproj/project.pbxproj @@ -289,6 +289,7 @@ 4AEBE8C22653FAD40031487F /* WorkflowMiddleware.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AEBE8C12653FAD40031487F /* WorkflowMiddleware.swift */; }; 4AEE468F25263B2E0045DA9F /* FileProviderExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 4AA621D6249A6A8400A0BCBD /* FileProviderExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 4AEE469225263B2E0045DA9F /* FileProviderExtensionUI.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 4AA621E4249A6A8400A0BCBD /* FileProviderExtensionUI.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 4AEE6EE12822A33400E1B35E /* NSFileProviderItemIdentifier+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AEE6EE02822A33400E1B35E /* NSFileProviderItemIdentifier+Database.swift */; }; 4AEECD2F279EA27300C6E2B5 /* FileProviderAdapterSetTagDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AEECD2E279EA27300C6E2B5 /* FileProviderAdapterSetTagDataTests.swift */; }; 4AEECD31279EA50D00C6E2B5 /* WorkingSetObservingMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AEECD30279EA50D00C6E2B5 /* WorkingSetObservingMock.swift */; }; 4AEECD33279EAACA00C6E2B5 /* FileProviderAdapterSetFavoriteRankTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AEECD32279EAACA00C6E2B5 /* FileProviderAdapterSetFavoriteRankTests.swift */; }; @@ -775,6 +776,7 @@ 4AEBE8BB2653F2FD0031487F /* ReparentTaskExecutor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReparentTaskExecutor.swift; sourceTree = ""; }; 4AEBE8BD2653F4280031487F /* UploadTaskExecutor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadTaskExecutor.swift; sourceTree = ""; }; 4AEBE8C12653FAD40031487F /* WorkflowMiddleware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkflowMiddleware.swift; sourceTree = ""; }; + 4AEE6EE02822A33400E1B35E /* NSFileProviderItemIdentifier+Database.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSFileProviderItemIdentifier+Database.swift"; sourceTree = ""; }; 4AEECD2E279EA27300C6E2B5 /* FileProviderAdapterSetTagDataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileProviderAdapterSetTagDataTests.swift; sourceTree = ""; }; 4AEECD30279EA50D00C6E2B5 /* WorkingSetObservingMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkingSetObservingMock.swift; sourceTree = ""; }; 4AEECD32279EAACA00C6E2B5 /* FileProviderAdapterSetFavoriteRankTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileProviderAdapterSetFavoriteRankTests.swift; sourceTree = ""; }; @@ -1668,6 +1670,7 @@ 4A2060CC2799645300DA6C62 /* FileProviderNotificatorManager.swift */, 740375F42587AEB50023FF53 /* ItemStatus.swift */, 4AB1D4EB27D0E027009060AB /* LocalURLProviderType.swift */, + 4AEE6EE02822A33400E1B35E /* NSFileProviderItemIdentifier+Database.swift */, 4AC1157527F5BD890023F51B /* Promise+AllIgnoringResult.swift */, 4ADD233F26737CD400374E4E /* RootFileProviderItem.swift */, 4ADC66C027A7F426002E6CC7 /* UnlockMonitor.swift */, @@ -2549,6 +2552,7 @@ 4A231B82271EF35400987492 /* DownloadTaskDBManager.swift in Sources */, 4A03BD6527DF4AEE00B96FA7 /* WorkflowFactoryLocking.swift in Sources */, 747F2F3A2587BC4B0072FB30 /* LockManager.swift in Sources */, + 4AEE6EE12822A33400E1B35E /* NSFileProviderItemIdentifier+Database.swift in Sources */, 4ADD233C267219E200374E4E /* LocalCachedFileInfo.swift in Sources */, 747F2F3B2587BC4B0072FB30 /* FileSystemLock.swift in Sources */, 4A231B80271EF2CA00987492 /* DownloadTaskRecord.swift in Sources */, diff --git a/CryptomatorFileProvider/DB/WorkingSetObserver.swift b/CryptomatorFileProvider/DB/WorkingSetObserver.swift index fb693809c..63f00eae8 100644 --- a/CryptomatorFileProvider/DB/WorkingSetObserver.swift +++ b/CryptomatorFileProvider/DB/WorkingSetObserver.swift @@ -22,8 +22,10 @@ class WorkingSetObserver: WorkingSetObserving { private let cachedFileManager: CachedFileManager private let notificator: FileProviderNotificatorType private var currentWorkingSetItems = Set() + private let domainIdentifier: NSFileProviderDomainIdentifier - init(database: DatabaseReader, notificator: FileProviderNotificatorType, uploadTaskManager: UploadTaskManager, cachedFileManager: CachedFileManager) { + init(domainIdentifier: NSFileProviderDomainIdentifier, database: DatabaseReader, notificator: FileProviderNotificatorType, uploadTaskManager: UploadTaskManager, cachedFileManager: CachedFileManager) { + self.domainIdentifier = domainIdentifier self.database = database self.notificator = notificator self.uploadTaskManager = uploadTaskManager @@ -72,7 +74,7 @@ class WorkingSetObserver: WorkingSetObserving { let localCachedFileInfo = try cachedFileManager.getLocalCachedFileInfo(for: metadata) let newestVersionLocallyCached = localCachedFileInfo?.isCurrentVersion(lastModifiedDateInCloud: metadata.lastModifiedDate) ?? false let localURL = localCachedFileInfo?.localURL - return FileProviderItem(metadata: metadata, newestVersionLocallyCached: newestVersionLocallyCached, localURL: localURL, error: uploadTasks[index]?.failedWithError) + return FileProviderItem(metadata: metadata, domainIdentifier: domainIdentifier, newestVersionLocallyCached: newestVersionLocallyCached, localURL: localURL, error: uploadTasks[index]?.failedWithError) } return items } diff --git a/CryptomatorFileProvider/FileProviderAdapter.swift b/CryptomatorFileProvider/FileProviderAdapter.swift index 897c06c6c..bc7a580b8 100644 --- a/CryptomatorFileProvider/FileProviderAdapter.swift +++ b/CryptomatorFileProvider/FileProviderAdapter.swift @@ -44,9 +44,11 @@ public class FileProviderAdapter: FileProviderAdapterType { private let notificator: FileProviderItemUpdateDelegate? private let fullVersionChecker: FullVersionChecker private let workflowFactory: WorkflowFactoryLocking + private let domainIdentifier: NSFileProviderDomainIdentifier - init(uploadTaskManager: UploadTaskManager, cachedFileManager: CachedFileManager, itemMetadataManager: ItemMetadataManager, reparentTaskManager: ReparentTaskManager, deletionTaskManager: DeletionTaskManager, itemEnumerationTaskManager: ItemEnumerationTaskManager, downloadTaskManager: DownloadTaskManager, scheduler: WorkflowScheduler, provider: CloudProvider, notificator: FileProviderItemUpdateDelegate? = nil, localURLProvider: LocalURLProviderType, fullVersionChecker: FullVersionChecker = UserDefaultsFullVersionChecker.shared) { + init(domainIdentifier: NSFileProviderDomainIdentifier, uploadTaskManager: UploadTaskManager, cachedFileManager: CachedFileManager, itemMetadataManager: ItemMetadataManager, reparentTaskManager: ReparentTaskManager, deletionTaskManager: DeletionTaskManager, itemEnumerationTaskManager: ItemEnumerationTaskManager, downloadTaskManager: DownloadTaskManager, scheduler: WorkflowScheduler, provider: CloudProvider, notificator: FileProviderItemUpdateDelegate? = nil, localURLProvider: LocalURLProviderType, fullVersionChecker: FullVersionChecker = UserDefaultsFullVersionChecker.shared) { self.lastUnlockedDate = Date() + self.domainIdentifier = domainIdentifier self.uploadTaskManager = uploadTaskManager self.cachedFileManager = cachedFileManager self.itemMetadataManager = itemMetadataManager @@ -61,7 +63,8 @@ public class FileProviderAdapter: FileProviderAdapterType { reparentTaskManager: reparentTaskManager, deletionTaskManager: deletionTaskManager, itemEnumerationTaskManager: itemEnumerationTaskManager, - downloadTaskManager: downloadTaskManager) + downloadTaskManager: downloadTaskManager, + domainIdentifier: domainIdentifier) self.workflowFactory = WorkflowFactoryLocking(lockManager: LockManager(), workflowFactory: factory) self.scheduler = scheduler self.provider = provider @@ -84,7 +87,7 @@ public class FileProviderAdapter: FileProviderAdapterType { let localCachedFileInfo = try cachedFileManager.getLocalCachedFileInfo(for: itemMetadata) let newestVersionLocallyCached = localCachedFileInfo?.isCurrentVersion(lastModifiedDateInCloud: itemMetadata.lastModifiedDate) ?? false let localURL = localCachedFileInfo?.localURL - return FileProviderItem(metadata: itemMetadata, newestVersionLocallyCached: newestVersionLocallyCached, localURL: localURL, error: uploadTask?.failedWithError) + return FileProviderItem(metadata: itemMetadata, domainIdentifier: domainIdentifier, newestVersionLocallyCached: newestVersionLocallyCached, localURL: localURL, error: uploadTask?.failedWithError) } // MARK: Enumerate Item @@ -140,7 +143,7 @@ public class FileProviderAdapter: FileProviderAdapterType { let localCachedFileInfo = try self.cachedFileManager.getLocalCachedFileInfo(for: metadata) let newestVersionLocallyCached = localCachedFileInfo?.isCurrentVersion(lastModifiedDateInCloud: metadata.lastModifiedDate) ?? false let localURL = localCachedFileInfo?.localURL - return FileProviderItem(metadata: metadata, newestVersionLocallyCached: newestVersionLocallyCached, localURL: localURL, error: uploadTasks[index]?.failedWithError) + return FileProviderItem(metadata: metadata, domainIdentifier: domainIdentifier, newestVersionLocallyCached: newestVersionLocallyCached, localURL: localURL, error: uploadTasks[index]?.failedWithError) } } catch { return Promise(error) @@ -218,7 +221,7 @@ public class FileProviderAdapter: FileProviderAdapterType { // Register LocalURL in the DB try cachedFileManager.cacheLocalFileInfo(for: placeholderMetadata.id!, localURL: localURL, lastModifiedDate: nil) - let item = FileProviderItem(metadata: placeholderMetadata, newestVersionLocallyCached: true, localURL: localURL) + let item = FileProviderItem(metadata: placeholderMetadata, domainIdentifier: domainIdentifier, newestVersionLocallyCached: true, localURL: localURL) let uploadTaskRecord = try registerFileInUploadQueue(with: localURL, itemMetadata: placeholderMetadata) return LocalItemImportResult(item: item, uploadTaskRecord: uploadTaskRecord) } @@ -412,7 +415,7 @@ public class FileProviderAdapter: FileProviderAdapterType { let localCachedFileInfo = try cachedFileManager.getLocalCachedFileInfo(for: itemMetadata) let newestVersionLocallyCached = localCachedFileInfo?.isCurrentVersion(lastModifiedDateInCloud: itemMetadata.lastModifiedDate) ?? false - let item = FileProviderItem(metadata: itemMetadata, newestVersionLocallyCached: newestVersionLocallyCached) + let item = FileProviderItem(metadata: itemMetadata, domainIdentifier: domainIdentifier, newestVersionLocallyCached: newestVersionLocallyCached) return MoveItemLocallyResult(item: item, reparentTaskRecord: taskRecord) } @@ -685,7 +688,7 @@ public class FileProviderAdapter: FileProviderAdapterType { try checkLocalItemCollision(for: cloudPath) let placeholderMetadata = ItemMetadata(name: name, type: .folder, size: nil, parentID: parentID, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: cloudPath, isPlaceholderItem: true) try itemMetadataManager.cacheMetadata(placeholderMetadata) - return FileProviderItem(metadata: placeholderMetadata, newestVersionLocallyCached: true) + return FileProviderItem(metadata: placeholderMetadata, domainIdentifier: domainIdentifier, newestVersionLocallyCached: true) } /** @@ -713,7 +716,7 @@ public class FileProviderAdapter: FileProviderAdapterType { do { _ = try deletionTaskManager.getTaskRecord(for: existingItemMetadata.id!) } catch DBManagerError.taskNotFound { - throw NSError.fileProviderErrorForCollision(with: FileProviderItem(metadata: existingItemMetadata)) + throw NSError.fileProviderErrorForCollision(with: FileProviderItem(metadata: existingItemMetadata, domainIdentifier: domainIdentifier)) } } } @@ -774,22 +777,17 @@ public class FileProviderAdapter: FileProviderAdapterType { } func convertFileProviderItemIdentifierToInt64(_ identifier: NSFileProviderItemIdentifier) throws -> Int64 { - switch identifier { - case .rootContainer: - return itemMetadataManager.getRootContainerID() - default: - guard let id = Int64(identifier.rawValue) else { - throw FileProviderAdapterError.unsupportedItemIdentifier - } - return id + guard let id = identifier.databaseValue else { + throw FileProviderAdapterError.unsupportedItemIdentifier } + return id } func convertIDToItemIdentifier(_ id: Int64) -> NSFileProviderItemIdentifier { if id == itemMetadataManager.getRootContainerID() { return .rootContainer } - return NSFileProviderItemIdentifier("\(id)") + return NSFileProviderItemIdentifier(domainIdentifier: domainIdentifier, itemID: id) } struct LocalItemImportResult { diff --git a/CryptomatorFileProvider/FileProviderAdapterManager.swift b/CryptomatorFileProvider/FileProviderAdapterManager.swift index a1b0dc18e..a69784ed8 100644 --- a/CryptomatorFileProvider/FileProviderAdapterManager.swift +++ b/CryptomatorFileProvider/FileProviderAdapterManager.swift @@ -64,7 +64,7 @@ public class FileProviderAdapterManager: FileProviderAdapterProviding { adapter = cachedAdapter } else { DDLogDebug("Try to automatically unlock \(domain.displayName) - \(domain.identifier)") - let autoUnlockItem = try autoUnlockVault(withVaultUID: vaultUID, dbPath: dbPath, delegate: delegate, notificator: notificator) + let autoUnlockItem = try autoUnlockVault(withVaultUID: vaultUID, domainIdentifier: domain.identifier, dbPath: dbPath, delegate: delegate, notificator: notificator) adapterCache.cacheItem(autoUnlockItem, identifier: domain.identifier) adapter = autoUnlockItem.adapter } @@ -78,7 +78,7 @@ public class FileProviderAdapterManager: FileProviderAdapterProviding { return } let provider = try vaultManager.manualUnlockVault(withUID: domainIdentifier.rawValue, kek: kek) - let item = try createAdapterCacheItem(cloudProvider: provider, dbPath: dbPath, delegate: delegate, notificator: notificator) + let item = try createAdapterCacheItem(domainIdentifier: domainIdentifier, cloudProvider: provider, dbPath: dbPath, delegate: delegate, notificator: notificator) try vaultKeepUnlockedSettings.setLastUsedDate(Date(), forVaultUID: domainIdentifier.rawValue) adapterCache.cacheItem(item, identifier: domainIdentifier) let notificator = try notificatorManager.getFileProviderNotificator(for: NSFileProviderDomain(identifier: domainIdentifier, displayName: "", pathRelativeToDocumentStorage: "")) @@ -119,7 +119,7 @@ public class FileProviderAdapterManager: FileProviderAdapterProviding { try maintenanceManager.disableMaintenanceMode() } - private func autoUnlockVault(withVaultUID vaultUID: String, dbPath: URL, delegate: FileProviderAdapterDelegate, notificator: FileProviderNotificatorType) throws -> AdapterCacheItem { + private func autoUnlockVault(withVaultUID vaultUID: String, domainIdentifier: NSFileProviderDomainIdentifier, dbPath: URL, delegate: FileProviderAdapterDelegate, notificator: FileProviderNotificatorType) throws -> AdapterCacheItem { guard vaultKeepUnlockedHelper.shouldAutoUnlockVault(withVaultUID: vaultUID) else { try masterkeyCacheManager.removeCachedMasterkey(forVaultUID: vaultUID) throw unlockMonitor.getUnlockError(forVaultUID: vaultUID) @@ -128,12 +128,12 @@ public class FileProviderAdapterManager: FileProviderAdapterProviding { throw unlockMonitor.getUnlockError(forVaultUID: vaultUID) } let provider = try vaultManager.createVaultProvider(withUID: vaultUID, masterkey: cachedMasterkey) - let adapterCacheItem = try createAdapterCacheItem(cloudProvider: provider, dbPath: dbPath, delegate: delegate, notificator: notificator) + let adapterCacheItem = try createAdapterCacheItem(domainIdentifier: domainIdentifier, cloudProvider: provider, dbPath: dbPath, delegate: delegate, notificator: notificator) notificator.refreshWorkingSet() return adapterCacheItem } - private func createAdapterCacheItem(cloudProvider: CloudProvider, dbPath: URL, delegate: FileProviderAdapterDelegate, notificator: FileProviderNotificatorType) throws -> AdapterCacheItem { + private func createAdapterCacheItem(domainIdentifier: NSFileProviderDomainIdentifier, cloudProvider: CloudProvider, dbPath: URL, delegate: FileProviderAdapterDelegate, notificator: FileProviderNotificatorType) throws -> AdapterCacheItem { let database = try DatabaseHelper.getMigratedDB(at: dbPath) let itemMetadataManager = ItemMetadataDBManager(database: database) let cachedFileManager = CachedFileDBManager(database: database) @@ -143,7 +143,8 @@ public class FileProviderAdapterManager: FileProviderAdapterProviding { let itemEnumerationTaskManager = try ItemEnumerationTaskDBManager(database: database) let downloadTaskManager = try DownloadTaskDBManager(database: database) let maintenanceManager = MaintenanceDBManager(database: database) - let adapter = FileProviderAdapter(uploadTaskManager: uploadTaskManager, + let adapter = FileProviderAdapter(domainIdentifier: domainIdentifier, + uploadTaskManager: uploadTaskManager, cachedFileManager: cachedFileManager, itemMetadataManager: itemMetadataManager, reparentTaskManager: reparentTaskManager, @@ -154,7 +155,7 @@ public class FileProviderAdapterManager: FileProviderAdapterProviding { provider: cloudProvider, notificator: notificator, localURLProvider: delegate) - let workingSetObserver = WorkingSetObserver(database: database, notificator: notificator, uploadTaskManager: uploadTaskManager, cachedFileManager: cachedFileManager) + let workingSetObserver = WorkingSetObserver(domainIdentifier: domainIdentifier, database: database, notificator: notificator, uploadTaskManager: uploadTaskManager, cachedFileManager: cachedFileManager) workingSetObserver.startObservation() return AdapterCacheItem(adapter: adapter, maintenanceManager: maintenanceManager, workingSetObserver: workingSetObserver) } diff --git a/CryptomatorFileProvider/FileProviderItem.swift b/CryptomatorFileProvider/FileProviderItem.swift index e9c363087..f31be9d33 100644 --- a/CryptomatorFileProvider/FileProviderItem.swift +++ b/CryptomatorFileProvider/FileProviderItem.swift @@ -21,10 +21,12 @@ public class FileProviderItem: NSObject, NSFileProviderItem { let error: Error? let newestVersionLocallyCached: Bool let localURL: URL? + let domainIdentifier: NSFileProviderDomainIdentifier private let fullVersionChecker: FullVersionChecker - init(metadata: ItemMetadata, newestVersionLocallyCached: Bool = false, localURL: URL? = nil, error: Error? = nil, fullVersionChecker: FullVersionChecker = UserDefaultsFullVersionChecker.shared) { + init(metadata: ItemMetadata, domainIdentifier: NSFileProviderDomainIdentifier, newestVersionLocallyCached: Bool = false, localURL: URL? = nil, error: Error? = nil, fullVersionChecker: FullVersionChecker = UserDefaultsFullVersionChecker.shared) { self.metadata = metadata + self.domainIdentifier = domainIdentifier self.error = error self.newestVersionLocallyCached = newestVersionLocallyCached self.localURL = localURL @@ -33,17 +35,18 @@ public class FileProviderItem: NSObject, NSFileProviderItem { public var itemIdentifier: NSFileProviderItemIdentifier { assert(metadata.id != nil) - if metadata.id == ItemMetadataDBManager.rootContainerId { + + guard let id = metadata.id, id != ItemMetadataDBManager.rootContainerId else { return .rootContainer } - return NSFileProviderItemIdentifier(String(metadata.id ?? -1)) // TODO: Change Optional Handling + return NSFileProviderItemIdentifier(domainIdentifier: domainIdentifier, itemID: id) } public var parentItemIdentifier: NSFileProviderItemIdentifier { if metadata.parentID == ItemMetadataDBManager.rootContainerId { return .rootContainer } - return NSFileProviderItemIdentifier(String(metadata.parentID)) + return NSFileProviderItemIdentifier(domainIdentifier: domainIdentifier, itemID: metadata.parentID) } public var capabilities: NSFileProviderItemCapabilities { diff --git a/CryptomatorFileProvider/LocalURLProviderType.swift b/CryptomatorFileProvider/LocalURLProviderType.swift index 4c468ca9e..ef2bf6506 100644 --- a/CryptomatorFileProvider/LocalURLProviderType.swift +++ b/CryptomatorFileProvider/LocalURLProviderType.swift @@ -11,6 +11,10 @@ import FileProvider import Foundation public protocol LocalURLProviderType: AnyObject { + /** + The identifier for the corresponding domain of the item identifiers. + */ + var domainIdentifier: NSFileProviderDomainIdentifier { get } /** Returns the item identifier directory for a given item identifier. @@ -46,11 +50,18 @@ public extension LocalURLProviderType { func persistentIdentifierForItem(at url: URL) -> NSFileProviderItemIdentifier? { let pathComponents = url.pathComponents assert(pathComponents.count > 2) - return NSFileProviderItemIdentifier(pathComponents[pathComponents.count - 2]) + guard let itemID = Int64(pathComponents[pathComponents.count - 2]) else { + return nil + } + return NSFileProviderItemIdentifier(domainIdentifier: domainIdentifier, itemID: itemID) } } public class LocalURLProvider: LocalURLProviderType { + public var domainIdentifier: NSFileProviderDomainIdentifier { + domain.identifier + } + private let domain: NSFileProviderDomain private let documentStorageURLProvider: DocumentStorageURLProvider diff --git a/CryptomatorFileProvider/Middleware/TaskExecutor/DownloadTaskExecutor.swift b/CryptomatorFileProvider/Middleware/TaskExecutor/DownloadTaskExecutor.swift index 4f77a04d1..9b2181e66 100644 --- a/CryptomatorFileProvider/Middleware/TaskExecutor/DownloadTaskExecutor.swift +++ b/CryptomatorFileProvider/Middleware/TaskExecutor/DownloadTaskExecutor.swift @@ -29,8 +29,10 @@ class DownloadTaskExecutor: WorkflowMiddleware { private let cachedFileManager: CachedFileManager private let downloadTaskManager: DownloadTaskManager private let provider: CloudProvider + private let domainIdentifier: NSFileProviderDomainIdentifier - init(provider: CloudProvider, itemMetadataManager: ItemMetadataManager, cachedFileManager: CachedFileManager, downloadTaskManager: DownloadTaskManager) { + init(domainIdentifier: NSFileProviderDomainIdentifier, provider: CloudProvider, itemMetadataManager: ItemMetadataManager, cachedFileManager: CachedFileManager, downloadTaskManager: DownloadTaskManager) { + self.domainIdentifier = domainIdentifier self.provider = provider self.itemMetadataManager = itemMetadataManager self.cachedFileManager = cachedFileManager @@ -85,6 +87,6 @@ class DownloadTaskExecutor: WorkflowMiddleware { itemMetadata.statusCode = .isUploaded try itemMetadataManager.updateMetadata(itemMetadata) try cachedFileManager.cacheLocalFileInfo(for: itemMetadata.id!, localURL: localURL, lastModifiedDate: lastModifiedDate) - return FileProviderItem(metadata: itemMetadata, newestVersionLocallyCached: true, localURL: localURL) + return FileProviderItem(metadata: itemMetadata, domainIdentifier: domainIdentifier, newestVersionLocallyCached: true, localURL: localURL) } } diff --git a/CryptomatorFileProvider/Middleware/TaskExecutor/FolderCreationTaskExecutor.swift b/CryptomatorFileProvider/Middleware/TaskExecutor/FolderCreationTaskExecutor.swift index c3c5055bf..23235e3b6 100644 --- a/CryptomatorFileProvider/Middleware/TaskExecutor/FolderCreationTaskExecutor.swift +++ b/CryptomatorFileProvider/Middleware/TaskExecutor/FolderCreationTaskExecutor.swift @@ -7,6 +7,7 @@ // import CryptomatorCloudAccessCore +import FileProvider import Foundation import Promises @@ -26,8 +27,10 @@ class FolderCreationTaskExecutor: WorkflowMiddleware { private let itemMetadataManager: ItemMetadataManager private let provider: CloudProvider + private let domainIdentifier: NSFileProviderDomainIdentifier - init(provider: CloudProvider, itemMetadataManager: ItemMetadataManager) { + init(domainIdentifier: NSFileProviderDomainIdentifier, provider: CloudProvider, itemMetadataManager: ItemMetadataManager) { + self.domainIdentifier = domainIdentifier self.provider = provider self.itemMetadataManager = itemMetadataManager } @@ -54,7 +57,7 @@ class FolderCreationTaskExecutor: WorkflowMiddleware { itemMetadata.statusCode = .isUploaded itemMetadata.isPlaceholderItem = false try self.itemMetadataManager.updateMetadata(itemMetadata) - return FileProviderItem(metadata: itemMetadata, newestVersionLocallyCached: true) + return FileProviderItem(metadata: itemMetadata, domainIdentifier: self.domainIdentifier, newestVersionLocallyCached: true) } } } diff --git a/CryptomatorFileProvider/Middleware/TaskExecutor/ItemEnumerationTaskExecutor.swift b/CryptomatorFileProvider/Middleware/TaskExecutor/ItemEnumerationTaskExecutor.swift index 0b147d5f5..db91a48d7 100644 --- a/CryptomatorFileProvider/Middleware/TaskExecutor/ItemEnumerationTaskExecutor.swift +++ b/CryptomatorFileProvider/Middleware/TaskExecutor/ItemEnumerationTaskExecutor.swift @@ -35,8 +35,10 @@ class ItemEnumerationTaskExecutor: WorkflowMiddleware { private let itemEnumerationTaskManager: ItemEnumerationTaskManager private let deleteItemHelper: DeleteItemHelper private let provider: CloudProvider + private let domainIdentifier: NSFileProviderDomainIdentifier - init(provider: CloudProvider, itemMetadataManager: ItemMetadataManager, cachedFileManager: CachedFileManager, uploadTaskManager: UploadTaskManager, reparentTaskManager: ReparentTaskManager, deletionTaskManager: DeletionTaskManager, itemEnumerationTaskManager: ItemEnumerationTaskManager, deleteItemHelper: DeleteItemHelper) { + init(domainIdentifier: NSFileProviderDomainIdentifier, provider: CloudProvider, itemMetadataManager: ItemMetadataManager, cachedFileManager: CachedFileManager, uploadTaskManager: UploadTaskManager, reparentTaskManager: ReparentTaskManager, deletionTaskManager: DeletionTaskManager, itemEnumerationTaskManager: ItemEnumerationTaskManager, deleteItemHelper: DeleteItemHelper) { + self.domainIdentifier = domainIdentifier self.provider = provider self.itemMetadataManager = itemMetadataManager self.cachedFileManager = cachedFileManager @@ -97,7 +99,7 @@ class ItemEnumerationTaskExecutor: WorkflowMiddleware { let localCachedFileInfo = try self.cachedFileManager.getLocalCachedFileInfo(for: metadata) let newestVersionLocallyCached = localCachedFileInfo?.isCurrentVersion(lastModifiedDateInCloud: metadata.lastModifiedDate) ?? false let localURL = localCachedFileInfo?.localURL - return FileProviderItem(metadata: metadata, newestVersionLocallyCached: newestVersionLocallyCached, localURL: localURL, error: uploadTasks[index]?.failedWithError) + return FileProviderItem(metadata: metadata, domainIdentifier: self.domainIdentifier, newestVersionLocallyCached: newestVersionLocallyCached, localURL: localURL, error: uploadTasks[index]?.failedWithError) } if let nextPageTokenData = itemList.nextPageToken?.data(using: .utf8) { return FileProviderItemList(items: items, nextPageToken: NSFileProviderPage(nextPageTokenData)) @@ -137,7 +139,7 @@ class ItemEnumerationTaskExecutor: WorkflowMiddleware { let localURL = localCachedFileInfo?.localURL let uploadTask = try self.uploadTaskManager.getTaskRecord(for: fileProviderItemMetadata) - let item = FileProviderItem(metadata: fileProviderItemMetadata, newestVersionLocallyCached: newestVersionLocallyCached, localURL: localURL, error: uploadTask?.failedWithError) + let item = FileProviderItem(metadata: fileProviderItemMetadata, domainIdentifier: self.domainIdentifier, newestVersionLocallyCached: newestVersionLocallyCached, localURL: localURL, error: uploadTask?.failedWithError) return FileProviderItemList(items: [item], nextPageToken: nil) } } diff --git a/CryptomatorFileProvider/Middleware/TaskExecutor/ReparentTaskExecutor.swift b/CryptomatorFileProvider/Middleware/TaskExecutor/ReparentTaskExecutor.swift index ae4a87757..6593c372a 100644 --- a/CryptomatorFileProvider/Middleware/TaskExecutor/ReparentTaskExecutor.swift +++ b/CryptomatorFileProvider/Middleware/TaskExecutor/ReparentTaskExecutor.swift @@ -7,6 +7,7 @@ // import CryptomatorCloudAccessCore +import FileProvider import Foundation import Promises @@ -28,8 +29,10 @@ class ReparentTaskExecutor: WorkflowMiddleware { private let reparentTaskManager: ReparentTaskManager private let itemMetadataManager: ItemMetadataManager private let cachedFileManager: CachedFileManager + private let domainIdentifier: NSFileProviderDomainIdentifier - init(provider: CloudProvider, reparentTaskManager: ReparentTaskManager, itemMetadataManager: ItemMetadataManager, cachedFileManager: CachedFileManager) { + init(domainIdentifier: NSFileProviderDomainIdentifier, provider: CloudProvider, reparentTaskManager: ReparentTaskManager, itemMetadataManager: ItemMetadataManager, cachedFileManager: CachedFileManager) { + self.domainIdentifier = domainIdentifier self.provider = provider self.reparentTaskManager = reparentTaskManager self.itemMetadataManager = itemMetadataManager @@ -57,7 +60,7 @@ class ReparentTaskExecutor: WorkflowMiddleware { let localCachedFileInfo = try self.cachedFileManager.getLocalCachedFileInfo(for: itemMetadata) let newestVersionLocallyCached = localCachedFileInfo?.isCurrentVersion(lastModifiedDateInCloud: itemMetadata.lastModifiedDate) ?? false try self.reparentTaskManager.removeTaskRecord(reparentTask.taskRecord) - return FileProviderItem(metadata: itemMetadata, newestVersionLocallyCached: newestVersionLocallyCached) + return FileProviderItem(metadata: itemMetadata, domainIdentifier: self.domainIdentifier, newestVersionLocallyCached: newestVersionLocallyCached) } } diff --git a/CryptomatorFileProvider/Middleware/TaskExecutor/UploadTaskExecutor.swift b/CryptomatorFileProvider/Middleware/TaskExecutor/UploadTaskExecutor.swift index 0604aa1b9..3899af6e3 100644 --- a/CryptomatorFileProvider/Middleware/TaskExecutor/UploadTaskExecutor.swift +++ b/CryptomatorFileProvider/Middleware/TaskExecutor/UploadTaskExecutor.swift @@ -30,8 +30,10 @@ class UploadTaskExecutor: WorkflowMiddleware { let cachedFileManager: CachedFileManager let itemMetadataManager: ItemMetadataManager let uploadTaskManager: UploadTaskManager + let domainIdentifier: NSFileProviderDomainIdentifier - init(provider: CloudProvider, cachedFileManager: CachedFileManager, itemMetadataManager: ItemMetadataManager, uploadTaskManager: UploadTaskManager) { + init(domainIdentifier: NSFileProviderDomainIdentifier, provider: CloudProvider, cachedFileManager: CachedFileManager, itemMetadataManager: ItemMetadataManager, uploadTaskManager: UploadTaskManager) { + self.domainIdentifier = domainIdentifier self.provider = provider self.cachedFileManager = cachedFileManager self.itemMetadataManager = itemMetadataManager @@ -88,11 +90,11 @@ class UploadTaskExecutor: WorkflowMiddleware { if localFileSizeBeforeUpload == cloudItemMetadata.size { DDLogInfo("uploadPostProcessing: received cloudItemMetadata seem to be correct: localSize = \(localFileSizeBeforeUpload ?? -1); cloudItemSize = \(cloudItemMetadata.size ?? -1)") try cachedFileManager.cacheLocalFileInfo(for: taskItemMetadata.id!, localURL: localURL, lastModifiedDate: cloudItemMetadata.lastModifiedDate) - return FileProviderItem(metadata: taskItemMetadata, newestVersionLocallyCached: true, localURL: localURL) + return FileProviderItem(metadata: taskItemMetadata, domainIdentifier: domainIdentifier, newestVersionLocallyCached: true, localURL: localURL) } else { DDLogInfo("uploadPostProcessing: received cloudItemMetadata do not belong to the version that was uploaded - size differs!") try cachedFileManager.removeCachedFile(for: taskItemMetadata.id!) - return FileProviderItem(metadata: taskItemMetadata) + return FileProviderItem(metadata: taskItemMetadata, domainIdentifier: domainIdentifier) } } } diff --git a/CryptomatorFileProvider/NSFileProviderItemIdentifier+Database.swift b/CryptomatorFileProvider/NSFileProviderItemIdentifier+Database.swift new file mode 100644 index 000000000..c7f09fd03 --- /dev/null +++ b/CryptomatorFileProvider/NSFileProviderItemIdentifier+Database.swift @@ -0,0 +1,63 @@ +// +// NSFileProviderItemIdentifier+Database.swift +// CryptomatorFileProvider +// +// Created by Philipp Schmid on 04.05.22. +// Copyright © 2022 Skymatic GmbH. All rights reserved. +// + +import FileProvider +import Foundation + +public extension NSFileProviderItemIdentifier { + static let rootContainerDatabaseValue: Int64 = 1 + private static let delimiter: Character = ":" + + /** + Preferred constructor to create an `NSFileProviderItemIdentifier`. + + An `NSFileProviderItemIdentifier` has the format: + `:` + + This ensures that the `NSFileProviderDomainIdentifier` can be derived from any `NSFileProviderItemIdentifier` (except the `.rootContainer` and `.workingSet`). + This is necessary because the `extensionContext.domainIdentifier` in the FileProviderExtensionUI is not guaranteed to be the `domainIdentifier` + of the currently visible `NSFileProviderDomain` and thus a correct XPC connection could not be established. + */ + init(domainIdentifier: NSFileProviderDomainIdentifier, itemID: Int64) { + if itemID == NSFileProviderItemIdentifier.rootContainerDatabaseValue { + self.init(rawValue: NSFileProviderItemIdentifier.rootContainer.rawValue) + } else { + self.init(rawValue: "\(domainIdentifier.rawValue)\(NSFileProviderItemIdentifier.delimiter)\(itemID)") + } + } + + /** + The identifier of the domain to which the item with this `NSFileProviderItemIdentifier` belongs. + + To use this attribute the `NSFileProviderItemIdentifier` must have been created with the constructor `init(domainIdentifier:itemID:)`. + This attribute is always nil for `.rootContainer` and `.workingSet`. + */ + var domainIdentifier: NSFileProviderDomainIdentifier? { + guard let index = rawValue.firstIndex(of: NSFileProviderItemIdentifier.delimiter) else { + return nil + } + let before = rawValue.prefix(upTo: index) + return NSFileProviderDomainIdentifier(String(before)) + } + + /** + Representation of the identifier as it is stored in the database. + + The database value corresponds to the `itemID` which was passed to the constructor `init(domainIdentifier:itemID:)` and `1` for the `.rootContainer`. + */ + var databaseValue: Int64? { + if self == .rootContainer { + return NSFileProviderItemIdentifier.rootContainerDatabaseValue + } + guard let index = rawValue.firstIndex(of: NSFileProviderItemIdentifier.delimiter) else { + return nil + } + let after = rawValue.suffix(from: index).dropFirst() + return Int64(after) + } +} diff --git a/CryptomatorFileProvider/Workflow/WorkflowFactory.swift b/CryptomatorFileProvider/Workflow/WorkflowFactory.swift index 65424ce8f..2ebe1387e 100644 --- a/CryptomatorFileProvider/Workflow/WorkflowFactory.swift +++ b/CryptomatorFileProvider/Workflow/WorkflowFactory.swift @@ -7,6 +7,7 @@ // import CryptomatorCloudAccessCore +import FileProvider import Foundation struct WorkflowFactory { @@ -19,6 +20,7 @@ struct WorkflowFactory { let itemEnumerationTaskManager: ItemEnumerationTaskManager let downloadTaskManager: DownloadTaskManager let dependencyFactory = WorkflowDependencyFactory() + let domainIdentifier: NSFileProviderDomainIdentifier func createWorkflow(for deletionTask: DeletionTask) -> Workflow { let taskExecutor = DeletionTaskExecutor(provider: provider, itemMetadataManager: itemMetadataManager) @@ -33,7 +35,7 @@ struct WorkflowFactory { func createWorkflow(for uploadTask: UploadTask) -> Workflow { let onlineItemNameCollisionHandler = OnlineItemNameCollisionHandler(itemMetadataManager: itemMetadataManager) - let taskExecutor = UploadTaskExecutor(provider: provider, cachedFileManager: cachedFileManager, itemMetadataManager: itemMetadataManager, uploadTaskManager: uploadTaskManager) + let taskExecutor = UploadTaskExecutor(domainIdentifier: domainIdentifier, provider: provider, cachedFileManager: cachedFileManager, itemMetadataManager: itemMetadataManager, uploadTaskManager: uploadTaskManager) let errorMapper = ErrorMapper() errorMapper.setNext(onlineItemNameCollisionHandler.eraseToAnyWorkflowMiddleware()) @@ -45,7 +47,7 @@ struct WorkflowFactory { } func createWorkflow(for downloadTask: DownloadTask) -> Workflow { - let taskExecutor = DownloadTaskExecutor(provider: provider, itemMetadataManager: itemMetadataManager, cachedFileManager: cachedFileManager, downloadTaskManager: downloadTaskManager) + let taskExecutor = DownloadTaskExecutor(domainIdentifier: domainIdentifier, provider: provider, itemMetadataManager: itemMetadataManager, cachedFileManager: cachedFileManager, downloadTaskManager: downloadTaskManager) let errorMapper = ErrorMapper() errorMapper.setNext(taskExecutor.eraseToAnyWorkflowMiddleware()) @@ -57,7 +59,7 @@ struct WorkflowFactory { func createWorkflow(for reparentTask: ReparentTask) -> Workflow { let onlineItemNameCollisionHandler = OnlineItemNameCollisionHandler(itemMetadataManager: itemMetadataManager) - let taskExecutor = ReparentTaskExecutor(provider: provider, reparentTaskManager: reparentTaskManager, itemMetadataManager: itemMetadataManager, cachedFileManager: cachedFileManager) + let taskExecutor = ReparentTaskExecutor(domainIdentifier: domainIdentifier, provider: provider, reparentTaskManager: reparentTaskManager, itemMetadataManager: itemMetadataManager, cachedFileManager: cachedFileManager) let errorMapper = ErrorMapper() errorMapper.setNext(onlineItemNameCollisionHandler.eraseToAnyWorkflowMiddleware()) @@ -72,7 +74,7 @@ struct WorkflowFactory { func createWorkflow(for itemEnumerationTask: ItemEnumerationTask) -> Workflow { let deleteItemHelper = DeleteItemHelper(itemMetadataManager: itemMetadataManager, cachedFileManager: cachedFileManager) - let taskExecutor = ItemEnumerationTaskExecutor(provider: provider, itemMetadataManager: itemMetadataManager, cachedFileManager: cachedFileManager, uploadTaskManager: uploadTaskManager, reparentTaskManager: reparentTaskManager, deletionTaskManager: deletionTaskManager, itemEnumerationTaskManager: itemEnumerationTaskManager, deleteItemHelper: deleteItemHelper) + let taskExecutor = ItemEnumerationTaskExecutor(domainIdentifier: domainIdentifier, provider: provider, itemMetadataManager: itemMetadataManager, cachedFileManager: cachedFileManager, uploadTaskManager: uploadTaskManager, reparentTaskManager: reparentTaskManager, deletionTaskManager: deletionTaskManager, itemEnumerationTaskManager: itemEnumerationTaskManager, deleteItemHelper: deleteItemHelper) let errorMapper = ErrorMapper() errorMapper.setNext(taskExecutor.eraseToAnyWorkflowMiddleware()) @@ -85,7 +87,7 @@ struct WorkflowFactory { func createWorkflow(for folderCreationTask: FolderCreationTask) -> Workflow { let onlineItemNameCollisionHandler = OnlineItemNameCollisionHandler(itemMetadataManager: itemMetadataManager) - let taskExecutor = FolderCreationTaskExecutor(provider: provider, itemMetadataManager: itemMetadataManager) + let taskExecutor = FolderCreationTaskExecutor(domainIdentifier: domainIdentifier, provider: provider, itemMetadataManager: itemMetadataManager) let errorMapper = ErrorMapper() errorMapper.setNext(onlineItemNameCollisionHandler.eraseToAnyWorkflowMiddleware()) onlineItemNameCollisionHandler.setNext(taskExecutor.eraseToAnyWorkflowMiddleware()) diff --git a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterCreateDirectoryTests.swift b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterCreateDirectoryTests.swift index f2f15344d..f1f910236 100644 --- a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterCreateDirectoryTests.swift +++ b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterCreateDirectoryTests.swift @@ -15,7 +15,7 @@ class FileProviderAdapterCreateDirectoryTests: FileProviderAdapterTestCase { let expectation = XCTestExpectation() let rootItemMetadata = ItemMetadata(id: metadataManagerMock.getRootContainerID(), name: "Home", type: .folder, size: nil, parentID: metadataManagerMock.getRootContainerID(), lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/"), isPlaceholderItem: false) try metadataManagerMock.cacheMetadata(rootItemMetadata) - let adapter = FileProviderAdapter(uploadTaskManager: uploadTaskManagerMock, cachedFileManager: cachedFileManagerMock, itemMetadataManager: metadataManagerMock, reparentTaskManager: reparentTaskManagerMock, deletionTaskManager: deletionTaskManagerMock, itemEnumerationTaskManager: itemEnumerationTaskManagerMock, downloadTaskManager: downloadTaskManagerMock, scheduler: WorkflowSchedulerMock(), provider: cloudProviderMock, localURLProvider: localURLProviderMock) + let adapter = FileProviderAdapter(domainIdentifier: .test, uploadTaskManager: uploadTaskManagerMock, cachedFileManager: cachedFileManagerMock, itemMetadataManager: metadataManagerMock, reparentTaskManager: reparentTaskManagerMock, deletionTaskManager: deletionTaskManagerMock, itemEnumerationTaskManager: itemEnumerationTaskManagerMock, downloadTaskManager: downloadTaskManagerMock, scheduler: WorkflowSchedulerMock(), provider: cloudProviderMock, localURLProvider: localURLProviderMock) adapter.createDirectory(withName: "TestFolder", inParentItemIdentifier: .rootContainer) { item, error in XCTAssertNil(error) guard let fileProviderItem = item as? FileProviderItem else { @@ -40,8 +40,8 @@ class FileProviderAdapterCreateDirectoryTests: FileProviderAdapterTestCase { func testCreateDirectoryFailsIfParentDoesNotExist() throws { let expectation = XCTestExpectation() - let adapter = FileProviderAdapter(uploadTaskManager: uploadTaskManagerMock, cachedFileManager: cachedFileManagerMock, itemMetadataManager: metadataManagerMock, reparentTaskManager: reparentTaskManagerMock, deletionTaskManager: deletionTaskManagerMock, itemEnumerationTaskManager: itemEnumerationTaskManagerMock, downloadTaskManager: downloadTaskManagerMock, scheduler: WorkflowSchedulerMock(), provider: cloudProviderMock, localURLProvider: LocalURLProviderMock()) - adapter.createDirectory(withName: "TestFolder", inParentItemIdentifier: NSFileProviderItemIdentifier("2")) { item, error in + let adapter = FileProviderAdapter(domainIdentifier: .test, uploadTaskManager: uploadTaskManagerMock, cachedFileManager: cachedFileManagerMock, itemMetadataManager: metadataManagerMock, reparentTaskManager: reparentTaskManagerMock, deletionTaskManager: deletionTaskManagerMock, itemEnumerationTaskManager: itemEnumerationTaskManagerMock, downloadTaskManager: downloadTaskManagerMock, scheduler: WorkflowSchedulerMock(), provider: cloudProviderMock, localURLProvider: LocalURLProviderMock()) + adapter.createDirectory(withName: "TestFolder", inParentItemIdentifier: NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: 2)) { item, error in XCTAssertNil(item) guard let error = error else { XCTFail("Error is nil") @@ -79,7 +79,7 @@ class FileProviderAdapterCreateDirectoryTests: FileProviderAdapterTestCase { } func testCreatePlaceholderItemForFolderFailsIfParentDoesNotExist() throws { - XCTAssertThrowsError(try adapter.createPlaceholderItemForFolder(withName: "TestFolder", in: NSFileProviderItemIdentifier("2"))) { error in + XCTAssertThrowsError(try adapter.createPlaceholderItemForFolder(withName: "TestFolder", in: NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: 2))) { error in guard case FileProviderAdapterError.parentFolderNotFound = error else { XCTFail("Throws the wrong error: \(error)") return diff --git a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterDeleteItemTests.swift b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterDeleteItemTests.swift index 240a8ce43..8832f69e2 100644 --- a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterDeleteItemTests.swift +++ b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterDeleteItemTests.swift @@ -18,7 +18,7 @@ class FileProviderAdapterDeleteItemTests: FileProviderAdapterTestCase { let itemMetadata = ItemMetadata(id: itemID, name: "test.txt", type: .file, size: nil, parentID: metadataManagerMock.getRootContainerID(), lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) try metadataManagerMock.cacheMetadata(itemMetadata) let adapter = createFullyMockedAdapter() - let itemIdentifier = NSFileProviderItemIdentifier(rawValue: String(itemID)) + let itemIdentifier = NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: itemID) adapter.deleteItem(withIdentifier: itemIdentifier) { error in XCTAssertNil(error) @@ -44,8 +44,8 @@ class FileProviderAdapterDeleteItemTests: FileProviderAdapterTestCase { let fileItemMetadata = ItemMetadata(id: fileItemID, name: "test.txt", type: .file, size: nil, parentID: folderItemID, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) try metadataManagerMock.cacheMetadata([folderItemMetadata, fileItemMetadata]) - let folderItemIdentifier = NSFileProviderItemIdentifier(rawValue: String(folderItemID)) - let fileItemIdentifier = NSFileProviderItemIdentifier(rawValue: String(fileItemID)) + let folderItemIdentifier = NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: folderItemID) + let fileItemIdentifier = NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: fileItemID) let localURLForItem = tmpDirectory.appendingPathComponent("/\(fileItemIdentifier)/test.txt") try cachedFileManagerMock.cacheLocalFileInfo(for: fileItemID, localURL: localURLForItem, lastModifiedDate: Date(timeIntervalSinceReferenceDate: 0)) @@ -72,7 +72,7 @@ class FileProviderAdapterDeleteItemTests: FileProviderAdapterTestCase { let itemMetadata = ItemMetadata(id: itemID, name: "test.txt", type: .file, size: nil, parentID: metadataManagerMock.getRootContainerID(), lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) try metadataManagerMock.cacheMetadata(itemMetadata) let adapter = createFullyMockedAdapter() - let itemIdentifier = NSFileProviderItemIdentifier(rawValue: String(itemID)) + let itemIdentifier = NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: itemID) let localURLForItem = tmpDirectory.appendingPathComponent("/\(itemIdentifier)/test.txt") try cachedFileManagerMock.cacheLocalFileInfo(for: itemID, localURL: localURLForItem, lastModifiedDate: Date(timeIntervalSinceReferenceDate: 0)) @@ -98,7 +98,7 @@ class FileProviderAdapterDeleteItemTests: FileProviderAdapterTestCase { func testDeleteItemWithNonExistentFile() throws { let expectation = XCTestExpectation() let itemID: Int64 = 2 - let itemIdentifier = NSFileProviderItemIdentifier(rawValue: String(itemID)) + let itemIdentifier = NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: itemID) let adapter = createFullyMockedAdapter() diff --git a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterEnumerateItemTests.swift b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterEnumerateItemTests.swift index 6f298f327..467e186e6 100644 --- a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterEnumerateItemTests.swift +++ b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterEnumerateItemTests.swift @@ -28,7 +28,7 @@ class FileProviderAdapterEnumerateItemTests: FileProviderAdapterTestCase { metadataManagerMock.workingSetMetadata = mockMetadata let expectation = XCTestExpectation() adapter.enumerateItems(for: .workingSet, withPageToken: nil).then { itemList in - XCTAssertEqual(mockMetadata.map { FileProviderItem(metadata: $0) }, itemList.items) + XCTAssertEqual(mockMetadata.map { FileProviderItem(metadata: $0, domainIdentifier: .test) }, itemList.items) XCTAssertNil(itemList.nextPageToken) }.catch { error in XCTFail("Error in promise: \(error)") diff --git a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterGetItemTests.swift b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterGetItemTests.swift index ca94624b8..e04925e9f 100644 --- a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterGetItemTests.swift +++ b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterGetItemTests.swift @@ -12,7 +12,7 @@ import XCTest class FileProviderAdapterGetItemTests: FileProviderAdapterTestCase { func testGetFileProviderItemThrowsForNonExistentItem() throws { - XCTAssertThrowsError(try adapter.item(for: NSFileProviderItemIdentifier("2")), "Did not throw for non existent Item") { error in + XCTAssertThrowsError(try adapter.item(for: NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: 2)), "Did not throw for non existent Item") { error in guard let fileProviderError = error as? NSFileProviderError else { XCTFail("Throws the wrong error: \(error)") return @@ -29,7 +29,7 @@ class FileProviderAdapterGetItemTests: FileProviderAdapterTestCase { let itemMetadata = ItemMetadata(id: id, name: "TestItem", type: .file, size: 100, parentID: ItemMetadataDBManager.rootContainerId, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/TestItem"), isPlaceholderItem: false) metadataManagerMock.cachedMetadata[id] = itemMetadata - let item = try adapter.item(for: NSFileProviderItemIdentifier(String(id))) + let item = try adapter.item(for: NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: id)) guard let fileProviderItem = item as? FileProviderItem else { XCTFail("Item is not a FileProviderItem") return @@ -49,7 +49,7 @@ class FileProviderAdapterGetItemTests: FileProviderAdapterTestCase { } return uploadTask } - let item = try adapter.item(for: NSFileProviderItemIdentifier(String(id))) + let item = try adapter.item(for: NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: id)) guard let fileProviderItem = item as? FileProviderItem else { XCTFail("Item is not a FileProviderItem") return diff --git a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterImportDirectoryTests.swift b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterImportDirectoryTests.swift index a4fe25e16..b9805f8e8 100644 --- a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterImportDirectoryTests.swift +++ b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterImportDirectoryTests.swift @@ -27,7 +27,7 @@ class FileProviderAdapterImportDirectoryTests: FileProviderAdapterTestCase { metadataManagerMock.cachedMetadata[1] = ItemMetadata(item: .init(name: "/", cloudPath: CloudPath("/"), itemType: .folder, lastModifiedDate: nil, size: nil), withParentID: 1) let provider = CloudProviderGraphMock() let scheduler = WorkflowSchedulerMock(maxParallelUploads: 2, maxParallelDownloads: 2) - let adapter = FileProviderAdapter(uploadTaskManager: uploadTaskManagerMock, cachedFileManager: cachedFileManagerMock, itemMetadataManager: metadataManagerMock, reparentTaskManager: reparentTaskManagerMock, deletionTaskManager: deletionTaskManagerMock, itemEnumerationTaskManager: itemEnumerationTaskManagerMock, downloadTaskManager: downloadTaskManagerMock, scheduler: scheduler, provider: provider, localURLProvider: localURLProviderMock) + let adapter = FileProviderAdapter(domainIdentifier: .test, uploadTaskManager: uploadTaskManagerMock, cachedFileManager: cachedFileManagerMock, itemMetadataManager: metadataManagerMock, reparentTaskManager: reparentTaskManagerMock, deletionTaskManager: deletionTaskManagerMock, itemEnumerationTaskManager: itemEnumerationTaskManagerMock, downloadTaskManager: downloadTaskManagerMock, scheduler: scheduler, provider: provider, localURLProvider: localURLProviderMock) var parentIdentifier: NSFileProviderItemIdentifier = .rootContainer for _ in 0 ..< 5 { diff --git a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterImportDocumentTests.swift b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterImportDocumentTests.swift index bcbfd1617..12e5def44 100644 --- a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterImportDocumentTests.swift +++ b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterImportDocumentTests.swift @@ -219,9 +219,9 @@ class FileProviderAdapterImportDocumentTests: FileProviderAdapterTestCase { let fileContent = "TestContent" try fileContent.write(to: fileURL, atomically: true, encoding: .utf8) - let adapter = FileProviderAdapter(uploadTaskManager: uploadTaskManagerMock, cachedFileManager: cachedFileManagerMock, itemMetadataManager: metadataManagerMock, reparentTaskManager: reparentTaskManagerMock, deletionTaskManager: deletionTaskManagerMock, itemEnumerationTaskManager: itemEnumerationTaskManagerMock, downloadTaskManager: downloadTaskManagerMock, scheduler: WorkflowSchedulerMock(), provider: cloudProviderMock, localURLProvider: localURLProviderMock) + let adapter = FileProviderAdapter(domainIdentifier: .test, uploadTaskManager: uploadTaskManagerMock, cachedFileManager: cachedFileManagerMock, itemMetadataManager: metadataManagerMock, reparentTaskManager: reparentTaskManagerMock, deletionTaskManager: deletionTaskManagerMock, itemEnumerationTaskManager: itemEnumerationTaskManagerMock, downloadTaskManager: downloadTaskManagerMock, scheduler: WorkflowSchedulerMock(), provider: cloudProviderMock, localURLProvider: localURLProviderMock) - adapter.deleteItem(withIdentifier: NSFileProviderItemIdentifier("\(itemID)"), completionHandler: ({ error in + adapter.deleteItem(withIdentifier: NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: itemID), completionHandler: ({ error in XCTAssertNil(error) adapter.importDocument(at: fileURL, toParentItemIdentifier: .rootContainer, completionHandler: ({ item, error in XCTAssertNil(error) @@ -265,13 +265,13 @@ class FileProviderAdapterImportDocumentTests: FileProviderAdapterTestCase { } private func assertLocalURLProviderCalledWithItemID() { - XCTAssertEqual([NSFileProviderItemIdentifier("\(itemID)")], localURLProviderMock.itemIdentifierDirectoryURLForItemWithPersistentIdentifierReceivedInvocations) + XCTAssertEqual([NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: itemID)], localURLProviderMock.itemIdentifierDirectoryURLForItemWithPersistentIdentifierReceivedInvocations) } private func assertAllExpectedPropertiesSet(for item: NSFileProviderItem) throws { let resourceValues = try expectedFileURL.resourceValues(forKeys: [.creationDateKey, .nameKey, .contentModificationDateKey, .typeIdentifierKey, .totalFileSizeKey]) - XCTAssertEqual(NSFileProviderItemIdentifier("\(itemID)"), item.itemIdentifier) + XCTAssertEqual(NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: itemID), item.itemIdentifier) XCTAssertEqual(.rootContainer, item.parentItemIdentifier) XCTAssertEqual(resourceValues.name, item.filename) XCTAssertEqual(resourceValues.contentModificationDate, item.contentModificationDate) diff --git a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterMoveItemTests.swift b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterMoveItemTests.swift index 91897282d..9c28ecbb4 100644 --- a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterMoveItemTests.swift +++ b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterMoveItemTests.swift @@ -28,8 +28,8 @@ class FileProviderAdapterMoveItemTests: FileProviderAdapterTestCase { let newParentItemMetadata = ItemMetadata(id: parentItemID, name: "Folder", type: .folder, size: nil, parentID: ItemMetadataDBManager.rootContainerId, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: targetParentCloudPath, isPlaceholderItem: false) try metadataManagerMock.cacheMetadata([itemMetadata, newParentItemMetadata]) - let itemIdentifier = NSFileProviderItemIdentifier(rawValue: String(itemID)) - let parentItemIdentifier = NSFileProviderItemIdentifier(rawValue: String(parentItemID)) + let itemIdentifier = NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: itemID) + let parentItemIdentifier = NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: parentItemID) let newName = "RenamedTest.txt" let result = try adapter.moveItemLocally(withIdentifier: itemIdentifier, toParentItemWithIdentifier: parentItemIdentifier, newName: newName) let item = result.item @@ -64,7 +64,7 @@ class FileProviderAdapterMoveItemTests: FileProviderAdapterTestCase { let itemID: Int64 = 2 let itemMetadata = ItemMetadata(id: itemID, name: "Test.txt", type: .file, size: nil, parentID: ItemMetadataDBManager.rootContainerId, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: sourceCloudPath, isPlaceholderItem: false) metadataManagerMock.cachedMetadata[itemID] = itemMetadata - let itemIdentifier = NSFileProviderItemIdentifier(rawValue: String(itemMetadata.id!)) + let itemIdentifier = NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: try XCTUnwrap(itemMetadata.id)) let newName = "RenamedTest.txt" let result = try adapter.moveItemLocally(withIdentifier: itemIdentifier, toParentItemWithIdentifier: nil, newName: newName) let item = result.item @@ -102,8 +102,8 @@ class FileProviderAdapterMoveItemTests: FileProviderAdapterTestCase { let newParentItemMetadata = ItemMetadata(id: parentItemID, name: "Folder", type: .folder, size: nil, parentID: ItemMetadataDBManager.rootContainerId, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: targetParentCloudPath, isPlaceholderItem: false) try metadataManagerMock.cacheMetadata([itemMetadata, newParentItemMetadata]) - let itemIdentifier = NSFileProviderItemIdentifier(rawValue: String(itemMetadata.id!)) - let parentItemIdentifier = NSFileProviderItemIdentifier(rawValue: String(newParentItemMetadata.id!)) + let itemIdentifier = NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: itemID) + let parentItemIdentifier = NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: parentItemID) let result = try adapter.moveItemLocally(withIdentifier: itemIdentifier, toParentItemWithIdentifier: parentItemIdentifier, newName: nil) let item = result.item XCTAssertEqual("Test.txt", item.filename) @@ -140,7 +140,7 @@ class FileProviderAdapterMoveItemTests: FileProviderAdapterTestCase { let itemMetadata = ItemMetadata(id: itemID, name: "Test.txt", type: .file, size: nil, parentID: ItemMetadataDBManager.rootContainerId, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: sourceCloudPath, isPlaceholderItem: false) metadataManagerMock.cachedMetadata[itemID] = itemMetadata let newName = "RenamedTest.txt" - let itemIdentifier = NSFileProviderItemIdentifier(rawValue: String(itemID)) + let itemIdentifier = NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: itemID) adapter.renameItem(withIdentifier: itemIdentifier, toName: newName) { item, error in XCTAssertNil(error) guard let fileProviderItem = item as? FileProviderItem else { @@ -190,8 +190,8 @@ class FileProviderAdapterMoveItemTests: FileProviderAdapterTestCase { let newParentItemMetadata = ItemMetadata(id: parentItemID, name: "Folder", type: .folder, size: nil, parentID: ItemMetadataDBManager.rootContainerId, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: targetParentCloudPath, isPlaceholderItem: false) try metadataManagerMock.cacheMetadata([itemMetadata, newParentItemMetadata]) - let itemIdentifier = NSFileProviderItemIdentifier(rawValue: String(itemID)) - let parentItemIdentifier = NSFileProviderItemIdentifier(rawValue: String(parentItemID)) + let itemIdentifier = NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: itemID) + let parentItemIdentifier = NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: parentItemID) adapter.reparentItem(withIdentifier: itemIdentifier, toParentItemWithIdentifier: parentItemIdentifier, newName: nil) { item, error in XCTAssertNil(error) guard let fileProviderItem = item as? FileProviderItem else { diff --git a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterSetFavoriteRankTests.swift b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterSetFavoriteRankTests.swift index e9c7082e6..eb4d3e78b 100644 --- a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterSetFavoriteRankTests.swift +++ b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterSetFavoriteRankTests.swift @@ -15,7 +15,7 @@ class FileProviderAdapterSetFavoriteRankTests: FileProviderAdapterTestCase { let expectation = XCTestExpectation() metadataManagerMock.cachedMetadata[2] = ItemMetadata(id: 2, name: "Test", type: .folder, size: nil, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/Test"), isPlaceholderItem: false, isCandidateForCacheCleanup: false, favoriteRank: nil, tagData: nil) let favoriteRank: NSNumber = 100 - let itemIdentifier = NSFileProviderItemIdentifier("2") + let itemIdentifier = NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: 2) adapter.setFavoriteRank(favoriteRank, forItemIdentifier: itemIdentifier) { item, error in XCTAssertNil(error) XCTAssertEqual(favoriteRank, item?.favoriteRank) diff --git a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterSetTagDataTests.swift b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterSetTagDataTests.swift index fae469013..f8ea072bd 100644 --- a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterSetTagDataTests.swift +++ b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterSetTagDataTests.swift @@ -12,11 +12,12 @@ import XCTest @testable import CryptomatorFileProvider class FileProviderAdapterSetTagDataTests: FileProviderAdapterTestCase { + let itemIdentifier = NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: 2) func testSetTagData() throws { let expectation = XCTestExpectation() metadataManagerMock.cachedMetadata[2] = ItemMetadata(id: 2, name: "Test", type: .file, size: nil, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/Test"), isPlaceholderItem: false, isCandidateForCacheCleanup: false, favoriteRank: nil, tagData: nil) let tagData = "Foo".data(using: .utf8)! - let itemIdentifier = NSFileProviderItemIdentifier("2") + adapter.setTagData(tagData, forItemIdentifier: itemIdentifier) { item, error in XCTAssertNil(error) XCTAssertEqual(tagData, item?.tagData) @@ -30,7 +31,6 @@ class FileProviderAdapterSetTagDataTests: FileProviderAdapterTestCase { let expectation = XCTestExpectation() metadataManagerMock.cachedMetadata[2] = ItemMetadata(id: 2, name: "Test", type: .file, size: nil, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/Test"), isPlaceholderItem: false, isCandidateForCacheCleanup: false, favoriteRank: nil, tagData: nil) let emptyTagData = Data() - let itemIdentifier = NSFileProviderItemIdentifier("2") adapter.setTagData(emptyTagData, forItemIdentifier: itemIdentifier) { item, error in XCTAssertNil(error) guard let item = item else { diff --git a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterStartProvidingItemTests.swift b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterStartProvidingItemTests.swift index 2d24edc99..25ea3bc05 100644 --- a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterStartProvidingItemTests.swift +++ b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterStartProvidingItemTests.swift @@ -108,7 +108,7 @@ class FileProviderAdapterStartProvidingItemTests: FileProviderAdapterTestCase { } wait(for: [expectation], timeout: 1.0) assertItemRemovedFromWorkingSet() - XCTAssertEqual([NSFileProviderItemIdentifier("3")], localURLProviderMock.itemIdentifierDirectoryURLForItemWithPersistentIdentifierReceivedInvocations) + XCTAssertEqual([NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: 3)], localURLProviderMock.itemIdentifierDirectoryURLForItemWithPersistentIdentifierReceivedInvocations) } func testStartProvidingItemWithTagData() throws { @@ -149,7 +149,7 @@ class FileProviderAdapterStartProvidingItemTests: FileProviderAdapterTestCase { } private func assertItemRemovedFromWorkingSet() { - XCTAssertEqual([String(itemID)], fileProviderItemUpdateDelegateMock.removeItemFromWorkingSetWithReceivedInvocations.map { $0.rawValue }) + XCTAssertEqual(["\(NSFileProviderDomainIdentifier.test.rawValue):\(itemID)"], fileProviderItemUpdateDelegateMock.removeItemFromWorkingSetWithReceivedInvocations.map { $0.rawValue }) } private func simulateExistingLocalFileByDownloadingFile() { diff --git a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterTestCase.swift b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterTestCase.swift index 768584320..f9e048b63 100644 --- a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterTestCase.swift +++ b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterTestCase.swift @@ -21,10 +21,12 @@ class FileProviderAdapterTestCase: CloudTaskExecutorTestCase { override func setUpWithError() throws { try super.setUpWithError() localURLProviderMock = LocalURLProviderMock() + localURLProviderMock.domainIdentifier = .test fileProviderItemUpdateDelegateMock = FileProviderItemUpdateDelegateMock() fullVersionCheckerMock = FullVersionCheckerMock() fullVersionCheckerMock.isFullVersion = true - adapter = FileProviderAdapter(uploadTaskManager: uploadTaskManagerMock, + adapter = FileProviderAdapter(domainIdentifier: .test, + uploadTaskManager: uploadTaskManagerMock, cachedFileManager: cachedFileManagerMock, itemMetadataManager: metadataManagerMock, reparentTaskManager: reparentTaskManagerMock, @@ -57,7 +59,7 @@ class FileProviderAdapterTestCase: CloudTaskExecutorTestCase { } func createFullyMockedAdapter() -> FileProviderAdapter { - return FileProviderAdapter(uploadTaskManager: uploadTaskManagerMock, cachedFileManager: cachedFileManagerMock, itemMetadataManager: metadataManagerMock, reparentTaskManager: reparentTaskManagerMock, deletionTaskManager: deletionTaskManagerMock, itemEnumerationTaskManager: itemEnumerationTaskManagerMock, downloadTaskManager: downloadTaskManagerMock, scheduler: WorkflowSchedulerMock(), provider: cloudProviderMock, localURLProvider: localURLProviderMock) + return FileProviderAdapter(domainIdentifier: .test, uploadTaskManager: uploadTaskManagerMock, cachedFileManager: cachedFileManagerMock, itemMetadataManager: metadataManagerMock, reparentTaskManager: reparentTaskManagerMock, deletionTaskManager: deletionTaskManagerMock, itemEnumerationTaskManager: itemEnumerationTaskManagerMock, downloadTaskManager: downloadTaskManagerMock, scheduler: WorkflowSchedulerMock(), provider: cloudProviderMock, localURLProvider: localURLProviderMock) } } @@ -66,3 +68,7 @@ extension UploadTaskRecord: Equatable { lhs.correspondingItem == rhs.correspondingItem && lhs.lastFailedUploadDate == rhs.lastFailedUploadDate && lhs.uploadErrorCode == rhs.uploadErrorCode && lhs.uploadErrorDomain == rhs.uploadErrorDomain } } + +extension NSFileProviderDomainIdentifier { + static let test = NSFileProviderDomainIdentifier("Test") +} diff --git a/CryptomatorFileProviderTests/FileProviderEnumeratorTests.swift b/CryptomatorFileProviderTests/FileProviderEnumeratorTests.swift index d90f6c83b..1a723d968 100644 --- a/CryptomatorFileProviderTests/FileProviderEnumeratorTests.swift +++ b/CryptomatorFileProviderTests/FileProviderEnumeratorTests.swift @@ -23,8 +23,8 @@ class FileProviderEnumeratorTestCase: XCTestCase { let dbPath = FileManager.default.temporaryDirectory let domain = NSFileProviderDomain(vaultUID: "VaultUID-12345", displayName: "Test Vault") let items: [FileProviderItem] = [ - .init(metadata: ItemMetadata(id: 2, name: "Test.txt", type: .file, size: 100, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/Test.txt"), isPlaceholderItem: false)), - .init(metadata: ItemMetadata(id: 3, name: "TestFolder", type: .folder, size: nil, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/TestFolder"), isPlaceholderItem: false)) + .init(metadata: ItemMetadata(id: 2, name: "Test.txt", type: .file, size: 100, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/Test.txt"), isPlaceholderItem: false), domainIdentifier: .test), + .init(metadata: ItemMetadata(id: 3, name: "TestFolder", type: .folder, size: nil, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/TestFolder"), isPlaceholderItem: false), domainIdentifier: .test) ] let deleteItemIdentifiers = [1, 2, 3].map { NSFileProviderItemIdentifier("\($0)") } diff --git a/CryptomatorFileProviderTests/FileProviderItemTests.swift b/CryptomatorFileProviderTests/FileProviderItemTests.swift index c55c42b74..1ff19ecd6 100644 --- a/CryptomatorFileProviderTests/FileProviderItemTests.swift +++ b/CryptomatorFileProviderTests/FileProviderItemTests.swift @@ -16,7 +16,7 @@ class FileProviderItemTests: XCTestCase { func testRootItem() { let cloudPath = CloudPath("/") let metadata = ItemMetadata(id: ItemMetadataDBManager.rootContainerId, name: "root", type: .folder, size: nil, parentID: ItemMetadataDBManager.rootContainerId, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) - let item = FileProviderItem(metadata: metadata) + let item = FileProviderItem(metadata: metadata, domainIdentifier: .test) XCTAssertEqual(NSFileProviderItemIdentifier.rootContainer, item.itemIdentifier) XCTAssertEqual(NSFileProviderItemIdentifier.rootContainer, item.parentItemIdentifier) XCTAssertEqual("public.folder", item.typeIdentifier) @@ -25,8 +25,8 @@ class FileProviderItemTests: XCTestCase { func testFileItem() { let cloudPath = CloudPath("/test.txt") let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: ItemMetadataDBManager.rootContainerId, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) - let item = FileProviderItem(metadata: metadata) - XCTAssertEqual(NSFileProviderItemIdentifier("2"), item.itemIdentifier) + let item = FileProviderItem(metadata: metadata, domainIdentifier: .test) + XCTAssertEqual(NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: 2), item.itemIdentifier) XCTAssertEqual(NSFileProviderItemIdentifier.rootContainer, item.parentItemIdentifier) XCTAssertEqual("test.txt", item.filename) XCTAssertEqual(100, item.documentSize) @@ -40,8 +40,8 @@ class FileProviderItemTests: XCTestCase { func testFolderItem() { let cloudPath = CloudPath("/test Folder/") let metadata = ItemMetadata(id: 2, name: "test Folder", type: .folder, size: nil, parentID: ItemMetadataDBManager.rootContainerId, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) - let item = FileProviderItem(metadata: metadata) - XCTAssertEqual(NSFileProviderItemIdentifier("2"), item.itemIdentifier) + let item = FileProviderItem(metadata: metadata, domainIdentifier: .test) + XCTAssertEqual(NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: 2), item.itemIdentifier) XCTAssertEqual(NSFileProviderItemIdentifier.rootContainer, item.parentItemIdentifier) XCTAssertEqual("test Folder", item.filename) XCTAssertNil(item.documentSize) @@ -57,8 +57,8 @@ class FileProviderItemTests: XCTestCase { let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: ItemMetadataDBManager.rootContainerId, lastModifiedDate: nil, statusCode: .uploadError, cloudPath: cloudPath, isPlaceholderItem: false) let lastFailedUploadDate = Date(timeIntervalSinceReferenceDate: 0) let failedUploadTask = UploadTaskRecord(correspondingItem: 2, lastFailedUploadDate: lastFailedUploadDate, uploadErrorCode: NSFileProviderError.insufficientQuota.rawValue, uploadErrorDomain: NSFileProviderErrorDomain) - let item = FileProviderItem(metadata: metadata, error: failedUploadTask.failedWithError) - XCTAssertEqual(NSFileProviderItemIdentifier("2"), item.itemIdentifier) + let item = FileProviderItem(metadata: metadata, domainIdentifier: .test, error: failedUploadTask.failedWithError) + XCTAssertEqual(NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: 2), item.itemIdentifier) XCTAssertEqual(NSFileProviderItemIdentifier.rootContainer, item.parentItemIdentifier) XCTAssertEqual("test.txt", item.filename) XCTAssertEqual(100, item.documentSize) @@ -80,8 +80,8 @@ class FileProviderItemTests: XCTestCase { try FileManager.default.createDirectory(at: tmpDir, withIntermediateDirectories: false) let localURL = tmpDir.appendingPathComponent("test.txt") - let item = FileProviderItem(metadata: metadata, newestVersionLocallyCached: false, localURL: localURL) - XCTAssertEqual(NSFileProviderItemIdentifier("2"), item.itemIdentifier) + let item = FileProviderItem(metadata: metadata, domainIdentifier: .test, newestVersionLocallyCached: false, localURL: localURL) + XCTAssertEqual(NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: 2), item.itemIdentifier) XCTAssertEqual(NSFileProviderItemIdentifier.rootContainer, item.parentItemIdentifier) XCTAssertEqual("test.txt", item.filename) XCTAssertEqual(100, item.documentSize) @@ -93,8 +93,8 @@ class FileProviderItemTests: XCTestCase { try "Foo".write(to: localURL, atomically: true, encoding: .utf8) - let newestItem = FileProviderItem(metadata: metadata, newestVersionLocallyCached: false, localURL: localURL) - XCTAssertEqual(NSFileProviderItemIdentifier("2"), newestItem.itemIdentifier) + let newestItem = FileProviderItem(metadata: metadata, domainIdentifier: .test, newestVersionLocallyCached: false, localURL: localURL) + XCTAssertEqual(NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: 2), newestItem.itemIdentifier) XCTAssertEqual(NSFileProviderItemIdentifier.rootContainer, newestItem.parentItemIdentifier) XCTAssertEqual("test.txt", newestItem.filename) XCTAssertEqual(100, newestItem.documentSize) @@ -113,7 +113,7 @@ class FileProviderItemTests: XCTestCase { let cloudPath = CloudPath("/test.txt") let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: ItemMetadataDBManager.rootContainerId, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: cloudPath, isPlaceholderItem: false) - let item = FileProviderItem(metadata: metadata, fullVersionChecker: fullVersionCheckerMock) + let item = FileProviderItem(metadata: metadata, domainIdentifier: .test, fullVersionChecker: fullVersionCheckerMock) XCTAssertEqual(NSFileProviderItemCapabilities.allowsReading, item.capabilities) } @@ -123,7 +123,7 @@ class FileProviderItemTests: XCTestCase { let cloudPath = CloudPath("/test") let metadata = ItemMetadata(id: 2, name: "test", type: .folder, size: nil, parentID: ItemMetadataDBManager.rootContainerId, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: cloudPath, isPlaceholderItem: false) - let item = FileProviderItem(metadata: metadata, fullVersionChecker: fullVersionCheckerMock) + let item = FileProviderItem(metadata: metadata, domainIdentifier: .test, fullVersionChecker: fullVersionCheckerMock) XCTAssertEqual([.allowsAddingSubItems, .allowsContentEnumerating, .allowsReading, .allowsDeleting, .allowsRenaming, .allowsReparenting], item.capabilities) } @@ -133,7 +133,7 @@ class FileProviderItemTests: XCTestCase { let cloudPath = CloudPath("/test.txt") let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: ItemMetadataDBManager.rootContainerId, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) - let item = FileProviderItem(metadata: metadata, fullVersionChecker: fullVersionCheckerMock) + let item = FileProviderItem(metadata: metadata, domainIdentifier: .test, fullVersionChecker: fullVersionCheckerMock) XCTAssertEqual(NSFileProviderItemCapabilities.allowsReading, item.capabilities) } @@ -143,7 +143,7 @@ class FileProviderItemTests: XCTestCase { let cloudPath = CloudPath("/test.txt") let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: ItemMetadataDBManager.rootContainerId, lastModifiedDate: nil, statusCode: .uploadError, cloudPath: cloudPath, isPlaceholderItem: false) - let item = FileProviderItem(metadata: metadata, fullVersionChecker: fullVersionCheckerMock) + let item = FileProviderItem(metadata: metadata, domainIdentifier: .test, fullVersionChecker: fullVersionCheckerMock) XCTAssertEqual(NSFileProviderItemCapabilities.allowsDeleting, item.capabilities) } @@ -153,7 +153,7 @@ class FileProviderItemTests: XCTestCase { let cloudPath = CloudPath("/test") let metadata = ItemMetadata(id: 2, name: "test", type: .folder, size: 100, parentID: ItemMetadataDBManager.rootContainerId, lastModifiedDate: nil, statusCode: .uploadError, cloudPath: cloudPath, isPlaceholderItem: false) - let item = FileProviderItem(metadata: metadata, fullVersionChecker: fullVersionCheckerMock) + let item = FileProviderItem(metadata: metadata, domainIdentifier: .test, fullVersionChecker: fullVersionCheckerMock) XCTAssertEqual(NSFileProviderItemCapabilities.allowsDeleting, item.capabilities) } } diff --git a/CryptomatorFileProviderTests/FileProviderNotificatorTests.swift b/CryptomatorFileProviderTests/FileProviderNotificatorTests.swift index 61616890d..650d54507 100644 --- a/CryptomatorFileProviderTests/FileProviderNotificatorTests.swift +++ b/CryptomatorFileProviderTests/FileProviderNotificatorTests.swift @@ -14,11 +14,11 @@ import XCTest class FileProviderNotificatorTests: XCTestCase { var notificator: FileProviderNotificator! var enumerationSignalingMock: EnumerationSignalingMock! - let deleteItemIdentifiers = [1, 2, 3].map { NSFileProviderItemIdentifier("\($0)") } + let deleteItemIdentifiers = [1, 2, 3].map { NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: $0) } let updatedMetadataIDs: [Int64] = [2, 3, 4] - lazy var updatedItemIdentifiers = updatedMetadataIDs.map { NSFileProviderItemIdentifier("\($0)") } + lazy var updatedItemIdentifiers = updatedMetadataIDs.map { NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: $0) } lazy var updatedItems: [FileProviderItem] = updatedMetadataIDs.map { - FileProviderItem(metadata: ItemMetadata(id: $0, name: "\($0)", type: .file, size: nil, parentID: 0, lastModifiedDate: nil, statusCode: .isDownloading, cloudPath: CloudPath("/\($0)"), isPlaceholderItem: false)) + FileProviderItem(metadata: ItemMetadata(id: $0, name: "\($0)", type: .file, size: nil, parentID: 0, lastModifiedDate: nil, statusCode: .isDownloading, cloudPath: CloudPath("/\($0)"), isPlaceholderItem: false), domainIdentifier: .test) } override func setUpWithError() throws { @@ -48,7 +48,7 @@ class FileProviderNotificatorTests: XCTestCase { notificator.removeItemsFromWorkingSet(with: deleteItemIdentifiers) notificator.updateWorkingSetItems(updatedItems) - XCTAssertEqual([NSFileProviderItemIdentifier("1")], getSortedItemIdentifiersToDeleteFromWorkingSet()) + XCTAssertEqual([NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: 1)], getSortedItemIdentifiersToDeleteFromWorkingSet()) assertUpdateWorkingSetHasUpdatedItems() XCTAssertFalse(enumerationSignalingMock.signalEnumeratorForCompletionHandlerCalled) } diff --git a/CryptomatorFileProviderTests/Middleware/TaskExecutor/DownloadTaskExecutorTests.swift b/CryptomatorFileProviderTests/Middleware/TaskExecutor/DownloadTaskExecutorTests.swift index 27becde07..8405769da 100644 --- a/CryptomatorFileProviderTests/Middleware/TaskExecutor/DownloadTaskExecutorTests.swift +++ b/CryptomatorFileProviderTests/Middleware/TaskExecutor/DownloadTaskExecutorTests.swift @@ -24,7 +24,7 @@ class DownloadTaskExecutorTests: CloudTaskExecutorTestCase { let downloadTaskRecord = DownloadTaskRecord(correspondingItem: itemMetadata.id!, replaceExisting: false, localURL: localURL) let downloadTask = DownloadTask(taskRecord: downloadTaskRecord, itemMetadata: itemMetadata) - let taskExecutor = DownloadTaskExecutor(provider: cloudProviderMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock, downloadTaskManager: downloadTaskManagerMock) + let taskExecutor = DownloadTaskExecutor(domainIdentifier: .test, provider: cloudProviderMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock, downloadTaskManager: downloadTaskManagerMock) taskExecutor.execute(task: downloadTask).then { _ in let localContent = try Data(contentsOf: localURL) @@ -63,7 +63,7 @@ class DownloadTaskExecutorTests: CloudTaskExecutorTestCase { let downloadTaskRecord = DownloadTaskRecord(correspondingItem: itemMetadata.id!, replaceExisting: false, localURL: localURL) let downloadTask = DownloadTask(taskRecord: downloadTaskRecord, itemMetadata: itemMetadata) - let taskExecutor = DownloadTaskExecutor(provider: errorCloudProviderMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock, downloadTaskManager: downloadTaskManagerMock) + let taskExecutor = DownloadTaskExecutor(domainIdentifier: .test, provider: errorCloudProviderMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock, downloadTaskManager: downloadTaskManagerMock) taskExecutor.execute(task: downloadTask).then { _ in XCTFail("Promise should not fulfill if the provider fails with an error") @@ -94,7 +94,7 @@ class DownloadTaskExecutorTests: CloudTaskExecutorTestCase { let downloadTaskRecord = DownloadTaskRecord(correspondingItem: itemMetadata.id!, replaceExisting: true, localURL: localURL) let downloadTask = DownloadTask(taskRecord: downloadTaskRecord, itemMetadata: itemMetadata) - let taskExecutor = DownloadTaskExecutor(provider: cloudProviderMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock, downloadTaskManager: downloadTaskManagerMock) + let taskExecutor = DownloadTaskExecutor(domainIdentifier: .test, provider: cloudProviderMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock, downloadTaskManager: downloadTaskManagerMock) taskExecutor.execute(task: downloadTask).then { _ in let localContent = try Data(contentsOf: localURL) @@ -133,7 +133,7 @@ class DownloadTaskExecutorTests: CloudTaskExecutorTestCase { let lastModifiedDate = Date(timeIntervalSince1970: 0) - let taskExecutor = DownloadTaskExecutor(provider: cloudProviderMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock, downloadTaskManager: downloadTaskManagerMock) + let taskExecutor = DownloadTaskExecutor(domainIdentifier: .test, provider: cloudProviderMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock, downloadTaskManager: downloadTaskManagerMock) let item = try taskExecutor.downloadPostProcessing(for: itemMetadata, lastModifiedDate: lastModifiedDate, localURL: localURL, downloadDestination: downloadDestination) XCTAssert(FileManager.default.fileExists(atPath: localURL.path)) @@ -163,7 +163,7 @@ class DownloadTaskExecutorTests: CloudTaskExecutorTestCase { let lastModifiedDate = Date(timeIntervalSince1970: 0) - let taskExecutor = DownloadTaskExecutor(provider: cloudProviderMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock, downloadTaskManager: downloadTaskManagerMock) + let taskExecutor = DownloadTaskExecutor(domainIdentifier: .test, provider: cloudProviderMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock, downloadTaskManager: downloadTaskManagerMock) let item = try taskExecutor.downloadPostProcessing(for: itemMetadata, lastModifiedDate: lastModifiedDate, localURL: localURL, downloadDestination: localURL) XCTAssert(FileManager.default.fileExists(atPath: localURL.path)) diff --git a/CryptomatorFileProviderTests/Middleware/TaskExecutor/FolderCreationTaskExecutorTests.swift b/CryptomatorFileProviderTests/Middleware/TaskExecutor/FolderCreationTaskExecutorTests.swift index 4737a1eb3..2b96f73f2 100644 --- a/CryptomatorFileProviderTests/Middleware/TaskExecutor/FolderCreationTaskExecutorTests.swift +++ b/CryptomatorFileProviderTests/Middleware/TaskExecutor/FolderCreationTaskExecutorTests.swift @@ -19,7 +19,7 @@ class FolderCreationTaskExecutorTests: CloudTaskExecutorTestCase { let itemMetadata = ItemMetadata(id: 2, name: "NewFolder", type: .folder, size: nil, parentID: metadataManagerMock.getRootContainerID(), lastModifiedDate: nil, statusCode: .isUploading, cloudPath: cloudPath, isPlaceholderItem: true, isCandidateForCacheCleanup: false) let task = FolderCreationTask(itemMetadata: itemMetadata) - let taskExecutor = FolderCreationTaskExecutor(provider: cloudProviderMock, itemMetadataManager: metadataManagerMock) + let taskExecutor = FolderCreationTaskExecutor(domainIdentifier: .test, provider: cloudProviderMock, itemMetadataManager: metadataManagerMock) taskExecutor.execute(task: task).then { item in XCTAssertEqual(1, self.metadataManagerMock.updatedMetadata.count) XCTAssertEqual(itemMetadata, self.metadataManagerMock.updatedMetadata[0]) @@ -47,7 +47,7 @@ class FolderCreationTaskExecutorTests: CloudTaskExecutorTestCase { } let task = FolderCreationTask(itemMetadata: itemMetadata) - let taskExecutor = FolderCreationTaskExecutor(provider: errorCloudProviderMock, itemMetadataManager: metadataManagerMock) + let taskExecutor = FolderCreationTaskExecutor(domainIdentifier: .test, provider: errorCloudProviderMock, itemMetadataManager: metadataManagerMock) taskExecutor.execute(task: task).then { _ in XCTFail("Promise should not fulfill if the provider fails with an error") }.catch { error in diff --git a/CryptomatorFileProviderTests/Middleware/TaskExecutor/ItemEnumerationTaskTests.swift b/CryptomatorFileProviderTests/Middleware/TaskExecutor/ItemEnumerationTaskTests.swift index c18b3f24e..fa029f52d 100644 --- a/CryptomatorFileProviderTests/Middleware/TaskExecutor/ItemEnumerationTaskTests.swift +++ b/CryptomatorFileProviderTests/Middleware/TaskExecutor/ItemEnumerationTaskTests.swift @@ -30,7 +30,7 @@ class ItemEnumerationTaskTests: CloudTaskExecutorTestCase { let enumerationTaskRecord = ItemEnumerationTaskRecord(correspondingItem: fileMetadata.id!, pageToken: nil) let enumerationTask = ItemEnumerationTask(taskRecord: enumerationTaskRecord, itemMetadata: fileMetadata) - let taskExecutor = ItemEnumerationTaskExecutor(provider: cloudProviderMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock, uploadTaskManager: uploadTaskManagerMock, reparentTaskManager: reparentTaskManagerMock, deletionTaskManager: deletionTaskManagerMock, itemEnumerationTaskManager: itemEnumerationTaskManagerMock, deleteItemHelper: deleteItemHelper) + let taskExecutor = ItemEnumerationTaskExecutor(domainIdentifier: .test, provider: cloudProviderMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock, uploadTaskManager: uploadTaskManagerMock, reparentTaskManager: reparentTaskManagerMock, deletionTaskManager: deletionTaskManagerMock, itemEnumerationTaskManager: itemEnumerationTaskManagerMock, deleteItemHelper: deleteItemHelper) taskExecutor.execute(task: enumerationTask).then { itemList in XCTAssertEqual(1, itemList.items.count) @@ -81,7 +81,7 @@ class ItemEnumerationTaskTests: CloudTaskExecutorTestCase { let enumerationTaskRecord = ItemEnumerationTaskRecord(correspondingItem: fileMetadata.id!, pageToken: nil) let enumerationTask = ItemEnumerationTask(taskRecord: enumerationTaskRecord, itemMetadata: fileMetadata) - let taskExecutor = ItemEnumerationTaskExecutor(provider: cloudProviderMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock, uploadTaskManager: uploadTaskManagerMock, reparentTaskManager: reparentTaskManagerMock, deletionTaskManager: deletionTaskManagerMock, itemEnumerationTaskManager: itemEnumerationTaskManagerMock, deleteItemHelper: deleteItemHelper) + let taskExecutor = ItemEnumerationTaskExecutor(domainIdentifier: .test, provider: cloudProviderMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock, uploadTaskManager: uploadTaskManagerMock, reparentTaskManager: reparentTaskManagerMock, deletionTaskManager: deletionTaskManagerMock, itemEnumerationTaskManager: itemEnumerationTaskManagerMock, deleteItemHelper: deleteItemHelper) taskExecutor.execute(task: enumerationTask).then { itemList in XCTAssertEqual(1, itemList.items.count) @@ -127,7 +127,7 @@ class ItemEnumerationTaskTests: CloudTaskExecutorTestCase { let enumerationTaskRecord = ItemEnumerationTaskRecord(correspondingItem: fileMetadata.id!, pageToken: nil) let enumerationTask = ItemEnumerationTask(taskRecord: enumerationTaskRecord, itemMetadata: fileMetadata) - let taskExecutor = ItemEnumerationTaskExecutor(provider: cloudProviderMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock, uploadTaskManager: uploadTaskManagerMock, reparentTaskManager: reparentTaskManagerMock, deletionTaskManager: deletionTaskManagerMock, itemEnumerationTaskManager: itemEnumerationTaskManagerMock, deleteItemHelper: deleteItemHelper) + let taskExecutor = ItemEnumerationTaskExecutor(domainIdentifier: .test, provider: cloudProviderMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock, uploadTaskManager: uploadTaskManagerMock, reparentTaskManager: reparentTaskManagerMock, deletionTaskManager: deletionTaskManagerMock, itemEnumerationTaskManager: itemEnumerationTaskManagerMock, deleteItemHelper: deleteItemHelper) taskExecutor.execute(task: enumerationTask).then { itemList in XCTAssertEqual(1, itemList.items.count) @@ -182,7 +182,7 @@ class ItemEnumerationTaskTests: CloudTaskExecutorTestCase { Promise(CloudTaskTestError.correctPassthrough) } - let taskExecutor = ItemEnumerationTaskExecutor(provider: errorCloudProviderMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock, uploadTaskManager: uploadTaskManagerMock, reparentTaskManager: reparentTaskManagerMock, deletionTaskManager: deletionTaskManagerMock, itemEnumerationTaskManager: itemEnumerationTaskManagerMock, deleteItemHelper: deleteItemHelper) + let taskExecutor = ItemEnumerationTaskExecutor(domainIdentifier: .test, provider: errorCloudProviderMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock, uploadTaskManager: uploadTaskManagerMock, reparentTaskManager: reparentTaskManagerMock, deletionTaskManager: deletionTaskManagerMock, itemEnumerationTaskManager: itemEnumerationTaskManagerMock, deleteItemHelper: deleteItemHelper) taskExecutor.execute(task: enumerationTask).then { _ in XCTFail("Promise should not fulfill if the provider fails with an error") @@ -218,12 +218,12 @@ class ItemEnumerationTaskTests: CloudTaskExecutorTestCase { ItemMetadata(id: 5, name: "File 3", type: .file, size: 14, parentID: ItemMetadataDBManager.rootContainerId, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/File 3"), isPlaceholderItem: false), ItemMetadata(id: 6, name: "File 4", type: .file, size: 14, parentID: ItemMetadataDBManager.rootContainerId, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/File 4"), isPlaceholderItem: false)] - let expectedRootFolderFileProviderItems = expectedItemMetadataInsideRootFolder.map { FileProviderItem(metadata: $0) } + let expectedRootFolderFileProviderItems = expectedItemMetadataInsideRootFolder.map { FileProviderItem(metadata: $0, domainIdentifier: .test) } let expectedItemMetadataInsideSubFolder = [ItemMetadata(id: 7, name: "Directory 2", type: .folder, size: 0, parentID: 2, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/Directory 1/Directory 2"), isPlaceholderItem: false), ItemMetadata(id: 8, name: "File 5", type: .file, size: 14, parentID: 2, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/Directory 1/File 5"), isPlaceholderItem: false)] - let expectedSubFolderFileProviderItems = expectedItemMetadataInsideSubFolder.map { FileProviderItem(metadata: $0) } + let expectedSubFolderFileProviderItems = expectedItemMetadataInsideSubFolder.map { FileProviderItem(metadata: $0, domainIdentifier: .test) } - let taskExecutor = ItemEnumerationTaskExecutor(provider: cloudProviderMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock, uploadTaskManager: uploadTaskManagerMock, reparentTaskManager: reparentTaskManagerMock, deletionTaskManager: deletionTaskManagerMock, itemEnumerationTaskManager: itemEnumerationTaskManagerMock, deleteItemHelper: deleteItemHelper) + let taskExecutor = ItemEnumerationTaskExecutor(domainIdentifier: .test, provider: cloudProviderMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock, uploadTaskManager: uploadTaskManagerMock, reparentTaskManager: reparentTaskManagerMock, deletionTaskManager: deletionTaskManagerMock, itemEnumerationTaskManager: itemEnumerationTaskManagerMock, deleteItemHelper: deleteItemHelper) taskExecutor.execute(task: enumerationTask).then { fileProviderItemList -> FileProviderItem in XCTAssertEqual(5, fileProviderItemList.items.count) @@ -274,18 +274,18 @@ class ItemEnumerationTaskTests: CloudTaskExecutorTestCase { let enumerationTaskRecord = ItemEnumerationTaskRecord(correspondingItem: rootItemMetadata.id!, pageToken: nil) let enumerationTask = ItemEnumerationTask(taskRecord: enumerationTaskRecord, itemMetadata: rootItemMetadata) - let expectedRootFolderFileProviderItems = [FileProviderItem(metadata: ItemMetadata(id: 2, name: "Directory 1", type: .folder, size: 0, parentID: ItemMetadataDBManager.rootContainerId, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/Directory 1/"), isPlaceholderItem: false)), - FileProviderItem(metadata: ItemMetadata(id: 3, name: "File 1", type: .file, size: 14, parentID: ItemMetadataDBManager.rootContainerId, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/File 1"), isPlaceholderItem: false)), - FileProviderItem(metadata: ItemMetadata(id: 4, name: "File 2", type: .file, size: 14, parentID: ItemMetadataDBManager.rootContainerId, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/File 3"), isPlaceholderItem: false)), - FileProviderItem(metadata: ItemMetadata(id: 5, name: "File 3", type: .file, size: 14, parentID: ItemMetadataDBManager.rootContainerId, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/File 3"), isPlaceholderItem: false)), - FileProviderItem(metadata: ItemMetadata(id: 6, name: "File 4", type: .file, size: 14, parentID: ItemMetadataDBManager.rootContainerId, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/File 4"), isPlaceholderItem: false))] - let expectedChangedRootFolderFileProviderItems = [FileProviderItem(metadata: ItemMetadata(id: 2, name: "Directory 1", type: .folder, size: 0, parentID: ItemMetadataDBManager.rootContainerId, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/Directory 1/"), isPlaceholderItem: false)), - FileProviderItem(metadata: ItemMetadata(id: 4, name: "File 2", type: .file, size: 14, parentID: ItemMetadataDBManager.rootContainerId, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/File 3"), isPlaceholderItem: false)), - FileProviderItem(metadata: ItemMetadata(id: 5, name: "File 3", type: .file, size: 14, parentID: ItemMetadataDBManager.rootContainerId, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/File 3"), isPlaceholderItem: false)), - FileProviderItem(metadata: ItemMetadata(id: 6, name: "File 4", type: .file, size: 14, parentID: ItemMetadataDBManager.rootContainerId, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/File 4"), isPlaceholderItem: false)), - FileProviderItem(metadata: ItemMetadata(id: 7, name: "NewFileFromCloud", type: .file, size: 24, parentID: ItemMetadataDBManager.rootContainerId, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/NewFileFromCloud"), isPlaceholderItem: false))] + let expectedRootFolderFileProviderItems = [FileProviderItem(metadata: ItemMetadata(id: 2, name: "Directory 1", type: .folder, size: 0, parentID: ItemMetadataDBManager.rootContainerId, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/Directory 1/"), isPlaceholderItem: false), domainIdentifier: .test), + FileProviderItem(metadata: ItemMetadata(id: 3, name: "File 1", type: .file, size: 14, parentID: ItemMetadataDBManager.rootContainerId, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/File 1"), isPlaceholderItem: false), domainIdentifier: .test), + FileProviderItem(metadata: ItemMetadata(id: 4, name: "File 2", type: .file, size: 14, parentID: ItemMetadataDBManager.rootContainerId, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/File 3"), isPlaceholderItem: false), domainIdentifier: .test), + FileProviderItem(metadata: ItemMetadata(id: 5, name: "File 3", type: .file, size: 14, parentID: ItemMetadataDBManager.rootContainerId, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/File 3"), isPlaceholderItem: false), domainIdentifier: .test), + FileProviderItem(metadata: ItemMetadata(id: 6, name: "File 4", type: .file, size: 14, parentID: ItemMetadataDBManager.rootContainerId, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/File 4"), isPlaceholderItem: false), domainIdentifier: .test)] + let expectedChangedRootFolderFileProviderItems = [FileProviderItem(metadata: ItemMetadata(id: 2, name: "Directory 1", type: .folder, size: 0, parentID: ItemMetadataDBManager.rootContainerId, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/Directory 1/"), isPlaceholderItem: false), domainIdentifier: .test), + FileProviderItem(metadata: ItemMetadata(id: 4, name: "File 2", type: .file, size: 14, parentID: ItemMetadataDBManager.rootContainerId, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/File 3"), isPlaceholderItem: false), domainIdentifier: .test), + FileProviderItem(metadata: ItemMetadata(id: 5, name: "File 3", type: .file, size: 14, parentID: ItemMetadataDBManager.rootContainerId, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/File 3"), isPlaceholderItem: false), domainIdentifier: .test), + FileProviderItem(metadata: ItemMetadata(id: 6, name: "File 4", type: .file, size: 14, parentID: ItemMetadataDBManager.rootContainerId, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/File 4"), isPlaceholderItem: false), domainIdentifier: .test), + FileProviderItem(metadata: ItemMetadata(id: 7, name: "NewFileFromCloud", type: .file, size: 24, parentID: ItemMetadataDBManager.rootContainerId, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/NewFileFromCloud"), isPlaceholderItem: false), domainIdentifier: .test)] - let taskExecutor = ItemEnumerationTaskExecutor(provider: cloudProviderMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock, uploadTaskManager: uploadTaskManagerMock, reparentTaskManager: reparentTaskManagerMock, deletionTaskManager: deletionTaskManagerMock, itemEnumerationTaskManager: itemEnumerationTaskManagerMock, deleteItemHelper: deleteItemHelper) + let taskExecutor = ItemEnumerationTaskExecutor(domainIdentifier: .test, provider: cloudProviderMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock, uploadTaskManager: uploadTaskManagerMock, reparentTaskManager: reparentTaskManagerMock, deletionTaskManager: deletionTaskManagerMock, itemEnumerationTaskManager: itemEnumerationTaskManagerMock, deleteItemHelper: deleteItemHelper) taskExecutor.execute(task: enumerationTask).then { fileProviderItemList -> Promise in XCTAssertEqual(5, fileProviderItemList.items.count) @@ -318,7 +318,7 @@ class ItemEnumerationTaskTests: CloudTaskExecutorTestCase { let enumerationTaskRecord = ItemEnumerationTaskRecord(correspondingItem: rootItemMetadata.id!, pageToken: nil) let enumerationTask = ItemEnumerationTask(taskRecord: enumerationTaskRecord, itemMetadata: rootItemMetadata) - let taskExecutor = ItemEnumerationTaskExecutor(provider: cloudProviderMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock, uploadTaskManager: uploadTaskManagerMock, reparentTaskManager: reparentTaskManagerMock, deletionTaskManager: deletionTaskManagerMock, itemEnumerationTaskManager: itemEnumerationTaskManagerMock, deleteItemHelper: deleteItemHelper) + let taskExecutor = ItemEnumerationTaskExecutor(domainIdentifier: .test, provider: cloudProviderMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock, uploadTaskManager: uploadTaskManagerMock, reparentTaskManager: reparentTaskManagerMock, deletionTaskManager: deletionTaskManagerMock, itemEnumerationTaskManager: itemEnumerationTaskManagerMock, deleteItemHelper: deleteItemHelper) let lastFailedUploadDate = Date() taskExecutor.execute(task: enumerationTask).then { _ -> Void in @@ -369,7 +369,7 @@ class ItemEnumerationTaskTests: CloudTaskExecutorTestCase { let enumerationTaskRecord = ItemEnumerationTaskRecord(correspondingItem: rootItemMetadata.id!, pageToken: nil) let enumerationTask = ItemEnumerationTask(taskRecord: enumerationTaskRecord, itemMetadata: rootItemMetadata) - let taskExecutor = ItemEnumerationTaskExecutor(provider: paginatedMockedProvider, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock, uploadTaskManager: uploadTaskManagerMock, reparentTaskManager: reparentTaskManagerMock, deletionTaskManager: deletionTaskManagerMock, itemEnumerationTaskManager: itemEnumerationTaskManagerMock, deleteItemHelper: deleteItemHelper) + let taskExecutor = ItemEnumerationTaskExecutor(domainIdentifier: .test, provider: paginatedMockedProvider, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock, uploadTaskManager: uploadTaskManagerMock, reparentTaskManager: reparentTaskManagerMock, deletionTaskManager: deletionTaskManagerMock, itemEnumerationTaskManager: itemEnumerationTaskManagerMock, deleteItemHelper: deleteItemHelper) let id: Int64 = 2 let itemMetadata = ItemMetadata(id: 2, name: "TestItem", type: .file, size: nil, parentID: ItemMetadataDBManager.rootContainerId, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/TestItem"), isPlaceholderItem: false) metadataManagerMock.cachedMetadata[itemMetadata.id!] = itemMetadata @@ -405,7 +405,7 @@ class ItemEnumerationTaskTests: CloudTaskExecutorTestCase { let enumerationTaskRecord = ItemEnumerationTaskRecord(correspondingItem: rootItemMetadata.id!, pageToken: nil) let enumerationTask = ItemEnumerationTask(taskRecord: enumerationTaskRecord, itemMetadata: rootItemMetadata) - let taskExecutor = ItemEnumerationTaskExecutor(provider: paginatedMockedProvider, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock, uploadTaskManager: uploadTaskManagerMock, reparentTaskManager: reparentTaskManagerMock, deletionTaskManager: deletionTaskManagerMock, itemEnumerationTaskManager: itemEnumerationTaskManagerMock, deleteItemHelper: deleteItemHelper) + let taskExecutor = ItemEnumerationTaskExecutor(domainIdentifier: .test, provider: paginatedMockedProvider, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock, uploadTaskManager: uploadTaskManagerMock, reparentTaskManager: reparentTaskManagerMock, deletionTaskManager: deletionTaskManagerMock, itemEnumerationTaskManager: itemEnumerationTaskManagerMock, deleteItemHelper: deleteItemHelper) taskExecutor.execute(task: enumerationTask).then { fileProviderItemList -> Promise in XCTAssertEqual(2, fileProviderItemList.items.count) @@ -453,7 +453,7 @@ class ItemEnumerationTaskTests: CloudTaskExecutorTestCase { let enumerationTaskRecord = ItemEnumerationTaskRecord(correspondingItem: rootItemMetadata.id!, pageToken: nil) let enumerationTask = ItemEnumerationTask(taskRecord: enumerationTaskRecord, itemMetadata: rootItemMetadata) - let taskExecutor = ItemEnumerationTaskExecutor(provider: cloudProviderMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock, uploadTaskManager: uploadTaskManagerMock, reparentTaskManager: reparentTaskManagerMock, deletionTaskManager: deletionTaskManagerMock, itemEnumerationTaskManager: itemEnumerationTaskManagerMock, deleteItemHelper: deleteItemHelper) + let taskExecutor = ItemEnumerationTaskExecutor(domainIdentifier: .test, provider: cloudProviderMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock, uploadTaskManager: uploadTaskManagerMock, reparentTaskManager: reparentTaskManagerMock, deletionTaskManager: deletionTaskManagerMock, itemEnumerationTaskManager: itemEnumerationTaskManagerMock, deleteItemHelper: deleteItemHelper) let newCloudPath = CloudPath("/RenamedItem") taskExecutor.execute(task: enumerationTask).then { fileProviderItemList -> Promise in @@ -499,7 +499,7 @@ class ItemEnumerationTaskTests: CloudTaskExecutorTestCase { let enumerationTaskRecord = ItemEnumerationTaskRecord(correspondingItem: rootItemMetadata.id!, pageToken: nil) let enumerationTask = ItemEnumerationTask(taskRecord: enumerationTaskRecord, itemMetadata: rootItemMetadata) - let taskExecutor = ItemEnumerationTaskExecutor(provider: cloudProviderMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock, uploadTaskManager: uploadTaskManagerMock, reparentTaskManager: reparentTaskManagerMock, deletionTaskManager: deletionTaskManagerMock, itemEnumerationTaskManager: itemEnumerationTaskManagerMock, deleteItemHelper: deleteItemHelper) + let taskExecutor = ItemEnumerationTaskExecutor(domainIdentifier: .test, provider: cloudProviderMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock, uploadTaskManager: uploadTaskManagerMock, reparentTaskManager: reparentTaskManagerMock, deletionTaskManager: deletionTaskManagerMock, itemEnumerationTaskManager: itemEnumerationTaskManagerMock, deleteItemHelper: deleteItemHelper) taskExecutor.execute(task: enumerationTask).then { fileProviderItemList -> Promise in XCTAssertEqual(1, self.itemEnumerationTaskManagerMock.removedTaskRecords.count) @@ -545,7 +545,7 @@ class ItemEnumerationTaskTests: CloudTaskExecutorTestCase { Promise(CloudTaskTestError.correctPassthrough) } - let taskExecutor = ItemEnumerationTaskExecutor(provider: errorCloudProviderMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock, uploadTaskManager: uploadTaskManagerMock, reparentTaskManager: reparentTaskManagerMock, deletionTaskManager: deletionTaskManagerMock, itemEnumerationTaskManager: itemEnumerationTaskManagerMock, deleteItemHelper: deleteItemHelper) + let taskExecutor = ItemEnumerationTaskExecutor(domainIdentifier: .test, provider: errorCloudProviderMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock, uploadTaskManager: uploadTaskManagerMock, reparentTaskManager: reparentTaskManagerMock, deletionTaskManager: deletionTaskManagerMock, itemEnumerationTaskManager: itemEnumerationTaskManagerMock, deleteItemHelper: deleteItemHelper) taskExecutor.execute(task: enumerationTask).then { _ in XCTFail("Promise should not fulfill if the provider fails with an error") diff --git a/CryptomatorFileProviderTests/Middleware/TaskExecutor/ReparentTaskExecutorTests.swift b/CryptomatorFileProviderTests/Middleware/TaskExecutor/ReparentTaskExecutorTests.swift index f3f10fcec..5f636da7f 100644 --- a/CryptomatorFileProviderTests/Middleware/TaskExecutor/ReparentTaskExecutorTests.swift +++ b/CryptomatorFileProviderTests/Middleware/TaskExecutor/ReparentTaskExecutorTests.swift @@ -22,7 +22,7 @@ class ReparentTaskExecutorTests: CloudTaskExecutorTestCase { let reparentTaskRecord = ReparentTaskRecord(correspondingItem: itemMetadata.id!, sourceCloudPath: sourceCloudPath, targetCloudPath: targetCloudPath, oldParentID: itemMetadata.parentID, newParentID: itemMetadata.parentID) let reparentTask = ReparentTask(taskRecord: reparentTaskRecord, itemMetadata: itemMetadata) - let taskExecutor = ReparentTaskExecutor(provider: cloudProviderMock, reparentTaskManager: reparentTaskManagerMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock) + let taskExecutor = ReparentTaskExecutor(domainIdentifier: .test, provider: cloudProviderMock, reparentTaskManager: reparentTaskManagerMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock) taskExecutor.execute(task: reparentTask).then { item in XCTAssertEqual(.rootContainer, item.parentItemIdentifier) @@ -54,7 +54,7 @@ class ReparentTaskExecutorTests: CloudTaskExecutorTestCase { let reparentTaskRecord = ReparentTaskRecord(correspondingItem: itemMetadata.id!, sourceCloudPath: sourceCloudPath, targetCloudPath: targetCloudPath, oldParentID: itemMetadata.parentID, newParentID: itemMetadata.parentID) let reparentTask = ReparentTask(taskRecord: reparentTaskRecord, itemMetadata: itemMetadata) - let taskExecutor = ReparentTaskExecutor(provider: cloudProviderMock, reparentTaskManager: reparentTaskManagerMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock) + let taskExecutor = ReparentTaskExecutor(domainIdentifier: .test, provider: cloudProviderMock, reparentTaskManager: reparentTaskManagerMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock) taskExecutor.execute(task: reparentTask).then { item in XCTAssertEqual(.rootContainer, item.parentItemIdentifier) @@ -91,7 +91,7 @@ class ReparentTaskExecutorTests: CloudTaskExecutorTestCase { let reparentTaskRecord = ReparentTaskRecord(correspondingItem: itemMetadata.id!, sourceCloudPath: sourceCloudPath, targetCloudPath: targetCloudPath, oldParentID: itemMetadata.parentID, newParentID: itemMetadata.parentID) let reparentTask = ReparentTask(taskRecord: reparentTaskRecord, itemMetadata: itemMetadata) - let taskExecutor = ReparentTaskExecutor(provider: errorCloudProviderMock, reparentTaskManager: reparentTaskManagerMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock) + let taskExecutor = ReparentTaskExecutor(domainIdentifier: .test, provider: errorCloudProviderMock, reparentTaskManager: reparentTaskManagerMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock) taskExecutor.execute(task: reparentTask).then { _ in XCTFail("Promise should not fulfill if the provider fails with an error") }.catch { error in @@ -121,7 +121,7 @@ class ReparentTaskExecutorTests: CloudTaskExecutorTestCase { let reparentTaskRecord = ReparentTaskRecord(correspondingItem: itemMetadata.id!, sourceCloudPath: sourceCloudPath, targetCloudPath: targetCloudPath, oldParentID: itemMetadata.parentID, newParentID: itemMetadata.parentID) let reparentTask = ReparentTask(taskRecord: reparentTaskRecord, itemMetadata: itemMetadata) - let taskExecutor = ReparentTaskExecutor(provider: errorCloudProviderMock, reparentTaskManager: reparentTaskManagerMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock) + let taskExecutor = ReparentTaskExecutor(domainIdentifier: .test, provider: errorCloudProviderMock, reparentTaskManager: reparentTaskManagerMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock) taskExecutor.execute(task: reparentTask).then { _ in XCTFail("Promise should not fulfill if the provider fails with an error") }.catch { error in diff --git a/CryptomatorFileProviderTests/Middleware/TaskExecutor/UploadTaskExecutorTests.swift b/CryptomatorFileProviderTests/Middleware/TaskExecutor/UploadTaskExecutorTests.swift index 8294d1c76..cf6487543 100644 --- a/CryptomatorFileProviderTests/Middleware/TaskExecutor/UploadTaskExecutorTests.swift +++ b/CryptomatorFileProviderTests/Middleware/TaskExecutor/UploadTaskExecutorTests.swift @@ -21,7 +21,7 @@ class UploadTaskExecutorTests: CloudTaskExecutorTestCase { let itemMetadata = ItemMetadata(id: itemID, name: "FileToBeUploaded", type: .file, size: nil, parentID: metadataManagerMock.getRootContainerID(), lastModifiedDate: nil, statusCode: .isUploading, cloudPath: cloudPath, isPlaceholderItem: true, isCandidateForCacheCleanup: false) cachedFileManagerMock.cachedLocalFileInfo[itemID] = LocalCachedFileInfo(lastModifiedDate: nil, correspondingItem: itemID, localLastModifiedDate: Date(), localURL: localURL) - let uploadTaskExecutor = UploadTaskExecutor(provider: cloudProviderMock, cachedFileManager: cachedFileManagerMock, itemMetadataManager: metadataManagerMock, uploadTaskManager: uploadTaskManagerMock) + let uploadTaskExecutor = UploadTaskExecutor(domainIdentifier: .test, provider: cloudProviderMock, cachedFileManager: cachedFileManagerMock, itemMetadataManager: metadataManagerMock, uploadTaskManager: uploadTaskManagerMock) let mockedCloudDate = Date(timeIntervalSinceReferenceDate: 0) cloudProviderMock.lastModifiedDate[itemMetadata.cloudPath.path] = mockedCloudDate @@ -61,7 +61,7 @@ class UploadTaskExecutorTests: CloudTaskExecutorTestCase { let mockedCloudDate = Date(timeIntervalSinceReferenceDate: 0) cloudProviderMock.lastModifiedDate[itemMetadata.cloudPath.path] = mockedCloudDate - let uploadTaskExecutor = UploadTaskExecutor(provider: cloudProviderMock, cachedFileManager: cachedFileManagerMock, itemMetadataManager: metadataManagerMock, uploadTaskManager: uploadTaskManagerMock) + let uploadTaskExecutor = UploadTaskExecutor(domainIdentifier: .test, provider: cloudProviderMock, cachedFileManager: cachedFileManagerMock, itemMetadataManager: metadataManagerMock, uploadTaskManager: uploadTaskManagerMock) let uploadTaskRecord = UploadTaskRecord(correspondingItem: itemMetadata.id!, lastFailedUploadDate: nil, uploadErrorCode: nil, uploadErrorDomain: nil) let uploadTask = UploadTask(taskRecord: uploadTaskRecord, itemMetadata: itemMetadata) @@ -92,7 +92,7 @@ class UploadTaskExecutorTests: CloudTaskExecutorTestCase { let mockedCloudDate = Date(timeIntervalSinceReferenceDate: 0) cloudProviderUploadInconsistencyMock.lastModifiedDate[itemMetadata.cloudPath.path] = mockedCloudDate - let uploadTaskExecutor = UploadTaskExecutor(provider: cloudProviderUploadInconsistencyMock, cachedFileManager: cachedFileManagerMock, itemMetadataManager: metadataManagerMock, uploadTaskManager: uploadTaskManagerMock) + let uploadTaskExecutor = UploadTaskExecutor(domainIdentifier: .test, provider: cloudProviderUploadInconsistencyMock, cachedFileManager: cachedFileManagerMock, itemMetadataManager: metadataManagerMock, uploadTaskManager: uploadTaskManagerMock) let uploadTaskRecord = UploadTaskRecord(correspondingItem: itemMetadata.id!, lastFailedUploadDate: nil, uploadErrorCode: nil, uploadErrorDomain: nil) let uploadTask = UploadTask(taskRecord: uploadTaskRecord, itemMetadata: itemMetadata) @@ -138,7 +138,7 @@ class UploadTaskExecutorTests: CloudTaskExecutorTestCase { Promise(CloudTaskTestError.correctPassthrough) } - let uploadTaskExecutor = UploadTaskExecutor(provider: errorCloudProviderMock, cachedFileManager: cachedFileManagerMock, itemMetadataManager: metadataManagerMock, uploadTaskManager: uploadTaskManagerMock) + let uploadTaskExecutor = UploadTaskExecutor(domainIdentifier: .test, provider: errorCloudProviderMock, cachedFileManager: cachedFileManagerMock, itemMetadataManager: metadataManagerMock, uploadTaskManager: uploadTaskManagerMock) let uploadTaskRecord = UploadTaskRecord(correspondingItem: itemMetadata.id!, lastFailedUploadDate: nil, uploadErrorCode: nil, uploadErrorDomain: nil) let uploadTask = UploadTask(taskRecord: uploadTaskRecord, itemMetadata: itemMetadata) diff --git a/CryptomatorFileProviderTests/Mocks/LocalURLProviderMock.swift b/CryptomatorFileProviderTests/Mocks/LocalURLProviderMock.swift index 31a268245..9d2cd6dad 100644 --- a/CryptomatorFileProviderTests/Mocks/LocalURLProviderMock.swift +++ b/CryptomatorFileProviderTests/Mocks/LocalURLProviderMock.swift @@ -12,6 +12,15 @@ import Foundation // swiftlint:disable all final class LocalURLProviderMock: LocalURLProviderType { + // MARK: - domainIdentifier + + var domainIdentifier: NSFileProviderDomainIdentifier { + get { underlyingDomainIdentifier } + set(value) { underlyingDomainIdentifier = value } + } + + private var underlyingDomainIdentifier: NSFileProviderDomainIdentifier! + // MARK: - itemIdentifierDirectoryURLForItem var itemIdentifierDirectoryURLForItemWithPersistentIdentifierCallsCount = 0 diff --git a/CryptomatorFileProviderTests/WorkingSetObserverTests.swift b/CryptomatorFileProviderTests/WorkingSetObserverTests.swift index bededbce7..034a0bca1 100644 --- a/CryptomatorFileProviderTests/WorkingSetObserverTests.swift +++ b/CryptomatorFileProviderTests/WorkingSetObserverTests.swift @@ -16,12 +16,12 @@ class WorkingSetObserverTests: XCTestCase { var notificatorMock: FileProviderNotificatorTypeMock! let updatedMetadataIDs: [Int64] = [1, 2, 3] lazy var updatedItems: [FileProviderItem] = updatedMetadataIDs.map { - FileProviderItem(metadata: ItemMetadata(id: $0, name: "\($0)", type: .file, size: nil, parentID: 0, lastModifiedDate: nil, statusCode: .isDownloading, cloudPath: CloudPath("/\($0)"), isPlaceholderItem: false)) + FileProviderItem(metadata: ItemMetadata(id: $0, name: "\($0)", type: .file, size: nil, parentID: 0, lastModifiedDate: nil, statusCode: .isDownloading, cloudPath: CloudPath("/\($0)"), isPlaceholderItem: false), domainIdentifier: .test) } override func setUpWithError() throws { notificatorMock = FileProviderNotificatorTypeMock() - observer = WorkingSetObserver(database: DatabaseQueue(), notificator: notificatorMock, uploadTaskManager: UploadTaskManagerMock(), cachedFileManager: CloudTaskExecutorTestCase.CachedFileManagerMock()) + observer = WorkingSetObserver(domainIdentifier: .test, database: DatabaseQueue(), notificator: notificatorMock, uploadTaskManager: UploadTaskManagerMock(), cachedFileManager: CloudTaskExecutorTestCase.CachedFileManagerMock()) } func testHandleNewWorkingSetUpdate() throws { From d4815a256458558f155038d2814df176d08d29cc Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Mon, 9 May 2022 15:35:11 +0200 Subject: [PATCH 05/18] Added the option to recover uploads via custom FileProvider actions; fixed error reporting for failed uploads --- Cryptomator.xcodeproj/project.pbxproj | 36 +++++++ .../FileProviderXPC/UploadRetrying.swift | 23 +++++ .../Actions/FileProviderAction.swift | 14 +++ .../DB/UploadTaskDBManager.swift | 7 ++ .../FileProviderAdapter.swift | 30 +++++- .../FileProviderItem.swift | 6 ++ .../LocalURLProviderType.swift | 6 +- .../Middleware/ErrorMapper.swift | 14 ++- .../TaskExecutor/UploadTaskExecutor.swift | 26 ++++- CryptomatorFileProvider/ProgressManager.swift | 43 +++++++++ .../UploadRetryingServiceSource.swift | 60 ++++++++++++ ...ileProviderAdapterEnumerateItemTests.swift | 8 ++ .../InMemoryProgressManagerTests.swift | 46 +++++++++ .../UploadTaskExecutorTests.swift | 42 +++++---- .../Mocks/FileProviderAdapterTypeMock.swift | 18 ++++ .../Mocks/ProgressManagerMock.swift | 50 ++++++++++ .../UploadRetryingServiceSourceTests.swift | 67 +++++++++++++ .../FileProviderExtension.swift | 3 +- FileProviderExtensionUI/Info.plist | 20 ++++ .../RootViewController.swift | 68 ++++++++++++++ .../UploadProgressAlertController.swift | 94 +++++++++++++++++++ SharedResources/en.lproj/Localizable.strings | 2 + 22 files changed, 653 insertions(+), 30 deletions(-) create mode 100644 CryptomatorCommon/Sources/CryptomatorCommonCore/FileProviderXPC/UploadRetrying.swift create mode 100644 CryptomatorFileProvider/Actions/FileProviderAction.swift create mode 100644 CryptomatorFileProvider/ProgressManager.swift create mode 100644 CryptomatorFileProvider/ServiceSource/UploadRetryingServiceSource.swift create mode 100644 CryptomatorFileProviderTests/InMemoryProgressManagerTests.swift create mode 100644 CryptomatorFileProviderTests/Mocks/ProgressManagerMock.swift create mode 100644 CryptomatorFileProviderTests/UploadRetryingServiceSourceTests.swift create mode 100644 FileProviderExtensionUI/UploadProgressAlertController.swift diff --git a/Cryptomator.xcodeproj/project.pbxproj b/Cryptomator.xcodeproj/project.pbxproj index 469cc2692..2af804558 100644 --- a/Cryptomator.xcodeproj/project.pbxproj +++ b/Cryptomator.xcodeproj/project.pbxproj @@ -178,6 +178,7 @@ 4A707802278DC32800AEF4CE /* VaultKeepUnlockedViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A707801278DC32800AEF4CE /* VaultKeepUnlockedViewModel.swift */; }; 4A707804278DC37F00AEF4CE /* VaultKeepUnlockedViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A707803278DC37F00AEF4CE /* VaultKeepUnlockedViewModelTests.swift */; }; 4A717CD924C835740048E08F /* ReparentTaskManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A717CD824C835740048E08F /* ReparentTaskManagerTests.swift */; }; + 4A74DBB1282132EC00A332C4 /* FileProviderAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A74DBB0282132EC00A332C4 /* FileProviderAction.swift */; }; 4A753DB92678A226005F79C1 /* OpenExistingLegacyVaultPasswordViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A753DB82678A226005F79C1 /* OpenExistingLegacyVaultPasswordViewModel.swift */; }; 4A797F8F24AC6731007DDBE1 /* FileProviderItemTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A797F8E24AC6731007DDBE1 /* FileProviderItemTests.swift */; }; 4A797F9624AC9936007DDBE1 /* CustomCloudProviderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A797F9524AC9936007DDBE1 /* CustomCloudProviderMock.swift */; }; @@ -221,6 +222,7 @@ 4AA22BFB261CA69F00A17486 /* WebDAVAuthenticationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA22BFA261CA69F00A17486 /* WebDAVAuthenticationViewController.swift */; }; 4AA22C16261CA8D800A17486 /* URLFieldCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA22C15261CA8D800A17486 /* URLFieldCell.swift */; }; 4AA22C1E261CA94700A17486 /* UsernameFieldCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA22C1D261CA94700A17486 /* UsernameFieldCell.swift */; }; + 4AA2531928216BFD003B45EE /* UploadRetryingServiceSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA2531828216BFD003B45EE /* UploadRetryingServiceSource.swift */; }; 4AA2531B28216E45003B45EE /* ServiceSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA2531A28216E45003B45EE /* ServiceSource.swift */; }; 4AA621D9249A6A8400A0BCBD /* FileProviderExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA621D8249A6A8400A0BCBD /* FileProviderExtension.swift */; }; 4AA8613725C19D4F002A59F5 /* DetectedMasterkeyViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA8613625C19D4F002A59F5 /* DetectedMasterkeyViewModel.swift */; }; @@ -290,6 +292,7 @@ 4AEE468F25263B2E0045DA9F /* FileProviderExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 4AA621D6249A6A8400A0BCBD /* FileProviderExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 4AEE469225263B2E0045DA9F /* FileProviderExtensionUI.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 4AA621E4249A6A8400A0BCBD /* FileProviderExtensionUI.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 4AEE6EE12822A33400E1B35E /* NSFileProviderItemIdentifier+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AEE6EE02822A33400E1B35E /* NSFileProviderItemIdentifier+Database.swift */; }; + 4AEE6EEA2825716400E1B35E /* ProgressManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AEE6EE92825716400E1B35E /* ProgressManager.swift */; }; 4AEECD2F279EA27300C6E2B5 /* FileProviderAdapterSetTagDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AEECD2E279EA27300C6E2B5 /* FileProviderAdapterSetTagDataTests.swift */; }; 4AEECD31279EA50D00C6E2B5 /* WorkingSetObservingMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AEECD30279EA50D00C6E2B5 /* WorkingSetObservingMock.swift */; }; 4AEECD33279EAACA00C6E2B5 /* FileProviderAdapterSetFavoriteRankTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AEECD32279EAACA00C6E2B5 /* FileProviderAdapterSetFavoriteRankTests.swift */; }; @@ -316,6 +319,10 @@ 4AF91CEB25A7306E00ACF01E /* DatabaseManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AF91CEA25A7306E00ACF01E /* DatabaseManagerTests.swift */; }; 4AF91CF425A8BB0D00ACF01E /* VaultListViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AF91CF325A8BB0D00ACF01E /* VaultListViewModelTests.swift */; }; 4AF91D0D25A8D5EF00ACF01E /* ListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AF91D0C25A8D5EF00ACF01E /* ListViewModel.swift */; }; + 4AFBFA142829206D00E30818 /* UploadProgressAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AFBFA132829206D00E30818 /* UploadProgressAlertController.swift */; }; + 4AFBFA1628293FE200E30818 /* UploadRetryingServiceSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AFBFA1528293FE200E30818 /* UploadRetryingServiceSourceTests.swift */; }; + 4AFBFA182829414A00E30818 /* ProgressManagerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AFBFA172829414A00E30818 /* ProgressManagerMock.swift */; }; + 4AFBFA1A282946BF00E30818 /* InMemoryProgressManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AFBFA19282946BF00E30818 /* InMemoryProgressManagerTests.swift */; }; 4AFCE4CB25B8419D0069C4FC /* FolderChoosing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AFCE4CA25B8419D0069C4FC /* FolderChoosing.swift */; }; 4AFCE4D425B842830069C4FC /* AccountListing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AFCE4D325B842830069C4FC /* AccountListing.swift */; }; 4AFCE4DD25B8514F0069C4FC /* EditableTableViewHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AFCE4DC25B8514F0069C4FC /* EditableTableViewHeader.swift */; }; @@ -657,6 +664,7 @@ 4A707801278DC32800AEF4CE /* VaultKeepUnlockedViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaultKeepUnlockedViewModel.swift; sourceTree = ""; }; 4A707803278DC37F00AEF4CE /* VaultKeepUnlockedViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaultKeepUnlockedViewModelTests.swift; sourceTree = ""; }; 4A717CD824C835740048E08F /* ReparentTaskManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReparentTaskManagerTests.swift; sourceTree = ""; }; + 4A74DBB0282132EC00A332C4 /* FileProviderAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileProviderAction.swift; sourceTree = ""; }; 4A753DB82678A226005F79C1 /* OpenExistingLegacyVaultPasswordViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenExistingLegacyVaultPasswordViewModel.swift; sourceTree = ""; }; 4A797F8E24AC6731007DDBE1 /* FileProviderItemTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileProviderItemTests.swift; sourceTree = ""; }; 4A797F9524AC9936007DDBE1 /* CustomCloudProviderMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomCloudProviderMock.swift; sourceTree = ""; }; @@ -699,6 +707,7 @@ 4AA22BFA261CA69F00A17486 /* WebDAVAuthenticationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebDAVAuthenticationViewController.swift; sourceTree = ""; }; 4AA22C15261CA8D800A17486 /* URLFieldCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLFieldCell.swift; sourceTree = ""; }; 4AA22C1D261CA94700A17486 /* UsernameFieldCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UsernameFieldCell.swift; sourceTree = ""; }; + 4AA2531828216BFD003B45EE /* UploadRetryingServiceSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadRetryingServiceSource.swift; sourceTree = ""; }; 4AA2531A28216E45003B45EE /* ServiceSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceSource.swift; sourceTree = ""; }; 4AA621D6249A6A8400A0BCBD /* FileProviderExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = FileProviderExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 4AA621D8249A6A8400A0BCBD /* FileProviderExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileProviderExtension.swift; sourceTree = ""; }; @@ -777,6 +786,7 @@ 4AEBE8BD2653F4280031487F /* UploadTaskExecutor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadTaskExecutor.swift; sourceTree = ""; }; 4AEBE8C12653FAD40031487F /* WorkflowMiddleware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkflowMiddleware.swift; sourceTree = ""; }; 4AEE6EE02822A33400E1B35E /* NSFileProviderItemIdentifier+Database.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSFileProviderItemIdentifier+Database.swift"; sourceTree = ""; }; + 4AEE6EE92825716400E1B35E /* ProgressManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressManager.swift; sourceTree = ""; }; 4AEECD2E279EA27300C6E2B5 /* FileProviderAdapterSetTagDataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileProviderAdapterSetTagDataTests.swift; sourceTree = ""; }; 4AEECD30279EA50D00C6E2B5 /* WorkingSetObservingMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkingSetObservingMock.swift; sourceTree = ""; }; 4AEECD32279EAACA00C6E2B5 /* FileProviderAdapterSetFavoriteRankTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileProviderAdapterSetFavoriteRankTests.swift; sourceTree = ""; }; @@ -803,6 +813,10 @@ 4AF91CEA25A7306E00ACF01E /* DatabaseManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseManagerTests.swift; sourceTree = ""; }; 4AF91CF325A8BB0D00ACF01E /* VaultListViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaultListViewModelTests.swift; sourceTree = ""; }; 4AF91D0C25A8D5EF00ACF01E /* ListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListViewModel.swift; sourceTree = ""; }; + 4AFBFA132829206D00E30818 /* UploadProgressAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadProgressAlertController.swift; sourceTree = ""; }; + 4AFBFA1528293FE200E30818 /* UploadRetryingServiceSourceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadRetryingServiceSourceTests.swift; sourceTree = ""; }; + 4AFBFA172829414A00E30818 /* ProgressManagerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressManagerMock.swift; sourceTree = ""; }; + 4AFBFA19282946BF00E30818 /* InMemoryProgressManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InMemoryProgressManagerTests.swift; sourceTree = ""; }; 4AFCE4CA25B8419D0069C4FC /* FolderChoosing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FolderChoosing.swift; sourceTree = ""; }; 4AFCE4D325B842830069C4FC /* AccountListing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountListing.swift; sourceTree = ""; }; 4AFCE4DC25B8514F0069C4FC /* EditableTableViewHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditableTableViewHeader.swift; sourceTree = ""; }; @@ -1042,10 +1056,12 @@ 4AEECD38279EB1EB00C6E2B5 /* FileProviderEnumeratorTests.swift */, 4A797F8E24AC6731007DDBE1 /* FileProviderItemTests.swift */, 4A9C8DFC27A007C2000063E4 /* FileProviderNotificatorTests.swift */, + 4AFBFA19282946BF00E30818 /* InMemoryProgressManagerTests.swift */, 4AB1D4EF27D20420009060AB /* LocalURLProviderTests.swift */, 4AEFF7F327145CB400D6CB99 /* LogLevelUpdatingServiceSourceTests.swift */, 4AC1157727F5BEFD0023F51B /* Promise+AllIgnoringResultsTests.swift */, 4ADC66C427A7F6D6002E6CC7 /* UnlockMonitorTests.swift */, + 4AFBFA1528293FE200E30818 /* UploadRetryingServiceSourceTests.swift */, 4A4F47F224B875070033328B /* URL+NameCollisionExtensionTests.swift */, 4AE5196427F48D6600BA6E4A /* WorkflowDependencyFactoryTests.swift */, 4AE5196627F495BF00BA6E4A /* WorkflowDependencyTasksCollectionMock.swift */, @@ -1250,6 +1266,14 @@ path = KeepUnlocked; sourceTree = ""; }; + 4A74DBAF2821312200A332C4 /* Actions */ = { + isa = PBXGroup; + children = ( + 4A74DBB0282132EC00A332C4 /* FileProviderAction.swift */, + ); + path = Actions; + sourceTree = ""; + }; 4A7B97CA25B6F7340044B7FB /* CloudAccountList */ = { isa = PBXGroup; children = ( @@ -1445,6 +1469,7 @@ children = ( 4AEFF7F127145ADD00D6CB99 /* LogLevelUpdatingServiceSource.swift */, 4AA2531A28216E45003B45EE /* ServiceSource.swift */, + 4AA2531828216BFD003B45EE /* UploadRetryingServiceSource.swift */, 4A24001926AE9F3A009DBC2E /* VaultLockingServiceSource.swift */, 4A9BED63268F1DB000721BAA /* VaultUnlockingServiceSource.swift */, ); @@ -1476,6 +1501,7 @@ 4A6A520C268B5EF7006F7368 /* RootViewController.swift */, 4A9BED65268F2D9C00721BAA /* UnlockVaultViewController.swift */, 4AFD8C0E269304A700F77BA6 /* UnlockVaultViewModel.swift */, + 4AFBFA132829206D00E30818 /* UploadProgressAlertController.swift */, 4A804083276952C600D7D999 /* Snapshots */, ); path = FileProviderExtensionUI; @@ -1538,6 +1564,7 @@ 4ADC66C627A95E67002E6CC7 /* UnlockMonitorTaskExecutorMock.swift */, 4AB1D4F127D20510009060AB /* DocumentStorageURLProviderMock.swift */, 4AAD444627E26D1800D16707 /* UploadTaskManagerMock.swift */, + 4AFBFA172829414A00E30818 /* ProgressManagerMock.swift */, ); path = Mocks; sourceTree = ""; @@ -1671,10 +1698,12 @@ 740375F42587AEB50023FF53 /* ItemStatus.swift */, 4AB1D4EB27D0E027009060AB /* LocalURLProviderType.swift */, 4AEE6EE02822A33400E1B35E /* NSFileProviderItemIdentifier+Database.swift */, + 4AEE6EE92825716400E1B35E /* ProgressManager.swift */, 4AC1157527F5BD890023F51B /* Promise+AllIgnoringResult.swift */, 4ADD233F26737CD400374E4E /* RootFileProviderItem.swift */, 4ADC66C027A7F426002E6CC7 /* UnlockMonitor.swift */, 740375F52587AEB50023FF53 /* URL+NameCollisionExtension.swift */, + 4A74DBAF2821312200A332C4 /* Actions */, 4AE0D8D82653D8F200DF5D22 /* CloudTask */, 740376022587AEB60023FF53 /* DB */, 740375FD2587AEB60023FF53 /* Locks */, @@ -2232,7 +2261,9 @@ 4ADC66C727A95E67002E6CC7 /* UnlockMonitorTaskExecutorMock.swift in Sources */, 4A797F9824AC9A1B007DDBE1 /* CustomCloudProviderMockTests.swift in Sources */, 4AAD444727E26D1800D16707 /* UploadTaskManagerMock.swift in Sources */, + 4AFBFA1A282946BF00E30818 /* InMemoryProgressManagerTests.swift in Sources */, 4A079FB928084134009AD932 /* WorkingSetEnumerationTests.swift in Sources */, + 4AFBFA1628293FE200E30818 /* UploadRetryingServiceSourceTests.swift in Sources */, 4AB1C33C265E9DBC00DC7A49 /* CloudTaskExecutorTestCase.swift in Sources */, 4AE5196727F495BF00BA6E4A /* WorkflowDependencyTasksCollectionMock.swift in Sources */, 4AC1157827F5BEFD0023F51B /* Promise+AllIgnoringResultsTests.swift in Sources */, @@ -2279,6 +2310,7 @@ 4A8F14A2266A302A00ADBCE4 /* FileProviderAdapterTestCase.swift in Sources */, 4AB1D4F027D20420009060AB /* LocalURLProviderTests.swift in Sources */, 4A3E2FEC271DC9670090BD44 /* MaintenanceManagerTests.swift in Sources */, + 4AFBFA182829414A00E30818 /* ProgressManagerMock.swift in Sources */, 4A717CD924C835740048E08F /* ReparentTaskManagerTests.swift in Sources */, 4A9C8E0327A016CF000063E4 /* WorkingSetObserverTests.swift in Sources */, 4AB1C33A265E9D8600DC7A49 /* UploadTaskExecutorTests.swift in Sources */, @@ -2322,6 +2354,7 @@ buildActionMask = 2147483647; files = ( 4A6A520D268B5EF7006F7368 /* RootViewController.swift in Sources */, + 4AFBFA142829206D00E30818 /* UploadProgressAlertController.swift in Sources */, 4A804082276952C300D7D999 /* FileProviderCoordinatorSnapshotMock.swift in Sources */, 4A9BED66268F2D9D00721BAA /* UnlockVaultViewController.swift in Sources */, 4A6A521B268B7147006F7368 /* FileProviderCoordinator.swift in Sources */, @@ -2554,6 +2587,7 @@ 747F2F3A2587BC4B0072FB30 /* LockManager.swift in Sources */, 4AEE6EE12822A33400E1B35E /* NSFileProviderItemIdentifier+Database.swift in Sources */, 4ADD233C267219E200374E4E /* LocalCachedFileInfo.swift in Sources */, + 4AA2531928216BFD003B45EE /* UploadRetryingServiceSource.swift in Sources */, 747F2F3B2587BC4B0072FB30 /* FileSystemLock.swift in Sources */, 4A231B80271EF2CA00987492 /* DownloadTaskRecord.swift in Sources */, 4A511D592664F290000A0E01 /* DeleteItemHelper.swift in Sources */, @@ -2610,7 +2644,9 @@ 4AE5196927F4A24D00BA6E4A /* WorkflowDependencyFactory.swift in Sources */, 4A3E2FEE271DCA160090BD44 /* MaintenanceDBManager.swift in Sources */, 4AEBE8BC2653F2FD0031487F /* ReparentTaskExecutor.swift in Sources */, + 4AEE6EEA2825716400E1B35E /* ProgressManager.swift in Sources */, 747F2F2C2587BC260072FB30 /* DeletionTaskDBManager.swift in Sources */, + 4A74DBB1282132EC00A332C4 /* FileProviderAction.swift in Sources */, 747F2F2D2587BC260072FB30 /* FileProviderItemList.swift in Sources */, 747F2F2E2587BC260072FB30 /* FileProviderItem.swift in Sources */, 4A8F149E266A2A8200ADBCE4 /* FileProviderAdapter.swift in Sources */, diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/FileProviderXPC/UploadRetrying.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/FileProviderXPC/UploadRetrying.swift new file mode 100644 index 000000000..2b6e3eb60 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/FileProviderXPC/UploadRetrying.swift @@ -0,0 +1,23 @@ +// +// UploadRetrying.swift +// CryptomatorCommonCore +// +// Created by Philipp Schmid on 03.05.22. +// Copyright © 2021 Skymatic GmbH. All rights reserved. +// + +import FileProvider +import Foundation + +@objc public protocol UploadRetrying: NSFileProviderServiceSource { + /** + Retries the upload for the given item identifiers. + */ + func retryUpload(for itemIdentifiers: [NSFileProviderItemIdentifier], reply: @escaping (Error?) -> Void) + + func getCurrentFractionalUploadProgress(for itemIdentifier: NSFileProviderItemIdentifier, reply: @escaping (NSNumber?) -> Void) +} + +public extension NSFileProviderServiceName { + static let uploadRetryingService = NSFileProviderServiceName("org.cryptomator.ios.upload-retrying") +} diff --git a/CryptomatorFileProvider/Actions/FileProviderAction.swift b/CryptomatorFileProvider/Actions/FileProviderAction.swift new file mode 100644 index 000000000..bea6e1772 --- /dev/null +++ b/CryptomatorFileProvider/Actions/FileProviderAction.swift @@ -0,0 +1,14 @@ +// +// FileProviderAction.swift +// CryptomatorFileProvider +// +// Created by Philipp Schmid on 03.05.22. +// Copyright © 2022 Skymatic GmbH. All rights reserved. +// + +import Foundation + +public enum FileProviderAction: String { + case retryUpload = "org.cryptomator.ios.fileprovider.retryUpload" + case retryWaitingUpload = "org.cryptomator.ios.fileprovider.retryWaitingUpload" +} diff --git a/CryptomatorFileProvider/DB/UploadTaskDBManager.swift b/CryptomatorFileProvider/DB/UploadTaskDBManager.swift index 27f43aac0..8d1a97149 100644 --- a/CryptomatorFileProvider/DB/UploadTaskDBManager.swift +++ b/CryptomatorFileProvider/DB/UploadTaskDBManager.swift @@ -44,6 +44,13 @@ extension UploadTaskManager { } try removeTaskRecord(for: id) } + + func updateTaskRecord(for itemMetadata: ItemMetadata, with error: NSError) throws { + guard let id = itemMetadata.id else { + throw DBManagerError.nonSavedItemMetadata + } + try updateTaskRecord(with: id, lastFailedUploadDate: Date(), uploadErrorCode: error.code, uploadErrorDomain: error.domain) + } } class UploadTaskDBManager: UploadTaskManager { diff --git a/CryptomatorFileProvider/FileProviderAdapter.swift b/CryptomatorFileProvider/FileProviderAdapter.swift index bc7a580b8..b1001326b 100644 --- a/CryptomatorFileProvider/FileProviderAdapter.swift +++ b/CryptomatorFileProvider/FileProviderAdapter.swift @@ -27,6 +27,7 @@ public protocol FileProviderAdapterType: AnyObject { func startProvidingItem(at url: URL, completionHandler: @escaping ((_ error: Error?) -> Void)) func setFavoriteRank(_ favoriteRank: NSNumber?, forItemIdentifier itemIdentifier: NSFileProviderItemIdentifier, completionHandler: @escaping (NSFileProviderItem?, Error?) -> Void) func setTagData(_ tagData: Data?, forItemIdentifier itemIdentifier: NSFileProviderItemIdentifier, completionHandler: @escaping (NSFileProviderItem?, Error?) -> Void) + func retryUpload(for itemIdentifier: NSFileProviderItemIdentifier) } public class FileProviderAdapter: FileProviderAdapterType { @@ -261,7 +262,7 @@ public class FileProviderAdapter: FileProviderAdapterType { let itemMetadata = try getCachedMetadata(for: itemIdentifier) uploadTaskRecord = try registerFileInUploadQueue(with: url, itemMetadata: itemMetadata) } catch { - DDLogError("itemChanged - failed to register file in upload queue with url: \(url) and identifier: \(itemIdentifier)") + DDLogError("itemChanged - register file in upload queue with url: \(url) and identifier: \(itemIdentifier) failed with error: \(error)") return } uploadFile(taskRecord: uploadTaskRecord).then { item in @@ -269,6 +270,31 @@ public class FileProviderAdapter: FileProviderAdapterType { } } + public func retryUpload(for itemIdentifier: NSFileProviderItemIdentifier) { + let uploadTaskRecord: UploadTaskRecord + let itemMetadata: ItemMetadata + let localCachedFileInfo: LocalCachedFileInfo + do { + itemMetadata = try getCachedMetadata(for: itemIdentifier) + guard let retrievedLocalCachedFileInfo = try cachedFileManager.getLocalCachedFileInfo(for: itemMetadata) else { + DDLogError("retryUpload - retrievedLocalCachedFileInfo is nil for identifier: \(itemIdentifier)") + return + } + localCachedFileInfo = retrievedLocalCachedFileInfo + uploadTaskRecord = try registerFileInUploadQueue(with: localCachedFileInfo.localURL, itemMetadata: itemMetadata) + } catch { + DDLogError("retryUpload - get existing uploadTaskRecord for identifier: \(itemIdentifier) failed with error: \(error)") + return + } + let newestVersionLocallyCached = localCachedFileInfo.isCurrentVersion(lastModifiedDateInCloud: itemMetadata.lastModifiedDate) + let localURL = localCachedFileInfo.localURL + let item = FileProviderItem(metadata: itemMetadata, domainIdentifier: domainIdentifier, newestVersionLocallyCached: newestVersionLocallyCached, localURL: localURL, error: nil) + notificator?.signalUpdate(for: item) + uploadFile(taskRecord: uploadTaskRecord).then { item in + self.notificator?.signalUpdate(for: item) + } + } + func uploadFile(taskRecord: UploadTaskRecord, completionHandler: ((Error?) -> Void)? = nil) -> Promise { let task: UploadTask do { @@ -593,7 +619,7 @@ public class FileProviderAdapter: FileProviderAdapterType { } catch { return Promise(error) } - if itemMetadata.statusCode == .isUploading { + if itemMetadata.statusCode == .isUploading || itemMetadata.statusCode == .uploadError { return Promise(true) } return enumerateItems(for: identifier, withPageToken: nil).then { itemList -> Bool in diff --git a/CryptomatorFileProvider/FileProviderItem.swift b/CryptomatorFileProvider/FileProviderItem.swift index f31be9d33..d2e190b10 100644 --- a/CryptomatorFileProvider/FileProviderItem.swift +++ b/CryptomatorFileProvider/FileProviderItem.swift @@ -156,4 +156,10 @@ public class FileProviderItem: NSObject, NSFileProviderItem { public var tagData: Data? { return metadata.tagData } + + /// Workaround to access the `isUploading` and `uploadingError` property in the `NSExtensionFileProviderActionActivationRule` + public var userInfo: [AnyHashable: Any]? { + return ["isUploading": isUploading, + "hasUploadError": uploadingError != nil] + } } diff --git a/CryptomatorFileProvider/LocalURLProviderType.swift b/CryptomatorFileProvider/LocalURLProviderType.swift index ef2bf6506..acce72f6b 100644 --- a/CryptomatorFileProvider/LocalURLProviderType.swift +++ b/CryptomatorFileProvider/LocalURLProviderType.swift @@ -75,7 +75,11 @@ public class LocalURLProvider: LocalURLProviderType { if identifier == .rootContainer { return baseStorageDirectoryURL } - return baseStorageDirectoryURL?.appendingPathComponent(identifier.rawValue, isDirectory: true) + if let itemID = identifier.databaseValue { + return baseStorageDirectoryURL?.appendingPathComponent(String(itemID), isDirectory: true) + } else { + return baseStorageDirectoryURL?.appendingPathComponent(identifier.rawValue, isDirectory: true) + } } private func getBaseStorageDirectory() -> URL? { diff --git a/CryptomatorFileProvider/Middleware/ErrorMapper.swift b/CryptomatorFileProvider/Middleware/ErrorMapper.swift index bcacd1389..3b9fbe7f1 100644 --- a/CryptomatorFileProvider/Middleware/ErrorMapper.swift +++ b/CryptomatorFileProvider/Middleware/ErrorMapper.swift @@ -38,13 +38,19 @@ class ErrorMapper: WorkflowMiddleware { } private func mapError(_ error: Error) -> Error { - guard let cloudProviderError = error as? CloudProviderError else { - return error + return error.toPresentableError() + } +} + +extension Error { + func toPresentableError() -> Error { + guard let cloudProviderError = self as? CloudProviderError else { + return self } switch cloudProviderError { case .itemNotFound, .parentFolderDoesNotExist: return NSFileProviderError(.noSuchItem) - case .itemAlreadyExists: + case .itemAlreadyExists, .itemTypeMismatch: return NSFileProviderError(.filenameCollision) case .pageTokenInvalid: return NSFileProviderError(.syncAnchorExpired) @@ -54,8 +60,6 @@ class ErrorMapper: WorkflowMiddleware { return NSFileProviderError(.notAuthenticated) case .noInternetConnection: return NSFileProviderError(.serverUnreachable) - default: - return cloudProviderError } } } diff --git a/CryptomatorFileProvider/Middleware/TaskExecutor/UploadTaskExecutor.swift b/CryptomatorFileProvider/Middleware/TaskExecutor/UploadTaskExecutor.swift index 3899af6e3..169d90644 100644 --- a/CryptomatorFileProvider/Middleware/TaskExecutor/UploadTaskExecutor.swift +++ b/CryptomatorFileProvider/Middleware/TaskExecutor/UploadTaskExecutor.swift @@ -31,13 +31,15 @@ class UploadTaskExecutor: WorkflowMiddleware { let itemMetadataManager: ItemMetadataManager let uploadTaskManager: UploadTaskManager let domainIdentifier: NSFileProviderDomainIdentifier + let progressManager: ProgressManager - init(domainIdentifier: NSFileProviderDomainIdentifier, provider: CloudProvider, cachedFileManager: CachedFileManager, itemMetadataManager: ItemMetadataManager, uploadTaskManager: UploadTaskManager) { + init(domainIdentifier: NSFileProviderDomainIdentifier, provider: CloudProvider, cachedFileManager: CachedFileManager, itemMetadataManager: ItemMetadataManager, uploadTaskManager: UploadTaskManager, progressManager: ProgressManager = InMemoryProgressManager.shared) { self.domainIdentifier = domainIdentifier self.provider = provider self.cachedFileManager = cachedFileManager self.itemMetadataManager = itemMetadataManager self.uploadTaskManager = uploadTaskManager + self.progressManager = progressManager } func execute(task: CloudTask) -> Promise { @@ -63,8 +65,17 @@ class UploadTaskExecutor: WorkflowMiddleware { return Promise(error) } - return provider.uploadFile(from: localURL, to: itemMetadata.cloudPath, replaceExisting: !itemMetadata.isPlaceholderItem).then { cloudItemMetadata in + let progress = Progress(totalUnitCount: 1) + if let itemID = itemMetadata.id { + progressManager.saveProgress(progress, for: NSFileProviderItemIdentifier(domainIdentifier: domainIdentifier, itemID: itemID)) + } + progress.becomeCurrent(withPendingUnitCount: 1) + let uploadPromise = provider.uploadFile(from: localURL, to: itemMetadata.cloudPath, replaceExisting: !itemMetadata.isPlaceholderItem) + progress.resignCurrent() + return uploadPromise.then { cloudItemMetadata in try self.uploadPostProcessing(taskItemMetadata: itemMetadata, cloudItemMetadata: cloudItemMetadata, localURL: localURL, localFileSizeBeforeUpload: localFileSize) + }.recover { error -> FileProviderItem in + try self.handleUploadError(error, taskItemMetadata: itemMetadata) } } @@ -97,4 +108,15 @@ class UploadTaskExecutor: WorkflowMiddleware { return FileProviderItem(metadata: taskItemMetadata, domainIdentifier: domainIdentifier) } } + + func handleUploadError(_ error: Error, taskItemMetadata: ItemMetadata) throws -> FileProviderItem { + let convertedError = error.toPresentableError() + try uploadTaskManager.updateTaskRecord(for: taskItemMetadata, with: convertedError as NSError) + taskItemMetadata.statusCode = .uploadError + try itemMetadataManager.updateMetadata(taskItemMetadata) + let localCachedFileInfo = try cachedFileManager.getLocalCachedFileInfo(for: taskItemMetadata) + let newestVersionLocallyCached = localCachedFileInfo?.isCurrentVersion(lastModifiedDateInCloud: taskItemMetadata.lastModifiedDate) ?? false + let localURL = localCachedFileInfo?.localURL + return FileProviderItem(metadata: taskItemMetadata, domainIdentifier: domainIdentifier, newestVersionLocallyCached: newestVersionLocallyCached, localURL: localURL, error: convertedError) + } } diff --git a/CryptomatorFileProvider/ProgressManager.swift b/CryptomatorFileProvider/ProgressManager.swift new file mode 100644 index 000000000..87ad360b4 --- /dev/null +++ b/CryptomatorFileProvider/ProgressManager.swift @@ -0,0 +1,43 @@ +// +// ProgressManager.swift +// CryptomatorFileProvider +// +// Created by Philipp Schmid on 06.05.22. +// Copyright © 2022 Skymatic GmbH. All rights reserved. +// + +import FileProvider +import Foundation + +protocol ProgressManager { + /** + Returns the progress for the given `itemIdentifier` - `nil` if no progress could be found for the given `itemIdentifier`. + */ + func getProgress(for itemIdentifier: NSFileProviderItemIdentifier) -> Progress? + + /** + Saves the progress for the given `itemIdentifier`. + + - Note: If a progress object already exists for the given `itemIdentifier`, it will be replaced. + */ + func saveProgress(_ progress: Progress, for itemIdentifier: NSFileProviderItemIdentifier) +} + +class InMemoryProgressManager: ProgressManager { + static let shared = InMemoryProgressManager() + + private let queue = DispatchQueue(label: "InMemoryProgressManager", attributes: .concurrent) + private lazy var progressDictionary = [NSFileProviderItemIdentifier: Progress]() + + func getProgress(for itemIdentifier: NSFileProviderItemIdentifier) -> Progress? { + return queue.sync { + progressDictionary[itemIdentifier] + } + } + + func saveProgress(_ progress: Progress, for itemIdentifier: NSFileProviderItemIdentifier) { + queue.async(flags: .barrier) { + self.progressDictionary[itemIdentifier] = progress + } + } +} diff --git a/CryptomatorFileProvider/ServiceSource/UploadRetryingServiceSource.swift b/CryptomatorFileProvider/ServiceSource/UploadRetryingServiceSource.swift new file mode 100644 index 000000000..b1b76df1d --- /dev/null +++ b/CryptomatorFileProvider/ServiceSource/UploadRetryingServiceSource.swift @@ -0,0 +1,60 @@ +// +// UploadRetryingServiceSource.swift +// CryptomatorFileProvider +// +// Created by Philipp Schmid on 03.05.22. +// Copyright © 2022 Skymatic GmbH. All rights reserved. +// + +import CryptomatorCommonCore +import FileProvider +import Foundation + +public class UploadRetryingServiceSource: ServiceSource, UploadRetrying { + private let adapterManager: FileProviderAdapterProviding + private let domain: NSFileProviderDomain + private let notificator: FileProviderNotificatorType + private let dbPath: URL + private let localURLProvider: LocalURLProviderType + private let progressManager: ProgressManager + + public convenience init(domain: NSFileProviderDomain, notificator: FileProviderNotificatorType, dbPath: URL, delegate: LocalURLProviderType) { + self.init(domain: domain, + notificator: notificator, + dbPath: dbPath, + delegate: delegate, + adapterManager: FileProviderAdapterManager.shared) + } + + init(domain: NSFileProviderDomain, notificator: FileProviderNotificatorType, dbPath: URL, delegate: LocalURLProviderType, adapterManager: FileProviderAdapterProviding = FileProviderAdapterManager.shared, progressManager: ProgressManager = InMemoryProgressManager.shared) { + self.domain = domain + self.notificator = notificator + self.dbPath = dbPath + self.localURLProvider = delegate + self.adapterManager = adapterManager + self.progressManager = progressManager + super.init(serviceName: .uploadRetryingService, exportedInterface: NSXPCInterface(with: UploadRetrying.self)) + } + + public func retryUpload(for itemIdentifiers: [NSFileProviderItemIdentifier], reply: @escaping (Error?) -> Void) { + let adapter: FileProviderAdapterType + do { + adapter = try adapterManager.getAdapter(forDomain: domain, + dbPath: dbPath, + delegate: localURLProvider, + notificator: notificator) + } catch { + reply(error) + return + } + for itemIdentifier in itemIdentifiers { + adapter.retryUpload(for: itemIdentifier) + } + reply(nil) + } + + public func getCurrentFractionalUploadProgress(for itemIdentifier: NSFileProviderItemIdentifier, reply: @escaping (NSNumber?) -> Void) { + let progress = progressManager.getProgress(for: itemIdentifier) + reply(progress?.fractionCompleted as NSNumber?) + } +} diff --git a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterEnumerateItemTests.swift b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterEnumerateItemTests.swift index 467e186e6..a7882b36f 100644 --- a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterEnumerateItemTests.swift +++ b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterEnumerateItemTests.swift @@ -18,6 +18,14 @@ class FileProviderAdapterEnumerateItemTests: FileProviderAdapterTestCase { } } + // MARK: Error Handling + + func testEnumerateItemsFailedWithNoInternetConnection() throws { + let metadata = ItemMetadata(id: 2, name: "noInternetConnection", type: .folder, size: nil, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/noInternetConnection"), isPlaceholderItem: false, isCandidateForCacheCleanup: false, favoriteRank: nil, tagData: Data()) + try metadataManagerMock.cacheMetadata(metadata) + XCTAssertRejects(adapter.enumerateItems(for: NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: 2), withPageToken: nil), with: NSFileProviderError(.serverUnreachable)) + } + // MARK: Enumerate Working Set func testWorkingSet() { diff --git a/CryptomatorFileProviderTests/InMemoryProgressManagerTests.swift b/CryptomatorFileProviderTests/InMemoryProgressManagerTests.swift new file mode 100644 index 000000000..742710b2b --- /dev/null +++ b/CryptomatorFileProviderTests/InMemoryProgressManagerTests.swift @@ -0,0 +1,46 @@ +// +// InMemoryProgressManagerTests.swift +// CryptomatorFileProviderTests +// +// Created by Philipp Schmid on 09.05.22. +// Copyright © 2022 Skymatic GmbH. All rights reserved. +// + +import XCTest +@testable import CryptomatorFileProvider + +class InMemoryProgressManagerTests: XCTestCase { + var progressManager: InMemoryProgressManager! + + override func setUpWithError() throws { + progressManager = InMemoryProgressManager() + } + + func testSaveProgress() { + let progress = Progress(totalUnitCount: 10) + let firstItemIdentifier = NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: 2) + progressManager.saveProgress(progress, for: firstItemIdentifier) + + let secondProgress = Progress(totalUnitCount: 20) + let secondItemIdentifier = NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: 3) + progressManager.saveProgress(secondProgress, for: secondItemIdentifier) + + XCTAssertEqual(progress, progressManager.getProgress(for: firstItemIdentifier)) + XCTAssertEqual(secondProgress, progressManager.getProgress(for: secondItemIdentifier)) + } + + func testSaveProgressOverwritesExisting() { + let progress = Progress(totalUnitCount: 10) + let itemIdentifier = NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: 2) + progressManager.saveProgress(progress, for: itemIdentifier) + + let secondProgress = Progress(totalUnitCount: 20) + progressManager.saveProgress(secondProgress, for: itemIdentifier) + XCTAssertEqual(secondProgress, progressManager.getProgress(for: itemIdentifier)) + } + + func testGetMissingProgress() { + let itemIdentifier = NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: 2) + XCTAssertNil(progressManager.getProgress(for: itemIdentifier)) + } +} diff --git a/CryptomatorFileProviderTests/Middleware/TaskExecutor/UploadTaskExecutorTests.swift b/CryptomatorFileProviderTests/Middleware/TaskExecutor/UploadTaskExecutorTests.swift index cf6487543..dcb3a2a6e 100644 --- a/CryptomatorFileProviderTests/Middleware/TaskExecutor/UploadTaskExecutorTests.swift +++ b/CryptomatorFileProviderTests/Middleware/TaskExecutor/UploadTaskExecutorTests.swift @@ -7,9 +7,9 @@ // import CryptomatorCloudAccessCore -import Promises import XCTest @testable import CryptomatorFileProvider +@testable import Promises class UploadTaskExecutorTests: CloudTaskExecutorTestCase { func testUploadFile() throws { @@ -18,10 +18,11 @@ class UploadTaskExecutorTests: CloudTaskExecutorTestCase { let localURL = tmpDirectory.appendingPathComponent("FileToBeUploaded", isDirectory: false) try "TestContent".write(to: localURL, atomically: true, encoding: .utf8) let cloudPath = CloudPath("/FileToBeUploaded") + let progressManagerMock = ProgressManagerMock() let itemMetadata = ItemMetadata(id: itemID, name: "FileToBeUploaded", type: .file, size: nil, parentID: metadataManagerMock.getRootContainerID(), lastModifiedDate: nil, statusCode: .isUploading, cloudPath: cloudPath, isPlaceholderItem: true, isCandidateForCacheCleanup: false) cachedFileManagerMock.cachedLocalFileInfo[itemID] = LocalCachedFileInfo(lastModifiedDate: nil, correspondingItem: itemID, localLastModifiedDate: Date(), localURL: localURL) - let uploadTaskExecutor = UploadTaskExecutor(domainIdentifier: .test, provider: cloudProviderMock, cachedFileManager: cachedFileManagerMock, itemMetadataManager: metadataManagerMock, uploadTaskManager: uploadTaskManagerMock) + let uploadTaskExecutor = UploadTaskExecutor(domainIdentifier: .test, provider: cloudProviderMock, cachedFileManager: cachedFileManagerMock, itemMetadataManager: metadataManagerMock, uploadTaskManager: uploadTaskManagerMock, progressManager: progressManagerMock) let mockedCloudDate = Date(timeIntervalSinceReferenceDate: 0) cloudProviderMock.lastModifiedDate[itemMetadata.cloudPath.path] = mockedCloudDate @@ -42,6 +43,10 @@ class UploadTaskExecutorTests: CloudTaskExecutorTestCase { // Verify that the upload task has been removed XCTAssertEqual([itemMetadata.id], self.uploadTaskManagerMock.removeTaskRecordForReceivedInvocations) + + // Verify that the corresponding upload progress has been saved + XCTAssertEqual(NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: itemID), progressManagerMock.saveProgressForReceivedArguments?.itemIdentifier) + XCTAssertEqual(1, progressManagerMock.saveProgressForCallsCount) } .catch { error in XCTFail("Promise failed with error: \(error)") @@ -124,9 +129,7 @@ class UploadTaskExecutorTests: CloudTaskExecutorTestCase { wait(for: [expectation], timeout: 1.0) } - func testUploadFileFailWithSameErrorAsProvider() throws { - let expectation = XCTestExpectation() - + func testUploadFileFailReportsUploadError() throws { let localURL = tmpDirectory.appendingPathComponent("itemNotFound.txt", isDirectory: false) try "".write(to: localURL, atomically: true, encoding: .utf8) let cloudPath = CloudPath("/itemNotFound.txt") @@ -135,7 +138,7 @@ class UploadTaskExecutorTests: CloudTaskExecutorTestCase { let errorCloudProviderMock = CloudProviderErrorMock() errorCloudProviderMock.uploadFileResponse = { _, _, _ in - Promise(CloudTaskTestError.correctPassthrough) + Promise(CloudProviderError.noInternetConnection) } let uploadTaskExecutor = UploadTaskExecutor(domainIdentifier: .test, provider: errorCloudProviderMock, cachedFileManager: cachedFileManagerMock, itemMetadataManager: metadataManagerMock, uploadTaskManager: uploadTaskManagerMock) @@ -143,19 +146,20 @@ class UploadTaskExecutorTests: CloudTaskExecutorTestCase { let uploadTaskRecord = UploadTaskRecord(correspondingItem: itemMetadata.id!, lastFailedUploadDate: nil, uploadErrorCode: nil, uploadErrorDomain: nil) let uploadTask = UploadTask(taskRecord: uploadTaskRecord, itemMetadata: itemMetadata) - uploadTaskExecutor.execute(task: uploadTask).then { _ in - XCTFail("Promise should not fulfill if the provider fails with an error") - }.catch { error in - guard case CloudTaskTestError.correctPassthrough = error else { - XCTFail("Promise rejected but with the wrong error: \(error)") - return - } - XCTAssert(self.metadataManagerMock.cachedMetadata.isEmpty, "Unexpected change of cached metadata.") - XCTAssertFalse(self.uploadTaskManagerMock.removeTaskRecordForCalled, "Unexpected removal of the upload task") - }.always { - expectation.fulfill() - } - wait(for: [expectation], timeout: 1.0) + let promise = uploadTaskExecutor.execute(task: uploadTask) + wait(for: promise) + let updatedItem = try XCTUnwrap(promise.value) + let expectedError = NSFileProviderError(.serverUnreachable)._nsError + XCTAssertEqual(expectedError, updatedItem.uploadingError as NSError?) + XCTAssertEqual(ItemStatus.uploadError, updatedItem.metadata.statusCode) + XCTAssertFalse(uploadTaskManagerMock.removeTaskRecordForCalled, "Unexpected removal of the upload task") + + let updatedTaskRecordReceivedArguments = uploadTaskManagerMock.updateTaskRecordWithLastFailedUploadDateUploadErrorCodeUploadErrorDomainReceivedArguments + + XCTAssertEqual(2, updatedTaskRecordReceivedArguments?.id) + XCTAssertEqual(expectedError.code, updatedTaskRecordReceivedArguments?.uploadErrorCode) + XCTAssertEqual(expectedError.domain, updatedTaskRecordReceivedArguments?.uploadErrorDomain) + XCTAssertEqual([itemMetadata], metadataManagerMock.updatedMetadata) } private class CloudProviderUploadInconsistencyMock: CustomCloudProviderMock { diff --git a/CryptomatorFileProviderTests/Mocks/FileProviderAdapterTypeMock.swift b/CryptomatorFileProviderTests/Mocks/FileProviderAdapterTypeMock.swift index d0302ee25..c896091a5 100644 --- a/CryptomatorFileProviderTests/Mocks/FileProviderAdapterTypeMock.swift +++ b/CryptomatorFileProviderTests/Mocks/FileProviderAdapterTypeMock.swift @@ -263,6 +263,24 @@ final class FileProviderAdapterTypeMock: FileProviderAdapterType { setTagDataForItemIdentifierCompletionHandlerReceivedInvocations.append((tagData: tagData, itemIdentifier: itemIdentifier, completionHandler: completionHandler)) setTagDataForItemIdentifierCompletionHandlerClosure?(tagData, itemIdentifier, completionHandler) } + + // MARK: - retryUpload + + var retryUploadForCallsCount = 0 + var retryUploadForCalled: Bool { + retryUploadForCallsCount > 0 + } + + var retryUploadForReceivedItemIdentifier: NSFileProviderItemIdentifier? + var retryUploadForReceivedInvocations: [NSFileProviderItemIdentifier] = [] + var retryUploadForClosure: ((NSFileProviderItemIdentifier) -> Void)? + + func retryUpload(for itemIdentifier: NSFileProviderItemIdentifier) { + retryUploadForCallsCount += 1 + retryUploadForReceivedItemIdentifier = itemIdentifier + retryUploadForReceivedInvocations.append(itemIdentifier) + retryUploadForClosure?(itemIdentifier) + } } // swiftlint:enable all diff --git a/CryptomatorFileProviderTests/Mocks/ProgressManagerMock.swift b/CryptomatorFileProviderTests/Mocks/ProgressManagerMock.swift new file mode 100644 index 000000000..f5bb4fb2c --- /dev/null +++ b/CryptomatorFileProviderTests/Mocks/ProgressManagerMock.swift @@ -0,0 +1,50 @@ +// +// ProgressManagerMock.swift +// CryptomatorFileProviderTests +// +// Created by Philipp Schmid on 09.05.22. +// Copyright © 2022 Skymatic GmbH. All rights reserved. +// + +import FileProvider +import Foundation +@testable import CryptomatorFileProvider + +final class ProgressManagerMock: ProgressManager { + // MARK: - getProgress + + var getProgressForCallsCount = 0 + var getProgressForCalled: Bool { + getProgressForCallsCount > 0 + } + + var getProgressForReceivedItemIdentifier: NSFileProviderItemIdentifier? + var getProgressForReceivedInvocations: [NSFileProviderItemIdentifier] = [] + var getProgressForReturnValue: Progress? + var getProgressForClosure: ((NSFileProviderItemIdentifier) -> Progress?)? + + func getProgress(for itemIdentifier: NSFileProviderItemIdentifier) -> Progress? { + getProgressForCallsCount += 1 + getProgressForReceivedItemIdentifier = itemIdentifier + getProgressForReceivedInvocations.append(itemIdentifier) + return getProgressForClosure.map({ $0(itemIdentifier) }) ?? getProgressForReturnValue + } + + // MARK: - saveProgress + + var saveProgressForCallsCount = 0 + var saveProgressForCalled: Bool { + saveProgressForCallsCount > 0 + } + + var saveProgressForReceivedArguments: (progress: Progress, itemIdentifier: NSFileProviderItemIdentifier)? + var saveProgressForReceivedInvocations: [(progress: Progress, itemIdentifier: NSFileProviderItemIdentifier)] = [] + var saveProgressForClosure: ((Progress, NSFileProviderItemIdentifier) -> Void)? + + func saveProgress(_ progress: Progress, for itemIdentifier: NSFileProviderItemIdentifier) { + saveProgressForCallsCount += 1 + saveProgressForReceivedArguments = (progress: progress, itemIdentifier: itemIdentifier) + saveProgressForReceivedInvocations.append((progress: progress, itemIdentifier: itemIdentifier)) + saveProgressForClosure?(progress, itemIdentifier) + } +} diff --git a/CryptomatorFileProviderTests/UploadRetryingServiceSourceTests.swift b/CryptomatorFileProviderTests/UploadRetryingServiceSourceTests.swift new file mode 100644 index 000000000..1809ba9fa --- /dev/null +++ b/CryptomatorFileProviderTests/UploadRetryingServiceSourceTests.swift @@ -0,0 +1,67 @@ +// +// UploadRetryingServiceSourceTests.swift +// CryptomatorFileProviderTests +// +// Created by Philipp Schmid on 09.05.22. +// Copyright © 2022 Skymatic GmbH. All rights reserved. +// + +import XCTest +@testable import CryptomatorFileProvider + +class UploadRetryingServiceSourceTests: XCTestCase { + var serviceSource: UploadRetryingServiceSource! + var adapterProvidingMock: FileProviderAdapterProvidingMock! + var urlProviderMock: LocalURLProviderMock! + var notificatorMock: FileProviderNotificatorTypeMock! + var progressManagerMock: ProgressManagerMock! + let dbPath = FileManager.default.temporaryDirectory + let testDomain = NSFileProviderDomain(identifier: .test, displayName: "Test", pathRelativeToDocumentStorage: "") + + override func setUpWithError() throws { + adapterProvidingMock = FileProviderAdapterProvidingMock() + urlProviderMock = LocalURLProviderMock() + notificatorMock = FileProviderNotificatorTypeMock() + progressManagerMock = ProgressManagerMock() + + serviceSource = UploadRetryingServiceSource(domain: testDomain, + notificator: notificatorMock, + dbPath: dbPath, + delegate: urlProviderMock, + adapterManager: adapterProvidingMock, + progressManager: progressManagerMock) + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testRetryUpload() throws { + let adapterMock = FileProviderAdapterTypeMock() + adapterProvidingMock.getAdapterForDomainDbPathDelegateNotificatorReturnValue = adapterMock + let expectation = XCTestExpectation() + let itemIdentifiers = [NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: 2), + NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: 3)] + serviceSource.retryUpload(for: itemIdentifiers) { error in + XCTAssertNil(error) + expectation.fulfill() + } + wait(for: [expectation], timeout: 1.0) + + XCTAssertEqual(itemIdentifiers, adapterMock.retryUploadForReceivedInvocations) + } + + func testGetCurrentFractionalUploadProgress() throws { + let expectation = XCTestExpectation() + let itemIdentifier = NSFileProviderItemIdentifier(domainIdentifier: .test, itemID: 2) + let mockedProgress = Progress(totalUnitCount: 100) + mockedProgress.completedUnitCount = 42 + progressManagerMock.getProgressForReturnValue = mockedProgress + serviceSource.getCurrentFractionalUploadProgress(for: itemIdentifier) { progress in + XCTAssertEqual(0.42, progress) + expectation.fulfill() + } + wait(for: [expectation], timeout: 1.0) + XCTAssertEqual([itemIdentifier], progressManagerMock.getProgressForReceivedInvocations) + } +} diff --git a/FileProviderExtension/FileProviderExtension.swift b/FileProviderExtension/FileProviderExtension.swift index 0b9ffef3b..5a47db1ff 100644 --- a/FileProviderExtension/FileProviderExtension.swift +++ b/FileProviderExtension/FileProviderExtension.swift @@ -259,8 +259,9 @@ class FileProviderExtension: NSFileProviderExtension { dbPath: dbPath, delegate: LocalURLProvider(domain: snapshotDomain))) #else - if let domain = domain, let localURLProvider = localURLProvider { + if let domain = domain, let localURLProvider = localURLProvider, let dbPath = dbPath, let notificator = notificator { serviceSources.append(VaultUnlockingServiceSource(domain: domain, notificator: notificator, dbPath: dbPath, delegate: localURLProvider)) + serviceSources.append(UploadRetryingServiceSource(domain: domain, notificator: notificator, dbPath: dbPath, delegate: localURLProvider)) } #endif serviceSources.append(VaultLockingServiceSource()) diff --git a/FileProviderExtensionUI/Info.plist b/FileProviderExtensionUI/Info.plist index b2c84152d..195dcf492 100644 --- a/FileProviderExtensionUI/Info.plist +++ b/FileProviderExtensionUI/Info.plist @@ -31,6 +31,26 @@ $(PRODUCT_MODULE_NAME).RootViewController NSExtensionPointIdentifier com.apple.fileprovider-actionsui + NSExtensionFileProviderActions + + + + NSExtensionFileProviderActionIdentifier + org.cryptomator.ios.fileprovider.retryUpload + NSExtensionFileProviderActionName + Retry Upload + NSExtensionFileProviderActionActivationRule + SUBQUERY( fileproviderItems, $fileproviderItem, $fileproviderItem.userInfo."hasUploadError" == YES).@count > 0 + + + NSExtensionFileProviderActionIdentifier + org.cryptomator.ios.fileprovider.retryWaitingUpload + NSExtensionFileProviderActionName + Retry Upload + NSExtensionFileProviderActionActivationRule + SUBQUERY( fileproviderItems, $fileproviderItem, $fileproviderItem.userInfo."isUploading" == YES).@count > 0 + + diff --git a/FileProviderExtensionUI/RootViewController.swift b/FileProviderExtensionUI/RootViewController.swift index ca5b8a0f7..3f8c55c7f 100644 --- a/FileProviderExtensionUI/RootViewController.swift +++ b/FileProviderExtensionUI/RootViewController.swift @@ -9,8 +9,10 @@ import CocoaLumberjackSwift import CryptomatorCloudAccessCore import CryptomatorCommonCore +import CryptomatorFileProvider import FileProviderUI import MSAL +import Promises import UIKit class RootViewController: FPUIActionExtensionViewController { @@ -84,9 +86,75 @@ class RootViewController: FPUIActionExtensionViewController { return {} }() + func retryUpload(for itemIdentifiers: [NSFileProviderItemIdentifier], domainIdentifier: NSFileProviderDomainIdentifier) { + let getXPCPromise: Promise> = FileProviderXPCConnector.shared.getXPC(serviceName: .uploadRetryingService, domainIdentifier: domainIdentifier) + getXPCPromise.then { xpc in + return wrap { + xpc.proxy.retryUpload(for: itemIdentifiers, reply: $0) + } + }.then { + if let error = $0 { + throw error + } + self.extensionContext.completeRequest() + }.catch { error in + DDLogError("Retry upload failed with error: \(error)") + self.extensionContext.cancelRequest(withError: NSError(domain: FPUIErrorDomain, code: Int(FPUIExtensionErrorCode.failed.rawValue), userInfo: nil)) + }.always { + FileProviderXPCConnector.shared.invalidateXPC(getXPCPromise) + } + } + + func showDomainNotFoundAlert() { + let alertController = RetryUploadAlertControllerFactory.createDomainNotFoundAlert(okAction: { [weak self] in + self?.cancel() + }) + present(alertController, animated: true) + } + + func showUploadProgressAlert(for itemIdentifiers: [NSFileProviderItemIdentifier], domainIdentifier: NSFileProviderDomainIdentifier) { + let getXPCPromise: Promise> = FileProviderXPCConnector.shared.getXPC(serviceName: .uploadRetryingService, domainIdentifier: domainIdentifier) + let progressAlert = RetryUploadAlertControllerFactory.createUploadProgressAlert(dismissAction: { + [weak self] in + self?.cancel() + }, retryAction: { [weak self] in + self?.retryUpload(for: itemIdentifiers, domainIdentifier: domainIdentifier) + }) + getXPCPromise.then { xpc -> Promise in + let observeProgressPromise = progressAlert.observeProgress(itemIdentifier: itemIdentifiers[0], proxy: xpc.proxy) + let alertActionPromise = progressAlert.alertActionTriggered + return race([observeProgressPromise, alertActionPromise]) + }.always { + self.extensionContext.completeRequest() + FileProviderXPCConnector.shared.invalidateXPC(getXPCPromise) + } + present(progressAlert, animated: true) + } + // MARK: - FPUIActionExtensionViewController override func prepare(forError error: Error) { coordinator.startWith(error: error) } + + override func prepare(forAction actionIdentifier: String, itemIdentifiers: [NSFileProviderItemIdentifier]) { + let action = FileProviderAction(rawValue: actionIdentifier) + switch action { + case .retryWaitingUpload: + if let domainIdentifier = itemIdentifiers.first?.domainIdentifier { + showUploadProgressAlert(for: itemIdentifiers, domainIdentifier: domainIdentifier) + } else { + showDomainNotFoundAlert() + } + case .retryUpload: + if let domainIdentifier = itemIdentifiers.first?.domainIdentifier { + retryUpload(for: itemIdentifiers, domainIdentifier: domainIdentifier) + } else { + showDomainNotFoundAlert() + } + case .none: + let error = NSError(domain: NSCocoaErrorDomain, code: NSFeatureUnsupportedError, userInfo: [:]) + extensionContext.cancelRequest(withError: error) + } + } } diff --git a/FileProviderExtensionUI/UploadProgressAlertController.swift b/FileProviderExtensionUI/UploadProgressAlertController.swift new file mode 100644 index 000000000..fc42130c0 --- /dev/null +++ b/FileProviderExtensionUI/UploadProgressAlertController.swift @@ -0,0 +1,94 @@ +// +// UploadProgressAlertController.swift +// FileProviderExtensionUI +// +// Created by Philipp Schmid on 09.05.22. +// Copyright © 2022 Skymatic GmbH. All rights reserved. +// + +import CryptomatorCommonCore +import Promises +import UIKit + +class UploadProgressAlertController: UIAlertController { + /// Promise fulfills as soon as a alert action gets triggered + let alertActionTriggered = Promise.pending() + + var progress: Double? { + didSet { + DispatchQueue.main.async { + if let progress = self.progress { + self.setMessage(with: progress) + } else { + self.setMessageForMissingProgress() + } + } + } + } + + private lazy var formatter: NumberFormatter = { + let formatter = NumberFormatter() + formatter.minimumFractionDigits = 0 + formatter.maximumFractionDigits = 2 + formatter.numberStyle = .decimal + return formatter + }() + + func setMessage(with progress: Double) { + let formattedProgress = formatter.string(from: progress * 100.0 as NSNumber) ?? "n/a" + let text = "Current Progress: \(formattedProgress)%\n\nIf you're noticing that the upload progress is stuck, you can retry the upload." + message = text + } + + func setMessageForMissingProgress() { + message = "Progress could not be determined. It may still be running in the background." + } + + func observeProgress(itemIdentifier: NSFileProviderItemIdentifier, proxy: UploadRetrying) -> Promise { + return Promise { fulfill, _ in + Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] timer in + let isVisible = self?.viewIfLoaded?.window != nil + if isVisible { + proxy.getCurrentFractionalUploadProgress(for: itemIdentifier) { number in + let currentProgress = number?.doubleValue + self?.progress = currentProgress + if let progress = currentProgress, progress >= 1.0 { + timer.invalidate() + fulfill(()) + } + } + } else { + timer.invalidate() + } + } + } + } +} + +enum RetryUploadAlertControllerFactory { + static func createUploadProgressAlert(dismissAction: @escaping () -> Void, retryAction: @escaping () -> Void) -> UploadProgressAlertController { + let alertController = UploadProgressAlertController(title: "Uploading…", message: "Connecting…", preferredStyle: .alert) + let dismissAlertAction = UIAlertAction(title: LocalizedString.getValue("Dismiss"), style: .cancel) { _ in + dismissAction() + alertController.alertActionTriggered.fulfill(()) + } + let retryAlertAction = UIAlertAction(title: LocalizedString.getValue("Retry"), style: .default) { _ in + retryAction() + alertController.alertActionTriggered.fulfill(()) + } + alertController.addAction(dismissAlertAction) + alertController.addAction(retryAlertAction) + alertController.preferredAction = dismissAlertAction + return alertController + } + + static func createDomainNotFoundAlert(okAction: @escaping () -> Void) -> UIAlertController { + let alertController = UIAlertController(title: "Error", message: "Could not find domain.", preferredStyle: .alert) + let okAlertAction = UIAlertAction(title: LocalizedString.getValue("common.button.ok"), style: .cancel) { _ in + okAction() + } + alertController.addAction(okAlertAction) + alertController.preferredAction = okAlertAction + return alertController + } +} diff --git a/SharedResources/en.lproj/Localizable.strings b/SharedResources/en.lproj/Localizable.strings index 9a8313728..6214be62f 100644 --- a/SharedResources/en.lproj/Localizable.strings +++ b/SharedResources/en.lproj/Localizable.strings @@ -255,3 +255,5 @@ "webDAVAuthenticator.error.unsupportedProtocol" = "Server doesn't seem to be WebDAV compatible. Please check if you've used the correct URL."; "webDAVAuthenticator.error.untrustedCertificate" = "Certificate of this server is untrusted. You may have to re-add this WebDAV connection."; + +"Retry Upload" = "Retry Upload"; From 1126699285d82fbe80e07885196b01a59b6241a3 Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Tue, 10 May 2022 11:31:41 +0200 Subject: [PATCH 06/18] Fixed regression that caused "Open in Files app" to stop working --- CryptomatorFileProvider/LocalURLProviderType.swift | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/CryptomatorFileProvider/LocalURLProviderType.swift b/CryptomatorFileProvider/LocalURLProviderType.swift index acce72f6b..653dcc0f5 100644 --- a/CryptomatorFileProvider/LocalURLProviderType.swift +++ b/CryptomatorFileProvider/LocalURLProviderType.swift @@ -46,14 +46,19 @@ public extension LocalURLProviderType { This implementation exploits the fact that the path structure has been defined as `//`. + + - Note: Returns the `.rootContainer` identifier for the special case that the passed `url` corresponds to the ``. This is necessary to support "Open in Files app". */ func persistentIdentifierForItem(at url: URL) -> NSFileProviderItemIdentifier? { let pathComponents = url.pathComponents assert(pathComponents.count > 2) - guard let itemID = Int64(pathComponents[pathComponents.count - 2]) else { + if let itemID = Int64(pathComponents[pathComponents.count - 2]) { + return NSFileProviderItemIdentifier(domainIdentifier: domainIdentifier, itemID: itemID) + } else if pathComponents.last == domainIdentifier.rawValue { + return .rootContainer + } else { return nil } - return NSFileProviderItemIdentifier(domainIdentifier: domainIdentifier, itemID: itemID) } } From 7ab2d83173526465e199d6cd116169d099a29d26 Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Tue, 10 May 2022 11:32:36 +0200 Subject: [PATCH 07/18] Added missing localization --- .../UploadProgressAlertController.swift | 15 ++++++++------- SharedResources/en.lproj/Localizable.strings | 7 +++++++ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/FileProviderExtensionUI/UploadProgressAlertController.swift b/FileProviderExtensionUI/UploadProgressAlertController.swift index fc42130c0..63401b0af 100644 --- a/FileProviderExtensionUI/UploadProgressAlertController.swift +++ b/FileProviderExtensionUI/UploadProgressAlertController.swift @@ -36,12 +36,11 @@ class UploadProgressAlertController: UIAlertController { func setMessage(with progress: Double) { let formattedProgress = formatter.string(from: progress * 100.0 as NSNumber) ?? "n/a" - let text = "Current Progress: \(formattedProgress)%\n\nIf you're noticing that the upload progress is stuck, you can retry the upload." - message = text + message = String(format: LocalizedString.getValue("fileProvider.uploadProgress.message"), formattedProgress) } func setMessageForMissingProgress() { - message = "Progress could not be determined. It may still be running in the background." + message = LocalizedString.getValue("fileProvider.uploadProgress.missing") } func observeProgress(itemIdentifier: NSFileProviderItemIdentifier, proxy: UploadRetrying) -> Promise { @@ -67,12 +66,14 @@ class UploadProgressAlertController: UIAlertController { enum RetryUploadAlertControllerFactory { static func createUploadProgressAlert(dismissAction: @escaping () -> Void, retryAction: @escaping () -> Void) -> UploadProgressAlertController { - let alertController = UploadProgressAlertController(title: "Uploading…", message: "Connecting…", preferredStyle: .alert) - let dismissAlertAction = UIAlertAction(title: LocalizedString.getValue("Dismiss"), style: .cancel) { _ in + let alertController = UploadProgressAlertController(title: LocalizedString.getValue("fileProvider.uploadProgress.title"), + message: LocalizedString.getValue("fileProvider.uploadProgress.connecting"), + preferredStyle: .alert) + let dismissAlertAction = UIAlertAction(title: LocalizedString.getValue("common.button.dismiss"), style: .cancel) { _ in dismissAction() alertController.alertActionTriggered.fulfill(()) } - let retryAlertAction = UIAlertAction(title: LocalizedString.getValue("Retry"), style: .default) { _ in + let retryAlertAction = UIAlertAction(title: LocalizedString.getValue("common.button.retry"), style: .default) { _ in retryAction() alertController.alertActionTriggered.fulfill(()) } @@ -83,7 +84,7 @@ enum RetryUploadAlertControllerFactory { } static func createDomainNotFoundAlert(okAction: @escaping () -> Void) -> UIAlertController { - let alertController = UIAlertController(title: "Error", message: "Could not find domain.", preferredStyle: .alert) + let alertController = UIAlertController(title: LocalizedString.getValue("common.alert.error.title"), message: LocalizedString.getValue("fileProvider.uploadProgress.missingDomainError"), preferredStyle: .alert) let okAlertAction = UIAlertAction(title: LocalizedString.getValue("common.button.ok"), style: .cancel) { _ in okAction() } diff --git a/SharedResources/en.lproj/Localizable.strings b/SharedResources/en.lproj/Localizable.strings index 6214be62f..6ab7bf3d0 100644 --- a/SharedResources/en.lproj/Localizable.strings +++ b/SharedResources/en.lproj/Localizable.strings @@ -13,6 +13,7 @@ "common.button.confirm" = "Confirm"; "common.button.create" = "Create"; "common.button.createFolder" = "Create Folder"; +"common.button.dismiss" = "Dismiss"; "common.button.done" = "Done"; "common.button.download" = "Download"; "common.button.edit" = "Edit"; @@ -20,6 +21,7 @@ "common.button.next" = "Next"; "common.button.ok" = "OK"; "common.button.remove" = "Remove"; +"common.button.retry" = "Retry"; "common.button.signOut" = "Sign Out"; "common.button.verify" = "Verify"; "common.cells.openInFilesApp" = "Open in Files App"; @@ -95,6 +97,11 @@ "fileProvider.error.defaultLock.title" = "Unlock Required"; "fileProvider.error.defaultLock.message" = "To access and show the contents of your vault, it has to be unlocked."; "fileProvider.error.unlockButton" = "Unlock"; +"fileProvider.uploadProgress.connecting" = "Connecting…"; +"fileProvider.uploadProgress.message" = "Current Progress: %@%\n\nIf you're noticing that the upload progress is stuck, you can retry the upload."; +"fileProvider.uploadProgress.missing" = "Progress could not be determined. It may still be running in the background."; +"fileProvider.uploadProgress.title" = "Uploading…"; +"fileProvider.uploadProgress.missingDomainError" = "Could not find domain."; "keepUnlocked.alert.title" = "Lock Vault?"; "keepUnlocked.alert.message" = "This change requires your vault to be locked in order to take effect."; From 1b42d35335480e01635f9272ba9f2542850aca61 Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Thu, 12 May 2022 15:46:12 +0200 Subject: [PATCH 08/18] adjusted percent formatting in upload progress --- FileProviderExtensionUI/UploadProgressAlertController.swift | 6 +++--- SharedResources/en.lproj/Localizable.strings | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/FileProviderExtensionUI/UploadProgressAlertController.swift b/FileProviderExtensionUI/UploadProgressAlertController.swift index 63401b0af..1b58ac673 100644 --- a/FileProviderExtensionUI/UploadProgressAlertController.swift +++ b/FileProviderExtensionUI/UploadProgressAlertController.swift @@ -28,14 +28,14 @@ class UploadProgressAlertController: UIAlertController { private lazy var formatter: NumberFormatter = { let formatter = NumberFormatter() - formatter.minimumFractionDigits = 0 + formatter.minimumFractionDigits = 2 formatter.maximumFractionDigits = 2 - formatter.numberStyle = .decimal + formatter.numberStyle = .percent return formatter }() func setMessage(with progress: Double) { - let formattedProgress = formatter.string(from: progress * 100.0 as NSNumber) ?? "n/a" + let formattedProgress = formatter.string(from: progress as NSNumber) ?? "n/a" message = String(format: LocalizedString.getValue("fileProvider.uploadProgress.message"), formattedProgress) } diff --git a/SharedResources/en.lproj/Localizable.strings b/SharedResources/en.lproj/Localizable.strings index 6ab7bf3d0..5de849984 100644 --- a/SharedResources/en.lproj/Localizable.strings +++ b/SharedResources/en.lproj/Localizable.strings @@ -98,7 +98,7 @@ "fileProvider.error.defaultLock.message" = "To access and show the contents of your vault, it has to be unlocked."; "fileProvider.error.unlockButton" = "Unlock"; "fileProvider.uploadProgress.connecting" = "Connecting…"; -"fileProvider.uploadProgress.message" = "Current Progress: %@%\n\nIf you're noticing that the upload progress is stuck, you can retry the upload."; +"fileProvider.uploadProgress.message" = "Current Progress: %@\n\nIf you're noticing that the upload progress is stuck, you can retry the upload."; "fileProvider.uploadProgress.missing" = "Progress could not be determined. It may still be running in the background."; "fileProvider.uploadProgress.title" = "Uploading…"; "fileProvider.uploadProgress.missingDomainError" = "Could not find domain."; From f42bba52611db1cddfd3ab299fe4fe7d72eea0c6 Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Thu, 12 May 2022 17:41:15 +0200 Subject: [PATCH 09/18] =?UTF-8?q?"Dismiss"=20=E2=86=92=20"Close"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FileProviderExtensionUI/UploadProgressAlertController.swift | 2 +- SharedResources/en.lproj/Localizable.strings | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/FileProviderExtensionUI/UploadProgressAlertController.swift b/FileProviderExtensionUI/UploadProgressAlertController.swift index 1b58ac673..7109cfb8c 100644 --- a/FileProviderExtensionUI/UploadProgressAlertController.swift +++ b/FileProviderExtensionUI/UploadProgressAlertController.swift @@ -69,7 +69,7 @@ enum RetryUploadAlertControllerFactory { let alertController = UploadProgressAlertController(title: LocalizedString.getValue("fileProvider.uploadProgress.title"), message: LocalizedString.getValue("fileProvider.uploadProgress.connecting"), preferredStyle: .alert) - let dismissAlertAction = UIAlertAction(title: LocalizedString.getValue("common.button.dismiss"), style: .cancel) { _ in + let dismissAlertAction = UIAlertAction(title: LocalizedString.getValue("common.button.close"), style: .cancel) { _ in dismissAction() alertController.alertActionTriggered.fulfill(()) } diff --git a/SharedResources/en.lproj/Localizable.strings b/SharedResources/en.lproj/Localizable.strings index 5de849984..81665e013 100644 --- a/SharedResources/en.lproj/Localizable.strings +++ b/SharedResources/en.lproj/Localizable.strings @@ -10,10 +10,10 @@ "common.button.cancel" = "Cancel"; "common.button.change" = "Change"; "common.button.choose" = "Choose"; +"common.button.close" = "Close"; "common.button.confirm" = "Confirm"; "common.button.create" = "Create"; "common.button.createFolder" = "Create Folder"; -"common.button.dismiss" = "Dismiss"; "common.button.done" = "Done"; "common.button.download" = "Download"; "common.button.edit" = "Edit"; From e8d885d92501ce7c2b22a344c6b699f70865579f Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Mon, 16 May 2022 15:29:03 +0200 Subject: [PATCH 10/18] Feature: Evict single file from cache (#220) --- Cryptomator.xcodeproj/project.pbxproj | 58 +++++++-- Cryptomator/Settings/SettingsViewModel.swift | 17 ++- .../FileProviderXPC/CacheManaging.swift | 99 +++++++++++++++ .../Manager/VaultDBManager.swift | 13 +- .../Mocks/CacheManagingMock.swift | 78 ++++++++++++ .../Actions/FileProviderAction.swift | 1 + .../CachedFileManagerFactory.swift | 29 +++++ .../DB/CachedFileDBManager.swift | 8 ++ .../DatabaseURLProvider.swift | 25 ++++ .../FileProviderItem.swift | 13 +- .../NSFileProviderDomainProvider.swift | 22 ++++ .../CacheManagingServiceSource.swift | 83 ++++++++++++ .../FileProviderItemTests.swift | 91 ++++++++++++++ .../Mocks/CacheManagerMock.swift | 119 ++++++++++++++++++ .../Mocks/CachedFileManagerFactoryMock.swift | 36 ++++++ .../NSFileProviderDomainProviderMock.swift | 37 ++++++ .../CacheManagingServiceSourceTests.swift | 117 +++++++++++++++++ .../LogLevelUpdatingServiceSourceTests.swift | 0 .../UploadRetryingServiceSourceTests.swift | 0 CryptomatorTests/SettingsViewModelTests.swift | 44 +++---- .../FileProviderExtension.swift | 5 + FileProviderExtensionUI/Info.plist | 12 +- .../RootViewController.swift | 51 +++++++- SharedResources/en.lproj/Localizable.strings | 4 + 24 files changed, 910 insertions(+), 52 deletions(-) create mode 100644 CryptomatorCommon/Sources/CryptomatorCommonCore/FileProviderXPC/CacheManaging.swift create mode 100644 CryptomatorCommon/Sources/CryptomatorCommonCore/Mocks/CacheManagingMock.swift create mode 100644 CryptomatorFileProvider/CachedFileManagerFactory.swift create mode 100644 CryptomatorFileProvider/DatabaseURLProvider.swift create mode 100644 CryptomatorFileProvider/NSFileProviderDomainProvider.swift create mode 100644 CryptomatorFileProvider/ServiceSource/CacheManagingServiceSource.swift create mode 100644 CryptomatorFileProviderTests/Mocks/CacheManagerMock.swift create mode 100644 CryptomatorFileProviderTests/Mocks/CachedFileManagerFactoryMock.swift create mode 100644 CryptomatorFileProviderTests/Mocks/NSFileProviderDomainProviderMock.swift create mode 100644 CryptomatorFileProviderTests/ServiceSource/CacheManagingServiceSourceTests.swift rename CryptomatorFileProviderTests/{ => ServiceSource}/LogLevelUpdatingServiceSourceTests.swift (100%) rename CryptomatorFileProviderTests/{ => ServiceSource}/UploadRetryingServiceSourceTests.swift (100%) diff --git a/Cryptomator.xcodeproj/project.pbxproj b/Cryptomator.xcodeproj/project.pbxproj index 2af804558..72e758a65 100644 --- a/Cryptomator.xcodeproj/project.pbxproj +++ b/Cryptomator.xcodeproj/project.pbxproj @@ -225,6 +225,14 @@ 4AA2531928216BFD003B45EE /* UploadRetryingServiceSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA2531828216BFD003B45EE /* UploadRetryingServiceSource.swift */; }; 4AA2531B28216E45003B45EE /* ServiceSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA2531A28216E45003B45EE /* ServiceSource.swift */; }; 4AA621D9249A6A8400A0BCBD /* FileProviderExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA621D8249A6A8400A0BCBD /* FileProviderExtension.swift */; }; + 4AA782D7282A7779001A71E3 /* CacheManagingServiceSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA782D6282A7779001A71E3 /* CacheManagingServiceSource.swift */; }; + 4AA782DA282A7A2E001A71E3 /* CacheManagingServiceSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA782D9282A7A2E001A71E3 /* CacheManagingServiceSourceTests.swift */; }; + 4AA782DC282A7F1A001A71E3 /* DatabaseURLProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA782DB282A7F1A001A71E3 /* DatabaseURLProvider.swift */; }; + 4AA782DE282A8250001A71E3 /* NSFileProviderDomainProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA782DD282A8250001A71E3 /* NSFileProviderDomainProvider.swift */; }; + 4AA782E0282A8609001A71E3 /* CachedFileManagerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA782DF282A8609001A71E3 /* CachedFileManagerFactory.swift */; }; + 4AA782E2282A8FC0001A71E3 /* CachedFileManagerFactoryMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA782E1282A8FC0001A71E3 /* CachedFileManagerFactoryMock.swift */; }; + 4AA782E4282A9007001A71E3 /* NSFileProviderDomainProviderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA782E3282A9007001A71E3 /* NSFileProviderDomainProviderMock.swift */; }; + 4AA782E6282A91BD001A71E3 /* CacheManagerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA782E5282A91BD001A71E3 /* CacheManagerMock.swift */; }; 4AA8613725C19D4F002A59F5 /* DetectedMasterkeyViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA8613625C19D4F002A59F5 /* DetectedMasterkeyViewModel.swift */; }; 4AA8614825C1C670002A59F5 /* OpenExistingVaultChooseFolderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA8614725C1C670002A59F5 /* OpenExistingVaultChooseFolderViewController.swift */; }; 4AA8615125C1DB5E002A59F5 /* OpenExistingVaultPasswordViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA8615025C1DB5E002A59F5 /* OpenExistingVaultPasswordViewController.swift */; }; @@ -715,6 +723,14 @@ 4AA621DF249A6A8400A0BCBD /* FileProviderExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = FileProviderExtension.entitlements; sourceTree = ""; }; 4AA621E4249A6A8400A0BCBD /* FileProviderExtensionUI.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = FileProviderExtensionUI.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 4AA621EB249A6A8400A0BCBD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 4AA782D6282A7779001A71E3 /* CacheManagingServiceSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheManagingServiceSource.swift; sourceTree = ""; }; + 4AA782D9282A7A2E001A71E3 /* CacheManagingServiceSourceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheManagingServiceSourceTests.swift; sourceTree = ""; }; + 4AA782DB282A7F1A001A71E3 /* DatabaseURLProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseURLProvider.swift; sourceTree = ""; }; + 4AA782DD282A8250001A71E3 /* NSFileProviderDomainProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSFileProviderDomainProvider.swift; sourceTree = ""; }; + 4AA782DF282A8609001A71E3 /* CachedFileManagerFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachedFileManagerFactory.swift; sourceTree = ""; }; + 4AA782E1282A8FC0001A71E3 /* CachedFileManagerFactoryMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachedFileManagerFactoryMock.swift; sourceTree = ""; }; + 4AA782E3282A9007001A71E3 /* NSFileProviderDomainProviderMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSFileProviderDomainProviderMock.swift; sourceTree = ""; }; + 4AA782E5282A91BD001A71E3 /* CacheManagerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheManagerMock.swift; sourceTree = ""; }; 4AA8613625C19D4F002A59F5 /* DetectedMasterkeyViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectedMasterkeyViewModel.swift; sourceTree = ""; }; 4AA8614725C1C670002A59F5 /* OpenExistingVaultChooseFolderViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenExistingVaultChooseFolderViewController.swift; sourceTree = ""; }; 4AA8615025C1DB5E002A59F5 /* OpenExistingVaultPasswordViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenExistingVaultPasswordViewController.swift; sourceTree = ""; }; @@ -1058,10 +1074,8 @@ 4A9C8DFC27A007C2000063E4 /* FileProviderNotificatorTests.swift */, 4AFBFA19282946BF00E30818 /* InMemoryProgressManagerTests.swift */, 4AB1D4EF27D20420009060AB /* LocalURLProviderTests.swift */, - 4AEFF7F327145CB400D6CB99 /* LogLevelUpdatingServiceSourceTests.swift */, 4AC1157727F5BEFD0023F51B /* Promise+AllIgnoringResultsTests.swift */, 4ADC66C427A7F6D6002E6CC7 /* UnlockMonitorTests.swift */, - 4AFBFA1528293FE200E30818 /* UploadRetryingServiceSourceTests.swift */, 4A4F47F224B875070033328B /* URL+NameCollisionExtensionTests.swift */, 4AE5196427F48D6600BA6E4A /* WorkflowDependencyFactoryTests.swift */, 4AE5196627F495BF00BA6E4A /* WorkflowDependencyTasksCollectionMock.swift */, @@ -1072,6 +1086,7 @@ 4A8F14A3266A302E00ADBCE4 /* FileProviderAdapter */, 4AB1C32E265CF59100DC7A49 /* Middleware */, 4AB6A897278F07DE0016B01E /* Mocks */, + 4AA782D8282A7A10001A71E3 /* ServiceSource */, ); path = CryptomatorFileProviderTests; sourceTree = ""; @@ -1467,6 +1482,7 @@ 4AA2531728216BD1003B45EE /* ServiceSource */ = { isa = PBXGroup; children = ( + 4AA782D6282A7779001A71E3 /* CacheManagingServiceSource.swift */, 4AEFF7F127145ADD00D6CB99 /* LogLevelUpdatingServiceSource.swift */, 4AA2531A28216E45003B45EE /* ServiceSource.swift */, 4AA2531828216BFD003B45EE /* UploadRetryingServiceSource.swift */, @@ -1507,6 +1523,16 @@ path = FileProviderExtensionUI; sourceTree = ""; }; + 4AA782D8282A7A10001A71E3 /* ServiceSource */ = { + isa = PBXGroup; + children = ( + 4AA782D9282A7A2E001A71E3 /* CacheManagingServiceSourceTests.swift */, + 4AEFF7F327145CB400D6CB99 /* LogLevelUpdatingServiceSourceTests.swift */, + 4AFBFA1528293FE200E30818 /* UploadRetryingServiceSourceTests.swift */, + ); + path = ServiceSource; + sourceTree = ""; + }; 4AA8613F25C1AC4D002A59F5 /* OpenExistingVault */ = { isa = PBXGroup; children = ( @@ -1546,25 +1572,28 @@ 4AB6A897278F07DE0016B01E /* Mocks */ = { isa = PBXGroup; children = ( + 4AA782E1282A8FC0001A71E3 /* CachedFileManagerFactoryMock.swift */, + 4AA782E5282A91BD001A71E3 /* CacheManagerMock.swift */, + 4A123EA724BEF5F0001D1CF7 /* CloudProviderPaginationMock.swift */, 4A797F9524AC9936007DDBE1 /* CustomCloudProviderMock.swift */, 4A797F9724AC9A1B007DDBE1 /* CustomCloudProviderMockTests.swift */, - 4A123EA724BEF5F0001D1CF7 /* CloudProviderPaginationMock.swift */, + 4AB1D4F127D20510009060AB /* DocumentStorageURLProviderMock.swift */, + 4A9C8E0027A0104E000063E4 /* EnumerationSignalingMock.swift */, 4AB6A893278F048D0016B01E /* FileProviderAdapterCacheTypeMock.swift */, + 4AEECD3C279EB4B200C6E2B5 /* FileProviderAdapterProvidingMock.swift */, 4AB6A895278F07B20016B01E /* FileProviderAdapterTypeMock.swift */, 4A2060CA2798302600DA6C62 /* FileProviderItemUpdateDelegateMock.swift */, 4A2060D0279AB32700DA6C62 /* FileProviderNotificatorManagerMock.swift */, 4A2060D2279AB38A00DA6C62 /* FileProviderNotificatorMock.swift */, 4AB1D4ED27D0E9EA009060AB /* LocalURLProviderMock.swift */, 4AB6A898278F084E0016B01E /* MaintenanceManagerMock.swift */, - 4AEECD3A279EB24300C6E2B5 /* NSFileProviderEnumerationObserverMock.swift */, 4AEECD3E279EC48200C6E2B5 /* NSFileProviderChangeObserverMock.swift */, - 4AEECD30279EA50D00C6E2B5 /* WorkingSetObservingMock.swift */, - 4AEECD3C279EB4B200C6E2B5 /* FileProviderAdapterProvidingMock.swift */, - 4A9C8E0027A0104E000063E4 /* EnumerationSignalingMock.swift */, + 4AA782E3282A9007001A71E3 /* NSFileProviderDomainProviderMock.swift */, + 4AEECD3A279EB24300C6E2B5 /* NSFileProviderEnumerationObserverMock.swift */, + 4AFBFA172829414A00E30818 /* ProgressManagerMock.swift */, 4ADC66C627A95E67002E6CC7 /* UnlockMonitorTaskExecutorMock.swift */, - 4AB1D4F127D20510009060AB /* DocumentStorageURLProviderMock.swift */, 4AAD444627E26D1800D16707 /* UploadTaskManagerMock.swift */, - 4AFBFA172829414A00E30818 /* ProgressManagerMock.swift */, + 4AEECD30279EA50D00C6E2B5 /* WorkingSetObservingMock.swift */, ); path = Mocks; sourceTree = ""; @@ -1681,8 +1710,10 @@ isa = PBXGroup; children = ( 740375F92587AEB50023FF53 /* CryptomatorFileProvider.h */, + 4AA782DF282A8609001A71E3 /* CachedFileManagerFactory.swift */, 4AE5196A27F595B100BA6E4A /* CloudPath+GetParent.swift */, 740375FA2587AEB50023FF53 /* CloudPath+NameCollision.swift */, + 4AA782DB282A7F1A001A71E3 /* DatabaseURLProvider.swift */, 4A511D582664F290000A0E01 /* DeleteItemHelper.swift */, 4A1673E9270C77CC0075C724 /* DocumentStorageURLProvider.swift */, 4AEECD36279EB15400C6E2B5 /* ErrorWrapper.swift */, @@ -1697,6 +1728,7 @@ 4A2060CC2799645300DA6C62 /* FileProviderNotificatorManager.swift */, 740375F42587AEB50023FF53 /* ItemStatus.swift */, 4AB1D4EB27D0E027009060AB /* LocalURLProviderType.swift */, + 4AA782DD282A8250001A71E3 /* NSFileProviderDomainProvider.swift */, 4AEE6EE02822A33400E1B35E /* NSFileProviderItemIdentifier+Database.swift */, 4AEE6EE92825716400E1B35E /* ProgressManager.swift */, 4AC1157527F5BD890023F51B /* Promise+AllIgnoringResult.swift */, @@ -2260,6 +2292,7 @@ 4A8F149A266A29C900ADBCE4 /* WorkflowMiddlewareMock.swift in Sources */, 4ADC66C727A95E67002E6CC7 /* UnlockMonitorTaskExecutorMock.swift in Sources */, 4A797F9824AC9A1B007DDBE1 /* CustomCloudProviderMockTests.swift in Sources */, + 4AA782DA282A7A2E001A71E3 /* CacheManagingServiceSourceTests.swift in Sources */, 4AAD444727E26D1800D16707 /* UploadTaskManagerMock.swift in Sources */, 4AFBFA1A282946BF00E30818 /* InMemoryProgressManagerTests.swift in Sources */, 4A079FB928084134009AD932 /* WorkingSetEnumerationTests.swift in Sources */, @@ -2275,6 +2308,7 @@ 4A2060D1279AB32700DA6C62 /* FileProviderNotificatorManagerMock.swift in Sources */, 4AEECD3F279EC48200C6E2B5 /* NSFileProviderChangeObserverMock.swift in Sources */, 4AEECD31279EA50D00C6E2B5 /* WorkingSetObservingMock.swift in Sources */, + 4AA782E2282A8FC0001A71E3 /* CachedFileManagerFactoryMock.swift in Sources */, 4A511D492660EE3F000A0E01 /* DeletionTaskExecutorTests.swift in Sources */, 4AEECD33279EAACA00C6E2B5 /* FileProviderAdapterSetFavoriteRankTests.swift in Sources */, 4A797F9624AC9936007DDBE1 /* CustomCloudProviderMock.swift in Sources */, @@ -2303,6 +2337,7 @@ 4A511D5326615439000A0E01 /* ReparentTaskExecutorTests.swift in Sources */, 4A248227266E27C5002D9F59 /* FolderCreationTaskExecutorTests.swift in Sources */, 4AB1D4EE27D0E9EA009060AB /* LocalURLProviderMock.swift in Sources */, + 4AA782E4282A9007001A71E3 /* NSFileProviderDomainProviderMock.swift in Sources */, 4A9C8DFD27A007C2000063E4 /* FileProviderNotificatorTests.swift in Sources */, 4AB1C325265CE69700DC7A49 /* DownloadTaskExecutorTests.swift in Sources */, 4AEECD3B279EB24300C6E2B5 /* NSFileProviderEnumerationObserverMock.swift in Sources */, @@ -2314,6 +2349,7 @@ 4A717CD924C835740048E08F /* ReparentTaskManagerTests.swift in Sources */, 4A9C8E0327A016CF000063E4 /* WorkingSetObserverTests.swift in Sources */, 4AB1C33A265E9D8600DC7A49 /* UploadTaskExecutorTests.swift in Sources */, + 4AA782E6282A91BD001A71E3 /* CacheManagerMock.swift in Sources */, 4A2245DC24A5E1C600DBA437 /* MetadataManagerTests.swift in Sources */, 4AB6A896278F07B20016B01E /* FileProviderAdapterTypeMock.swift in Sources */, 4AEECD39279EB1EB00C6E2B5 /* FileProviderEnumeratorTests.swift in Sources */, @@ -2599,6 +2635,8 @@ 4AE5196D27F59B1800BA6E4A /* WorkflowDependencyTaskCollection.swift in Sources */, 747F2F212587BC250072FB30 /* CloudPath+NameCollision.swift in Sources */, 4ADC66C227A7F49E002E6CC7 /* VaultLockingServiceSource.swift in Sources */, + 4AA782DC282A7F1A001A71E3 /* DatabaseURLProvider.swift in Sources */, + 4AA782E0282A8609001A71E3 /* CachedFileManagerFactory.swift in Sources */, 4A511D512661000F000A0E01 /* WorkflowFactory.swift in Sources */, 4A1673EA270C77CC0075C724 /* DocumentStorageURLProvider.swift in Sources */, 747F2F222587BC250072FB30 /* ItemMetadata.swift in Sources */, @@ -2632,6 +2670,7 @@ 4AA2531B28216E45003B45EE /* ServiceSource.swift in Sources */, 4A2060D5279AF67C00DA6C62 /* WorkingSetObserver.swift in Sources */, 4A511D5B26668E0C000A0E01 /* UploadTaskRecord.swift in Sources */, + 4AA782DE282A8250001A71E3 /* NSFileProviderDomainProvider.swift in Sources */, 4A511D5F26668E68000A0E01 /* DeletionTaskRecord.swift in Sources */, 4A248223266E266E002D9F59 /* FolderCreationTask.swift in Sources */, 747F2F2A2587BC250072FB30 /* DatabaseHelper.swift in Sources */, @@ -2644,6 +2683,7 @@ 4AE5196927F4A24D00BA6E4A /* WorkflowDependencyFactory.swift in Sources */, 4A3E2FEE271DCA160090BD44 /* MaintenanceDBManager.swift in Sources */, 4AEBE8BC2653F2FD0031487F /* ReparentTaskExecutor.swift in Sources */, + 4AA782D7282A7779001A71E3 /* CacheManagingServiceSource.swift in Sources */, 4AEE6EEA2825716400E1B35E /* ProgressManager.swift in Sources */, 747F2F2C2587BC260072FB30 /* DeletionTaskDBManager.swift in Sources */, 4A74DBB1282132EC00A332C4 /* FileProviderAction.swift in Sources */, diff --git a/Cryptomator/Settings/SettingsViewModel.swift b/Cryptomator/Settings/SettingsViewModel.swift index 2d4846ff0..14692f936 100644 --- a/Cryptomator/Settings/SettingsViewModel.swift +++ b/Cryptomator/Settings/SettingsViewModel.swift @@ -78,7 +78,6 @@ class SettingsViewModel: TableViewModel { return elements } - private let cacheManager: FileProviderCacheManager private let cacheSizeCellViewModel = LoadingWithLabelCellViewModel(title: LocalizedString.getValue("settings.cacheSize")) private let clearCacheButtonCellViewModel = ButtonCellViewModel(action: .clearCache, title: LocalizedString.getValue("settings.clearCache"), isEnabled: false) @@ -93,9 +92,8 @@ class SettingsViewModel: TableViewModel { private var subscribers = Set() private lazy var showDebugModeWarningPublisher = PassthroughSubject() - init(cacheManager: FileProviderCacheManager = FileProviderCacheManager(), cryptomatorSetttings: CryptomatorSettings = CryptomatorUserDefaults.shared, fileProviderConnector: FileProviderConnector = FileProviderXPCConnector.shared) { - self.cacheManager = cacheManager - self.cryptomatorSettings = cryptomatorSetttings + init(cryptomatorSettings: CryptomatorSettings = CryptomatorUserDefaults.shared, fileProviderConnector: FileProviderConnector = FileProviderXPCConnector.shared) { + self.cryptomatorSettings = cryptomatorSettings self.fileProviderConnector = fileProviderConnector } @@ -107,7 +105,11 @@ class SettingsViewModel: TableViewModel { self.clearCacheButtonCellViewModel.isEnabled.value = false } } - return cacheManager.getTotalLocalCacheSizeInBytes().then { totalCacheSizeInBytes -> Void in + let getXPCPromise: Promise> = fileProviderConnector.getXPC(serviceName: .cacheManaging, domain: nil) + return getXPCPromise.then { xpc in + xpc.proxy.getLocalCacheSizeInBytes() + }.then { receivedCacheSizeInBytes -> Void in + let totalCacheSizeInBytes = receivedCacheSizeInBytes?.intValue ?? 0 loading = false self.cacheSizeCellViewModel.isLoading.value = false self.clearCacheButtonCellViewModel.isEnabled.value = totalCacheSizeInBytes > 0 @@ -117,7 +119,10 @@ class SettingsViewModel: TableViewModel { } func clearCache() -> Promise { - return cacheManager.clearCache().then { + let getXPCPromise: Promise> = fileProviderConnector.getXPC(serviceName: .cacheManaging, domain: nil) + return getXPCPromise.then { xpc in + xpc.proxy.clearCache() + }.then { self.refreshCacheSize() } } diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/FileProviderXPC/CacheManaging.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/FileProviderXPC/CacheManaging.swift new file mode 100644 index 000000000..d5689f313 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/FileProviderXPC/CacheManaging.swift @@ -0,0 +1,99 @@ +// +// CacheManaging.swift +// CryptomatorCommonCore +// +// Created by Philipp Schmid on 10.05.22. +// Copyright © 2022 Skymatic GmbH. All rights reserved. +// + +import FileProvider +import Foundation +import Promises + +@objc public protocol CacheManaging: NSFileProviderServiceSource { + /** + Evicts the local file belonging to the given `identifier` from the cache. + + A file gets evicted from the cache only if there is no pending (or failed) upload for that file. + + `Reply` will be called with `nil` if the call was successful, otherwise the error will be passed as an `NSError`. + "Because communication over XPC is asynchronous, all methods in the protocol must have a return type of void. If you need to return data, you can define a reply block [...]" see: https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CreatingXPCServices.html + */ + func evictFileFromCache(with identifier: NSFileProviderItemIdentifier, reply: @escaping (NSError?) -> Void) + + /** + Clears the entire cache of all FileProviderDomains. + + Only files that do not have a pending or failed upload are removed from the cache. + + `Reply` will be called with `nil` if the call was successful, otherwise the error will be passed as an `NSError`. + "Because communication over XPC is asynchronous, all methods in the protocol must have a return type of void. If you need to return data, you can define a reply block [...]" see: https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CreatingXPCServices.html + */ + func clearCache(reply: @escaping (NSError?) -> Void) + + /** + Returns the total local cache size in bytes currently used by all FileProviderDomains. + + - Note: Only files that do not have a pending or failed upload are counted towards the cache. + + `Reply` will be called with the size of the local cache size in bytes if the call was successful, otherwise the error will be passed as an `NSError`. + "Because communication over XPC is asynchronous, all methods in the protocol must have a return type of void. If you need to return data, you can define a reply block [...]" see: https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CreatingXPCServices.html + */ + func getLocalCacheSizeInBytes(reply: @escaping (NSNumber?, NSError?) -> Void) +} + +public extension CacheManaging { + /** + Evicts the local file belonging to the given `identifier` from the cache. + + A file gets evicted from the cache only if there is no pending (or failed) upload for that file. + */ + func evictFileFromCache(with itemIdentifier: NSFileProviderItemIdentifier) -> Promise { + return wrap { + self.evictFileFromCache(with: itemIdentifier, reply: $0) + }.then { error -> Void in + if let error = error { + throw error + } + } + } + + func evictFilesFromCache(with itemIdentifiers: [NSFileProviderItemIdentifier]) -> Promise { + guard let itemIdentifier = itemIdentifiers.first else { + return Promise(()) + } + return evictFileFromCache(with: itemIdentifier).then { + self.evictFilesFromCache(with: Array(itemIdentifiers.dropFirst())) + } + } + + /** + Clears the entire cache of all FileProviderDomains. + + Only files that do not have a pending or failed upload are removed from the cache. + */ + func clearCache() -> Promise { + return wrap { + self.clearCache(reply: $0) + }.then { error -> Void in + if let error = error { + throw error + } + } + } + + /** + Returns the total local cache size in bytes currently used by all FileProviderDomains. + + - Note: Only files that do not have a pending or failed upload are counted towards the cache. + */ + func getLocalCacheSizeInBytes() -> Promise { + return wrap { + self.getLocalCacheSizeInBytes(reply: $0) + } + } +} + +public extension NSFileProviderServiceName { + static let cacheManaging = NSFileProviderServiceName("org.cryptomator.ios.cache-managing") +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultDBManager.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultDBManager.swift index c4b009cec..4294880ac 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultDBManager.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultDBManager.swift @@ -518,6 +518,17 @@ public extension NSFileProviderManager { public extension NSFileProviderDomain { convenience init(vaultUID: String, displayName: String) { - self.init(identifier: NSFileProviderDomainIdentifier(vaultUID), displayName: displayName, pathRelativeToDocumentStorage: vaultUID) + self.init(identifier: NSFileProviderDomainIdentifier(vaultUID), displayName: displayName) + } + + convenience init(identifier: NSFileProviderDomainIdentifier, displayName: String) { + self.init(identifier: identifier, displayName: displayName, pathRelativeToDocumentStorage: identifier.rawValue) + } + + /** + Creates a NSFileProviderDomain from a `NSFileProviderDomainIdentifier` where the `pathRelativeToDocumentStorage` equals to the raw value of the given `identifier` and an empty `displayName`. + */ + convenience init(identifier: NSFileProviderDomainIdentifier) { + self.init(identifier: identifier, displayName: "") } } diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Mocks/CacheManagingMock.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Mocks/CacheManagingMock.swift new file mode 100644 index 000000000..6d18b741b --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Mocks/CacheManagingMock.swift @@ -0,0 +1,78 @@ +// +// CacheManagingMock.swift +// CryptomatorCommonCore +// +// Created by Philipp Schmid on 11.05.22. +// Copyright © 2022 Skymatic GmbH. All rights reserved. +// + +#if DEBUG +import FileProvider +import Foundation + +// swiftlint:disable all + +final class CacheManagingMock: CacheManaging, NSFileProviderServiceSource { + let serviceName: NSFileProviderServiceName = .cacheManaging + + func makeListenerEndpoint() throws -> NSXPCListenerEndpoint { + throw NSError(domain: "MockError", code: -100) + } + + // MARK: - evictFileFromCache + + var evictFileFromCacheWithReplyCallsCount = 0 + var evictFileFromCacheWithReplyCalled: Bool { + evictFileFromCacheWithReplyCallsCount > 0 + } + + var evictFileFromCacheWithReplyReceivedArguments: (identifier: NSFileProviderItemIdentifier, reply: (NSError?) -> Void)? + var evictFileFromCacheWithReplyReceivedInvocations: [(identifier: NSFileProviderItemIdentifier, reply: (NSError?) -> Void)] = [] + var evictFileFromCacheWithReplyClosure: ((NSFileProviderItemIdentifier, @escaping (NSError?) -> Void) -> Void)? + + func evictFileFromCache(with identifier: NSFileProviderItemIdentifier, reply: @escaping (NSError?) -> Void) { + evictFileFromCacheWithReplyCallsCount += 1 + evictFileFromCacheWithReplyReceivedArguments = (identifier: identifier, reply: reply) + evictFileFromCacheWithReplyReceivedInvocations.append((identifier: identifier, reply: reply)) + evictFileFromCacheWithReplyClosure?(identifier, reply) + } + + // MARK: - clearCache + + var clearCacheReplyCallsCount = 0 + var clearCacheReplyCalled: Bool { + clearCacheReplyCallsCount > 0 + } + + var clearCacheReplyReceivedReply: ((NSError?) -> Void)? + var clearCacheReplyReceivedInvocations: [(NSError?) -> Void] = [] + var clearCacheReplyClosure: ((@escaping (NSError?) -> Void) -> Void)? + + func clearCache(reply: @escaping (NSError?) -> Void) { + clearCacheReplyCallsCount += 1 + clearCacheReplyReceivedReply = reply + clearCacheReplyReceivedInvocations.append(reply) + clearCacheReplyClosure?(reply) + } + + // MARK: - getLocalCacheSizeInBytes + + var getLocalCacheSizeInBytesReplyCallsCount = 0 + var getLocalCacheSizeInBytesReplyCalled: Bool { + getLocalCacheSizeInBytesReplyCallsCount > 0 + } + + var getLocalCacheSizeInBytesReplyReceivedReply: ((NSNumber?, NSError?) -> Void)? + var getLocalCacheSizeInBytesReplyReceivedInvocations: [(NSNumber?, NSError?) -> Void] = [] + var getLocalCacheSizeInBytesReplyClosure: ((@escaping (NSNumber?, NSError?) -> Void) -> Void)? + + func getLocalCacheSizeInBytes(reply: @escaping (NSNumber?, NSError?) -> Void) { + getLocalCacheSizeInBytesReplyCallsCount += 1 + getLocalCacheSizeInBytesReplyReceivedReply = reply + getLocalCacheSizeInBytesReplyReceivedInvocations.append(reply) + getLocalCacheSizeInBytesReplyClosure?(reply) + } +} + +// swiftlint:enable all +#endif diff --git a/CryptomatorFileProvider/Actions/FileProviderAction.swift b/CryptomatorFileProvider/Actions/FileProviderAction.swift index bea6e1772..a93f40ced 100644 --- a/CryptomatorFileProvider/Actions/FileProviderAction.swift +++ b/CryptomatorFileProvider/Actions/FileProviderAction.swift @@ -11,4 +11,5 @@ import Foundation public enum FileProviderAction: String { case retryUpload = "org.cryptomator.ios.fileprovider.retryUpload" case retryWaitingUpload = "org.cryptomator.ios.fileprovider.retryWaitingUpload" + case evictFileFromCache = "org.cryptomator.ios.fileprovider.evictFileFromCache" } diff --git a/CryptomatorFileProvider/CachedFileManagerFactory.swift b/CryptomatorFileProvider/CachedFileManagerFactory.swift new file mode 100644 index 000000000..c12e9ee8e --- /dev/null +++ b/CryptomatorFileProvider/CachedFileManagerFactory.swift @@ -0,0 +1,29 @@ +// +// CachedFileManagerFactory.swift +// CryptomatorFileProvider +// +// Created by Philipp Schmid on 10.05.22. +// Copyright © 2022 Skymatic GmbH. All rights reserved. +// + +import FileProvider +import Foundation + +protocol CachedFileManagerFactory { + func createCachedFileManager(for domain: NSFileProviderDomain) throws -> CachedFileManager +} + +struct CachedFileDBManagerFactory: CachedFileManagerFactory { + static let shared = CachedFileDBManagerFactory() + private let databaseURLProvider: DatabaseURLProvider + + init(databaseURLProvider: DatabaseURLProvider = .shared) { + self.databaseURLProvider = databaseURLProvider + } + + func createCachedFileManager(for domain: NSFileProviderDomain) throws -> CachedFileManager { + let databaseURL = databaseURLProvider.getDatabaseURL(for: domain) + let database = try DatabaseHelper.getMigratedDB(at: databaseURL) + return CachedFileDBManager(database: database) + } +} diff --git a/CryptomatorFileProvider/DB/CachedFileDBManager.swift b/CryptomatorFileProvider/DB/CachedFileDBManager.swift index 0d2fd3769..4ea85d6a6 100644 --- a/CryptomatorFileProvider/DB/CachedFileDBManager.swift +++ b/CryptomatorFileProvider/DB/CachedFileDBManager.swift @@ -6,6 +6,7 @@ // Copyright © 2020 Skymatic GmbH. All rights reserved. // +import FileProvider import Foundation import GRDB @@ -24,6 +25,13 @@ extension CachedFileManager { } return try getLocalCachedFileInfo(for: id) } + + func removeCachedFile(for itemIdentifier: NSFileProviderItemIdentifier) throws { + guard let itemID = itemIdentifier.databaseValue else { + return + } + try removeCachedFile(for: itemID) + } } enum CachedFileManagerError: Error { diff --git a/CryptomatorFileProvider/DatabaseURLProvider.swift b/CryptomatorFileProvider/DatabaseURLProvider.swift new file mode 100644 index 000000000..987b7d22c --- /dev/null +++ b/CryptomatorFileProvider/DatabaseURLProvider.swift @@ -0,0 +1,25 @@ +// +// DatabaseURLProvider.swift +// CryptomatorFileProvider +// +// Created by Philipp Schmid on 10.05.22. +// Copyright © 2022 Skymatic GmbH. All rights reserved. +// + +import FileProvider +import Foundation + +public struct DatabaseURLProvider { + public static let shared = DatabaseURLProvider(documentStorageURLProvider: NSFileProviderManager.default) + let documentStorageURLProvider: DocumentStorageURLProvider + + init(documentStorageURLProvider: DocumentStorageURLProvider) { + self.documentStorageURLProvider = documentStorageURLProvider + } + + public func getDatabaseURL(for domain: NSFileProviderDomain) -> URL { + let documentStorageURL = documentStorageURLProvider.documentStorageURL + let domainURL = documentStorageURL.appendingPathComponent(domain.pathRelativeToDocumentStorage, isDirectory: true) + return domainURL.appendingPathComponent("db.sqlite", isDirectory: false) + } +} diff --git a/CryptomatorFileProvider/FileProviderItem.swift b/CryptomatorFileProvider/FileProviderItem.swift index d2e190b10..bf1df9f5c 100644 --- a/CryptomatorFileProvider/FileProviderItem.swift +++ b/CryptomatorFileProvider/FileProviderItem.swift @@ -157,9 +157,16 @@ public class FileProviderItem: NSObject, NSFileProviderItem { return metadata.tagData } - /// Workaround to access the `isUploading` and `uploadingError` property in the `NSExtensionFileProviderActionActivationRule` + /** + Dictionary to add state information to the item. Entries are accessible to + user interaction predicates via the `Info.plist` of the FileProviderExtensionUI + + Used to enable the customized FileProviderExtensionUI actions. + */ public var userInfo: [AnyHashable: Any]? { - return ["isUploading": isUploading, - "hasUploadError": uploadingError != nil] + let isFolder = typeIdentifier == kUTTypeFolder as String + return ["enableRetryWaitingUploadAction": uploadingError == nil && isUploading && !isFolder, + "enableRetryFailedUploadAction": uploadingError != nil, + "enableEvictFileFromCacheAction": !isUploading && !isDownloading && isDownloaded && !isFolder] } } diff --git a/CryptomatorFileProvider/NSFileProviderDomainProvider.swift b/CryptomatorFileProvider/NSFileProviderDomainProvider.swift new file mode 100644 index 000000000..af148cbf5 --- /dev/null +++ b/CryptomatorFileProvider/NSFileProviderDomainProvider.swift @@ -0,0 +1,22 @@ +// +// NSFileProviderDomainProvider.swift +// CryptomatorFileProvider +// +// Created by Philipp Schmid on 10.05.22. +// Copyright © 2022 Skymatic GmbH. All rights reserved. +// + +import CryptomatorCommonCore +import FileProvider +import Foundation +import Promises + +protocol NSFileProviderDomainProvider { + func getDomains() -> Promise<[NSFileProviderDomain]> +} + +extension NSFileProviderManager: NSFileProviderDomainProvider { + func getDomains() -> Promise<[NSFileProviderDomain]> { + return NSFileProviderManager.getDomains() + } +} diff --git a/CryptomatorFileProvider/ServiceSource/CacheManagingServiceSource.swift b/CryptomatorFileProvider/ServiceSource/CacheManagingServiceSource.swift new file mode 100644 index 000000000..441c89fb2 --- /dev/null +++ b/CryptomatorFileProvider/ServiceSource/CacheManagingServiceSource.swift @@ -0,0 +1,83 @@ +// +// CacheManagingServiceSource.swift +// CryptomatorFileProvider +// +// Created by Philipp Schmid on 10.05.22. +// Copyright © 2022 Skymatic GmbH. All rights reserved. +// + +import CocoaLumberjackSwift +import CryptomatorCommonCore +import FileProvider +import Foundation + +public class CacheManagingServiceSource: ServiceSource, CacheManaging { + let domainProvider: NSFileProviderDomainProvider + let cachedManagerFactory: CachedFileManagerFactory + let notificator: FileProviderNotificatorType? + public var getItem: ((NSFileProviderItemIdentifier) -> NSFileProviderItem?)? + + public convenience init(notificator: FileProviderNotificatorType?) { + self.init(notificator: notificator, cachedManagerFactory: CachedFileDBManagerFactory.shared, domainProvider: NSFileProviderManager.default) + } + + init(notificator: FileProviderNotificatorType?, cachedManagerFactory: CachedFileManagerFactory, domainProvider: NSFileProviderDomainProvider) { + self.notificator = notificator + self.cachedManagerFactory = cachedManagerFactory + self.domainProvider = domainProvider + super.init(serviceName: .cacheManaging, exportedInterface: NSXPCInterface(with: CacheManaging.self)) + } + + public func evictFileFromCache(with identifier: NSFileProviderItemIdentifier, reply: @escaping (NSError?) -> Void) { + guard let domainIdentifier = identifier.domainIdentifier else { + DDLogError("Evict file from cache no domainIdentifier found for itemIdentifier: \(identifier)") + reply(FileProviderXPCConnectorError.domainNotFound as NSError) + return + } + let domain = NSFileProviderDomain(identifier: domainIdentifier) + do { + let cacheManager = try cachedManagerFactory.createCachedFileManager(for: domain) + try cacheManager.removeCachedFile(for: identifier) + if let item = getItem?(identifier) { + notificator?.signalUpdate(for: item) + } + reply(nil) + } catch { + DDLogError("Evict file from cache failed with error: \(error)") + reply(error as NSError) + } + } + + public func clearCache(reply: @escaping (NSError?) -> Void) { + domainProvider.getDomains().then { domains in + try self.clearCache(for: domains) + reply(nil) + }.catch { + reply($0 as NSError) + } + } + + public func getLocalCacheSizeInBytes(reply: @escaping (NSNumber?, NSError?) -> Void) { + domainProvider.getDomains().then { domains in + let totalCacheSize = try self.getLocalCacheSizeInBytes(for: domains) + reply(totalCacheSize as NSNumber, nil) + }.catch { + reply(nil, $0 as NSError) + } + } + + private func clearCache(for domains: [NSFileProviderDomain]) throws { + try domains.forEach { + let cacheManager = try cachedManagerFactory.createCachedFileManager(for: $0) + try cacheManager.clearCache() + } + } + + private func getLocalCacheSizeInBytes(for domains: [NSFileProviderDomain]) throws -> Int { + return try domains.reduce(0) { + let cacheManager = try cachedManagerFactory.createCachedFileManager(for: $1) + let cacheSizeInBytes = try cacheManager.getLocalCacheSizeInBytes() + return $0 + cacheSizeInBytes + } + } +} diff --git a/CryptomatorFileProviderTests/FileProviderItemTests.swift b/CryptomatorFileProviderTests/FileProviderItemTests.swift index 1ff19ecd6..3f4795d4e 100644 --- a/CryptomatorFileProviderTests/FileProviderItemTests.swift +++ b/CryptomatorFileProviderTests/FileProviderItemTests.swift @@ -156,4 +156,95 @@ class FileProviderItemTests: XCTestCase { let item = FileProviderItem(metadata: metadata, domainIdentifier: .test, fullVersionChecker: fullVersionCheckerMock) XCTAssertEqual(NSFileProviderItemCapabilities.allowsDeleting, item.capabilities) } + + // MARK: Evict File From Cache Action + + func testEvictFileFromCacheActionEnabled() throws { + let item = try createLocallyCachedFileProviderItem(with: .isUploaded) + let userInfo = try XCTUnwrap(item.userInfo) + XCTAssert(userInfo["enableEvictFileFromCacheAction"] as? Bool ?? false) + } + + func testEvictFileFromCacheActionDisabledForUploadingItem() throws { + let item = try createLocallyCachedFileProviderItem(with: .isUploading) + let userInfo = try XCTUnwrap(item.userInfo) + XCTAssertFalse(userInfo["enableEvictFileFromCacheAction"] as? Bool ?? true) + } + + func testEvictFileFromCacheActionDisabledForDownloadingItem() throws { + let item = try createLocallyCachedFileProviderItem(with: .isDownloading) + let userInfo = try XCTUnwrap(item.userInfo) + XCTAssertFalse(userInfo["enableEvictFileFromCacheAction"] as? Bool ?? true) + } + + func testEvictFileFromCacheActionDisabledForNotCachedFile() throws { + let cloudPath = CloudPath("/test") + let metadata = ItemMetadata(id: 2, name: "test", type: .file, size: 100, parentID: ItemMetadataDBManager.rootContainerId, lastModifiedDate: nil, statusCode: .isDownloading, cloudPath: cloudPath, isPlaceholderItem: false) + let item = FileProviderItem(metadata: metadata, domainIdentifier: .test) + let userInfo = try XCTUnwrap(item.userInfo) + XCTAssertFalse(userInfo["enableEvictFileFromCacheAction"] as? Bool ?? true) + } + + func testEvictFileFromCacheActionDisabledForFolder() throws { + let cloudPath = CloudPath("/test") + let metadata = ItemMetadata(id: 2, name: "test", type: .folder, size: 100, parentID: ItemMetadataDBManager.rootContainerId, lastModifiedDate: nil, statusCode: .isDownloading, cloudPath: cloudPath, isPlaceholderItem: false) + let item = FileProviderItem(metadata: metadata, domainIdentifier: .test) + let userInfo = try XCTUnwrap(item.userInfo) + XCTAssertFalse(userInfo["enableEvictFileFromCacheAction"] as? Bool ?? true) + } + + // - MARK: Retry Failed Upload Action + + func testRetryFailedUploadActionEnabled() throws { + let cloudPath = CloudPath("/test") + let metadata = ItemMetadata(id: 2, name: "test", type: .file, size: 100, parentID: ItemMetadataDBManager.rootContainerId, lastModifiedDate: nil, statusCode: .uploadError, cloudPath: cloudPath, isPlaceholderItem: false) + let item = FileProviderItem(metadata: metadata, domainIdentifier: .test, error: NSFileProviderError(.insufficientQuota)._nsError) + let userInfo = try XCTUnwrap(item.userInfo) + XCTAssertTrue(userInfo["enableRetryFailedUploadAction"] as? Bool ?? false) + } + + func testRetryFailedUploadActionDisabled() throws { + let cloudPath = CloudPath("/test") + let metadata = ItemMetadata(id: 2, name: "test", type: .file, size: 100, parentID: ItemMetadataDBManager.rootContainerId, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) + let item = FileProviderItem(metadata: metadata, domainIdentifier: .test) + let userInfo = try XCTUnwrap(item.userInfo) + XCTAssertFalse(userInfo["enableRetryFailedUploadAction"] as? Bool ?? true) + } + + // - MARK: Retry Waiting Upload Action + + func testRetryWaitingUploadActionEnabled() throws { + let cloudPath = CloudPath("/test") + let metadata = ItemMetadata(id: 2, name: "test", type: .file, size: 100, parentID: ItemMetadataDBManager.rootContainerId, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: cloudPath, isPlaceholderItem: false) + let item = FileProviderItem(metadata: metadata, domainIdentifier: .test) + let userInfo = try XCTUnwrap(item.userInfo) + XCTAssert(userInfo["enableRetryWaitingUploadAction"] as? Bool ?? false) + } + + func testRetryWaitingUploadActionDisabledForFolder() throws { + let cloudPath = CloudPath("/test") + let metadata = ItemMetadata(id: 2, name: "test", type: .folder, size: 100, parentID: ItemMetadataDBManager.rootContainerId, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: cloudPath, isPlaceholderItem: false) + let item = FileProviderItem(metadata: metadata, domainIdentifier: .test) + let userInfo = try XCTUnwrap(item.userInfo) + XCTAssertFalse(userInfo["enableRetryWaitingUploadAction"] as? Bool ?? true) + } + + func testRetryWaitingUploadActionDisabledForUploadError() throws { + let cloudPath = CloudPath("/test") + let metadata = ItemMetadata(id: 2, name: "test", type: .file, size: 100, parentID: ItemMetadataDBManager.rootContainerId, lastModifiedDate: nil, statusCode: .uploadError, cloudPath: cloudPath, isPlaceholderItem: false) + let item = FileProviderItem(metadata: metadata, domainIdentifier: .test, error: NSFileProviderError(.insufficientQuota)._nsError) + let userInfo = try XCTUnwrap(item.userInfo) + XCTAssertFalse(userInfo["enableRetryWaitingUploadAction"] as? Bool ?? true) + } + + private func createLocallyCachedFileProviderItem(with statusCode: ItemStatus) throws -> FileProviderItem { + let tmpDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + try FileManager.default.createDirectory(at: tmpDir, withIntermediateDirectories: false) + let localURL = tmpDir.appendingPathComponent("test.txt") + try "Foo".write(to: localURL, atomically: true, encoding: .utf8) + + let cloudPath = CloudPath("/test") + let metadata = ItemMetadata(id: 2, name: "test", type: .file, size: 100, parentID: ItemMetadataDBManager.rootContainerId, lastModifiedDate: nil, statusCode: statusCode, cloudPath: cloudPath, isPlaceholderItem: false) + return FileProviderItem(metadata: metadata, domainIdentifier: .test, localURL: localURL) + } } diff --git a/CryptomatorFileProviderTests/Mocks/CacheManagerMock.swift b/CryptomatorFileProviderTests/Mocks/CacheManagerMock.swift new file mode 100644 index 000000000..110d754bb --- /dev/null +++ b/CryptomatorFileProviderTests/Mocks/CacheManagerMock.swift @@ -0,0 +1,119 @@ +// +// CacheManagerMock.swift +// CryptomatorFileProviderTests +// +// Created by Philipp Schmid on 10.05.22. +// Copyright © 2022 Skymatic GmbH. All rights reserved. +// + +import Foundation +@testable import CryptomatorFileProvider + +// swiftlint:disable all +final class CachedFileManagerMock: CachedFileManager { + // MARK: - getLocalCachedFileInfo + + var getLocalCachedFileInfoForThrowableError: Error? + var getLocalCachedFileInfoForCallsCount = 0 + var getLocalCachedFileInfoForCalled: Bool { + getLocalCachedFileInfoForCallsCount > 0 + } + + var getLocalCachedFileInfoForReceivedId: Int64? + var getLocalCachedFileInfoForReceivedInvocations: [Int64] = [] + var getLocalCachedFileInfoForReturnValue: LocalCachedFileInfo? + var getLocalCachedFileInfoForClosure: ((Int64) throws -> LocalCachedFileInfo?)? + + func getLocalCachedFileInfo(for id: Int64) throws -> LocalCachedFileInfo? { + if let error = getLocalCachedFileInfoForThrowableError { + throw error + } + getLocalCachedFileInfoForCallsCount += 1 + getLocalCachedFileInfoForReceivedId = id + getLocalCachedFileInfoForReceivedInvocations.append(id) + return try getLocalCachedFileInfoForClosure.map({ try $0(id) }) ?? getLocalCachedFileInfoForReturnValue + } + + // MARK: - cacheLocalFileInfo + + var cacheLocalFileInfoForLocalURLLastModifiedDateThrowableError: Error? + var cacheLocalFileInfoForLocalURLLastModifiedDateCallsCount = 0 + var cacheLocalFileInfoForLocalURLLastModifiedDateCalled: Bool { + cacheLocalFileInfoForLocalURLLastModifiedDateCallsCount > 0 + } + + var cacheLocalFileInfoForLocalURLLastModifiedDateReceivedArguments: (id: Int64, localURL: URL, lastModifiedDate: Date?)? + var cacheLocalFileInfoForLocalURLLastModifiedDateReceivedInvocations: [(id: Int64, localURL: URL, lastModifiedDate: Date?)] = [] + var cacheLocalFileInfoForLocalURLLastModifiedDateClosure: ((Int64, URL, Date?) throws -> Void)? + + func cacheLocalFileInfo(for id: Int64, localURL: URL, lastModifiedDate: Date?) throws { + if let error = cacheLocalFileInfoForLocalURLLastModifiedDateThrowableError { + throw error + } + cacheLocalFileInfoForLocalURLLastModifiedDateCallsCount += 1 + cacheLocalFileInfoForLocalURLLastModifiedDateReceivedArguments = (id: id, localURL: localURL, lastModifiedDate: lastModifiedDate) + cacheLocalFileInfoForLocalURLLastModifiedDateReceivedInvocations.append((id: id, localURL: localURL, lastModifiedDate: lastModifiedDate)) + try cacheLocalFileInfoForLocalURLLastModifiedDateClosure?(id, localURL, lastModifiedDate) + } + + // MARK: - removeCachedFile + + var removeCachedFileForThrowableError: Error? + var removeCachedFileForCallsCount = 0 + var removeCachedFileForCalled: Bool { + removeCachedFileForCallsCount > 0 + } + + var removeCachedFileForReceivedId: Int64? + var removeCachedFileForReceivedInvocations: [Int64] = [] + var removeCachedFileForClosure: ((Int64) throws -> Void)? + + func removeCachedFile(for id: Int64) throws { + if let error = removeCachedFileForThrowableError { + throw error + } + removeCachedFileForCallsCount += 1 + removeCachedFileForReceivedId = id + removeCachedFileForReceivedInvocations.append(id) + try removeCachedFileForClosure?(id) + } + + // MARK: - clearCache + + var clearCacheThrowableError: Error? + var clearCacheCallsCount = 0 + var clearCacheCalled: Bool { + clearCacheCallsCount > 0 + } + + var clearCacheClosure: (() throws -> Void)? + + func clearCache() throws { + if let error = clearCacheThrowableError { + throw error + } + clearCacheCallsCount += 1 + try clearCacheClosure?() + } + + // MARK: - getLocalCacheSizeInBytes + + var getLocalCacheSizeInBytesThrowableError: Error? + var getLocalCacheSizeInBytesCallsCount = 0 + var getLocalCacheSizeInBytesCalled: Bool { + getLocalCacheSizeInBytesCallsCount > 0 + } + + var getLocalCacheSizeInBytesReturnValue: Int! + var getLocalCacheSizeInBytesClosure: (() throws -> Int)? + + func getLocalCacheSizeInBytes() throws -> Int { + if let error = getLocalCacheSizeInBytesThrowableError { + throw error + } + getLocalCacheSizeInBytesCallsCount += 1 + return try getLocalCacheSizeInBytesClosure.map({ try $0() }) ?? getLocalCacheSizeInBytesReturnValue + } +} + +// swiftlint:enable all diff --git a/CryptomatorFileProviderTests/Mocks/CachedFileManagerFactoryMock.swift b/CryptomatorFileProviderTests/Mocks/CachedFileManagerFactoryMock.swift new file mode 100644 index 000000000..f9b606aec --- /dev/null +++ b/CryptomatorFileProviderTests/Mocks/CachedFileManagerFactoryMock.swift @@ -0,0 +1,36 @@ +// +// CachedFileManagerFactoryMock.swift +// CryptomatorFileProviderTests +// +// Created by Philipp Schmid on 10.05.22. +// Copyright © 2022 Skymatic GmbH. All rights reserved. +// + +import FileProvider +import Foundation +@testable import CryptomatorFileProvider + +final class CachedFileManagerFactoryMock: CachedFileManagerFactory { + // MARK: - createCachedFileManager + + var createCachedFileManagerForThrowableError: Error? + var createCachedFileManagerForCallsCount = 0 + var createCachedFileManagerForCalled: Bool { + createCachedFileManagerForCallsCount > 0 + } + + var createCachedFileManagerForReceivedDomain: NSFileProviderDomain? + var createCachedFileManagerForReceivedInvocations: [NSFileProviderDomain] = [] + var createCachedFileManagerForReturnValue: CachedFileManager! + var createCachedFileManagerForClosure: ((NSFileProviderDomain) throws -> CachedFileManager)? + + func createCachedFileManager(for domain: NSFileProviderDomain) throws -> CachedFileManager { + if let error = createCachedFileManagerForThrowableError { + throw error + } + createCachedFileManagerForCallsCount += 1 + createCachedFileManagerForReceivedDomain = domain + createCachedFileManagerForReceivedInvocations.append(domain) + return try createCachedFileManagerForClosure.map({ try $0(domain) }) ?? createCachedFileManagerForReturnValue + } +} diff --git a/CryptomatorFileProviderTests/Mocks/NSFileProviderDomainProviderMock.swift b/CryptomatorFileProviderTests/Mocks/NSFileProviderDomainProviderMock.swift new file mode 100644 index 000000000..b47207c9d --- /dev/null +++ b/CryptomatorFileProviderTests/Mocks/NSFileProviderDomainProviderMock.swift @@ -0,0 +1,37 @@ +// +// NSFileProviderDomainProviderMock.swift +// CryptomatorFileProviderTests +// +// Created by Philipp Schmid on 10.05.22. +// Copyright © 2022 Skymatic GmbH. All rights reserved. +// + +import FileProvider +import Foundation +import Promises +@testable import CryptomatorFileProvider + +// swiftlint:disable all + +final class NSFileProviderDomainProviderMock: NSFileProviderDomainProvider { + // MARK: - getDomains + + var getDomainsThrowableError: Error? + var getDomainsCallsCount = 0 + var getDomainsCalled: Bool { + getDomainsCallsCount > 0 + } + + var getDomainsReturnValue: Promise<[NSFileProviderDomain]>! + var getDomainsClosure: (() -> Promise<[NSFileProviderDomain]>)? + + func getDomains() -> Promise<[NSFileProviderDomain]> { + if let error = getDomainsThrowableError { + return Promise(error) + } + getDomainsCallsCount += 1 + return getDomainsClosure.map({ $0() }) ?? getDomainsReturnValue + } +} + +// swiftlint:enable all diff --git a/CryptomatorFileProviderTests/ServiceSource/CacheManagingServiceSourceTests.swift b/CryptomatorFileProviderTests/ServiceSource/CacheManagingServiceSourceTests.swift new file mode 100644 index 000000000..1a47091c3 --- /dev/null +++ b/CryptomatorFileProviderTests/ServiceSource/CacheManagingServiceSourceTests.swift @@ -0,0 +1,117 @@ +// +// CacheManagingServiceSourceTests.swift +// CryptomatorFileProviderTests +// +// Created by Philipp Schmid on 10.05.22. +// Copyright © 2022 Skymatic GmbH. All rights reserved. +// + +import CryptomatorCloudAccessCore +import Promises +import XCTest +@testable import CryptomatorFileProvider + +class CacheManagingServiceSourceTests: XCTestCase { + var serviceSource: CacheManagingServiceSource! + var cacheManagerFactoryMock: CachedFileManagerFactoryMock! + var domainProviderMock: NSFileProviderDomainProviderMock! + var notificatorMock: FileProviderNotificatorTypeMock! + let domains = [NSFileProviderDomain(identifier: NSFileProviderDomainIdentifier("1")), + NSFileProviderDomain(identifier: NSFileProviderDomainIdentifier("2"))] + + override func setUpWithError() throws { + cacheManagerFactoryMock = CachedFileManagerFactoryMock() + domainProviderMock = NSFileProviderDomainProviderMock() + notificatorMock = FileProviderNotificatorTypeMock() + serviceSource = CacheManagingServiceSource(notificator: notificatorMock, cachedManagerFactory: cacheManagerFactoryMock, domainProvider: domainProviderMock) + } + + func testClearCache() { + let expectation = XCTestExpectation() + let cacheManagerMocks = [CachedFileManagerMock(), + CachedFileManagerMock()] + domainProviderMock.getDomainsReturnValue = Promise(domains) + cacheManagerFactoryMock.createCachedFileManagerForClosure = { domain in + guard let index = self.domains.firstIndex(of: domain) else { + throw NSError(domain: "TestError", code: -100, userInfo: nil) + } + return cacheManagerMocks[index] + } + + serviceSource.clearCache { error in + XCTAssertNil(error) + expectation.fulfill() + } + wait(for: [expectation], timeout: 1.0) + + XCTAssertEqual(1, domainProviderMock.getDomainsCallsCount) + // Assert created a CachedFileManager for every domain + XCTAssertEqual(domains, cacheManagerFactoryMock.createCachedFileManagerForReceivedInvocations) + // Assert cleared cache for every domain + XCTAssertEqual(1, cacheManagerMocks[0].clearCacheCallsCount) + XCTAssertEqual(1, cacheManagerMocks[1].clearCacheCallsCount) + } + + func testEvictFileFromCache() { + let expectation = XCTestExpectation() + let cacheManagerMock = CachedFileManagerMock() + cacheManagerFactoryMock.createCachedFileManagerForReturnValue = cacheManagerMock + let domainIdentifier = NSFileProviderDomainIdentifier("Test-Domain") + let itemID: Int64 = 2 + let itemIdentifier = NSFileProviderItemIdentifier(domainIdentifier: domainIdentifier, itemID: itemID) + let testItem = FileProviderItem(metadata: ItemMetadata(id: itemID, name: "Test", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/Test"), isPlaceholderItem: false), domainIdentifier: .test) + serviceSource.getItem = { receivedItemIdentifier in + guard itemIdentifier == receivedItemIdentifier else { + return nil + } + return testItem + } + serviceSource.evictFileFromCache(with: itemIdentifier) { error in + XCTAssertNil(error) + expectation.fulfill() + } + wait(for: [expectation], timeout: 1.0) + + XCTAssertEqual([itemID], cacheManagerMock.removeCachedFileForReceivedInvocations) + XCTAssertEqual([domainIdentifier], cacheManagerFactoryMock.createCachedFileManagerForReceivedInvocations.map { $0.identifier }) + // Assert signaled an update for the evicted item + XCTAssertEqual([testItem], notificatorMock.signalUpdateForReceivedInvocations as? [FileProviderItem]) + } + + func testGetLocalCacheSizeInBytes() { + let expectation = XCTestExpectation() + let expectedCacheSize: NSNumber = 42 + + let firstCacheManagerMock = CachedFileManagerMock() + firstCacheManagerMock.getLocalCacheSizeInBytesReturnValue = 20 + + let secondCacheManagerMock = CachedFileManagerMock() + secondCacheManagerMock.getLocalCacheSizeInBytesReturnValue = 22 + + let cacheManagerMocks = [firstCacheManagerMock, + secondCacheManagerMock] + domainProviderMock.getDomainsReturnValue = Promise(domains) + cacheManagerFactoryMock.createCachedFileManagerForClosure = { domain in + guard let index = self.domains.firstIndex(of: domain) else { + throw NSError(domain: "TestError", code: -100, userInfo: nil) + } + return cacheManagerMocks[index] + } + + serviceSource.getLocalCacheSizeInBytes { actualCacheSize, error in + XCTAssertNil(error) + XCTAssertEqual(expectedCacheSize, actualCacheSize) + expectation.fulfill() + } + wait(for: [expectation], timeout: 1.0) + + XCTAssertEqual(1, domainProviderMock.getDomainsCallsCount) + // Assert created a CachedFileManager for every domain + XCTAssertEqual(domains, cacheManagerFactoryMock.createCachedFileManagerForReceivedInvocations) + + // Assert called `getLocalCacheSizeInBytes()` for every domain + XCTAssertEqual(1, firstCacheManagerMock.getLocalCacheSizeInBytesCallsCount) + XCTAssertEqual(1, secondCacheManagerMock.getLocalCacheSizeInBytesCallsCount) + XCTAssertFalse(notificatorMock.signalUpdateForCalled) + } +} diff --git a/CryptomatorFileProviderTests/LogLevelUpdatingServiceSourceTests.swift b/CryptomatorFileProviderTests/ServiceSource/LogLevelUpdatingServiceSourceTests.swift similarity index 100% rename from CryptomatorFileProviderTests/LogLevelUpdatingServiceSourceTests.swift rename to CryptomatorFileProviderTests/ServiceSource/LogLevelUpdatingServiceSourceTests.swift diff --git a/CryptomatorFileProviderTests/UploadRetryingServiceSourceTests.swift b/CryptomatorFileProviderTests/ServiceSource/UploadRetryingServiceSourceTests.swift similarity index 100% rename from CryptomatorFileProviderTests/UploadRetryingServiceSourceTests.swift rename to CryptomatorFileProviderTests/ServiceSource/UploadRetryingServiceSourceTests.swift diff --git a/CryptomatorTests/SettingsViewModelTests.swift b/CryptomatorTests/SettingsViewModelTests.swift index 5371c359b..848651685 100644 --- a/CryptomatorTests/SettingsViewModelTests.swift +++ b/CryptomatorTests/SettingsViewModelTests.swift @@ -13,16 +13,19 @@ import XCTest @testable import CryptomatorFileProvider class SettingsViewModelTests: XCTestCase { - private var cacheManagerMock: FileProviderCacheManagerMock! private var cryptomatorSettingsMock: CryptomatorSettingsMock! private var fileProviderConnectorMock: FileProviderConnectorMock! var settingsViewModel: SettingsViewModel! + var cacheManagerMock: CacheManagingMock! override func setUpWithError() throws { - cacheManagerMock = FileProviderCacheManagerMock() + cacheManagerMock = CacheManagingMock() + cacheManagerMock.clearCacheReplyClosure = { reply in + reply(nil) + } cryptomatorSettingsMock = CryptomatorSettingsMock() fileProviderConnectorMock = FileProviderConnectorMock() - settingsViewModel = SettingsViewModel(cacheManager: cacheManagerMock, cryptomatorSetttings: cryptomatorSettingsMock, fileProviderConnector: fileProviderConnectorMock) + settingsViewModel = SettingsViewModel(cryptomatorSettings: cryptomatorSettingsMock, fileProviderConnector: fileProviderConnectorMock) } // - MARK: Cache Section @@ -54,7 +57,7 @@ class SettingsViewModelTests: XCTestCase { func testRefreshCacheSize() throws { let expectation = XCTestExpectation() let cacheSizeInBytes = 1024 * 1024 - cacheManagerMock.totalCacheSizeInBytes = cacheSizeInBytes + setCacheManagingResponse(to: cacheSizeInBytes) settingsViewModel.refreshCacheSize().always { expectation.fulfill() } @@ -81,6 +84,7 @@ class SettingsViewModelTests: XCTestCase { func testRefreshCacheSizeForEmptyCache() throws { let expectation = XCTestExpectation() + setCacheManagingResponse(to: 0) settingsViewModel.refreshCacheSize().always { expectation.fulfill() } @@ -91,13 +95,14 @@ class SettingsViewModelTests: XCTestCase { func testClearCache() throws { let expectation = XCTestExpectation() - cacheManagerMock.totalCacheSizeInBytes = 1024 * 1024 + let cacheSizeInBytes = 0 + setCacheManagingResponse(to: cacheSizeInBytes) settingsViewModel.clearCache().always { expectation.fulfill() } wait(for: [expectation], timeout: 1.0) - XCTAssertTrue(cacheManagerMock.clearedCache) + XCTAssertEqual(1, cacheManagerMock.clearCacheReplyCallsCount) checkEmptyCacheBehaviour() } @@ -254,29 +259,12 @@ class SettingsViewModelTests: XCTestCase { private func getSection(for identifier: SettingsSection) -> Section? { return settingsViewModel.sections.filter({ $0.id == identifier }).first } -} - -private class FileProviderCacheManagerMock: FileProviderCacheManager { - var totalCacheSizeInBytes = 0 - var clearedCache = false - init() { - super.init(documentStorageURLProvider: DocumentStorageURLProviderStub()) - } - - override func getTotalLocalCacheSizeInBytes() -> Promise { - return Promise(totalCacheSizeInBytes) - } - override func clearCache() -> Promise { - clearedCache = true - totalCacheSizeInBytes = 0 - return Promise(()) - } -} - -private class DocumentStorageURLProviderStub: DocumentStorageURLProvider { - var documentStorageURL: URL { - fatalError("not implemented") + private func setCacheManagingResponse(to totalCacheSizeInBytes: Int) { + cacheManagerMock.getLocalCacheSizeInBytesReplyClosure = { reply in + reply(totalCacheSizeInBytes as NSNumber, nil) + } + fileProviderConnectorMock.proxy = cacheManagerMock } } diff --git a/FileProviderExtension/FileProviderExtension.swift b/FileProviderExtension/FileProviderExtension.swift index 5a47db1ff..f1eea8c4c 100644 --- a/FileProviderExtension/FileProviderExtension.swift +++ b/FileProviderExtension/FileProviderExtension.swift @@ -264,6 +264,11 @@ class FileProviderExtension: NSFileProviderExtension { serviceSources.append(UploadRetryingServiceSource(domain: domain, notificator: notificator, dbPath: dbPath, delegate: localURLProvider)) } #endif + let cacheManagingServiceSource = CacheManagingServiceSource(notificator: notificator) + cacheManagingServiceSource.getItem = { [weak self] itemIdentifier in + try? self?.item(for: itemIdentifier) + } + serviceSources.append(cacheManagingServiceSource) serviceSources.append(VaultLockingServiceSource()) serviceSources.append(LogLevelUpdatingServiceSource()) return serviceSources diff --git a/FileProviderExtensionUI/Info.plist b/FileProviderExtensionUI/Info.plist index 195dcf492..c695c1640 100644 --- a/FileProviderExtensionUI/Info.plist +++ b/FileProviderExtensionUI/Info.plist @@ -40,7 +40,7 @@ NSExtensionFileProviderActionName Retry Upload NSExtensionFileProviderActionActivationRule - SUBQUERY( fileproviderItems, $fileproviderItem, $fileproviderItem.userInfo."hasUploadError" == YES).@count > 0 + SUBQUERY( fileproviderItems, $fileproviderItem, $fileproviderItem.userInfo."enableRetryFailedUploadAction" == YES).@count > 0 NSExtensionFileProviderActionIdentifier @@ -48,7 +48,15 @@ NSExtensionFileProviderActionName Retry Upload NSExtensionFileProviderActionActivationRule - SUBQUERY( fileproviderItems, $fileproviderItem, $fileproviderItem.userInfo."isUploading" == YES).@count > 0 + SUBQUERY( fileproviderItems, $fileproviderItem, $fileproviderItem.userInfo."enableRetryWaitingUploadAction" == YES).@count > 0 + + + NSExtensionFileProviderActionIdentifier + org.cryptomator.ios.fileprovider.evictFileFromCache + NSExtensionFileProviderActionName + Clear from Cache + NSExtensionFileProviderActionActivationRule + SUBQUERY( fileproviderItems, $fileproviderItem, $fileproviderItem.userInfo."enableEvictFileFromCacheAction" == YES).@count > 0 diff --git a/FileProviderExtensionUI/RootViewController.swift b/FileProviderExtensionUI/RootViewController.swift index 3f8c55c7f..71d940827 100644 --- a/FileProviderExtensionUI/RootViewController.swift +++ b/FileProviderExtensionUI/RootViewController.swift @@ -114,9 +114,8 @@ class RootViewController: FPUIActionExtensionViewController { func showUploadProgressAlert(for itemIdentifiers: [NSFileProviderItemIdentifier], domainIdentifier: NSFileProviderDomainIdentifier) { let getXPCPromise: Promise> = FileProviderXPCConnector.shared.getXPC(serviceName: .uploadRetryingService, domainIdentifier: domainIdentifier) - let progressAlert = RetryUploadAlertControllerFactory.createUploadProgressAlert(dismissAction: { - [weak self] in - self?.cancel() + let progressAlert = RetryUploadAlertControllerFactory.createUploadProgressAlert(dismissAction: { [weak self] in + self?.cancel() }, retryAction: { [weak self] in self?.retryUpload(for: itemIdentifiers, domainIdentifier: domainIdentifier) }) @@ -131,6 +130,46 @@ class RootViewController: FPUIActionExtensionViewController { present(progressAlert, animated: true) } + func showEvictFileFromCacheAlert(for itemIdentifiers: [NSFileProviderItemIdentifier], domainIdentifier: NSFileProviderDomainIdentifier) { + let alertController = UIAlertController(title: LocalizedString.getValue("fileProvider.clearFileFromCache.title"), + message: LocalizedString.getValue("fileProvider.clearFileFromCache.message"), + preferredStyle: .alert) + let deleteAction = UIAlertAction(title: LocalizedString.getValue("common.button.clear"), style: .destructive) { _ in + alertController.dismiss(animated: true) { + self.evictFilesFromCache(with: itemIdentifiers, domainIdentifier: domainIdentifier) + } + } + let cancelAction = UIAlertAction(title: LocalizedString.getValue("common.button.cancel"), style: .cancel) { _ in + self.cancel() + } + alertController.addAction(deleteAction) + alertController.addAction(cancelAction) + alertController.preferredAction = cancelAction + + present(alertController, animated: true) + } + + func evictFilesFromCache(with itemIdentifiers: [NSFileProviderItemIdentifier], domainIdentifier: NSFileProviderDomainIdentifier) { + let getXPCPromise: Promise> = FileProviderXPCConnector.shared.getXPC(serviceName: .cacheManaging, domainIdentifier: domainIdentifier) + getXPCPromise.then { xpc in + xpc.proxy.evictFilesFromCache(with: itemIdentifiers) + }.catch { error in + let alertController = UIAlertController(title: LocalizedString.getValue("common.alert.error.title"), + message: error.localizedDescription, + preferredStyle: .alert) + let okAction = UIAlertAction(title: LocalizedString.getValue("common.button.ok"), style: .default) { _ in + self.extensionContext.completeRequest() + } + alertController.addAction(okAction) + alertController.preferredAction = okAction + self.present(alertController, animated: true) + }.then { + self.extensionContext.completeRequest() + }.always { + FileProviderXPCConnector.shared.invalidateXPC(getXPCPromise) + } + } + // MARK: - FPUIActionExtensionViewController override func prepare(forError error: Error) { @@ -152,6 +191,12 @@ class RootViewController: FPUIActionExtensionViewController { } else { showDomainNotFoundAlert() } + case .evictFileFromCache: + if let domainIdentifier = itemIdentifiers.first?.domainIdentifier { + showEvictFileFromCacheAlert(for: itemIdentifiers, domainIdentifier: domainIdentifier) + } else { + showDomainNotFoundAlert() + } case .none: let error = NSError(domain: NSCocoaErrorDomain, code: NSFeatureUnsupportedError, userInfo: [:]) extensionContext.cancelRequest(withError: error) diff --git a/SharedResources/en.lproj/Localizable.strings b/SharedResources/en.lproj/Localizable.strings index 81665e013..8df12af0b 100644 --- a/SharedResources/en.lproj/Localizable.strings +++ b/SharedResources/en.lproj/Localizable.strings @@ -10,6 +10,7 @@ "common.button.cancel" = "Cancel"; "common.button.change" = "Change"; "common.button.choose" = "Choose"; +"common.button.clear" = "Clear"; "common.button.close" = "Close"; "common.button.confirm" = "Confirm"; "common.button.create" = "Create"; @@ -97,6 +98,8 @@ "fileProvider.error.defaultLock.title" = "Unlock Required"; "fileProvider.error.defaultLock.message" = "To access and show the contents of your vault, it has to be unlocked."; "fileProvider.error.unlockButton" = "Unlock"; +"fileProvider.clearFileFromCache.title" = "Clear File from Cache"; +"fileProvider.clearFileFromCache.message" = "This only removes the local file from your device and does not delete the file in the cloud."; "fileProvider.uploadProgress.connecting" = "Connecting…"; "fileProvider.uploadProgress.message" = "Current Progress: %@\n\nIf you're noticing that the upload progress is stuck, you can retry the upload."; "fileProvider.uploadProgress.missing" = "Progress could not be determined. It may still be running in the background."; @@ -264,3 +267,4 @@ "webDAVAuthenticator.error.untrustedCertificate" = "Certificate of this server is untrusted. You may have to re-add this WebDAV connection."; "Retry Upload" = "Retry Upload"; +"Clear from Cache" = "Clear from Cache"; From 1f4b9e8381ffa0a39e1e32dbb2ce8f45d6233998 Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Mon, 16 May 2022 15:33:26 +0200 Subject: [PATCH 11/18] Updated file provider action identifiers --- CryptomatorFileProvider/Actions/FileProviderAction.swift | 6 +++--- FileProviderExtensionUI/Info.plist | 6 +++--- FileProviderExtensionUI/RootViewController.swift | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CryptomatorFileProvider/Actions/FileProviderAction.swift b/CryptomatorFileProvider/Actions/FileProviderAction.swift index a93f40ced..1b9da43f9 100644 --- a/CryptomatorFileProvider/Actions/FileProviderAction.swift +++ b/CryptomatorFileProvider/Actions/FileProviderAction.swift @@ -9,7 +9,7 @@ import Foundation public enum FileProviderAction: String { - case retryUpload = "org.cryptomator.ios.fileprovider.retryUpload" - case retryWaitingUpload = "org.cryptomator.ios.fileprovider.retryWaitingUpload" - case evictFileFromCache = "org.cryptomator.ios.fileprovider.evictFileFromCache" + case retryFailedUpload = "org.cryptomator.ios.fileprovider.retry-failed-upload" + case retryWaitingUpload = "org.cryptomator.ios.fileprovider.retry-waiting-upload" + case evictFileFromCache = "org.cryptomator.ios.fileprovider.evict-file-from-cache" } diff --git a/FileProviderExtensionUI/Info.plist b/FileProviderExtensionUI/Info.plist index c695c1640..013fb353f 100644 --- a/FileProviderExtensionUI/Info.plist +++ b/FileProviderExtensionUI/Info.plist @@ -36,7 +36,7 @@ NSExtensionFileProviderActionIdentifier - org.cryptomator.ios.fileprovider.retryUpload + org.cryptomator.ios.fileprovider.retry-failed-upload NSExtensionFileProviderActionName Retry Upload NSExtensionFileProviderActionActivationRule @@ -44,7 +44,7 @@ NSExtensionFileProviderActionIdentifier - org.cryptomator.ios.fileprovider.retryWaitingUpload + org.cryptomator.ios.fileprovider.retry-waiting-upload NSExtensionFileProviderActionName Retry Upload NSExtensionFileProviderActionActivationRule @@ -52,7 +52,7 @@ NSExtensionFileProviderActionIdentifier - org.cryptomator.ios.fileprovider.evictFileFromCache + org.cryptomator.ios.fileprovider.evict-file-from-cache NSExtensionFileProviderActionName Clear from Cache NSExtensionFileProviderActionActivationRule diff --git a/FileProviderExtensionUI/RootViewController.swift b/FileProviderExtensionUI/RootViewController.swift index 71d940827..2da0ac122 100644 --- a/FileProviderExtensionUI/RootViewController.swift +++ b/FileProviderExtensionUI/RootViewController.swift @@ -185,7 +185,7 @@ class RootViewController: FPUIActionExtensionViewController { } else { showDomainNotFoundAlert() } - case .retryUpload: + case .retryFailedUpload: if let domainIdentifier = itemIdentifiers.first?.domainIdentifier { retryUpload(for: itemIdentifiers, domainIdentifier: domainIdentifier) } else { From 0510fc1303eb5605c7b377c7782c0c0d3e740257 Mon Sep 17 00:00:00 2001 From: Cryptobot Date: Mon, 16 May 2022 17:05:53 +0200 Subject: [PATCH 12/18] New Crowdin updates (#213) [ci skip] --- SharedResources/ar.lproj/Localizable.strings | 2 + SharedResources/bn.lproj/Localizable.strings | 2 + SharedResources/bs.lproj/Localizable.strings | 1 + SharedResources/ca.lproj/Localizable.strings | 2 + SharedResources/cs.lproj/Localizable.strings | 2 + SharedResources/de.lproj/Localizable.strings | 10 + SharedResources/el.lproj/Localizable.strings | 11 + SharedResources/es.lproj/Localizable.strings | 11 + SharedResources/fil.lproj/Localizable.strings | 2 + SharedResources/fr.lproj/Localizable.strings | 13 +- SharedResources/gl.lproj/Localizable.strings | 1 + SharedResources/he.lproj/Localizable.strings | 9 + SharedResources/hi.lproj/Localizable.strings | 2 + SharedResources/hr.lproj/Localizable.strings | 11 + SharedResources/hu.lproj/Localizable.strings | 2 + SharedResources/id.lproj/Localizable.strings | 32 +++ SharedResources/it.lproj/Localizable.strings | 14 +- SharedResources/ja.lproj/Localizable.strings | 11 + SharedResources/ko.lproj/Localizable.strings | 2 + SharedResources/lv.lproj/Localizable.strings | 1 + SharedResources/nb.lproj/Localizable.strings | 4 + SharedResources/nl.lproj/Localizable.strings | 8 + .../nn-NO.lproj/Localizable.strings | 1 + SharedResources/pa.lproj/Localizable.strings | 1 + SharedResources/pl.lproj/Localizable.strings | 15 +- .../pt-BR.lproj/Localizable.strings | 11 + SharedResources/pt.lproj/Localizable.strings | 1 + SharedResources/ro.lproj/Localizable.strings | 2 + SharedResources/ru.lproj/Localizable.strings | 10 + SharedResources/sk.lproj/Localizable.strings | 11 + .../sr-Latn.lproj/Localizable.strings | 1 + SharedResources/sr.lproj/Localizable.strings | 1 + SharedResources/sv.lproj/Localizable.strings | 11 + .../sw-TZ.lproj/Localizable.strings | 255 ++++++++++++++++++ SharedResources/ta.lproj/Localizable.strings | 1 + SharedResources/te.lproj/Localizable.strings | 1 + SharedResources/th.lproj/Localizable.strings | 1 + SharedResources/tr.lproj/Localizable.strings | 11 + SharedResources/uk.lproj/Localizable.strings | 2 + .../zh-Hans.lproj/Localizable.strings | 17 +- .../zh-Hant.lproj/Localizable.strings | 23 ++ 41 files changed, 521 insertions(+), 8 deletions(-) create mode 100644 SharedResources/sw-TZ.lproj/Localizable.strings diff --git a/SharedResources/ar.lproj/Localizable.strings b/SharedResources/ar.lproj/Localizable.strings index aae544048..50fab4151 100644 --- a/SharedResources/ar.lproj/Localizable.strings +++ b/SharedResources/ar.lproj/Localizable.strings @@ -10,6 +10,7 @@ "common.button.cancel" = "الغاء"; "common.button.change" = "تغيير"; "common.button.choose" = "اختر"; +"common.button.close" = "إغلاق"; "common.button.confirm" = "تأكيد"; "common.button.create" = "إنشاء"; "common.button.createFolder" = "إنشاء مجلد"; @@ -20,6 +21,7 @@ "common.button.next" = "التالي"; "common.button.ok" = "موافق"; "common.button.remove" = "حذف"; +"common.button.retry" = "اعد المحاولة"; "common.button.signOut" = "تسجيل الخروج"; "common.button.verify" = "التحقق"; "common.cells.openInFilesApp" = "فتح في تطبيق الملفات"; diff --git a/SharedResources/bn.lproj/Localizable.strings b/SharedResources/bn.lproj/Localizable.strings index 78d5e3871..1535cbaf0 100644 --- a/SharedResources/bn.lproj/Localizable.strings +++ b/SharedResources/bn.lproj/Localizable.strings @@ -10,6 +10,7 @@ "common.button.cancel" = "বাতিল করুন"; "common.button.change" = "পরিবর্তন করুন"; "common.button.choose" = "বাছুন"; +"common.button.close" = "বন্ধ করুন"; "common.button.confirm" = "নিশ্চিত করুন"; "common.button.create" = "তৈরি করুন"; "common.button.createFolder" = "ফোল্ডার তৈরি করুন"; @@ -20,6 +21,7 @@ "common.button.next" = "পরবর্তী"; "common.button.ok" = "আচ্ছা"; "common.button.remove" = "বাতিল"; +"common.button.retry" = "পুনরায় চেষ্টা করুন"; "common.button.signOut" = "সাইন আউট"; "common.button.verify" = "যাচাই করুন"; "common.cells.openInFilesApp" = "ফাইল আ্যপে খুলুন"; diff --git a/SharedResources/bs.lproj/Localizable.strings b/SharedResources/bs.lproj/Localizable.strings index 72eeb4e43..c8294ef1e 100644 --- a/SharedResources/bs.lproj/Localizable.strings +++ b/SharedResources/bs.lproj/Localizable.strings @@ -1,6 +1,7 @@ "common.button.cancel" = "Odustani"; "common.button.change" = "Izmjeni"; "common.button.choose" = "Odaberi"; +"common.button.close" = "Zatvori"; "common.button.confirm" = "Potvrdi"; "common.button.create" = "Dodaj"; "common.button.createFolder" = "Napravi folder"; diff --git a/SharedResources/ca.lproj/Localizable.strings b/SharedResources/ca.lproj/Localizable.strings index 81036ee44..d9d4586cc 100644 --- a/SharedResources/ca.lproj/Localizable.strings +++ b/SharedResources/ca.lproj/Localizable.strings @@ -10,6 +10,7 @@ "common.button.cancel" = "Cancel·la"; "common.button.change" = "Canvia"; "common.button.choose" = "Trieu"; +"common.button.close" = "Tanca"; "common.button.confirm" = "Confirma"; "common.button.create" = "Crear"; "common.button.createFolder" = "Crea una carpeta"; @@ -20,6 +21,7 @@ "common.button.next" = "Següent"; "common.button.ok" = "D'acord"; "common.button.remove" = "Elimina"; +"common.button.retry" = "Reintenta"; "common.button.signOut" = "Tanca la sessió"; "common.button.verify" = "Verificar"; "common.cells.openInFilesApp" = "Obrir amb l'aplicació de fitxers"; diff --git a/SharedResources/cs.lproj/Localizable.strings b/SharedResources/cs.lproj/Localizable.strings index bdc9b7846..3ffce3972 100644 --- a/SharedResources/cs.lproj/Localizable.strings +++ b/SharedResources/cs.lproj/Localizable.strings @@ -10,6 +10,7 @@ "common.button.cancel" = "Zrušit"; "common.button.change" = "Změnit"; "common.button.choose" = "Vybrat"; +"common.button.close" = "Zavřít"; "common.button.confirm" = "Potvrdit"; "common.button.create" = "Vytvořit"; "common.button.createFolder" = "Vytvořit složku"; @@ -20,6 +21,7 @@ "common.button.next" = "Další"; "common.button.ok" = "OK"; "common.button.remove" = "Odstranit"; +"common.button.retry" = "Opakovat"; "common.button.signOut" = "Odhlásit se"; "common.button.verify" = "Ověřit"; "common.cells.openInFilesApp" = "Otevřít v aplikaci Soubory"; diff --git a/SharedResources/de.lproj/Localizable.strings b/SharedResources/de.lproj/Localizable.strings index 4915f2b2a..e26e9e166 100644 --- a/SharedResources/de.lproj/Localizable.strings +++ b/SharedResources/de.lproj/Localizable.strings @@ -10,6 +10,7 @@ "common.button.cancel" = "Abbrechen"; "common.button.change" = "Ändern"; "common.button.choose" = "Auswählen"; +"common.button.close" = "Schließen"; "common.button.confirm" = "Bestätigen"; "common.button.create" = "Erstellen"; "common.button.createFolder" = "Ordner erstellen"; @@ -20,6 +21,7 @@ "common.button.next" = "Weiter"; "common.button.ok" = "OK"; "common.button.remove" = "Entfernen"; +"common.button.retry" = "Wiederholen"; "common.button.signOut" = "Ausloggen"; "common.button.verify" = "Verifizieren"; "common.cells.openInFilesApp" = "In der App „Dateien“ öffnen"; @@ -95,6 +97,10 @@ "fileProvider.error.defaultLock.title" = "Entsperren erforderlich"; "fileProvider.error.defaultLock.message" = "Um auf den Inhalt deines Tresors zuzugreifen und ihn anzuzeigen, muss dieser entsperrt werden."; "fileProvider.error.unlockButton" = "Entsperren"; +"fileProvider.uploadProgress.connecting" = "Verbindung wird hergestellt …"; +"fileProvider.uploadProgress.missing" = "Der Fortschritt konnte nicht ermittelt werden. Möglicherweise läuft der Upload noch im Hintergrund."; +"fileProvider.uploadProgress.title" = "Wird hochgeladen …"; +"fileProvider.uploadProgress.missingDomainError" = "Domain konnte nicht gefunden werden."; "keepUnlocked.alert.title" = "Tresor sperren?"; "keepUnlocked.alert.message" = "Damit diese Änderung wirksam werden kann, muss dein Tresor gesperrt werden."; @@ -149,6 +155,8 @@ "purchase.restorePurchase.fullVersionFound.alert.title" = "Wiederherstellung erfolgreich"; "purchase.restorePurchase.fullVersionNotFound.alert.title" = "Keine Vollversion"; "purchase.restorePurchase.fullVersionNotFound.alert.message" = "Wir konnten keine zuvor gekaufte Vollversion finden, die wiederhergestellt werden kann. Bitte versuche eine andere Option."; +"purchase.restorePurchase.eligibleForUpgrade.alert.title" = "Berechtigt für Upgrade"; +"purchase.restorePurchase.eligibleForUpgrade.alert.message" = "Du versuchst offenbar, eine ältere Version von Cryptomator zu aktualisieren. Wähle bitte in diesem Fall stattdessen die Option „Upgrade-Angebot“ aus."; "purchase.retry.button" = "Wiederholen"; "purchase.retry.footer" = "Verfügbare Produkte konnten nicht geladen werden."; "purchase.title" = "Vollversion freischalten"; @@ -253,3 +261,5 @@ "webDAVAuthenticator.error.unsupportedProtocol" = "Der Server scheint mit WebDAV nicht kompatibel zu sein. Bitte überprüfe, ob du die richtige URL verwendet hast."; "webDAVAuthenticator.error.untrustedCertificate" = "Das Zertifikat dieses Servers ist nicht vertrauenswürdig. Eventuell musst du diese WebDAV-Verbindung erneut hinzufügen."; + +"Retry Upload" = "Upload erneut versuchen"; diff --git a/SharedResources/el.lproj/Localizable.strings b/SharedResources/el.lproj/Localizable.strings index 809888db5..7120e5eb1 100644 --- a/SharedResources/el.lproj/Localizable.strings +++ b/SharedResources/el.lproj/Localizable.strings @@ -10,6 +10,7 @@ "common.button.cancel" = "Ακύρωση"; "common.button.change" = "Αλλαγή"; "common.button.choose" = "Επιλογή"; +"common.button.close" = "Κλείσιμο"; "common.button.confirm" = "Επιβεβαίωση"; "common.button.create" = "Δημιουργία"; "common.button.createFolder" = "Δημιουργία Φακέλου"; @@ -20,6 +21,7 @@ "common.button.next" = "Επόμενο"; "common.button.ok" = "ΟΚ"; "common.button.remove" = "Αφαίρεση"; +"common.button.retry" = "Επανάληψη"; "common.button.signOut" = "Αποσύνδεση"; "common.button.verify" = "Επαλήθευση"; "common.cells.openInFilesApp" = "Άνοιγμα στην εφαρμογή Αρχείων"; @@ -95,6 +97,11 @@ "fileProvider.error.defaultLock.title" = "Απαιτείται Ξεκλείδωμα"; "fileProvider.error.defaultLock.message" = "Για να αποκτήσετε πρόσβαση και να εμφανίσετε τα περιεχόμενα της κρύπτη σας, πρέπει να ξεκλειδωθεί."; "fileProvider.error.unlockButton" = "Ξεκλείδωμα"; +"fileProvider.uploadProgress.connecting" = "Σύνδεση…"; +"fileProvider.uploadProgress.message" = "Τρέχουσα Πρόοδος: %@%\n\nΑν παρατηρήσετε ότι η πρόοδος μεταφόρτωσης έχει κολλήσει, μπορείτε να δοκιμάσετε ξανά τη μεταφόρτωση."; +"fileProvider.uploadProgress.missing" = "Η πρόοδος δεν μπορεί να καθοριστεί. Μπορεί να εκτελείται στο παρασκήνιο."; +"fileProvider.uploadProgress.title" = "Μεταφόρτωση…"; +"fileProvider.uploadProgress.missingDomainError" = "Δεν ήταν δυνατή η εύρεση του τομέα."; "keepUnlocked.alert.title" = "Κλείδωμα Κρύπτης;"; "keepUnlocked.alert.message" = "Αυτή η αλλαγή απαιτεί η κρύπτη σας να κλειδωθεί για να τεθεί σε ισχύ."; @@ -149,6 +156,8 @@ "purchase.restorePurchase.fullVersionFound.alert.title" = "Επιτυχής Επαναφορά"; "purchase.restorePurchase.fullVersionNotFound.alert.title" = "Χωρίς Πλήρη Έκδοση"; "purchase.restorePurchase.fullVersionNotFound.alert.message" = "Δεν ήταν δυνατή η εύρεση μιας προηγούμενης πλήρους έκδοσης που θα μπορούσε να αποκατασταθεί. Παρακαλώ δοκιμάστε μια άλλη επιλογή."; +"purchase.restorePurchase.eligibleForUpgrade.alert.title" = "Επιλέξιμο για αναβάθμιση"; +"purchase.restorePurchase.eligibleForUpgrade.alert.message" = "Φαίνεται ότι προσπαθείτε να κάνετε αναβάθμιση από μια παλαιότερη έκδοση του Cryptomator. Σε αυτήν την περίπτωση, επιλέξτε την επιλογή \"Προσφορά Αναβάθμισης\"."; "purchase.retry.button" = "Επανάληψη"; "purchase.retry.footer" = "Δεν ήταν δυνατή η φόρτωση των διαθέσιμων προϊόντων."; "purchase.title" = "Ξεκλείδωμα Πλήρους Έκδοσης"; @@ -253,3 +262,5 @@ "webDAVAuthenticator.error.unsupportedProtocol" = "Ο διακομιστής δε φαίνεται να είναι συμβατός με WebDAV. Παρακαλώ ελέγξτε αν έχετε χρησιμοποιήσει τη σωστή διεύθυνση URL."; "webDAVAuthenticator.error.untrustedCertificate" = "Το πιστοποιητικό αυτού του διακομιστή δεν είναι έμπιστο. Ίσως χρειαστεί να προσθέσετε ξανά αυτή τη σύνδεση WebDAV."; + +"Retry Upload" = "Δοκιμάστε ξανά τη μεταφόρτωση"; diff --git a/SharedResources/es.lproj/Localizable.strings b/SharedResources/es.lproj/Localizable.strings index ed8e2c919..a8295a34b 100644 --- a/SharedResources/es.lproj/Localizable.strings +++ b/SharedResources/es.lproj/Localizable.strings @@ -10,6 +10,7 @@ "common.button.cancel" = "Cancelar"; "common.button.change" = "Modificar"; "common.button.choose" = "Seleccionar"; +"common.button.close" = "Cerrar"; "common.button.confirm" = "Confirmar"; "common.button.create" = "Crear"; "common.button.createFolder" = "Crear carpeta"; @@ -20,6 +21,7 @@ "common.button.next" = "Continuar"; "common.button.ok" = "Aceptar"; "common.button.remove" = "Eliminar"; +"common.button.retry" = "Reintentar"; "common.button.signOut" = "Cerrar sesión"; "common.button.verify" = "Verificar"; "common.cells.openInFilesApp" = "Abrir en la app de Archivos"; @@ -95,6 +97,11 @@ "fileProvider.error.defaultLock.title" = "Desbloqueo requerido"; "fileProvider.error.defaultLock.message" = "Para acceder y mostrar el contenido de su bóveda hay que desbloquearla."; "fileProvider.error.unlockButton" = "Desbloquear"; +"fileProvider.uploadProgress.connecting" = "Conectando…"; +"fileProvider.uploadProgress.message" = "Progreso actual: %@\n\nSi nota que el progreso de la carga está atascado, puede volver a intentar la carga."; +"fileProvider.uploadProgress.missing" = "No se pudo determinar el progreso. Puede que todavía se ejecute en segundo plano."; +"fileProvider.uploadProgress.title" = "Cargando…"; +"fileProvider.uploadProgress.missingDomainError" = "No se encontró el dominio."; "keepUnlocked.alert.title" = "¿Bloquear bóveda?"; "keepUnlocked.alert.message" = "Este cambio requiere que su bóveda esté bloqueada para que surta efecto."; @@ -149,6 +156,8 @@ "purchase.restorePurchase.fullVersionFound.alert.title" = "Restauración exitosa"; "purchase.restorePurchase.fullVersionNotFound.alert.title" = "Sin versión completa"; "purchase.restorePurchase.fullVersionNotFound.alert.message" = "No hemos podido encontrar una versión completa adquirida previamente que se pudo restaurar. Intente con otra opción."; +"purchase.restorePurchase.eligibleForUpgrade.alert.title" = "Elegible para actualizar"; +"purchase.restorePurchase.eligibleForUpgrade.alert.message" = "Parece que está intentando actualizar desde una versión anterior de Cryptomator. De ser así, seleccione la opción \"Oferta de actualización\"."; "purchase.retry.button" = "Reintentar"; "purchase.retry.footer" = "No se pudieron cargar los productos disponibles."; "purchase.title" = "Desbloquear versión completa"; @@ -253,3 +262,5 @@ "webDAVAuthenticator.error.unsupportedProtocol" = "El servidor no parece ser compatible con WebDAV. Compruebe si ha usado la URL correcta."; "webDAVAuthenticator.error.untrustedCertificate" = "El certificado de este servidor no es confiable. Es posible que tenga que volver a añadir esta conexión WebDAV."; + +"Retry Upload" = "Reintentar carga"; diff --git a/SharedResources/fil.lproj/Localizable.strings b/SharedResources/fil.lproj/Localizable.strings index 1dab56066..3bf66620c 100644 --- a/SharedResources/fil.lproj/Localizable.strings +++ b/SharedResources/fil.lproj/Localizable.strings @@ -2,6 +2,7 @@ "common.button.cancel" = "Kanselahin"; "common.button.change" = "Baguhin"; "common.button.choose" = "Pumili"; +"common.button.close" = "Isara"; "common.button.create" = "Gumawa"; "common.button.done" = "Tapos na"; "common.button.edit" = "I-edit"; @@ -9,6 +10,7 @@ "common.button.next" = "Sunod"; "common.button.ok" = "OK"; "common.button.remove" = "Tanggalin"; +"common.button.retry" = "Subukan muli"; "common.cells.url" = "URL"; "common.cells.username" = "Username"; diff --git a/SharedResources/fr.lproj/Localizable.strings b/SharedResources/fr.lproj/Localizable.strings index 84b13b005..938a73a78 100644 --- a/SharedResources/fr.lproj/Localizable.strings +++ b/SharedResources/fr.lproj/Localizable.strings @@ -10,16 +10,18 @@ "common.button.cancel" = "Annuler"; "common.button.change" = "Modifier"; "common.button.choose" = "Choisir"; +"common.button.close" = "Fermer"; "common.button.confirm" = "Confirmer"; "common.button.create" = "Créer"; "common.button.createFolder" = "Créer un répertoire"; -"common.button.done" = "Ok"; +"common.button.done" = "OK"; "common.button.download" = "Télécharger"; "common.button.edit" = "Éditer"; "common.button.enable" = "Activer"; "common.button.next" = "Suivant"; "common.button.ok" = "OK"; "common.button.remove" = "Retirer"; +"common.button.retry" = "Réessayer"; "common.button.signOut" = "Se déconnecter"; "common.button.verify" = "Vérifier"; "common.cells.openInFilesApp" = "Ouvrir dans l'application Fichiers"; @@ -95,6 +97,11 @@ "fileProvider.error.defaultLock.title" = "Déverrouillage requis"; "fileProvider.error.defaultLock.message" = "Pour accéder et afficher le contenu de votre coffre, il doit être déverrouillé."; "fileProvider.error.unlockButton" = "Déverrouiller"; +"fileProvider.uploadProgress.connecting" = "Connexion…"; +"fileProvider.uploadProgress.message" = "Progression actuelle: %@\n\nSi vous remarquez que la progression de l'envoi est bloquée, vous pouvez recommencer l'envoi."; +"fileProvider.uploadProgress.missing" = "La progression n'a pas pu être déterminée. Il se peut que cela soit encore en cours en arrière-plan."; +"fileProvider.uploadProgress.title" = "Envoi en cours…"; +"fileProvider.uploadProgress.missingDomainError" = "Impossible de trouver le domaine."; "keepUnlocked.alert.title" = "Verrouiller le coffre ?"; "keepUnlocked.alert.message" = "Cette modification nécessite que votre coffre soit verrouillé."; @@ -149,6 +156,8 @@ "purchase.restorePurchase.fullVersionFound.alert.title" = "Restauration réussie"; "purchase.restorePurchase.fullVersionNotFound.alert.title" = "Pas de version complète"; "purchase.restorePurchase.fullVersionNotFound.alert.message" = "Nous n'avons pas pu trouver une version complète précédemment achetée qui pourrait être restaurée. Veuillez essayer une autre option."; +"purchase.restorePurchase.eligibleForUpgrade.alert.title" = "Éligible pour une mise à jour"; +"purchase.restorePurchase.eligibleForUpgrade.alert.message" = "Il semble que vous essayiez de mettre à jour depuis une ancienne version de Cryptomator. Veuillez sélectionner l'option \"Offre de mise à niveau\" à la place."; "purchase.retry.button" = "Réessayer"; "purchase.retry.footer" = "Impossible de charger les produits disponibles."; "purchase.title" = "Débloquer la version complète"; @@ -253,3 +262,5 @@ "webDAVAuthenticator.error.unsupportedProtocol" = "Le serveur ne semble pas être compatible WebDAV. Veuillez vérifier si vous avez utilisé la bonne URL."; "webDAVAuthenticator.error.untrustedCertificate" = "Le certificat de ce serveur n'est pas fiable. Vous devrez peut-être ré-ajouter cette connexion WebDAV."; + +"Retry Upload" = "Réessayer l'envoi"; diff --git a/SharedResources/gl.lproj/Localizable.strings b/SharedResources/gl.lproj/Localizable.strings index 7b873d5c4..09373a7bc 100644 --- a/SharedResources/gl.lproj/Localizable.strings +++ b/SharedResources/gl.lproj/Localizable.strings @@ -2,6 +2,7 @@ "common.button.create" = "Crear"; "common.button.edit" = "Editar"; "common.button.remove" = "Eliminar"; +"common.button.retry" = "Tentar de novo"; "common.cells.url" = "URL"; "common.cells.username" = "Nome de usuario"; "addVault.createNewVault.password.confirmPassword.alert.message" = "IMPORTANTE: Se esqueces o teu contrasinal, non será posíbel recuperar os teus datos."; diff --git a/SharedResources/he.lproj/Localizable.strings b/SharedResources/he.lproj/Localizable.strings index 3a32e9854..021e3c608 100644 --- a/SharedResources/he.lproj/Localizable.strings +++ b/SharedResources/he.lproj/Localizable.strings @@ -10,6 +10,7 @@ "common.button.cancel" = "ביטול"; "common.button.change" = "שנה"; "common.button.choose" = "בחר"; +"common.button.close" = "סגירה"; "common.button.create" = "צור"; "common.button.createFolder" = "צור תיקייה"; "common.button.done" = "סיום"; @@ -19,6 +20,7 @@ "common.button.next" = "המשך"; "common.button.ok" = "אישור"; "common.button.remove" = "הסר"; +"common.button.retry" = "לנסות שוב"; "common.cells.password" = "סיסמה"; "common.cells.url" = "כתובת URL"; "common.cells.username" = "שם משתמש"; @@ -44,6 +46,11 @@ "fileProvider.onboarding.title" = "ברוך הבא"; "fileProvider.error.unlockButton" = "בטל נעילה"; +"fileProvider.uploadProgress.connecting" = "התחברות…"; +"fileProvider.uploadProgress.message" = "תהליך נוכחי: %@\n\nאם תהליך ההעלאה תקוע, ניתן לנסות שוב."; +"fileProvider.uploadProgress.missing" = "לא ניתן לקבוע את תהליך ההתקדמות. ייתכן והתהליך עדיין רץ ברקע."; +"fileProvider.uploadProgress.title" = "העלאה…"; +"fileProvider.uploadProgress.missingDomainError" = "לא ניתן למצוא את הדומיין."; "onboarding.title" = "ברוך הבא"; "onboarding.button.continue" = "המשך"; @@ -61,3 +68,5 @@ "webDAVAuthentication.httpConnection.alert.title" = "להשתמש ב-HTTPS?"; "webDAVAuthentication.httpConnection.alert.message" = "HTTP הוא פרוטוקול לא מאובטח. אנו ממליצים להשתמש ב HTTPS במקום. אם אתה מבין את הסיכונים בכך, אתה יכול להמשיך להשתמש ב HTTP."; "webDAVAuthentication.httpConnection.change" = "עבור ל-HTTPS"; + +"Retry Upload" = "לנסות העלאה שוב"; diff --git a/SharedResources/hi.lproj/Localizable.strings b/SharedResources/hi.lproj/Localizable.strings index 4cef8c95b..7a88c8cdd 100644 --- a/SharedResources/hi.lproj/Localizable.strings +++ b/SharedResources/hi.lproj/Localizable.strings @@ -10,6 +10,7 @@ "common.button.cancel" = "रद्द करें"; "common.button.change" = "बदलें"; "common.button.choose" = "चुनें"; +"common.button.close" = "बंद करें"; "common.button.confirm" = "पुष्टि करें"; "common.button.create" = "बनाएं"; "common.button.createFolder" = "फोल्डर बनाएं"; @@ -20,6 +21,7 @@ "common.button.next" = "अगला"; "common.button.ok" = "ठीक है"; "common.button.remove" = "हटाएँ"; +"common.button.retry" = "पुन: प्रयास करें"; "common.button.signOut" = "साइन आउट करें"; "common.button.verify" = "सत्यापित करें"; "common.cells.openInFilesApp" = "दस्तावेज ऐप में खोलें"; diff --git a/SharedResources/hr.lproj/Localizable.strings b/SharedResources/hr.lproj/Localizable.strings index 97fa5cc5a..c9ee24d45 100644 --- a/SharedResources/hr.lproj/Localizable.strings +++ b/SharedResources/hr.lproj/Localizable.strings @@ -10,6 +10,7 @@ "common.button.cancel" = "Odustani"; "common.button.change" = "Promijeni"; "common.button.choose" = "Odaberi"; +"common.button.close" = "Zatvori"; "common.button.confirm" = "Potvrdi"; "common.button.create" = "Izradi"; "common.button.createFolder" = "Izradi mapu"; @@ -20,6 +21,7 @@ "common.button.next" = "Sljedeći"; "common.button.ok" = "U redu"; "common.button.remove" = "Ukloni"; +"common.button.retry" = "Pokušaj ponovno"; "common.button.signOut" = "Odjavi se"; "common.button.verify" = "Potvrdi"; "common.cells.openInFilesApp" = "Otvori u Files aplikaciji"; @@ -95,6 +97,11 @@ "fileProvider.error.defaultLock.title" = "Potrebno je otključanje"; "fileProvider.error.defaultLock.message" = "Za pristup i prikaz sadržaja Vašeg trezora, on mora biti otključan."; "fileProvider.error.unlockButton" = "Otključaj"; +"fileProvider.uploadProgress.connecting" = "Spajanje…"; +"fileProvider.uploadProgress.message" = "Trenutni napredak: %@\n\nAko primijetite da je napredak učitavanja zapeo, možete ponovno pokušati s prijenosom."; +"fileProvider.uploadProgress.missing" = "Napredak se ne može utvrditi. Možda još uvijek radi u pozadini."; +"fileProvider.uploadProgress.title" = "Učitavanje…"; +"fileProvider.uploadProgress.missingDomainError" = "Nije moguće pronaći domenu."; "keepUnlocked.alert.title" = "Zaključati trezor?"; "keepUnlocked.alert.message" = "Ova promjena zahtijeva da vaš trezor bude zaključan kako bi stupila na snagu."; @@ -149,6 +156,8 @@ "purchase.restorePurchase.fullVersionFound.alert.title" = "Obnavljanje uspješno"; "purchase.restorePurchase.fullVersionNotFound.alert.title" = "Nema pune verzije"; "purchase.restorePurchase.fullVersionNotFound.alert.message" = "Nismo uspjeli pronaći prethodno kupljenu punu verziju koja bi se mogla vratiti. Pokušajte s drugom opcijom."; +"purchase.restorePurchase.eligibleForUpgrade.alert.title" = "Ispunjava uvjete za nadogradnju"; +"purchase.restorePurchase.eligibleForUpgrade.alert.message" = "Čini se da pokušavate nadograditi sa starije verzije Cryptomator-a. U tom slučaju, odaberite opciju \"Ponuda za nadogradnju\"."; "purchase.retry.button" = "Pokušaj ponovno"; "purchase.retry.footer" = "Nije moguće učitati dostupne proizvode."; "purchase.title" = "Otključaj punu verziju"; @@ -253,3 +262,5 @@ "webDAVAuthenticator.error.unsupportedProtocol" = "Čini se da poslužitelj nije kompatibilan s WebDAV-om. Provjerite jeste li upotrijebili ispravan URL."; "webDAVAuthenticator.error.untrustedCertificate" = "Certifikat ovog poslužitelja nije pouzdan. Možda ćete morati ponovno dodati ovu WebDAV vezu."; + +"Retry Upload" = "Ponovno pokušaj prijenos"; diff --git a/SharedResources/hu.lproj/Localizable.strings b/SharedResources/hu.lproj/Localizable.strings index 15ef85357..1f7608cbc 100644 --- a/SharedResources/hu.lproj/Localizable.strings +++ b/SharedResources/hu.lproj/Localizable.strings @@ -2,6 +2,7 @@ "common.button.cancel" = "Mégse"; "common.button.change" = "Változtat"; "common.button.choose" = "Választás"; +"common.button.close" = "Bezár"; "common.button.create" = "Létrehozás"; "common.button.done" = "Kész"; "common.button.edit" = "Szerkeszt"; @@ -9,6 +10,7 @@ "common.button.next" = "Következő"; "common.button.ok" = "OK"; "common.button.remove" = "Eltávolítás"; +"common.button.retry" = "Újra"; "common.cells.password" = "Jelszó"; "common.cells.url" = "ULR"; "common.cells.username" = "Felhasználónév"; diff --git a/SharedResources/id.lproj/Localizable.strings b/SharedResources/id.lproj/Localizable.strings index f154f02b5..6c2198db7 100644 --- a/SharedResources/id.lproj/Localizable.strings +++ b/SharedResources/id.lproj/Localizable.strings @@ -10,6 +10,7 @@ "common.button.cancel" = "Batalkan"; "common.button.change" = "Ganti"; "common.button.choose" = "Pilih"; +"common.button.close" = "Tutup"; "common.button.confirm" = "Konfirmasi"; "common.button.create" = "Buat"; "common.button.createFolder" = "Buat Folder"; @@ -20,6 +21,7 @@ "common.button.next" = "Lanjut"; "common.button.ok" = "OK"; "common.button.remove" = "Hapus"; +"common.button.retry" = "Coba lagi"; "common.button.signOut" = "Keluar"; "common.button.verify" = "Buka"; "common.cells.openInFilesApp" = "Buka di Aplikasi File"; @@ -35,6 +37,7 @@ "addVault.title" = "Tambah Vault"; "addVault.createNewVault.title" = "Buat Vault Baru"; +"addVault.createNewVault.purchase" = "Membuat vault baru memerlukan Cryptomator versi lengkap."; "addVault.createNewVault.setVaultName.header.title" = "Pilih nama vault."; "addVault.createNewVault.setVaultName.cells.name" = "Nama Vault"; "addVault.createNewVault.setVaultName.error.emptyVaultName" = "Nama Vault tidak boleh kosong."; @@ -124,6 +127,24 @@ "onboarding.button.continue" = "Lanjut"; "purchase.beginFreeTrial.alert.title" = "Masa Percobaan Terbuka"; +"purchase.expiredTrial" = "Masa percobaan Anda telah berakhir."; +"purchase.footer.privacyPolicy" = "Kebijakan Privasi"; +"purchase.footer.termsOfUse" = "Persyaratan Penggunaan"; +"purchase.header.feature.familySharing" = "Berbagi dengan keluarga"; +"purchase.header.feature.openSource" = "Pengembangan sumber terbuka"; +"purchase.header.feature.writeAccess" = "Akses tulis ke vault-vault Anda"; +"purchase.product.donateAndUpgrade" = "Donasi & Tingkatkan"; +"purchase.product.freeUpgrade" = "Tingkatkan Gratis"; +"purchase.product.lifetimeLicense" = "Lisensi Seumur Hidup"; +"purchase.product.lifetimeLicense.duration" = "sekali"; +"purchase.product.pricing.free" = "Gratis"; +"purchase.product.trial" = "Percobaan 30-Hari"; +"purchase.product.trial.expirationDate" = "Tanggal Kadaluarsa: %@"; +"purchase.product.trial.duration" = "untuk 30 hari"; +"purchase.product.yearlySubscription" = "Berlangganan Tahunan"; +"purchase.product.yearlySubscription.duration" = "tiap tahun"; +"purchase.readOnlyMode.alert.title" = "Mode Read-Only"; +"purchase.readOnlyMode.alert.message" = "Anda bisa membuka Cryptomator versi lengkap nanti di pengaturan dan untuk sekarang menggunakan mode read-only."; "purchase.restorePurchase.button" = "Pulihkan Pembelian"; "purchase.restorePurchase.validTrialFound.alert.title" = "Masa Percobaan Diperpanjang"; "purchase.restorePurchase.validTrialFound.alert.message" = "Sekarang Anda bisa menggunakan versi lengkap dari Cryptomator dalam waktu terbatas. Masa percobaan Anda akan berakhir pada %@. Setelah masa percobaan habis, Anda hanya bisa mengakses vault Anda dalam mode read-only."; @@ -135,6 +156,7 @@ "purchase.title" = "Buka Versi Lengkap"; "purchase.unlockedFullVersion.message" = "Sekarang Anda bisa menggunakan Cryptomator versi lengkap. Selamat berenkripsi ria!"; "purchase.unlockedFullVersion.title" = "Terima Kasih"; +"purchase.error.unknown" = "Pembelian ini tidak tersedia di App Store untuk alasan yang tidak diketahui. Silakan coba lagi nanti.\n\nJika kesalahan ini berlanjut, coba mulai ulang perangkat Anda atau keluar dan masuk kembali ke ID Apple Anda di pengaturan iOS."; "settings.title" = "Pengaturan"; "settings.aboutCryptomator" = "Tentang Cryptomator"; @@ -163,6 +185,9 @@ "snapshots.main.vault3" = "/Dokumen"; "snapshots.main.vault4" = "/Perjalanan ke California"; +"trialStatus.active" = "Aktifkan"; +"trialStatus.expired" = "Kadaluarsa"; + "unlockVault.button.unlock" = "Buka Kunci"; "unlockVault.button.unlockVia" = "Buka memalui %@"; "unlockVault.password.footer" = "Masukkan kata sandi untuk \"%@\"."; @@ -175,8 +200,11 @@ "untrustedTLSCertificate.message" = "Sertifikat TLS of \"%@\" tidak valid. Apakah Anda yakin?\n\n SHA-256: %@"; "untrustedTLSCertificate.add" = "Percayai"; "untrustedTLSCertificate.dismiss" = "Jangan Percaya"; + +"upgrade.title" = "Penawaran Upgrade"; "upgrade.notEligible.alert.title" = "Upgrade gagal"; "upgrade.notEligible.alert.message" = "Cryptomator tidak bisa menemukan versi yang lebih lama terpasang di perangkat Anda saat ini. Jika Anda sudah membelinya, silahkan download kembali melalui App Store dan coba kembali."; +"upgrade.info" = "Terima kasih telah mempercayai Cryptomator sejak versi pertama. Sebagai pengguna setia, Anda berhak mendapatkan upgrade gratis."; "urlSession.error.httpError.401" = "Nama pengguna dan/atau kata sandi salah."; "urlSession.error.httpError.403" = "Insufficient rights to requested resource."; @@ -217,9 +245,13 @@ "vaultList.remove.alert.message" = "Ini hanya akan menghapus vault dari daftar vault. Tidak ada data terenkripsi yang akan dihapus. Anda dapat menambahkan kembali vault nanti."; "vaultProviderFactory.error.unsupportedVaultConfig" = "Konfigurasi Vault tidak didukung. Pastikan Anda menjalankan Cryptomator versi terbaru."; +"vaultProviderFactory.error.unsupportedVaultVersion" = "Vault versi %ld tidak didukung. Vault ini telah dibuat menggunakan versi Cryptomator yang lebih lama ataupun lebih baru."; "webDAVAuthentication.progress" = "Mengotentikasi…"; "webDAVAuthentication.httpConnection.alert.title" = "Gunakan HTTPS?"; "webDAVAuthentication.httpConnection.alert.message" = "Penggunaan HTTP tidak aman. Kami menyarankan untuk menggunakan HTTPS sebagai gantinya. Jika Anda mengetahui risikonya, Anda dapat melanjutkan dengan HTTP."; "webDAVAuthentication.httpConnection.change" = "Ubah ke HTTPS"; "webDAVAuthentication.httpConnection.continue" = "Pertahankan HTTP"; + +"webDAVAuthenticator.error.unsupportedProtocol" = "Server tampaknya tidak kompatibel dengan WebDAV. Harap periksa apakah Anda telah menggunakan URL yang benar."; +"webDAVAuthenticator.error.untrustedCertificate" = "Sertifikat server ini tidak tepercaya. Anda mungkin harus menambahkan kembali koneksi WebDAV ini."; diff --git a/SharedResources/it.lproj/Localizable.strings b/SharedResources/it.lproj/Localizable.strings index f42cffec4..d941f06de 100644 --- a/SharedResources/it.lproj/Localizable.strings +++ b/SharedResources/it.lproj/Localizable.strings @@ -8,8 +8,9 @@ "common.alert.error.title" = "Errore"; "common.alert.attention.title" = "Attenzione"; "common.button.cancel" = "Annulla"; -"common.button.change" = "Modifica"; +"common.button.change" = "Aggiorna"; "common.button.choose" = "Scegli"; +"common.button.close" = "Chiudi"; "common.button.confirm" = "Conferma"; "common.button.create" = "Crea"; "common.button.createFolder" = "Crea Cartella"; @@ -20,6 +21,7 @@ "common.button.next" = "Avanti"; "common.button.ok" = "OK"; "common.button.remove" = "Rimuovi"; +"common.button.retry" = "Riprova"; "common.button.signOut" = "Disconnettiti"; "common.button.verify" = "Verifica"; "common.cells.openInFilesApp" = "Apri nell'App File"; @@ -39,7 +41,7 @@ "addVault.createNewVault.setVaultName.header.title" = "Scegli un nome per la tua cassaforte."; "addVault.createNewVault.setVaultName.cells.name" = "Nome della Cassaforte"; "addVault.createNewVault.setVaultName.error.emptyVaultName" = "Il nome della cassaforte non può essere vuoto."; -"addVault.createNewVault.chooseCloud.header" = "Dove Cryptomator dovrebbe memorizzare i file crittografati della tua cassaforte?"; +"addVault.createNewVault.chooseCloud.header" = "Dove dovrebbe memorizzare Cryptomator i file crittografati della tua cassaforte?"; "addVault.createNewVault.chooseFolder.error.vaultNameCollision" = "\"%@\" esiste già in questa posizione. Scegli un nome o una posizione della cassaforte diverso."; "addVault.createNewVault.detectedMasterkey.text" = "Cryptomator ha rilevato una cassaforte esistente in questa posizione.\nPer poterne creare una nuova, sei pregato di tornare indietro e scegliere una cartella differente."; "addVault.createNewVault.password.enterPassword.header" = "Inserisci una nuova password."; @@ -95,6 +97,10 @@ "fileProvider.error.defaultLock.title" = "Richiesto Sblocco"; "fileProvider.error.defaultLock.message" = "La cassaforte deve essere sbloccata per accedere e vedere il contenuto."; "fileProvider.error.unlockButton" = "Sblocca"; +"fileProvider.uploadProgress.connecting" = "Connessione…"; +"fileProvider.uploadProgress.missing" = "Non è stato possibile determinare i progressi, che potrebbero essere ancora in esecuzione in background."; +"fileProvider.uploadProgress.title" = "Caricamento…"; +"fileProvider.uploadProgress.missingDomainError" = "Impossibile trovare il dominio."; "keepUnlocked.alert.title" = "Blocco la cassaforte?"; "keepUnlocked.alert.message" = "Questa modifica richiede che la cassaforte sia bloccata per avere effetto."; @@ -149,6 +155,8 @@ "purchase.restorePurchase.fullVersionFound.alert.title" = "Ripristino Riuscito"; "purchase.restorePurchase.fullVersionNotFound.alert.title" = "Nessuna Versione Completa"; "purchase.restorePurchase.fullVersionNotFound.alert.message" = "Non siamo riusciti a trovare una versione completa acquistata precedentemente ripristinabile. Sei pregato di provare un'altra opzione."; +"purchase.restorePurchase.eligibleForUpgrade.alert.title" = "Idoneo per l'aggiornamento"; +"purchase.restorePurchase.eligibleForUpgrade.alert.message" = "Sembra che si stia cercando di aggiornare da una versione precedente di Cryptomator. In questo caso, si prega di selezionare l'opzione \"Offerta di aggiornamento\"."; "purchase.retry.button" = "Riprova"; "purchase.retry.footer" = "Impossibile caricare i prodotti disponibili."; "purchase.title" = "Sblocca la Versione Completa"; @@ -253,3 +261,5 @@ "webDAVAuthenticator.error.unsupportedProtocol" = "Il server non sembra essere compatibile con WebDAV. Controlla se hai usato l'URL corretto."; "webDAVAuthenticator.error.untrustedCertificate" = "Il certificato di questo server non è attendibile. Potrebbe essere necessario aggiungere nuovamente questa connessione WebDAV."; + +"Retry Upload" = "Riprova A Caricare"; diff --git a/SharedResources/ja.lproj/Localizable.strings b/SharedResources/ja.lproj/Localizable.strings index 6b941a36b..d541de475 100644 --- a/SharedResources/ja.lproj/Localizable.strings +++ b/SharedResources/ja.lproj/Localizable.strings @@ -10,6 +10,7 @@ "common.button.cancel" = "キャンセル"; "common.button.change" = "変更"; "common.button.choose" = "選択"; +"common.button.close" = "閉じる"; "common.button.confirm" = "確認"; "common.button.create" = "作成"; "common.button.createFolder" = "フォルダーを作成"; @@ -20,6 +21,7 @@ "common.button.next" = "次へ"; "common.button.ok" = "OK"; "common.button.remove" = "削除"; +"common.button.retry" = "再試行"; "common.button.signOut" = "サインアウト"; "common.button.verify" = "認証"; "common.cells.openInFilesApp" = "ファイル アプリで開く"; @@ -137,21 +139,26 @@ "purchase.product.lifetimeLicense.duration" = "ワンタイム"; "purchase.product.pricing.free" = "無料"; "purchase.product.trial" = "30日間のトライアル"; +"purchase.product.trial.expirationDate" = "有効期限: %@"; "purchase.product.trial.duration" = "30日間"; "purchase.product.yearlySubscription" = "年間サブスクリプション"; "purchase.product.yearlySubscription.duration" = "毎年"; "purchase.readOnlyMode.alert.title" = "読み取り専用モード"; +"purchase.readOnlyMode.alert.message" = "今は読み取り専用モードで使用することができます。後で設定から Cryptomator のフルバージョンにすることができます。"; "purchase.restorePurchase.button" = "購入情報の復元"; "purchase.restorePurchase.validTrialFound.alert.title" = "試用が継続されました"; "purchase.restorePurchase.validTrialFound.alert.message" = "Cryptomatorのフルバージョンを期間限定で使用できるようになりました。試用期間は %@までです。 その後も、金庫は読み取り専用モードでアクセスすることができます。"; "purchase.restorePurchase.fullVersionFound.alert.title" = "正常に復元されました"; "purchase.restorePurchase.fullVersionNotFound.alert.title" = "フルバージョンではありません"; "purchase.restorePurchase.fullVersionNotFound.alert.message" = "以前購入したフルバージョンが見つかりませんでした。別のオプションをお試しください。"; +"purchase.restorePurchase.eligibleForUpgrade.alert.title" = "アップグレード可能"; +"purchase.restorePurchase.eligibleForUpgrade.alert.message" = "古いバージョンの Cryptomator からアップグレードしようとしているようです。その場合は、代わりに「Upgrade Offer」オプションを選択してください。"; "purchase.retry.button" = "再試行"; "purchase.retry.footer" = "利用可能な製品を読み込めませんでした。"; "purchase.title" = "フル バージョンのロックを解除"; "purchase.unlockedFullVersion.message" = "これでCryptomatorのフルバージョンを使用できます。"; "purchase.unlockedFullVersion.title" = "ありがとうございます"; +"purchase.error.unknown" = "この購入は不明な理由によりApp Storeで利用できません。後でもう一度お試しください。\n\nこのエラーが解決しない場合、 デバイスを再起動するか、iOS の設定で Apple ID にサインアウトしてください。"; "settings.title" = "設定"; "settings.aboutCryptomator" = "Cryptomator について"; @@ -199,6 +206,7 @@ "upgrade.title" = "アップグレードオファー"; "upgrade.notEligible.alert.title" = "アップグレードできませんでした"; "upgrade.notEligible.alert.message" = "Cryptomatorは、お使いのデバイスにインストールされている古いバージョンを検出できませんでした。すでに購入していた場合はApp Storeから再度ダウンロードしてもう一度お試しください。"; +"upgrade.info" = "バージョン1以来、Cryptomatorを信頼していただきありがとうございます。ロイヤル・ユーザーとして、あなたは無料のアップグレードの対象となります。"; "urlSession.error.httpError.401" = "ユーザー名またはパスワードが間違っています。"; "urlSession.error.httpError.403" = "リクエストされたリソースの権限が不足しています。"; @@ -246,3 +254,6 @@ "webDAVAuthentication.httpConnection.alert.message" = "HTTP は安全ではありません。代わりに HTTPS を使用することを推奨します。リスクを承知の上であれば HTTP をご使用ください。"; "webDAVAuthentication.httpConnection.change" = "HTTPS を使用"; "webDAVAuthentication.httpConnection.continue" = "HTTP を保持"; + +"webDAVAuthenticator.error.unsupportedProtocol" = "サーバーに WebDAV との互換性がありません。正しい URL を使用しているか確認してください"; +"webDAVAuthenticator.error.untrustedCertificate" = "このサーバーの証明書は信頼されていません。WebDAV 接続を再追加する必要があります。"; diff --git a/SharedResources/ko.lproj/Localizable.strings b/SharedResources/ko.lproj/Localizable.strings index 961a201c6..a0c69f081 100644 --- a/SharedResources/ko.lproj/Localizable.strings +++ b/SharedResources/ko.lproj/Localizable.strings @@ -10,6 +10,7 @@ "common.button.cancel" = "취소"; "common.button.change" = "변경"; "common.button.choose" = "선택"; +"common.button.close" = "닫기"; "common.button.confirm" = "확인"; "common.button.create" = "생성"; "common.button.createFolder" = "폴더 생성"; @@ -19,6 +20,7 @@ "common.button.next" = "다음"; "common.button.ok" = "OK"; "common.button.remove" = "제거"; +"common.button.retry" = "재시도"; "common.button.signOut" = "로그아웃"; "common.cells.password" = "비밀번호"; "common.cells.url" = "URL"; diff --git a/SharedResources/lv.lproj/Localizable.strings b/SharedResources/lv.lproj/Localizable.strings index 54bfc662e..da3698ff5 100644 --- a/SharedResources/lv.lproj/Localizable.strings +++ b/SharedResources/lv.lproj/Localizable.strings @@ -1,5 +1,6 @@ "common.button.cancel" = "Atcelt"; "common.button.change" = "Mainīt"; +"common.button.close" = "Aizvērt"; "common.button.done" = "Darīts"; "common.button.next" = "Tālāk"; "common.cells.password" = "Parole"; diff --git a/SharedResources/nb.lproj/Localizable.strings b/SharedResources/nb.lproj/Localizable.strings index 877cea118..f1c11c0a3 100644 --- a/SharedResources/nb.lproj/Localizable.strings +++ b/SharedResources/nb.lproj/Localizable.strings @@ -10,6 +10,7 @@ "common.button.cancel" = "Avbryt"; "common.button.change" = "Endre"; "common.button.choose" = "Velg"; +"common.button.close" = "Lukk"; "common.button.confirm" = "Bekreft"; "common.button.create" = "Opprett"; "common.button.createFolder" = "Opprett mappe"; @@ -20,6 +21,7 @@ "common.button.next" = "Neste"; "common.button.ok" = "Ok"; "common.button.remove" = "Fjern"; +"common.button.retry" = "Prøv igjen"; "common.button.signOut" = "Logg ut"; "common.button.verify" = "Bekreft"; "common.cells.openInFilesApp" = "Åpne i Filer-appen"; @@ -149,6 +151,8 @@ "purchase.restorePurchase.fullVersionFound.alert.title" = "Gjenoppretting vellykket"; "purchase.restorePurchase.fullVersionNotFound.alert.title" = "Ingen fullversjon"; "purchase.restorePurchase.fullVersionNotFound.alert.message" = "Vi klarte ikke å finne en tidligere kjøpt fullversjon som kunne gjenopprettes. Prøv et annet alternativ."; +"purchase.restorePurchase.eligibleForUpgrade.alert.title" = "Kvalifisert for oppgradering"; +"purchase.restorePurchase.eligibleForUpgrade.alert.message" = "Det ser ut til at du prøver å oppgradere fra en eldre versjon av Cryptomator. I så fall kan du velge alternativet \"Oppgraderingstilbud\" i stedet."; "purchase.retry.button" = "Prøv igjen"; "purchase.retry.footer" = "Kunne ikke laste tilgjengelige produkter."; "purchase.title" = "Lås opp fullversjonen"; diff --git a/SharedResources/nl.lproj/Localizable.strings b/SharedResources/nl.lproj/Localizable.strings index 8689ed9d2..ab9e4cedb 100644 --- a/SharedResources/nl.lproj/Localizable.strings +++ b/SharedResources/nl.lproj/Localizable.strings @@ -10,6 +10,7 @@ "common.button.cancel" = "Annuleren"; "common.button.change" = "Wijzig"; "common.button.choose" = "Kies"; +"common.button.close" = "Sluiten"; "common.button.confirm" = "Bevestig"; "common.button.create" = "Maak"; "common.button.createFolder" = "Map aanmaken"; @@ -20,6 +21,7 @@ "common.button.next" = "Volgende"; "common.button.ok" = "Ok"; "common.button.remove" = "Verwijderen"; +"common.button.retry" = "Opnieuw proberen"; "common.button.signOut" = "Uitloggen"; "common.button.verify" = "Verifiëren"; "common.cells.openInFilesApp" = "Open in Bestanden App"; @@ -137,6 +139,7 @@ "purchase.product.lifetimeLicense.duration" = "eenmalig"; "purchase.product.pricing.free" = "Gratis"; "purchase.product.trial" = "30-dagen proefperiode"; +"purchase.product.trial.expirationDate" = "Vervaldatum: %@"; "purchase.product.trial.duration" = "voor 30 dagen"; "purchase.product.yearlySubscription" = "Jaarabonnement"; "purchase.product.yearlySubscription.duration" = "jaarlijks"; @@ -148,6 +151,8 @@ "purchase.restorePurchase.fullVersionFound.alert.title" = "Herstellen geslaagd"; "purchase.restorePurchase.fullVersionNotFound.alert.title" = "Geen volledige versie"; "purchase.restorePurchase.fullVersionNotFound.alert.message" = "We konden geen eerder aangeschafte volledige versie vinden die hersteld kon worden. Probeer een andere optie."; +"purchase.restorePurchase.eligibleForUpgrade.alert.title" = "Komt in aanmerking voor upgrade"; +"purchase.restorePurchase.eligibleForUpgrade.alert.message" = "Het lijkt erop dat u probeert te upgraden via een oudere versie van Cryptomator. In dat geval, selecteer de \"Upgrade-aanbieding\" optie."; "purchase.retry.button" = "Opnieuw proberen"; "purchase.retry.footer" = "De beschikbare producten konden niet worden geladen."; "purchase.title" = "Volledige versie ontgrendelen"; @@ -249,3 +254,6 @@ "webDAVAuthentication.httpConnection.alert.message" = "Het gebruik van HTTP is onveilig. We raden aan om in plaats daarvan HTTPS te gebruiken. Als u de risico's weet, kunt u doorgaan met HTTP."; "webDAVAuthentication.httpConnection.change" = "Verander naar HTTPS"; "webDAVAuthentication.httpConnection.continue" = "HTTP behouden"; + +"webDAVAuthenticator.error.unsupportedProtocol" = "Server lijkt niet compatibel te zijn met WebDAV. Controleer of je de juiste URL hebt gebruikt."; +"webDAVAuthenticator.error.untrustedCertificate" = "Certificaat van deze server is niet vertrouwd. Je moet mogelijk deze WebDAV verbinding opnieuw toevoegen."; diff --git a/SharedResources/nn-NO.lproj/Localizable.strings b/SharedResources/nn-NO.lproj/Localizable.strings index 6f1830e2d..f82be6028 100644 --- a/SharedResources/nn-NO.lproj/Localizable.strings +++ b/SharedResources/nn-NO.lproj/Localizable.strings @@ -1,6 +1,7 @@ "common.button.cancel" = "Avbryt"; "common.button.change" = "Endre"; "common.button.choose" = "Vel"; +"common.button.close" = "Lukk"; "common.button.done" = "Ferdig"; "common.button.next" = "Neste"; "common.cells.password" = "Passord"; diff --git a/SharedResources/pa.lproj/Localizable.strings b/SharedResources/pa.lproj/Localizable.strings index 92cecda51..52494d230 100644 --- a/SharedResources/pa.lproj/Localizable.strings +++ b/SharedResources/pa.lproj/Localizable.strings @@ -1,6 +1,7 @@ "common.button.cancel" = "ਰੱਦ ਕਰੋ"; "common.button.change" = "ਬਦਲੋ"; "common.button.choose" = "ਚੁਣੋ"; +"common.button.close" = "ਬੰਦ ਕਰੋ"; "common.button.done" = "ਮੁਕੰਮਲ"; "common.button.next" = "ਅੱਗੇ"; "common.cells.password" = "ਪਾਸਵਰਡ"; diff --git a/SharedResources/pl.lproj/Localizable.strings b/SharedResources/pl.lproj/Localizable.strings index f3bfc30a2..e7b8c3b9d 100644 --- a/SharedResources/pl.lproj/Localizable.strings +++ b/SharedResources/pl.lproj/Localizable.strings @@ -10,6 +10,7 @@ "common.button.cancel" = "Anuluj"; "common.button.change" = "Zmień"; "common.button.choose" = "Wybierz"; +"common.button.close" = "Zamknij"; "common.button.confirm" = "Zatwierdź"; "common.button.create" = "Utwórz"; "common.button.createFolder" = "Utwórz Folder"; @@ -20,6 +21,7 @@ "common.button.next" = "Dalej"; "common.button.ok" = "OK"; "common.button.remove" = "Usuń"; +"common.button.retry" = "Ponów"; "common.button.signOut" = "Wyloguj"; "common.button.verify" = "Potwierdź"; "common.cells.openInFilesApp" = "Otwórz w aplikacji Pliki"; @@ -95,6 +97,11 @@ "fileProvider.error.defaultLock.title" = "Wymagane odblokowanie"; "fileProvider.error.defaultLock.message" = "Sejf musi być odblokowany aby zobaczyć i mieć dostęp do jego zawartości."; "fileProvider.error.unlockButton" = "Odblokuj"; +"fileProvider.uploadProgress.connecting" = "Łączenie..."; +"fileProvider.uploadProgress.message" = "Bieżący postęp: %@\n\nJeśli zauważysz, że postęp przesyłania zawiesił się, możesz spróbować ponownie przesłać dane."; +"fileProvider.uploadProgress.missing" = "Nie można zweryfikować postępu. Może on nadal działać w tle."; +"fileProvider.uploadProgress.title" = "Przesyłanie..."; +"fileProvider.uploadProgress.missingDomainError" = "Nie można odnaleźć domeny."; "keepUnlocked.alert.title" = "Zablokować sejf?"; "keepUnlocked.alert.message" = "Wprowadzenie tej zmiany wymaga zablokowania sejfu."; @@ -149,6 +156,8 @@ "purchase.restorePurchase.fullVersionFound.alert.title" = "Przywrócono pomyślnie"; "purchase.restorePurchase.fullVersionNotFound.alert.title" = "Brak pełnej wersji"; "purchase.restorePurchase.fullVersionNotFound.alert.message" = "Nie mogliśmy znaleźć wcześniej zakupionej pełnej wersji, która mogłaby zostać przywrócona. Proszę spróbować innej opcji."; +"purchase.restorePurchase.eligibleForUpgrade.alert.title" = "Dostępna aktualizacja"; +"purchase.restorePurchase.eligibleForUpgrade.alert.message" = "Wygląda na to, że próbujesz uaktualnić ze starszej wersji Cryptomator. W takim przypadku wybierz opcję \"Uaktualnij Ofertę\"."; "purchase.retry.button" = "Ponów"; "purchase.retry.footer" = "Nie można załadować dostępnych produktów."; "purchase.title" = "Odblokuj pełną wersję"; @@ -159,8 +168,8 @@ "settings.title" = "Ustawienia"; "settings.aboutCryptomator" = "O Cryptomator"; "settings.aboutCryptomator.title" = "Wersja %@ (%@)"; -"settings.cacheSize" = "Rozmiar Cache"; -"settings.clearCache" = "Wyczyść Cache"; +"settings.cacheSize" = "Rozmiar pamięci podręcznej"; +"settings.clearCache" = "Wyczyść pamięć podręczną"; "settings.cloudServices" = "Usługi w chmurze"; "settings.contact" = "Kontakt"; "settings.debugMode" = "Tryb debugowania"; @@ -253,3 +262,5 @@ "webDAVAuthenticator.error.unsupportedProtocol" = "Serwer wydaje się niekompatybilny z WebDAV. Proszę sprawdzić, czy użyto poprawnego adresu URL."; "webDAVAuthenticator.error.untrustedCertificate" = "Certyfikat tego serwera nie jest zaufany. Może być konieczne ponowne dodanie połączenia WebDAV."; + +"Retry Upload" = "Ponów wysyłanie"; diff --git a/SharedResources/pt-BR.lproj/Localizable.strings b/SharedResources/pt-BR.lproj/Localizable.strings index 7cb7d10ab..bcc99dfe7 100644 --- a/SharedResources/pt-BR.lproj/Localizable.strings +++ b/SharedResources/pt-BR.lproj/Localizable.strings @@ -10,6 +10,7 @@ "common.button.cancel" = "Cancelar"; "common.button.change" = "Alterar"; "common.button.choose" = "Escolher"; +"common.button.close" = "Fechar"; "common.button.confirm" = "Confirmar"; "common.button.create" = "Criar"; "common.button.createFolder" = "Criar pasta"; @@ -20,6 +21,7 @@ "common.button.next" = "Próximo"; "common.button.ok" = "Ok"; "common.button.remove" = "Remover"; +"common.button.retry" = "Tentar Novamente"; "common.button.signOut" = "Finalizar sessão"; "common.button.verify" = "Verificar"; "common.cells.openInFilesApp" = "Abrir no App de arquivos"; @@ -95,6 +97,11 @@ "fileProvider.error.defaultLock.title" = "Desbloqueio necessário"; "fileProvider.error.defaultLock.message" = "Para acessar e mostrar o conteúdo do seu cofre, ele precisa ser desbloqueado."; "fileProvider.error.unlockButton" = "Desbloquear"; +"fileProvider.uploadProgress.connecting" = "Conectando…"; +"fileProvider.uploadProgress.message" = "Progresso atual: %@\n\nSe você notar que o progresso do upload está travado, pode tentar novamente o upload."; +"fileProvider.uploadProgress.missing" = "Não foi possível determinar o progresso, que pode estar sendo executado em segundo plano."; +"fileProvider.uploadProgress.title" = "Enviando…"; +"fileProvider.uploadProgress.missingDomainError" = "Não foi possível encontrar o domínio."; "keepUnlocked.alert.title" = "Trancar cofre?"; "keepUnlocked.alert.message" = "Para que esta alteração tenha efeito é necessário que seu cofre esteja trancado."; @@ -149,6 +156,8 @@ "purchase.restorePurchase.fullVersionFound.alert.title" = "Restaurado com sucesso"; "purchase.restorePurchase.fullVersionNotFound.alert.title" = "Desbloqueie a versão completa"; "purchase.restorePurchase.fullVersionNotFound.alert.message" = "Não foi possível encontrar uma compra anterior da versão completa que poderia ser restaurada. Por favor, tente outra opção."; +"purchase.restorePurchase.eligibleForUpgrade.alert.title" = "Selecionado para Atualizar"; +"purchase.restorePurchase.eligibleForUpgrade.alert.message" = "Parece que você está tentando atualizar de uma versão muito antiga do Cryptomator. Nesse caso, selecione a opção “Oferta de atualização”."; "purchase.retry.button" = "Tentar Novamente"; "purchase.retry.footer" = "Não foi possível carregar os produtos disponíveis."; "purchase.title" = "Desbloqueie a Versão Completa"; @@ -254,3 +263,5 @@ Para mover o seu cofre, por favor, volte e escolha uma pasta diferente."; "webDAVAuthenticator.error.unsupportedProtocol" = "O servidor não parece ser compatível com WebDAV. Por favor, verifique se você usou a URL correta."; "webDAVAuthenticator.error.untrustedCertificate" = "O certificado deste servidor não é confiável. Você pode ter que adicionar novamente esta conexão WebDAV."; + +"Retry Upload" = "Tentar o envio novamente"; diff --git a/SharedResources/pt.lproj/Localizable.strings b/SharedResources/pt.lproj/Localizable.strings index 68d759823..a7dd53a60 100644 --- a/SharedResources/pt.lproj/Localizable.strings +++ b/SharedResources/pt.lproj/Localizable.strings @@ -10,6 +10,7 @@ "common.button.cancel" = "Cancelar"; "common.button.change" = "Alterar"; "common.button.choose" = "Escolher"; +"common.button.close" = "Fechar"; "common.button.confirm" = "Confirmar"; "common.button.create" = "Criar"; "common.button.createFolder" = "Criar Pasta"; diff --git a/SharedResources/ro.lproj/Localizable.strings b/SharedResources/ro.lproj/Localizable.strings index 6b8a9e44b..1f28fb70e 100644 --- a/SharedResources/ro.lproj/Localizable.strings +++ b/SharedResources/ro.lproj/Localizable.strings @@ -10,6 +10,7 @@ "common.button.cancel" = "Anulează"; "common.button.change" = "Modifică"; "common.button.choose" = "Alegeți"; +"common.button.close" = "Închide"; "common.button.confirm" = "Confirmați"; "common.button.create" = "Creează"; "common.button.createFolder" = "Creează dosar"; @@ -19,6 +20,7 @@ "common.button.next" = "Următor"; "common.button.ok" = "OK"; "common.button.remove" = "Șterge"; +"common.button.retry" = "Încercați din nou"; "common.button.signOut" = "Deconectare"; "common.button.verify" = "Verifică"; "common.cells.openInFilesApp" = "Deschide în aplicația de fișiere"; diff --git a/SharedResources/ru.lproj/Localizable.strings b/SharedResources/ru.lproj/Localizable.strings index 53f5054a4..a73506371 100644 --- a/SharedResources/ru.lproj/Localizable.strings +++ b/SharedResources/ru.lproj/Localizable.strings @@ -10,6 +10,7 @@ "common.button.cancel" = "Отмена"; "common.button.change" = "Изменить"; "common.button.choose" = "Выбрать"; +"common.button.close" = "Закрыть"; "common.button.confirm" = "Подтвердить"; "common.button.create" = "Создать"; "common.button.createFolder" = "Создать папку"; @@ -20,6 +21,7 @@ "common.button.next" = "Далее"; "common.button.ok" = "OK"; "common.button.remove" = "Удалить"; +"common.button.retry" = "Повторить"; "common.button.signOut" = "Выйти"; "common.button.verify" = "Проверить"; "common.cells.openInFilesApp" = "Открыть в \"Файлах\""; @@ -95,6 +97,10 @@ "fileProvider.error.defaultLock.title" = "Требуется разблокировка"; "fileProvider.error.defaultLock.message" = "Хранилище должно быть разблокировано, чтобы получить доступ к его содержимому."; "fileProvider.error.unlockButton" = "Разблокировать"; +"fileProvider.uploadProgress.connecting" = "Подключение…"; +"fileProvider.uploadProgress.missing" = "Невозможно определить прогресс. Он может продолжаться в фоновом режиме."; +"fileProvider.uploadProgress.title" = "Загрузка…"; +"fileProvider.uploadProgress.missingDomainError" = "Домен не найден."; "keepUnlocked.alert.title" = "Заблокировать хранилище?"; "keepUnlocked.alert.message" = "Чтобы изменение вступило в силу, необходимо заблокировать хранилище."; @@ -149,6 +155,8 @@ "purchase.restorePurchase.fullVersionFound.alert.title" = "Успешно восстановлено"; "purchase.restorePurchase.fullVersionNotFound.alert.title" = "Полной версии нет"; "purchase.restorePurchase.fullVersionNotFound.alert.message" = "Не удалось найти ранее приобретённую полную версию, которая может быть восстановлена. Попробуйте другой вариант."; +"purchase.restorePurchase.eligibleForUpgrade.alert.title" = "Доступно для обновления"; +"purchase.restorePurchase.eligibleForUpgrade.alert.message" = "Похоже, вы пытаетесь выполнить обновление со старой версии Cryptomator. В этом случае выберите опцию предложения по обновлению."; "purchase.retry.button" = "Повторить"; "purchase.retry.footer" = "Не удалось загрузить доступные продукты."; "purchase.title" = "Получить полную версию"; @@ -253,3 +261,5 @@ "webDAVAuthenticator.error.unsupportedProtocol" = "Похоже, сервер не совместим с WebDAV. Проверьте, правильно ли указан URL."; "webDAVAuthenticator.error.untrustedCertificate" = "Сертификат этого сервера ненадёжен. Возможно, вам придётся добавить это подключение WebDAV ещё раз."; + +"Retry Upload" = "Повторить загрузку"; diff --git a/SharedResources/sk.lproj/Localizable.strings b/SharedResources/sk.lproj/Localizable.strings index b6c3f57c9..c77106c1e 100644 --- a/SharedResources/sk.lproj/Localizable.strings +++ b/SharedResources/sk.lproj/Localizable.strings @@ -10,6 +10,7 @@ "common.button.cancel" = "Zrušiť"; "common.button.change" = "Zmeniť"; "common.button.choose" = "Vybrať"; +"common.button.close" = "Zavrieť"; "common.button.confirm" = "Potvrdiť"; "common.button.create" = "Vytvoriť"; "common.button.createFolder" = "Vytvoriť adresár"; @@ -20,6 +21,7 @@ "common.button.next" = "Ďalej"; "common.button.ok" = "OK"; "common.button.remove" = "Odstrániť"; +"common.button.retry" = "Skúsiť znovu"; "common.button.signOut" = "Odhlásiť"; "common.button.verify" = "Overiť"; "common.cells.openInFilesApp" = "Otvoriť v súborovej aplikácii"; @@ -95,6 +97,11 @@ "fileProvider.error.defaultLock.title" = "Odomknutie požadované"; "fileProvider.error.defaultLock.message" = "K prístupu a videniu obsahu Vášho trezora, musí byť odomknutý."; "fileProvider.error.unlockButton" = "Odomknúť"; +"fileProvider.uploadProgress.connecting" = "Pripájanie…"; +"fileProvider.uploadProgress.message" = "Aktuálny priebeh: %@\n\nV prípade, že pozorujete zaseknutý priebeh nahrávania, môžte zopakovať nahrávanie."; +"fileProvider.uploadProgress.missing" = "Pokrok nebol spozorovaný. Môže však stále prebiehať na pozadí."; +"fileProvider.uploadProgress.title" = "Nahrávanie…"; +"fileProvider.uploadProgress.missingDomainError" = "Nepodarilo sa nájsť doménu."; "keepUnlocked.alert.title" = "Zamknúť trezor?"; "keepUnlocked.alert.message" = "Táto zmena vyžaduje Váš trezor uzamknúť aby sa príkaz uplatnil."; @@ -149,6 +156,8 @@ "purchase.restorePurchase.fullVersionFound.alert.title" = "Obnovenie úspešné"; "purchase.restorePurchase.fullVersionNotFound.alert.title" = "Nie Plná Verzia"; "purchase.restorePurchase.fullVersionNotFound.alert.message" = "Nevieme nájsť predchádzajúcu zakúpenú plnú verziu čo by mohla byť obnovená. Prosím skúste inú možnosť."; +"purchase.restorePurchase.eligibleForUpgrade.alert.title" = "Vhodný pre aktualizáciu"; +"purchase.restorePurchase.eligibleForUpgrade.alert.message" = "Zdá sa že sa pokúšate o aktualizáciu zo staršej verzie Cryptomator-a. V tomto prípade prosím radšej vyberte voľbu \"Aktualizačná ponuka\"."; "purchase.retry.button" = "Skúsiť znovu"; "purchase.retry.footer" = "Nemôžem nahrať dostupné produkty."; "purchase.title" = "Odomknúť plnú verziu"; @@ -252,3 +261,5 @@ "webDAVAuthenticator.error.unsupportedProtocol" = "Zdá sa že server nie je WebDAV kompatibilný. Prosím skontrolujte či ste použili správnu URL."; "webDAVAuthenticator.error.untrustedCertificate" = "Certifikát tohto servera nie je dôveryhodný. Môžte znovu-pridať toto WebDAV spojenie."; + +"Retry Upload" = "Opakovať nahratie"; diff --git a/SharedResources/sr-Latn.lproj/Localizable.strings b/SharedResources/sr-Latn.lproj/Localizable.strings index 2a3b34c34..135b7e526 100644 --- a/SharedResources/sr-Latn.lproj/Localizable.strings +++ b/SharedResources/sr-Latn.lproj/Localizable.strings @@ -1,6 +1,7 @@ "common.button.cancel" = "Otkaži"; "common.button.change" = "Izmeni"; "common.button.choose" = "Izaberi"; +"common.button.close" = "Zatvori"; "common.button.done" = "Završeno"; "common.button.next" = "Dalje"; diff --git a/SharedResources/sr.lproj/Localizable.strings b/SharedResources/sr.lproj/Localizable.strings index 6714f4636..434a70a36 100644 --- a/SharedResources/sr.lproj/Localizable.strings +++ b/SharedResources/sr.lproj/Localizable.strings @@ -1,6 +1,7 @@ "common.button.cancel" = "Откажи"; "common.button.change" = "Измени"; "common.button.choose" = "Изабери"; +"common.button.close" = "Затвори"; "common.button.done" = "Завршено"; "common.button.next" = "Даље"; "common.button.remove" = "Уклони"; diff --git a/SharedResources/sv.lproj/Localizable.strings b/SharedResources/sv.lproj/Localizable.strings index e46ecd64b..376548324 100644 --- a/SharedResources/sv.lproj/Localizable.strings +++ b/SharedResources/sv.lproj/Localizable.strings @@ -10,6 +10,7 @@ "common.button.cancel" = "Avbryt"; "common.button.change" = "Ändra"; "common.button.choose" = "Välj"; +"common.button.close" = "Stäng"; "common.button.confirm" = "Bekräfta"; "common.button.create" = "Skapa"; "common.button.createFolder" = "Skapa mapp"; @@ -20,6 +21,7 @@ "common.button.next" = "Nästa"; "common.button.ok" = "OK"; "common.button.remove" = "Ta bort"; +"common.button.retry" = "Försök igen"; "common.button.signOut" = "Logga ut"; "common.button.verify" = "Verifiera"; "common.cells.openInFilesApp" = "Öppna i Filer-appen"; @@ -95,6 +97,11 @@ "fileProvider.error.defaultLock.title" = "Upplåsning krävs"; "fileProvider.error.defaultLock.message" = "För att komma åt och visa innehållet i ditt valv måste det låsas upp."; "fileProvider.error.unlockButton" = "Lås upp"; +"fileProvider.uploadProgress.connecting" = "Ansluter…"; +"fileProvider.uploadProgress.message" = "Status: %@\n\nOm du märker att uppladdningsprocessen har fastnat kan du försöka ladda upp den igen."; +"fileProvider.uploadProgress.missing" = "Processen kunde inte fastställas. Det kan fortfarande köras i bakgrunden."; +"fileProvider.uploadProgress.title" = "Laddar upp…"; +"fileProvider.uploadProgress.missingDomainError" = "Kunde inte hitta domänen."; "keepUnlocked.alert.title" = "Lås valv?"; "keepUnlocked.alert.message" = "Denna ändring kräver att ditt valv låses för att träda i kraft."; @@ -149,6 +156,8 @@ "purchase.restorePurchase.fullVersionFound.alert.title" = "Återställning lyckades"; "purchase.restorePurchase.fullVersionNotFound.alert.title" = "Ingen fullversion"; "purchase.restorePurchase.fullVersionNotFound.alert.message" = "Vi kunde inte hitta en tidigare köpt fullversion som kunde återställas. Försök med ett annat alternativ."; +"purchase.restorePurchase.eligibleForUpgrade.alert.title" = "Godkänd för uppgradering"; +"purchase.restorePurchase.eligibleForUpgrade.alert.message" = "Det verkar som om du försöker uppgradera från en äldre version av Cryptomator. Välj i så fall alternativet \"Uppgraderings-erbjudande\"."; "purchase.retry.button" = "Försök igen"; "purchase.retry.footer" = "Kunde inte ladda tillgängliga produkter."; "purchase.title" = "Lås upp fullversion"; @@ -253,3 +262,5 @@ "webDAVAuthenticator.error.unsupportedProtocol" = "Servern verkar inte vara WebDAV-kompatibel. Kontrollera att du har använt rätt URL."; "webDAVAuthenticator.error.untrustedCertificate" = "Certifikatet för denna server är inte betrott. Du kan behöva lägga till denna WebDAV-anslutning igen."; + +"Retry Upload" = "Ladda upp igen"; diff --git a/SharedResources/sw-TZ.lproj/Localizable.strings b/SharedResources/sw-TZ.lproj/Localizable.strings new file mode 100644 index 000000000..68e1254a0 --- /dev/null +++ b/SharedResources/sw-TZ.lproj/Localizable.strings @@ -0,0 +1,255 @@ +/* + Localizable.strings + Cryptomator + + Copyright © 2021 Skymatic GmbH. All rights reserved. +*/ + +"common.alert.error.title" = "Kosa"; +"common.alert.attention.title" = "Makini"; +"common.button.cancel" = "Katisha"; +"common.button.change" = "Badilisha"; +"common.button.choose" = "Chagua"; +"common.button.close" = "Futa"; +"common.button.confirm" = "Thibitisha"; +"common.button.create" = "Unda"; +"common.button.createFolder" = "Unda kabrasha"; +"common.button.done" = "Tayari"; +"common.button.download" = "Kupakua"; +"common.button.edit" = "Hariri"; +"common.button.enable" = "Wezesha"; +"common.button.next" = "Nyingine"; +"common.button.ok" = "Sawa"; +"common.button.remove" = "Ondoa"; +"common.button.retry" = "Jaribu tena"; +"common.button.signOut" = "Ondoka"; +"common.button.verify" = "Thibitisha"; +"common.cells.openInFilesApp" = "Fungua katika Programu ya Mafaili"; +"common.cells.password" = "Neno la siri"; +"common.cells.url" = "URL"; +"common.cells.username" = "Jina la mtumiaji"; +"common.footer.learnMore" = "Jifunze zaidi."; + +"accountList.header.title" = "Uhalalishaji"; +"accountList.emptyList.message" = "Gonga hapa ili kuongeza akaunti"; +"accountList.signOut.alert.title" = "Ondoa Kuba Zinazohusiana?"; +"accountList.signOut.alert.message" = "Kwa kujisajili, kuba zote zinazohusiana zitaondolewa kwenye orodha ya kuba. Hakuna data iliyosimbwa kwa njia fiche itakayofutwa. Unaweza kuingia tena na kuongeza tena kuba baadaye."; + +"addVault.title" = "Ongeza Kuba"; +"addVault.createNewVault.title" = "Unda kuba mpya"; +"addVault.createNewVault.purchase" = "Kuunda kuba mpya inahitaji toleo kamili la Cryptomator."; +"addVault.createNewVault.setVaultName.header.title" = "Chagua jina la kuba."; +"addVault.createNewVault.setVaultName.cells.name" = "Jina la kuba"; +"addVault.createNewVault.setVaultName.error.emptyVaultName" = "Jina la Kuba haliwezi kuwa tupu."; +"addVault.createNewVault.chooseCloud.header" = "Cryptomator inapaswa kuhifadhi wapi mafaili yaliyosimbwa kwa njia fiche za kuba yako?"; +"addVault.createNewVault.chooseFolder.error.vaultNameCollision" = "\"%@\"tayari ipo katika mahali hapa. Chagua jina tofauti la kuba au mahali."; +"addVault.createNewVault.detectedMasterkey.text" = "Cryptomator imegundua kuba iliyopo katika eneo hili.\nIn ili kuunda kuba mpya, tafadhali rudi nyuma na uchague folda tofauti."; +"addVault.createNewVault.password.enterPassword.header" = "Weka neno la siri jipya."; +"addVault.createNewVault.password.confirmPassword.header" = "Thibitisha neno la siri jipya."; +"addVault.createNewVault.password.confirmPassword.alert.title" = "Thibitisha nenola siri?"; +"addVault.createNewVault.password.confirmPassword.alert.message" = "MUHIMU: Ikiwa unasahau neno la siri lako, hakuna njia ya kurejesha data yako."; +"addVault.createNewVault.password.error.emptyPassword" = "Neno la siri haliwezi kuwa tupu."; +"addVault.createNewVault.password.error.nonMatchingPasswords" = "Maneno ya siri hayaoani."; +"addVault.createNewVault.password.error.tooShortPassword" = "Neno la siri lazima iwe na angalau vibambo 8."; +"addVault.createNewVault.progress" = "Unda Kuba…"; +"addVault.openExistingVault.title" = "Fungua Kuba iliyopo"; +"addVault.openExistingVault.chooseCloud.header" = "Kuba iko wapi?"; +"addVault.openExistingVault.detectedMasterkey.text" = "Cryptomator aligundua kuba \"%@\".\nWould unapenda kuongeza kuba hii?"; +"addVault.openExistingVault.detectedMasterkey.add" = "Ongeza kuba hii"; +"addVault.openExistingVault.password.footer" = "Ingiza neno la siri la \"%@\"."; +"addVault.openExistingVault.progress" = "Inaongeza Kuba…"; +"addVault.success.info" = "Imefanikiwa kuongeza kuba \"%@\".\nFikia kuba hii kupitia programu ya Mafaili."; +"addVault.success.footer" = "Ikiwa haujafanya hivyo, wezesha Cryptomator katika programu ya Mafaili."; + +"biometryType.faceID" = "Kitambulisho cha Uso"; +"biometryType.touchID" = "Kitambulisho cha kugusa"; + +"changePassword.error.invalidOldPassword" = "Neno la siri la sasa si sahihi. Tafadhali jaribu tena."; +"changePassword.header.currentPassword.title" = "Ingiza neno la siri la sasa."; +"changePassword.header.newPassword.title" = "Weka neno la siri jipya."; +"changePassword.header.newPasswordConfirmation.title" = "Thibitisha neno la siri jipya."; +"changePassword.progress" = "Kubadilisha neno la siri…"; + +"chooseFolder.emptyFolder.footer" = "Kabrasha ni Tupu"; +"chooseFolder.createNewFolder.header.title" = "Chagua jina la kabrasha."; +"chooseFolder.createNewFolder.cells.name" = "Jina la Kabrasha"; +"chooseFolder.createNewFolder.error.emptyFolderName" = "Jina la Kuba haliwezi kuwa tupu."; +"chooseFolder.createNewFolder.progress" = "Kuunda kabrasha…"; + +"cloudProvider.error.itemNotFound" = "\"%@\" haikuweza kupatikana."; +"cloudProvider.error.itemAlreadyExists" = "\"%@\" tayari ipo."; +"cloudProvider.error.itemTypeMismatch" = "\"%@\" ina aina ya kipengee kisichotarajiwa."; +"cloudProvider.error.parentFolderDoesNotExist" = "Folda ya mzazi \"%@\" haipo."; +"cloudProvider.error.pageTokenInvalid" = "Kuchora yaliyomo kwenye mpangilio orodha hakukuweza kuendelea."; +"cloudProvider.error.quotaInsufficient" = "Hifadhi yako haina nafasi ya kutosha."; +"cloudProvider.error.unauthorized" = "Haiwezi kufanya uendeshaji usioidhinishwa."; +"cloudProvider.error.noInternetConnection" = "Muunganisho wa tovuti unahitajika kwa uendeshaji huu."; + +"cloudProviderType.localFileSystem" = "Mtoa Faili Mwingine"; + +"fileProvider.onboarding.title" = "Karibu"; +"fileProvider.onboarding.info" = "Shukrani kwa kuchagua Cryptomator kulinda faili zako. Ili kuanza, nenda kwenye programu kuu na uongeze kuba."; +"fileProvider.onboarding.button.openCryptomator" = "Fungua Cryptomator"; +"fileProvider.error.biometricalAuthCanceled.title" = "Fungua Imekatishwa"; +"fileProvider.error.biometricalAuthCanceled.message" = "Fungua kupitia %@ haikufanikiwa. Tafadhali jaribu tena."; +"fileProvider.error.biometricalAuthWrongPassword.title" = "Neno la siri lisilo sahihi"; +"fileProvider.error.biometricalAuthWrongPassword.message" = "Neno la siri ambalo limehifadhiwa kwa %@ sio sahihi. Tafadhali jaribu tena na ingiza neno la siri lako ili kuwezesha tena %@."; +"fileProvider.error.defaultLock.title" = "Fungua Inahitajika"; +"fileProvider.error.defaultLock.message" = "Ili kufikia na kuonyesha yaliyomo kwenye vault yako, inapaswa kufunguliwa."; +"fileProvider.error.unlockButton" = "Fungua"; + +"keepUnlocked.alert.title" = "Funga Kuba?"; +"keepUnlocked.alert.message" = "Mabadiliko haya yanahitaji kuba yako kufungwa ili kuanza kutumika."; +"keepUnlocked.alert.confirm" = "Thibitisha & Funga Sasa"; +"keepUnlocked.header" = "Bainisha kwa muda gani unataka kuba hii ibaki wazi wakati haifanyi kazi."; +"keepUnlocked.footer.auto" = "Kuruhusu iOS kuamua inamaanisha kuwa Cryptomator inaweza kukomeshwa wakati wowote ili kufungua kumbukumbu, ambayo hufunga moja kwa moja kuba."; +"keepUnlocked.footer.on" = "Kutumia chaguo lililoteuliwa, nakala ya ufunguo wako inahitaji kuhifadhiwa kwenye kiti cha vitufe cha iOS, mradi tu kuba imefunguliwa."; +"keepUnlockedDuration.auto" = "Ruhusu iOS Iamue Kiotomatiki"; +"keepUnlockedDuration.auto.shortDisplayName" = "Otomatiki"; + +"localFileSystemAuthentication.createNewVault.header" = "Katika skrini inayofuata, chagua eneo la kuhifadhi kwa kuba yako mpya."; +"localFileSystemAuthentication.createNewVault.button" = "Teua Mahali pa Hifadhi"; +"localFileSystemAuthentication.createNewVault.error.detectedExistingVault" = "Kuba tayari ipo katika mahali hapa. Tafadhali jaribu tena na mahali tofauti pa kuhifadhi."; +"localFileSystemAuthentication.openExistingVault.header" = "Katika skrini inayofuata, chagua folda ya kuba yako iliyopo."; +"localFileSystemAuthentication.openExistingVault.button" = "Teua Kabrasha la Kuba"; +"localFileSystemAuthentication.openExistingVault.error.noVaultFound" = "Kabrasha lililoteuliwa sio kuba. Tafadhali jaribu tena na kabrasha tofauti."; +"localFileSystemAuthentication.info.footer" = "Watoa huduma wa faili ambao ni kijivu nje hawaungi mkono \"kuokota folda\" Hii sio kikomo cha Cryptomator."; + +"maintenanceModeError.runningCloudTask" = "Uendeshaji hauwezi kufanywa kwa sababu shughuli zingine za mandharinyuma za kuba hii zinapaswa kumaliza kwanza. Tafadhali jaribu tena baadaye."; + +"nameValidation.error.endsWithPeriod" = "Huwezi kutumia jina ambalo linaisha kwa kipindi. Tafadhali chagua jina jingine."; +"nameValidation.error.endsWithSpace" = "Huwezi kutumia jina ambalo linaisha kwa kipindi. Tafadhali chagua jina jingine."; +"nameValidation.error.containsIllegalCharacter" = "Huwezi kutumia jina ambalo lina \"%@\" Tafadhali chagua jina jingine."; + +"onboarding.title" = "Karibu"; +"onboarding.info" = "Shukrani kwa kuchagua Cryptomator kulinda mafaili yako.\n\nWith Cryptomator, ufunguo wa data yako uko mikononi mwako. Cryptomator husimba data yako haraka na kwa urahisi.\n\n Programu hii imeunganishwa kikamilifu kwenye programu ya Faili. Hakikisha kuwezesha Cryptomator katika programu ya Mafaili baadaye kufikia kuba zako."; +"onboarding.button.continue" = "Endelea"; + +"purchase.beginFreeTrial.alert.title" = "Jaribu imefunguliwa"; +"purchase.expiredTrial" = "Jaribu lako limeisha."; +"purchase.footer.privacyPolicy" = "Sera ya Faragha"; +"purchase.footer.termsOfUse" = "Masharti ya Matumizi"; +"purchase.header.feature.familySharing" = "Kushiriki kwa familia"; +"purchase.header.feature.openSource" = "Maendeleo ya chanzo-wazi"; +"purchase.header.feature.writeAccess" = "Andika ufikiaji wa kuba zako"; +"purchase.product.donateAndUpgrade" = "Changia & Boresha"; +"purchase.product.freeUpgrade" = "Uboreshaji wa Bure"; +"purchase.product.lifetimeLicense" = "Leseni ya Maisha"; +"purchase.product.lifetimeLicense.duration" = "wakati-mmoja"; +"purchase.product.pricing.free" = "Bure"; +"purchase.product.trial" = "Jaribio la Siku-30"; +"purchase.product.trial.expirationDate" = "Tarehe ya Kumalizika:%@"; +"purchase.product.trial.duration" = "kwa siku 30"; +"purchase.product.yearlySubscription" = "Usajili wa Kila Mwaka"; +"purchase.product.yearlySubscription.duration" = "kila mwaka"; +"purchase.readOnlyMode.alert.title" = "Hali ya Kusoma-Tu"; +"purchase.readOnlyMode.alert.message" = "Unaweza kufungua toleo kamili la Cryptomator baadaye katika mipangilio na kuitumia katika hali ya kusoma-tu kwa sasa."; +"purchase.restorePurchase.button" = "Rejesha Ununuzi"; +"purchase.restorePurchase.validTrialFound.alert.title" = "Kesi yaendelea"; +"purchase.restorePurchase.validTrialFound.alert.message" = "Sasa unaweza kutumia toleo kamili la Cryptomator kwa muda mdogo. Jaribio lako linaisha kwenye %@. Baada ya hapo, vaults zako bado zitapatikana katika hali ya kusoma-tu."; +"purchase.restorePurchase.fullVersionFound.alert.title" = "Urejeshi Imefanikiwa"; +"purchase.restorePurchase.fullVersionNotFound.alert.title" = "Hakuna Toleo Kamili"; +"purchase.restorePurchase.fullVersionNotFound.alert.message" = "Hatukuweza kupata toleo kamili lililonunuliwa hapo awali ambalo linaweza kurejeshwa. Tafadhali jaribu chaguo jingine."; +"purchase.restorePurchase.eligibleForUpgrade.alert.title" = "Inastahiki kwa Kuboresha"; +"purchase.restorePurchase.eligibleForUpgrade.alert.message" = "Inaonekana kama kwamba unajaribu kuboresha kutoka kwa toleo la zamani la Cryptomator. Katika kesi hiyo, tafadhali chagua chaguo la \"Boresha Ofa\" badala yake."; +"purchase.retry.button" = "Jaribu tena"; +"purchase.retry.footer" = "Haikuweza kupakia bidhaa zinazopatikana."; +"purchase.title" = "Fungua Toleo Kamili"; +"purchase.unlockedFullVersion.message" = "Sasa unaweza kutumia toleo kamili la Cryptomator. Furaha kusimba!"; +"purchase.unlockedFullVersion.title" = "Asante"; +"purchase.error.unknown" = "Ununuzi huu haupatikani katika App Store kwa sababu isiyojulikana. Tafadhali jaribu tena baadaye.\n\nIf kosa hili linaendelea, jaribu kuanzisha upya kifaa chako au kuingia na kurudi kwenye Kitambulisho chako cha Apple katika mipangilio ya iOS."; + +"settings.title" = "Kipimo"; +"settings.aboutCryptomator" = "Kuhusu Cryptomator"; +"settings.aboutCryptomator.title" = "Toleo %@ (%@)"; +"settings.cacheSize" = "Ukubwa wa Kache"; +"settings.clearCache" = "Ondoa Kache"; +"settings.cloudServices" = "Huduma za wingu"; +"settings.contact" = "Wasiliana"; +"settings.debugMode" = "Rekebisha Hali"; +"settings.debugMode.alert.message" = "Katika hali hii, data nyeti inaweza kuandikwa kwa faili ya kumbukumbu kwenye kifaa chako (kwa mfano, majina ya faili na njia). Nywila, vidakuzi, n.k. zimetengwa wazi.\n\nKukumbuka ili kuzima hali ya utatuzi haraka iwezekanavyo."; +"settings.manageSubscriptions" = "Simamia Usajili"; +"settings.rateApp" = "Programu ya Kiwango"; +"settings.sendLogFile" = "Tuma Faili logi"; +"settings.unlockFullVersion" = "Fungua Toleo Kamili"; +"snapshots.fileprovider.file2" = "/Uwasilishaji wa Mwisho.ufunguo"; +"snapshots.fileprovider.file4" = "/Pendekezo.docx"; +"snapshots.fileprovider.file5" = "/Taarifa.pdf"; +"snapshots.fileprovider.folder3" = "/Mradi wa Siri"; +"snapshots.fileprovider.folder2" = "/Ankara"; +"snapshots.fileprovider.folder1" = "/Vyeti"; +"snapshots.main.vault1" = "/Kazi"; +"snapshots.main.vault2" = "/Familia"; +"snapshots.main.vault3" = "/Nyaraka"; +"snapshots.main.vault4" = "/Safari ya kwenda California"; + +"trialStatus.active" = "Amilifu"; +"trialStatus.expired" = "Imeisha muda wake"; + +"unlockVault.button.unlock" = "Fungua"; +"unlockVault.button.unlockVia" = "Fungua kupitia %@"; +"unlockVault.password.footer" = "Ingiza neno la siri la \"%@\"."; +"unlockVault.enableBiometricalUnlock.switch" = "Wezesha %@"; +"unlockVault.enableBiometricalUnlock.footer" = "Badala ya kufungua kuba yako na neno la siri lako, unaweza kuifungua kupitia %@."; +"unlockVault.evaluatePolicy.reason" = "Fungua kuba yako"; +"unlockVault.progress" = "Fungua…"; + +"untrustedTLSCertificate.title" = "Cheti batili cha TLS"; +"untrustedTLSCertificate.message" = "Cheti cha TLS cha \"%@\" ni batili. Unataka kuiamini hata hivyo?\n \nSHA-256: % @"; +"untrustedTLSCertificate.add" = "Amini"; +"untrustedTLSCertificate.dismiss" = "Usiamini"; + +"upgrade.title" = "Ofa ya Kuboresha"; +"upgrade.notEligible.alert.title" = "Kupandisha daraja kumeshindikana"; +"upgrade.notEligible.alert.message" = "Cryptomator haikuweza kugundua toleo la zamani lililosakinishwa kwenye kifaa chako. Ikiwa uliinunua, tafadhali pakua tena kutoka kwa App Store na ujaribu tena."; +"upgrade.info" = "Shukrani kwa kuamini Cryptomator tangu toleo la kwanza. Kama mtumiaji mwaminifu, unastahiki uboreshaji wa bure."; + +"urlSession.error.httpError.401" = "Jina la mtumiaji lisilo sahihi na / au nenosiri."; +"urlSession.error.httpError.403" = "Haki za kutosha za rasilimali zilizoombwa."; +"urlSession.error.httpError.404" = "Rasilimali iliyoombwa haipatikani."; +"urlSession.error.httpError.405" = "Njia ya ombi haitegemezwi na rasilimali lengwa."; +"urlSession.error.httpError.409" = "Omba mgogoro na hali ya sasa ya rasilimali lengwa."; +"urlSession.error.httpError.412" = "Upatikanaji wa rasilimali lengwa umekataliwa."; +"urlSession.error.httpError.default" = "Muunganisho wa mtandao umeshindikana na msimbo wa hali %ld."; +"urlSession.error.unexpectedResponse" = "Kulikuwa na majibu ya mtandao yasiyotarajiwa."; + +"vaultAccountManager.error.vaultAccountAlreadyExists" = "Tayari umeongeza kuba hii."; + +"vaultDetail.button.changeVaultPassword" = "Badilisha Neno la siri"; +"vaultDetail.button.lock" = "Funga Sasa"; +"vaultDetail.button.moveVault" = "Hamisha"; +"vaultDetail.button.removeVault" = "Ondoa kutoka kwenye Orodha ya Kuba"; +"vaultDetail.button.renameVault" = "Ita jina jipya"; +"vaultDetail.changePassword.footer" = "Chagua neno la siri kali la kuba yako ambayo tu unajua na kuiweka mahali salama."; +"vaultDetail.disabledBiometricalUnlock.footer" = "Ukiwezesha %@, nenosiri lako la kuba litahifadhiwa kwenye kitufe cha iOS."; +"vaultDetail.enabledBiometricalUnlock.footer" = "Neno la siri lako ya kuba itahitajika tu ikiwa uthibitishaji %@ utashindwa."; +"vaultDetail.info.footer.accessVault" = "Fikia kuba kupitia programu ya Mafaili."; +"vaultDetail.info.footer.accountInfo" = "Umeingia kama %@ kupitia %@."; +"vaultDetail.keepUnlocked.title" = "Fungua Muda"; +"vaultDetail.keepUnlocked.footer.off" = "Fungua itahitajika wakati Cryptomator imekatishwa na programu ya Mafaili."; +"vaultDetail.keepUnlocked.footer.limitedDuration" = "Fungua itahitajika wakati kuba yako imekosa kazi kwa %@."; +"vaultDetail.keepUnlocked.footer.unlimitedDuration" = "Hakuna kufungua itahitajika isipokuwa imefungwa kwa kikuli."; +"vaultDetail.locked.footer" = "Kuba yako imefungwa kwa sasa."; +"vaultDetail.moveVault.detectedMasterkey.text" = "Cryptomator imegundua kuba iliyopo katika eneo hili.\nIn ili kuhamisha vault yako, tafadhali rudi nyuma na uchague folda tofauti."; +"vaultDetail.moveVault.progress" = "Kusonga…"; +"vaultDetail.removeVault.footer" = "Hii itaondoa tu kuba kutoka kwenye orodha ya kuba na sio kufuta faili zozote zilizosimbwa kwa njia fiche."; +"vaultDetail.renameVault.progress" = "Inaita jina jipya…"; +"vaultDetail.unlocked.footer" = "Kuba yako imefunguliwa kwa sasa katika programu ya Mafaili."; +"vaultDetail.unlockVault.footer" = "Ingiza nenosiri la \"%@\" ili kuihifadhi kwenye kitufe cha iOS na kuwezesha %@."; + +"vaultList.header.title" = "Kuba"; +"vaultList.emptyList.message" = "Gonga hapa ili kuongeza kuba"; +"vaultList.remove.alert.title" = "Ondoa Kuba?"; +"vaultList.remove.alert.message" = "Hii itaondoa tu kuba kutoka kwenye orodha ya kuba. Hakuna data iliyosimbwa kwa njia fiche itakayofutwa. Unaweza kuongeza tena kuba baadaye."; + +"vaultProviderFactory.error.unsupportedVaultConfig" = "Usanidi wa Vault hautegemezwi. Tafadhali hakikisha kuwa unaendesha toleo la hivi karibuni la Cryptomator."; +"vaultProviderFactory.error.unsupportedVaultVersion" = "Toleo la Kuba %ld halitegemezwi. Kuba hii imeundwa na toleo la zamani au jipya zaidi la Cryptomator."; + +"webDAVAuthentication.progress" = "Inathibitisha…"; +"webDAVAuthentication.httpConnection.alert.title" = "Tumia HTTPS?"; +"webDAVAuthentication.httpConnection.alert.message" = "Matumizi ya HTTP ni salama. Tunapendekeza kutumia HTTPS badala yake. Ikiwa unajua hatari, unaweza kuendelea na HTTP."; +"webDAVAuthentication.httpConnection.change" = "Badilisha hadi HTTPS"; +"webDAVAuthentication.httpConnection.continue" = "Weka HTTP"; + +"webDAVAuthenticator.error.unsupportedProtocol" = "Seva haionekani kuwa inaoana na WebDAV. Tafadhali angalia ikiwa umetumia URL sahihi."; +"webDAVAuthenticator.error.untrustedCertificate" = "Cheti cha seva hii hakiaminiki. Huenda ukalazimika kuongeza upya muunganisho huu wa WebDAV."; diff --git a/SharedResources/ta.lproj/Localizable.strings b/SharedResources/ta.lproj/Localizable.strings index 175f09591..11ec8caae 100644 --- a/SharedResources/ta.lproj/Localizable.strings +++ b/SharedResources/ta.lproj/Localizable.strings @@ -1,6 +1,7 @@ "common.alert.attention.title" = "கவனிக்க"; "common.button.enable" = "இயக்கு"; "common.button.ok" = "சரி"; +"common.button.retry" = "மீண்டும் முயற்சிக்கவும்"; "common.cells.url" = "இணையமுகவரி"; "common.cells.username" = "பயனர்பெயர்"; "purchase.retry.button" = "மீண்டும் முயற்சிக்கவும்"; diff --git a/SharedResources/te.lproj/Localizable.strings b/SharedResources/te.lproj/Localizable.strings index 4dfd1b96c..d10b8a052 100644 --- a/SharedResources/te.lproj/Localizable.strings +++ b/SharedResources/te.lproj/Localizable.strings @@ -5,6 +5,7 @@ "common.button.enable" = "ప్రారంభించు"; "common.button.ok" = "సరే"; "common.button.remove" = "తొలగించు"; +"common.button.retry" = "మళ్ళీ చేయండి"; "common.cells.url" = "URL"; "common.cells.username" = "వినియోగదారు పేరు"; "purchase.retry.button" = "మళ్ళీ చేయండి"; diff --git a/SharedResources/th.lproj/Localizable.strings b/SharedResources/th.lproj/Localizable.strings index 9d86f5bcf..2b7fe343a 100644 --- a/SharedResources/th.lproj/Localizable.strings +++ b/SharedResources/th.lproj/Localizable.strings @@ -1,5 +1,6 @@ "common.button.cancel" = "ยกเลิก"; "common.button.change" = "เปลี่ยน"; +"common.button.close" = "ปิด"; "common.button.done" = "เสร็จสิ้น"; "common.button.next" = "ถัดไป"; diff --git a/SharedResources/tr.lproj/Localizable.strings b/SharedResources/tr.lproj/Localizable.strings index 568c94bda..b8f041ad1 100644 --- a/SharedResources/tr.lproj/Localizable.strings +++ b/SharedResources/tr.lproj/Localizable.strings @@ -10,6 +10,7 @@ "common.button.cancel" = "İptal"; "common.button.change" = "Değiştir"; "common.button.choose" = "Seç"; +"common.button.close" = "Kapat"; "common.button.confirm" = "Onayla"; "common.button.create" = "Oluştur"; "common.button.createFolder" = "Klasör Oluştur"; @@ -20,6 +21,7 @@ "common.button.next" = "Sonraki"; "common.button.ok" = "Tamam"; "common.button.remove" = "Kaldır"; +"common.button.retry" = "Yeniden dene"; "common.button.signOut" = "Çıkış Yap"; "common.button.verify" = "Doğrula"; "common.cells.openInFilesApp" = "Dosyalar Uygulamasında Aç"; @@ -95,6 +97,11 @@ "fileProvider.error.defaultLock.title" = "Kilit açma gerekli"; "fileProvider.error.defaultLock.message" = "Kasanızın içeriğine erişmek ve göstermek için kilidinin açılması gerekir."; "fileProvider.error.unlockButton" = "Kilidi Aç"; +"fileProvider.uploadProgress.connecting" = "Bağlanıyor…"; +"fileProvider.uploadProgress.message" = "Mevcut Süreç: %@\n\nYükleme sürecinin takıldığını fark ederseniz, yeniden yüklemeyi deneyebilirsiniz."; +"fileProvider.uploadProgress.missing" = "İlerleme saptanamadı. Hala arka planda çalışıyor olabilir."; +"fileProvider.uploadProgress.title" = "Yükleniyor…"; +"fileProvider.uploadProgress.missingDomainError" = "Alan adı bulunamadı."; "keepUnlocked.alert.title" = "Kasa kilitlensin mi?"; "keepUnlocked.alert.message" = "Yaptığınız değişikliğin geçerli olması için kasanızın kilitlenmesi gerekmektedir."; @@ -149,6 +156,8 @@ "purchase.restorePurchase.fullVersionFound.alert.title" = "Geri Yükleme Başarılı"; "purchase.restorePurchase.fullVersionNotFound.alert.title" = "Tam Sürüm Yok"; "purchase.restorePurchase.fullVersionNotFound.alert.message" = "Geri yüklenebilecek önceden satın alınmış bir tam sürüm bulamadık. Lütfen başka bir seçenek deneyin."; +"purchase.restorePurchase.eligibleForUpgrade.alert.title" = "Yükseltme için uygun"; +"purchase.restorePurchase.eligibleForUpgrade.alert.message" = "Görünüşe göre daha eski bir Cryptomator sürümünden yükseltmeye çalışıyorsunuz. Bu durumda, lütfen bunun yerine \"Yükseltme Teklifi\" seçeneğini seçin."; "purchase.retry.button" = "Yeniden dene"; "purchase.retry.footer" = "Mevcut ürünler yüklenemedi."; "purchase.title" = "Tam Sürümün Kilidini Aç"; @@ -253,3 +262,5 @@ "webDAVAuthenticator.error.unsupportedProtocol" = "Sunucu, WebDAV uyumlu görünmüyor. Lütfen doğru URL'yi kullanıp kullanmadığınızı kontrol edin."; "webDAVAuthenticator.error.untrustedCertificate" = "Bu sunucunun sertifikası güvenilir değil. Bu WebDAV bağlantısını yeniden eklemeniz gerekebilir."; + +"Retry Upload" = "Yüklemeyi yeniden dene"; diff --git a/SharedResources/uk.lproj/Localizable.strings b/SharedResources/uk.lproj/Localizable.strings index 36d5494f7..d324a5cfd 100644 --- a/SharedResources/uk.lproj/Localizable.strings +++ b/SharedResources/uk.lproj/Localizable.strings @@ -10,6 +10,7 @@ "common.button.cancel" = "Відмінити"; "common.button.change" = "Змінити"; "common.button.choose" = "Оберіть"; +"common.button.close" = "Закрити"; "common.button.confirm" = "Підтвердити"; "common.button.create" = "Створити"; "common.button.createFolder" = "Створити теку"; @@ -20,6 +21,7 @@ "common.button.next" = "Далі"; "common.button.ok" = "Гаразд"; "common.button.remove" = "Прибрати"; +"common.button.retry" = "Повторити"; "common.button.signOut" = "Вийти"; "common.button.verify" = "Перевірити"; "common.cells.openInFilesApp" = "Відкрити в Files"; diff --git a/SharedResources/zh-Hans.lproj/Localizable.strings b/SharedResources/zh-Hans.lproj/Localizable.strings index 9ecfbdefd..fab4bdb55 100644 --- a/SharedResources/zh-Hans.lproj/Localizable.strings +++ b/SharedResources/zh-Hans.lproj/Localizable.strings @@ -10,6 +10,7 @@ "common.button.cancel" = "取消"; "common.button.change" = "更改"; "common.button.choose" = "选择"; +"common.button.close" = "关闭"; "common.button.confirm" = "确认"; "common.button.create" = "新建"; "common.button.createFolder" = "新建文件夹"; @@ -20,6 +21,7 @@ "common.button.next" = "下一步"; "common.button.ok" = "确定"; "common.button.remove" = "删除"; +"common.button.retry" = "重试"; "common.button.signOut" = "退出登录"; "common.button.verify" = "验证"; "common.cells.openInFilesApp" = "在文管应用中打开"; @@ -76,7 +78,7 @@ "cloudProvider.error.itemNotFound" = "找不到 %@\"。"; "cloudProvider.error.itemAlreadyExists" = "\"%@\" 已存在。"; -"cloudProvider.error.itemTypeMismatch" = "\"%@\" 有一个不寻常的项目类型。"; +"cloudProvider.error.itemTypeMismatch" = "\"%@\" 有一个非预期的文件类型。"; "cloudProvider.error.parentFolderDoesNotExist" = "父文件夹 \"%@\" 不存在。"; "cloudProvider.error.pageTokenInvalid" = "无法继续获取目录内容。"; "cloudProvider.error.quotaInsufficient" = "您的存储空间不足。"; @@ -95,6 +97,11 @@ "fileProvider.error.defaultLock.title" = "需要解锁"; "fileProvider.error.defaultLock.message" = "需解锁才能访问和显示您的保险库中的内容。"; "fileProvider.error.unlockButton" = "解锁"; +"fileProvider.uploadProgress.connecting" = "正在连接…"; +"fileProvider.uploadProgress.message" = "当前进度:%@\n\n若发现上传进度卡住,请重试上传"; +"fileProvider.uploadProgress.missing" = "无法确定上传进度,可能仍在后台运行"; +"fileProvider.uploadProgress.title" = "正在上传…"; +"fileProvider.uploadProgress.missingDomainError" = "找不到域名"; "keepUnlocked.alert.title" = "锁定保险库?"; "keepUnlocked.alert.message" = "此更改需要锁定您的保险库才能生效。"; @@ -149,6 +156,8 @@ "purchase.restorePurchase.fullVersionFound.alert.title" = "恢复成功"; "purchase.restorePurchase.fullVersionNotFound.alert.title" = "未购买完整版"; "purchase.restorePurchase.fullVersionNotFound.alert.message" = "我们无法找到可供恢复的完整版本。请尝试其他选项。"; +"purchase.restorePurchase.eligibleForUpgrade.alert.title" = "升级资格"; +"purchase.restorePurchase.eligibleForUpgrade.alert.message" = "似乎您正在从 Cryptomator 的旧版本进行升级。在这种情况下,请选择“升级优惠”选项。"; "purchase.retry.button" = "重试"; "purchase.retry.footer" = "无法加载可用产品。"; "purchase.title" = "解锁完整版"; @@ -164,7 +173,7 @@ "settings.cloudServices" = "云服务"; "settings.contact" = "联系"; "settings.debugMode" = "调试模式"; -"settings.debugMode.alert.message" = "在此模式下,敏感数据可能会写入您设备上的日志文件 (例如文件名和路径)。密码、cookies等除外\n\n请记住尽快关闭调试模式"; +"settings.debugMode.alert.message" = "在此模式下,敏感数据可能会写入您设备上的日志文件 (例如文件名和路径)。密码、cookies等除外\n\n请记住要尽快关闭调试模式"; "settings.manageSubscriptions" = "管理订阅"; "settings.rateApp" = "给应用评分"; "settings.sendLogFile" = "发送日志文件"; @@ -211,7 +220,7 @@ "urlSession.error.httpError.409" = "请求与目标资源的当前状态冲突。"; "urlSession.error.httpError.412" = "访问目标资源受阻。"; "urlSession.error.httpError.default" = "网络连接失败,状态代码 %ld。"; -"urlSession.error.unexpectedResponse" = "意外的网络响应。"; +"urlSession.error.unexpectedResponse" = "非预期的网络响应。"; "vaultAccountManager.error.vaultAccountAlreadyExists" = "您已添加了此保险库"; @@ -253,3 +262,5 @@ "webDAVAuthenticator.error.unsupportedProtocol" = "服务器似乎不兼容 WebDAV,请检查链接是否正确"; "webDAVAuthenticator.error.untrustedCertificate" = "此服务器的证书不受信任,你可能需要重新添加此 WebDAV 连接"; + +"Retry Upload" = "重试上传"; diff --git a/SharedResources/zh-Hant.lproj/Localizable.strings b/SharedResources/zh-Hant.lproj/Localizable.strings index 89bcf2656..43aff619b 100644 --- a/SharedResources/zh-Hant.lproj/Localizable.strings +++ b/SharedResources/zh-Hant.lproj/Localizable.strings @@ -10,6 +10,7 @@ "common.button.cancel" = "取消"; "common.button.change" = "修改"; "common.button.choose" = "選擇"; +"common.button.close" = "關閉"; "common.button.confirm" = "確認"; "common.button.create" = "新建"; "common.button.createFolder" = "建立資料夾"; @@ -20,6 +21,7 @@ "common.button.next" = "繼續"; "common.button.ok" = "確認"; "common.button.remove" = "移除"; +"common.button.retry" = "重試"; "common.button.signOut" = "登出"; "common.button.verify" = "驗證"; "common.cells.openInFilesApp" = "在「檔案」應用程式中開啟"; @@ -34,10 +36,12 @@ "addVault.title" = "新增加密檔案庫"; "addVault.createNewVault.title" = "新建加密檔案庫"; +"addVault.createNewVault.purchase" = "創建新的加密檔案庫需要 Cryptomator 完整版。"; "addVault.createNewVault.setVaultName.header.title" = "為加密檔案庫命名."; "addVault.createNewVault.setVaultName.cells.name" = "加密檔案庫名稱"; "addVault.createNewVault.setVaultName.error.emptyVaultName" = "加密檔案庫名稱不可留空。"; "addVault.createNewVault.chooseCloud.header" = "Cryptomator 應該將您加密後的檔案存放在哪裡?"; +"addVault.createNewVault.chooseFolder.error.vaultNameCollision" = "“%@”已經在此位置,請更換加密檔案庫名或位置"; "addVault.createNewVault.password.enterPassword.header" = "輸入新密碼."; "addVault.createNewVault.password.confirmPassword.header" = "確認新密碼."; "addVault.createNewVault.password.confirmPassword.alert.title" = "確認密碼?"; @@ -51,9 +55,13 @@ "addVault.openExistingVault.detectedMasterkey.add" = "添加這個加密檔案庫"; "addVault.openExistingVault.password.footer" = "輸入 「%@」 的密碼:"; "addVault.openExistingVault.progress" = "正在添加加密檔案庫……"; +"addVault.success.info" = "成功添加加密檔案庫“%@”\n現在可以通過文件管理訪問加密檔案庫"; +"addVault.success.footer" = "如果尚未打開,請在文件應用程序中啟用 Cryptomator。"; "biometryType.faceID" = "Face ID"; "biometryType.touchID" = "Touch ID"; + +"changePassword.error.invalidOldPassword" = "你輸入的密碼錯誤,請再試一次。"; "changePassword.header.currentPassword.title" = "請輸入入前的密碼"; "changePassword.header.newPassword.title" = "輸入新密碼."; "changePassword.header.newPasswordConfirmation.title" = "確認新密碼."; @@ -67,21 +75,35 @@ "cloudProvider.error.itemNotFound" = "無法找到「%@」。"; "cloudProvider.error.itemAlreadyExists" = "「%@」已經存在。"; +"cloudProvider.error.itemTypeMismatch" = "“%@”有一個非預期的文件類型"; +"cloudProvider.error.parentFolderDoesNotExist" = "父文件夾“%@”不存在"; "cloudProvider.error.quotaInsufficient" = "您的存儲已空間不足"; "cloudProvider.error.unauthorized" = "無法執行未授權的操作"; "cloudProviderType.localFileSystem" = "其他儲存空間"; "fileProvider.onboarding.title" = "歡迎"; +"fileProvider.onboarding.info" = "感謝您選擇 Cryptomator 來保護您的文件。若要開始,請前往主應用程序並添加一個加密檔案庫"; "fileProvider.onboarding.button.openCryptomator" = "打開 Cryptomator"; "fileProvider.error.biometricalAuthCanceled.title" = "解鎖取消"; +"fileProvider.error.biometricalAuthCanceled.message" = "通過“%@”解鎖失敗,請重試"; "fileProvider.error.biometricalAuthWrongPassword.title" = "密碼錯誤"; +"fileProvider.error.biometricalAuthWrongPassword.message" = "保存在%@的密碼錯誤,請重試並輸入你的密碼來重新啟用%@"; "fileProvider.error.defaultLock.title" = "需要解鎖"; +"fileProvider.error.defaultLock.message" = "需要解鎖才能訪問和展示加密檔案庫"; "fileProvider.error.unlockButton" = "解鎖"; + +"keepUnlocked.alert.title" = "鎖定加密檔案庫?"; +"keepUnlocked.alert.message" = "此項更改需要鎖定加密檔案庫後才能生效"; "keepUnlocked.alert.confirm" = "確認並上鎖"; +"keepUnlocked.header" = "指定您希望此加密檔案庫在空閒時保持解鎖狀態的時間"; +"keepUnlockedDuration.auto" = "由 iOS 自動決定"; "keepUnlockedDuration.auto.shortDisplayName" = "自動"; "keepUnlockedDuration.indefinite" = "無期限"; + +"localFileSystemAuthentication.createNewVault.header" = "請在下一個屏幕選擇新加密檔案庫的存儲位置"; "localFileSystemAuthentication.createNewVault.button" = "選取儲存位置"; +"localFileSystemAuthentication.openExistingVault.header" = "請在下一屏中選擇已存在加密檔案庫所在的文件夾"; "localFileSystemAuthentication.openExistingVault.button" = "選取加密檔案庫的資料夾"; "onboarding.title" = "歡迎"; @@ -102,6 +124,7 @@ "settings.cloudServices" = "雲端服務"; "settings.contact" = "聯絡方式"; "settings.debugMode" = "調試模式"; +"settings.debugMode.alert.message" = "在此模式下,敏感數據可能會寫入您設備上的日誌文件(例如文件名和路徑)。 密碼、cookies 等被明確排除在外。\n\n請記住盡快禁用調試模式。"; "settings.rateApp" = "為應用程式評分"; "unlockVault.button.unlock" = "解鎖"; From 8d47bda61f6df03a8f8f8f09ccf9dba583e086b7 Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Mon, 16 May 2022 17:11:03 +0200 Subject: [PATCH 13/18] Enabled Traditional Chinese and Swahili (Tanzania) translations [ci skip] --- Cryptomator.xcodeproj/project.pbxproj | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Cryptomator.xcodeproj/project.pbxproj b/Cryptomator.xcodeproj/project.pbxproj index 72e758a65..06af97605 100644 --- a/Cryptomator.xcodeproj/project.pbxproj +++ b/Cryptomator.xcodeproj/project.pbxproj @@ -893,6 +893,8 @@ 74267A1A26A5799A004C61BC /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/Localizable.strings; sourceTree = ""; }; 74267A1C26A5799F004C61BC /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = ""; }; 74267A1D26A579A4004C61BC /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; + 74397A842832A05E00CB9410 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Localizable.strings"; sourceTree = ""; }; + 74397A852832A09B00CB9410 /* sw-TZ */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "sw-TZ"; path = "sw-TZ.lproj/Localizable.strings"; sourceTree = ""; }; 7460FFED26FB6C100018BCC4 /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = pt.lproj/Localizable.strings; sourceTree = ""; }; 7460FFEE26FCC6FC0018BCC4 /* OnboardingNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingNavigationController.swift; sourceTree = ""; }; 7469AD99266E26B0000DCD45 /* URL+Zip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Zip.swift"; sourceTree = ""; }; @@ -2108,8 +2110,10 @@ ru, sk, sv, + "sw-TZ", tr, "zh-Hans", + "zh-Hant", ); mainGroup = 4A5E5B202453119100BD6298; packageReferences = ( @@ -2787,8 +2791,10 @@ 74267A1626A5798C004C61BC /* ru */, 74267A1726A57990004C61BC /* sk */, 74267A1A26A5799A004C61BC /* sv */, + 74397A852832A09B00CB9410 /* sw-TZ */, 74267A1C26A5799F004C61BC /* tr */, 74267A1D26A579A4004C61BC /* zh-Hans */, + 74397A842832A05E00CB9410 /* zh-Hant */, ); name = Localizable.strings; sourceTree = ""; From 667cc9b6ddd8ad0d8abc2dab074c601508097dd9 Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Mon, 16 May 2022 17:38:22 +0200 Subject: [PATCH 14/18] Preparing 2.2.5 --- Cryptomator.xcodeproj/project.pbxproj | 4 ++-- fastlane/changelog.txt | 6 +++--- fastlane/metadata/de-DE/release_notes.txt | 6 +++--- fastlane/metadata/en-US/release_notes.txt | 6 +++--- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Cryptomator.xcodeproj/project.pbxproj b/Cryptomator.xcodeproj/project.pbxproj index 06af97605..f4e36ee1d 100644 --- a/Cryptomator.xcodeproj/project.pbxproj +++ b/Cryptomator.xcodeproj/project.pbxproj @@ -2960,7 +2960,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 2.2.4; + MARKETING_VERSION = 2.2.5; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -3022,7 +3022,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 2.2.4; + MARKETING_VERSION = 2.2.5; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "-Xfrontend -warn-long-expression-type-checking=200 -Xfrontend -warn-long-function-bodies=200"; diff --git a/fastlane/changelog.txt b/fastlane/changelog.txt index 74cf466d9..1ce934ebf 100644 --- a/fastlane/changelog.txt +++ b/fastlane/changelog.txt @@ -1,3 +1,3 @@ -- Added Bangla and Croatian translations -- Fixed unexpected background activity, which may have caused battery drain issues in some cases (#206, #212) -- Fixed cleanup of residual vaults after reinstalling the app (#209) \ No newline at end of file +- Added "Retry Upload" action (#219) +- Added "Clear from Cache" action (#149, #220) +- Added Traditional Chinese and Swahili (Tanzania) translations \ No newline at end of file diff --git a/fastlane/metadata/de-DE/release_notes.txt b/fastlane/metadata/de-DE/release_notes.txt index 989b8bcbb..9dbc6cb9f 100644 --- a/fastlane/metadata/de-DE/release_notes.txt +++ b/fastlane/metadata/de-DE/release_notes.txt @@ -1,3 +1,3 @@ -- Übersetzungen für Bangla und Kroatisch hinzugefügt -- Unerwartete Hintergrundaktivität behoben, die in manchen Fällen für eine hohe Akkulast sorgte (#206, #212) -- Bereinigung von verbleibenden Tresoren nach Neuinstallation der App behoben (#209) \ No newline at end of file +- Funktion "Upload erneut versuchen" hinzugefügt (#219) +- Funktion "Aus Zwischenspeicher entfernen" hinzugefügt (#149, #220) +- Übersetzungen für Chinesisch (Langzeichen) und Swahili (Tansania) hinzugefügt \ No newline at end of file diff --git a/fastlane/metadata/en-US/release_notes.txt b/fastlane/metadata/en-US/release_notes.txt index 74cf466d9..1ce934ebf 100644 --- a/fastlane/metadata/en-US/release_notes.txt +++ b/fastlane/metadata/en-US/release_notes.txt @@ -1,3 +1,3 @@ -- Added Bangla and Croatian translations -- Fixed unexpected background activity, which may have caused battery drain issues in some cases (#206, #212) -- Fixed cleanup of residual vaults after reinstalling the app (#209) \ No newline at end of file +- Added "Retry Upload" action (#219) +- Added "Clear from Cache" action (#149, #220) +- Added Traditional Chinese and Swahili (Tanzania) translations \ No newline at end of file From c0ca351d267b72ea4f8e37a11ba51ce546b734f9 Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Mon, 16 May 2022 18:27:05 +0200 Subject: [PATCH 15/18] Updated release notes [ci skip] --- fastlane/changelog.txt | 1 + fastlane/metadata/de-DE/release_notes.txt | 1 + fastlane/metadata/en-US/release_notes.txt | 1 + 3 files changed, 3 insertions(+) diff --git a/fastlane/changelog.txt b/fastlane/changelog.txt index 1ce934ebf..f7a0dd9d3 100644 --- a/fastlane/changelog.txt +++ b/fastlane/changelog.txt @@ -1,3 +1,4 @@ +- Failed uploads are now actually shown with an error instead of being stuck in "Waiting" - Added "Retry Upload" action (#219) - Added "Clear from Cache" action (#149, #220) - Added Traditional Chinese and Swahili (Tanzania) translations \ No newline at end of file diff --git a/fastlane/metadata/de-DE/release_notes.txt b/fastlane/metadata/de-DE/release_notes.txt index 9dbc6cb9f..f63ce7318 100644 --- a/fastlane/metadata/de-DE/release_notes.txt +++ b/fastlane/metadata/de-DE/release_notes.txt @@ -1,3 +1,4 @@ +- Fehlgeschlagene Uploads werden nun tatsächlich mit einer Fehlermeldung angezeigt, statt in „Warten“ zu hängen - Funktion "Upload erneut versuchen" hinzugefügt (#219) - Funktion "Aus Zwischenspeicher entfernen" hinzugefügt (#149, #220) - Übersetzungen für Chinesisch (Langzeichen) und Swahili (Tansania) hinzugefügt \ No newline at end of file diff --git a/fastlane/metadata/en-US/release_notes.txt b/fastlane/metadata/en-US/release_notes.txt index 1ce934ebf..f7a0dd9d3 100644 --- a/fastlane/metadata/en-US/release_notes.txt +++ b/fastlane/metadata/en-US/release_notes.txt @@ -1,3 +1,4 @@ +- Failed uploads are now actually shown with an error instead of being stuck in "Waiting" - Added "Retry Upload" action (#219) - Added "Clear from Cache" action (#149, #220) - Added Traditional Chinese and Swahili (Tanzania) translations \ No newline at end of file From 117502e9700fb3d6ccb6e374311df6cb66d7ceb3 Mon Sep 17 00:00:00 2001 From: Cryptobot Date: Mon, 23 May 2022 16:31:21 +0200 Subject: [PATCH 16/18] New Crowdin updates (#221) [ci skip] --- SharedResources/de.lproj/Localizable.strings | 4 + SharedResources/el.lproj/Localizable.strings | 4 + SharedResources/es.lproj/Localizable.strings | 4 + SharedResources/fr.lproj/Localizable.strings | 4 + SharedResources/hi.lproj/Localizable.strings | 4 + SharedResources/hr.lproj/Localizable.strings | 4 + SharedResources/id.lproj/Localizable.strings | 15 +- SharedResources/it.lproj/Localizable.strings | 4 + SharedResources/ja.lproj/Localizable.strings | 11 + SharedResources/nl.lproj/Localizable.strings | 11 + SharedResources/pl.lproj/Localizable.strings | 6 +- .../pt-BR.lproj/Localizable.strings | 4 + SharedResources/ru.lproj/Localizable.strings | 5 + SharedResources/sk.lproj/Localizable.strings | 4 + SharedResources/sv.lproj/Localizable.strings | 6 +- .../sw-TZ.lproj/Localizable.strings | 15 + SharedResources/tr.lproj/Localizable.strings | 4 + .../zh-HK.lproj/Localizable.strings | 271 ++++++++++++++++++ .../zh-Hans.lproj/Localizable.strings | 4 + .../zh-Hant.lproj/Localizable.strings | 129 ++++++++- 20 files changed, 500 insertions(+), 13 deletions(-) diff --git a/SharedResources/de.lproj/Localizable.strings b/SharedResources/de.lproj/Localizable.strings index e26e9e166..c70b30c10 100644 --- a/SharedResources/de.lproj/Localizable.strings +++ b/SharedResources/de.lproj/Localizable.strings @@ -10,6 +10,7 @@ "common.button.cancel" = "Abbrechen"; "common.button.change" = "Ändern"; "common.button.choose" = "Auswählen"; +"common.button.clear" = "Löschen"; "common.button.close" = "Schließen"; "common.button.confirm" = "Bestätigen"; "common.button.create" = "Erstellen"; @@ -97,6 +98,8 @@ "fileProvider.error.defaultLock.title" = "Entsperren erforderlich"; "fileProvider.error.defaultLock.message" = "Um auf den Inhalt deines Tresors zuzugreifen und ihn anzuzeigen, muss dieser entsperrt werden."; "fileProvider.error.unlockButton" = "Entsperren"; +"fileProvider.clearFileFromCache.title" = "Datei aus Zwischenspeicher entfernen"; +"fileProvider.clearFileFromCache.message" = "Dies entfernt nur die lokale Datei von Ihrem Gerät und löscht nicht die Datei in der Cloud."; "fileProvider.uploadProgress.connecting" = "Verbindung wird hergestellt …"; "fileProvider.uploadProgress.missing" = "Der Fortschritt konnte nicht ermittelt werden. Möglicherweise läuft der Upload noch im Hintergrund."; "fileProvider.uploadProgress.title" = "Wird hochgeladen …"; @@ -263,3 +266,4 @@ "webDAVAuthenticator.error.untrustedCertificate" = "Das Zertifikat dieses Servers ist nicht vertrauenswürdig. Eventuell musst du diese WebDAV-Verbindung erneut hinzufügen."; "Retry Upload" = "Upload erneut versuchen"; +"Clear from Cache" = "Aus Zwischenspeicher entfernen"; diff --git a/SharedResources/el.lproj/Localizable.strings b/SharedResources/el.lproj/Localizable.strings index 7120e5eb1..cdd1c9d46 100644 --- a/SharedResources/el.lproj/Localizable.strings +++ b/SharedResources/el.lproj/Localizable.strings @@ -10,6 +10,7 @@ "common.button.cancel" = "Ακύρωση"; "common.button.change" = "Αλλαγή"; "common.button.choose" = "Επιλογή"; +"common.button.clear" = "Εκκαθάριση"; "common.button.close" = "Κλείσιμο"; "common.button.confirm" = "Επιβεβαίωση"; "common.button.create" = "Δημιουργία"; @@ -97,6 +98,8 @@ "fileProvider.error.defaultLock.title" = "Απαιτείται Ξεκλείδωμα"; "fileProvider.error.defaultLock.message" = "Για να αποκτήσετε πρόσβαση και να εμφανίσετε τα περιεχόμενα της κρύπτη σας, πρέπει να ξεκλειδωθεί."; "fileProvider.error.unlockButton" = "Ξεκλείδωμα"; +"fileProvider.clearFileFromCache.title" = "Εκκαθάριση αρχείου από την προσωρινή μνήμη"; +"fileProvider.clearFileFromCache.message" = "Αυτό καταργεί μόνο το τοπικό αρχείο από τη συσκευή σας και δεν διαγράφει το αρχείο στο cloud."; "fileProvider.uploadProgress.connecting" = "Σύνδεση…"; "fileProvider.uploadProgress.message" = "Τρέχουσα Πρόοδος: %@%\n\nΑν παρατηρήσετε ότι η πρόοδος μεταφόρτωσης έχει κολλήσει, μπορείτε να δοκιμάσετε ξανά τη μεταφόρτωση."; "fileProvider.uploadProgress.missing" = "Η πρόοδος δεν μπορεί να καθοριστεί. Μπορεί να εκτελείται στο παρασκήνιο."; @@ -264,3 +267,4 @@ "webDAVAuthenticator.error.untrustedCertificate" = "Το πιστοποιητικό αυτού του διακομιστή δεν είναι έμπιστο. Ίσως χρειαστεί να προσθέσετε ξανά αυτή τη σύνδεση WebDAV."; "Retry Upload" = "Δοκιμάστε ξανά τη μεταφόρτωση"; +"Clear from Cache" = "Εκκαθάριση από τη μνήμη Cache"; diff --git a/SharedResources/es.lproj/Localizable.strings b/SharedResources/es.lproj/Localizable.strings index a8295a34b..035e46e41 100644 --- a/SharedResources/es.lproj/Localizable.strings +++ b/SharedResources/es.lproj/Localizable.strings @@ -10,6 +10,7 @@ "common.button.cancel" = "Cancelar"; "common.button.change" = "Modificar"; "common.button.choose" = "Seleccionar"; +"common.button.clear" = "Borrar"; "common.button.close" = "Cerrar"; "common.button.confirm" = "Confirmar"; "common.button.create" = "Crear"; @@ -97,6 +98,8 @@ "fileProvider.error.defaultLock.title" = "Desbloqueo requerido"; "fileProvider.error.defaultLock.message" = "Para acceder y mostrar el contenido de su bóveda hay que desbloquearla."; "fileProvider.error.unlockButton" = "Desbloquear"; +"fileProvider.clearFileFromCache.title" = "Borrar archivo de la caché"; +"fileProvider.clearFileFromCache.message" = "Esto solo elimina el archivo local de su dispositivo y no lo elimina de la nube."; "fileProvider.uploadProgress.connecting" = "Conectando…"; "fileProvider.uploadProgress.message" = "Progreso actual: %@\n\nSi nota que el progreso de la carga está atascado, puede volver a intentar la carga."; "fileProvider.uploadProgress.missing" = "No se pudo determinar el progreso. Puede que todavía se ejecute en segundo plano."; @@ -264,3 +267,4 @@ "webDAVAuthenticator.error.untrustedCertificate" = "El certificado de este servidor no es confiable. Es posible que tenga que volver a añadir esta conexión WebDAV."; "Retry Upload" = "Reintentar carga"; +"Clear from Cache" = "Borrar de la caché"; diff --git a/SharedResources/fr.lproj/Localizable.strings b/SharedResources/fr.lproj/Localizable.strings index 938a73a78..b04be6753 100644 --- a/SharedResources/fr.lproj/Localizable.strings +++ b/SharedResources/fr.lproj/Localizable.strings @@ -10,6 +10,7 @@ "common.button.cancel" = "Annuler"; "common.button.change" = "Modifier"; "common.button.choose" = "Choisir"; +"common.button.clear" = "Effacer"; "common.button.close" = "Fermer"; "common.button.confirm" = "Confirmer"; "common.button.create" = "Créer"; @@ -97,6 +98,8 @@ "fileProvider.error.defaultLock.title" = "Déverrouillage requis"; "fileProvider.error.defaultLock.message" = "Pour accéder et afficher le contenu de votre coffre, il doit être déverrouillé."; "fileProvider.error.unlockButton" = "Déverrouiller"; +"fileProvider.clearFileFromCache.title" = "Effacer le fichier du cache"; +"fileProvider.clearFileFromCache.message" = "Cela ne supprime le fichier local que de votre appareil et ne supprime pas le fichier dans le cloud."; "fileProvider.uploadProgress.connecting" = "Connexion…"; "fileProvider.uploadProgress.message" = "Progression actuelle: %@\n\nSi vous remarquez que la progression de l'envoi est bloquée, vous pouvez recommencer l'envoi."; "fileProvider.uploadProgress.missing" = "La progression n'a pas pu être déterminée. Il se peut que cela soit encore en cours en arrière-plan."; @@ -264,3 +267,4 @@ "webDAVAuthenticator.error.untrustedCertificate" = "Le certificat de ce serveur n'est pas fiable. Vous devrez peut-être ré-ajouter cette connexion WebDAV."; "Retry Upload" = "Réessayer l'envoi"; +"Clear from Cache" = "Effacer du cache"; diff --git a/SharedResources/hi.lproj/Localizable.strings b/SharedResources/hi.lproj/Localizable.strings index 7a88c8cdd..e491146b5 100644 --- a/SharedResources/hi.lproj/Localizable.strings +++ b/SharedResources/hi.lproj/Localizable.strings @@ -10,6 +10,7 @@ "common.button.cancel" = "रद्द करें"; "common.button.change" = "बदलें"; "common.button.choose" = "चुनें"; +"common.button.clear" = "साफ़ करें"; "common.button.close" = "बंद करें"; "common.button.confirm" = "पुष्टि करें"; "common.button.create" = "बनाएं"; @@ -83,6 +84,8 @@ "fileProvider.onboarding.info" = "आपके दस्तावेज़ों की सुरक्षा के हेतु क्रिप्टोमेटर का चयन करने के लिए आपका धन्यवाद। शुरू करने के लिए मुख्य ऐप में जाएँ और एक कक्ष तो जोड़ें।"; "fileProvider.onboarding.button.openCryptomator" = "क्रिप्टोमेटर खोलें"; "fileProvider.error.unlockButton" = "अनलॉक करें"; +"fileProvider.clearFileFromCache.title" = "कैशे से फ़ाइल साफ़ करें"; +"fileProvider.clearFileFromCache.message" = "यह केवल आपके डिवाइस से स्थानीय फ़ाइल को हटाता है और क्लाउड में फ़ाइल को नहीं हटाता"; "localFileSystemAuthentication.createNewVault.header" = "अगले स्क्रीन पर अपने कक्ष के लिए भंडार स्थान का चयन करें।"; "localFileSystemAuthentication.createNewVault.button" = "संग्रहण का स्थान चयन करें"; @@ -136,3 +139,4 @@ "vaultList.header.title" = "कक्षों का नाम"; "vaultList.emptyList.message" = "कक्ष जोड़ने के लिए यहाँ दबाएँ"; "vaultList.remove.alert.title" = "कक्ष हटाना चाहेंगे?"; +"Clear from Cache" = "कैशे से फ़ाइल साफ़ करें"; diff --git a/SharedResources/hr.lproj/Localizable.strings b/SharedResources/hr.lproj/Localizable.strings index c9ee24d45..5028dd650 100644 --- a/SharedResources/hr.lproj/Localizable.strings +++ b/SharedResources/hr.lproj/Localizable.strings @@ -10,6 +10,7 @@ "common.button.cancel" = "Odustani"; "common.button.change" = "Promijeni"; "common.button.choose" = "Odaberi"; +"common.button.clear" = "Očisti"; "common.button.close" = "Zatvori"; "common.button.confirm" = "Potvrdi"; "common.button.create" = "Izradi"; @@ -97,6 +98,8 @@ "fileProvider.error.defaultLock.title" = "Potrebno je otključanje"; "fileProvider.error.defaultLock.message" = "Za pristup i prikaz sadržaja Vašeg trezora, on mora biti otključan."; "fileProvider.error.unlockButton" = "Otključaj"; +"fileProvider.clearFileFromCache.title" = "Očisti datoteku iz predmemorije"; +"fileProvider.clearFileFromCache.message" = "Ovo samo uklanja lokalnu datoteku s vašeg uređaja, a ne briše datoteku u oblaku."; "fileProvider.uploadProgress.connecting" = "Spajanje…"; "fileProvider.uploadProgress.message" = "Trenutni napredak: %@\n\nAko primijetite da je napredak učitavanja zapeo, možete ponovno pokušati s prijenosom."; "fileProvider.uploadProgress.missing" = "Napredak se ne može utvrditi. Možda još uvijek radi u pozadini."; @@ -264,3 +267,4 @@ "webDAVAuthenticator.error.untrustedCertificate" = "Certifikat ovog poslužitelja nije pouzdan. Možda ćete morati ponovno dodati ovu WebDAV vezu."; "Retry Upload" = "Ponovno pokušaj prijenos"; +"Clear from Cache" = "Očisti iz predmemorije"; diff --git a/SharedResources/id.lproj/Localizable.strings b/SharedResources/id.lproj/Localizable.strings index 6c2198db7..ecccd80e7 100644 --- a/SharedResources/id.lproj/Localizable.strings +++ b/SharedResources/id.lproj/Localizable.strings @@ -10,6 +10,7 @@ "common.button.cancel" = "Batalkan"; "common.button.change" = "Ganti"; "common.button.choose" = "Pilih"; +"common.button.clear" = "Hapus"; "common.button.close" = "Tutup"; "common.button.confirm" = "Konfirmasi"; "common.button.create" = "Buat"; @@ -97,6 +98,13 @@ "fileProvider.error.defaultLock.title" = "Kunci Perlu Dibuka"; "fileProvider.error.defaultLock.message" = "Kunci perlu dibuka untuk mengakses dan menunjukkan isi vault Anda."; "fileProvider.error.unlockButton" = "Buka Kunci"; +"fileProvider.clearFileFromCache.title" = "Hapus File dari Cache"; +"fileProvider.clearFileFromCache.message" = "Ini hanya akan menghapus file lokal di perangkat Anda dan tidak akan menghapus file yang ada di cloud."; +"fileProvider.uploadProgress.connecting" = "Menyambungkan…"; +"fileProvider.uploadProgress.message" = "Kemajuan Proses Saat Ini: %@\n\nJika proses Upload tidak mengalami peningkatan, Anda bisa mencoba unggah kembali."; +"fileProvider.uploadProgress.missing" = "Kemajuan proses tidak dapat ditentukan. Kemungkinan masih berjalan di latar belakang."; +"fileProvider.uploadProgress.title" = "Mengunggah…"; +"fileProvider.uploadProgress.missingDomainError" = "Tidak dapat menemukan domain."; "keepUnlocked.alert.title" = "Kunci Vault?"; "keepUnlocked.alert.message" = "Perubahan berikut memerlukan vault Anda dalam kondisi terbuka agar bisa diterapkan."; @@ -151,6 +159,8 @@ "purchase.restorePurchase.fullVersionFound.alert.title" = "Pemulihan Berhasil"; "purchase.restorePurchase.fullVersionNotFound.alert.title" = "Versi Lengkap Tidak Tersedia"; "purchase.restorePurchase.fullVersionNotFound.alert.message" = "Kami tidak dapat menemukan versi lengkap yang sudah dibeli untuk dipulihkan. Silahkan coba opsi lain."; +"purchase.restorePurchase.eligibleForUpgrade.alert.title" = "Berhak untuk Peningkatan"; +"purchase.restorePurchase.eligibleForUpgrade.alert.message" = "Sepertinya Anda mencoba melakukan peningkatan dari versi Cryptomator yang lebih lama. Jika demikian, silakan pilih opsi \"Penawaran Peningkatan\"."; "purchase.retry.button" = "Coba lagi"; "purchase.retry.footer" = "Tidak dapat memuat produk yang tersedia."; "purchase.title" = "Buka Versi Lengkap"; @@ -201,7 +211,7 @@ "untrustedTLSCertificate.add" = "Percayai"; "untrustedTLSCertificate.dismiss" = "Jangan Percaya"; -"upgrade.title" = "Penawaran Upgrade"; +"upgrade.title" = "Penawaran Peningkatan"; "upgrade.notEligible.alert.title" = "Upgrade gagal"; "upgrade.notEligible.alert.message" = "Cryptomator tidak bisa menemukan versi yang lebih lama terpasang di perangkat Anda saat ini. Jika Anda sudah membelinya, silahkan download kembali melalui App Store dan coba kembali."; "upgrade.info" = "Terima kasih telah mempercayai Cryptomator sejak versi pertama. Sebagai pengguna setia, Anda berhak mendapatkan upgrade gratis."; @@ -255,3 +265,6 @@ "webDAVAuthenticator.error.unsupportedProtocol" = "Server tampaknya tidak kompatibel dengan WebDAV. Harap periksa apakah Anda telah menggunakan URL yang benar."; "webDAVAuthenticator.error.untrustedCertificate" = "Sertifikat server ini tidak tepercaya. Anda mungkin harus menambahkan kembali koneksi WebDAV ini."; + +"Retry Upload" = "Unggah Kembali"; +"Clear from Cache" = "Hapus dari Cache"; diff --git a/SharedResources/it.lproj/Localizable.strings b/SharedResources/it.lproj/Localizable.strings index d941f06de..ea587fa15 100644 --- a/SharedResources/it.lproj/Localizable.strings +++ b/SharedResources/it.lproj/Localizable.strings @@ -10,6 +10,7 @@ "common.button.cancel" = "Annulla"; "common.button.change" = "Aggiorna"; "common.button.choose" = "Scegli"; +"common.button.clear" = "Cancella"; "common.button.close" = "Chiudi"; "common.button.confirm" = "Conferma"; "common.button.create" = "Crea"; @@ -97,6 +98,8 @@ "fileProvider.error.defaultLock.title" = "Richiesto Sblocco"; "fileProvider.error.defaultLock.message" = "La cassaforte deve essere sbloccata per accedere e vedere il contenuto."; "fileProvider.error.unlockButton" = "Sblocca"; +"fileProvider.clearFileFromCache.title" = "Cancella file dalla cache"; +"fileProvider.clearFileFromCache.message" = "Questo rimuove solo il file locale dal dispositivo e non elimina il file nel cloud."; "fileProvider.uploadProgress.connecting" = "Connessione…"; "fileProvider.uploadProgress.missing" = "Non è stato possibile determinare i progressi, che potrebbero essere ancora in esecuzione in background."; "fileProvider.uploadProgress.title" = "Caricamento…"; @@ -263,3 +266,4 @@ "webDAVAuthenticator.error.untrustedCertificate" = "Il certificato di questo server non è attendibile. Potrebbe essere necessario aggiungere nuovamente questa connessione WebDAV."; "Retry Upload" = "Riprova A Caricare"; +"Clear from Cache" = "Elimina dalla cache"; diff --git a/SharedResources/ja.lproj/Localizable.strings b/SharedResources/ja.lproj/Localizable.strings index d541de475..8d6945715 100644 --- a/SharedResources/ja.lproj/Localizable.strings +++ b/SharedResources/ja.lproj/Localizable.strings @@ -10,6 +10,7 @@ "common.button.cancel" = "キャンセル"; "common.button.change" = "変更"; "common.button.choose" = "選択"; +"common.button.clear" = "明らか"; "common.button.close" = "閉じる"; "common.button.confirm" = "確認"; "common.button.create" = "作成"; @@ -97,6 +98,13 @@ "fileProvider.error.defaultLock.title" = "ロック解除が必要です"; "fileProvider.error.defaultLock.message" = "保管庫の内容にアクセスして表示するには、ロックを解除する必要があります。"; "fileProvider.error.unlockButton" = "解錠"; +"fileProvider.clearFileFromCache.title" = "キャッシュからファイルをクリア"; +"fileProvider.clearFileFromCache.message" = "これにより、デバイスからローカルファイルが削除されるだけで、クラウド内のファイルは削除されません。"; +"fileProvider.uploadProgress.connecting" = "接続する..."; +"fileProvider.uploadProgress.message" = "現在の進行:%@\n\nアップロードの進行状況が止まっていることに気付いた場合は、アップロードを再試行できます。"; +"fileProvider.uploadProgress.missing" = "進捗状況を確認できませんでした。 まだバックグラウンドで実行されている可能性があります。"; +"fileProvider.uploadProgress.title" = "アップロード中"; +"fileProvider.uploadProgress.missingDomainError" = "ドメインが見つかりませんでした。"; "keepUnlocked.alert.title" = "金庫を施錠しますか?"; "keepUnlocked.alert.message" = "この変更を適用するには金庫を施錠する必要があります。"; @@ -257,3 +265,6 @@ "webDAVAuthenticator.error.unsupportedProtocol" = "サーバーに WebDAV との互換性がありません。正しい URL を使用しているか確認してください"; "webDAVAuthenticator.error.untrustedCertificate" = "このサーバーの証明書は信頼されていません。WebDAV 接続を再追加する必要があります。"; + +"Retry Upload" = "アップロードを再試行"; +"Clear from Cache" = "キャッシュからクリア"; diff --git a/SharedResources/nl.lproj/Localizable.strings b/SharedResources/nl.lproj/Localizable.strings index ab9e4cedb..a435a2033 100644 --- a/SharedResources/nl.lproj/Localizable.strings +++ b/SharedResources/nl.lproj/Localizable.strings @@ -10,6 +10,7 @@ "common.button.cancel" = "Annuleren"; "common.button.change" = "Wijzig"; "common.button.choose" = "Kies"; +"common.button.clear" = "Wissen"; "common.button.close" = "Sluiten"; "common.button.confirm" = "Bevestig"; "common.button.create" = "Maak"; @@ -97,6 +98,13 @@ "fileProvider.error.defaultLock.title" = "Ontgrendeling vereist"; "fileProvider.error.defaultLock.message" = "Om toegang te krijgen tot de inhoud van je kluis, moet deze worden ontgrendeld."; "fileProvider.error.unlockButton" = "Ontgrendel"; +"fileProvider.clearFileFromCache.title" = "Wissen uit cache"; +"fileProvider.clearFileFromCache.message" = "Dit verwijdert alleen het lokale bestand van uw apparaat en verwijdert het bestand in de cloud niet."; +"fileProvider.uploadProgress.connecting" = "Verbinden…"; +"fileProvider.uploadProgress.message" = "Huidige vooruitgang: %@\n\n Als je merkt dat de upload vastzit, dan kan je deze opnieuw proberen."; +"fileProvider.uploadProgress.missing" = "De vooruitgang kon niet worden bepaald. Misschien wordt deze nog steeds op de achtergrond uitgevoerd."; +"fileProvider.uploadProgress.title" = "Uploaden…"; +"fileProvider.uploadProgress.missingDomainError" = "Kan domein niet vinden."; "keepUnlocked.alert.title" = "Kluis vergrendelen?"; "keepUnlocked.alert.message" = "Deze wijziging vereist dat je kluis vergrendeld is om van kracht te worden."; @@ -257,3 +265,6 @@ "webDAVAuthenticator.error.unsupportedProtocol" = "Server lijkt niet compatibel te zijn met WebDAV. Controleer of je de juiste URL hebt gebruikt."; "webDAVAuthenticator.error.untrustedCertificate" = "Certificaat van deze server is niet vertrouwd. Je moet mogelijk deze WebDAV verbinding opnieuw toevoegen."; + +"Retry Upload" = "Upload nogmaals proberen"; +"Clear from Cache" = "Wissen uit cache"; diff --git a/SharedResources/pl.lproj/Localizable.strings b/SharedResources/pl.lproj/Localizable.strings index e7b8c3b9d..f07f9c296 100644 --- a/SharedResources/pl.lproj/Localizable.strings +++ b/SharedResources/pl.lproj/Localizable.strings @@ -10,6 +10,7 @@ "common.button.cancel" = "Anuluj"; "common.button.change" = "Zmień"; "common.button.choose" = "Wybierz"; +"common.button.clear" = "Wyczyść"; "common.button.close" = "Zamknij"; "common.button.confirm" = "Zatwierdź"; "common.button.create" = "Utwórz"; @@ -97,6 +98,8 @@ "fileProvider.error.defaultLock.title" = "Wymagane odblokowanie"; "fileProvider.error.defaultLock.message" = "Sejf musi być odblokowany aby zobaczyć i mieć dostęp do jego zawartości."; "fileProvider.error.unlockButton" = "Odblokuj"; +"fileProvider.clearFileFromCache.title" = "Usuń plik z pamięci podręcznej"; +"fileProvider.clearFileFromCache.message" = "Powoduje to jedynie usunięcie pliku lokalnego z urządzenia, a nie usunięcie pliku w chmurze."; "fileProvider.uploadProgress.connecting" = "Łączenie..."; "fileProvider.uploadProgress.message" = "Bieżący postęp: %@\n\nJeśli zauważysz, że postęp przesyłania zawiesił się, możesz spróbować ponownie przesłać dane."; "fileProvider.uploadProgress.missing" = "Nie można zweryfikować postępu. Może on nadal działać w tle."; @@ -156,7 +159,7 @@ "purchase.restorePurchase.fullVersionFound.alert.title" = "Przywrócono pomyślnie"; "purchase.restorePurchase.fullVersionNotFound.alert.title" = "Brak pełnej wersji"; "purchase.restorePurchase.fullVersionNotFound.alert.message" = "Nie mogliśmy znaleźć wcześniej zakupionej pełnej wersji, która mogłaby zostać przywrócona. Proszę spróbować innej opcji."; -"purchase.restorePurchase.eligibleForUpgrade.alert.title" = "Dostępna aktualizacja"; +"purchase.restorePurchase.eligibleForUpgrade.alert.title" = "Możliwość ulepszenia wersji"; "purchase.restorePurchase.eligibleForUpgrade.alert.message" = "Wygląda na to, że próbujesz uaktualnić ze starszej wersji Cryptomator. W takim przypadku wybierz opcję \"Uaktualnij Ofertę\"."; "purchase.retry.button" = "Ponów"; "purchase.retry.footer" = "Nie można załadować dostępnych produktów."; @@ -264,3 +267,4 @@ "webDAVAuthenticator.error.untrustedCertificate" = "Certyfikat tego serwera nie jest zaufany. Może być konieczne ponowne dodanie połączenia WebDAV."; "Retry Upload" = "Ponów wysyłanie"; +"Clear from Cache" = "Usuń z pamięci podręcznej"; diff --git a/SharedResources/pt-BR.lproj/Localizable.strings b/SharedResources/pt-BR.lproj/Localizable.strings index bcc99dfe7..8116a4a65 100644 --- a/SharedResources/pt-BR.lproj/Localizable.strings +++ b/SharedResources/pt-BR.lproj/Localizable.strings @@ -10,6 +10,7 @@ "common.button.cancel" = "Cancelar"; "common.button.change" = "Alterar"; "common.button.choose" = "Escolher"; +"common.button.clear" = "Limpar"; "common.button.close" = "Fechar"; "common.button.confirm" = "Confirmar"; "common.button.create" = "Criar"; @@ -97,6 +98,8 @@ "fileProvider.error.defaultLock.title" = "Desbloqueio necessário"; "fileProvider.error.defaultLock.message" = "Para acessar e mostrar o conteúdo do seu cofre, ele precisa ser desbloqueado."; "fileProvider.error.unlockButton" = "Desbloquear"; +"fileProvider.clearFileFromCache.title" = "Limpar Arquivos Temporários"; +"fileProvider.clearFileFromCache.message" = "Isso apenas remove o arquivo do seu dispositivo local e não o exclui da nuvem."; "fileProvider.uploadProgress.connecting" = "Conectando…"; "fileProvider.uploadProgress.message" = "Progresso atual: %@\n\nSe você notar que o progresso do upload está travado, pode tentar novamente o upload."; "fileProvider.uploadProgress.missing" = "Não foi possível determinar o progresso, que pode estar sendo executado em segundo plano."; @@ -265,3 +268,4 @@ Para mover o seu cofre, por favor, volte e escolha uma pasta diferente."; "webDAVAuthenticator.error.untrustedCertificate" = "O certificado deste servidor não é confiável. Você pode ter que adicionar novamente esta conexão WebDAV."; "Retry Upload" = "Tentar o envio novamente"; +"Clear from Cache" = "Limpar dos Temporários"; diff --git a/SharedResources/ru.lproj/Localizable.strings b/SharedResources/ru.lproj/Localizable.strings index a73506371..4680771d6 100644 --- a/SharedResources/ru.lproj/Localizable.strings +++ b/SharedResources/ru.lproj/Localizable.strings @@ -10,6 +10,7 @@ "common.button.cancel" = "Отмена"; "common.button.change" = "Изменить"; "common.button.choose" = "Выбрать"; +"common.button.clear" = "Очистить"; "common.button.close" = "Закрыть"; "common.button.confirm" = "Подтвердить"; "common.button.create" = "Создать"; @@ -97,7 +98,10 @@ "fileProvider.error.defaultLock.title" = "Требуется разблокировка"; "fileProvider.error.defaultLock.message" = "Хранилище должно быть разблокировано, чтобы получить доступ к его содержимому."; "fileProvider.error.unlockButton" = "Разблокировать"; +"fileProvider.clearFileFromCache.title" = "Удалить файл из кэша"; +"fileProvider.clearFileFromCache.message" = "Эта операция удаляет только локальный файл с вашего устройства, но не из облака."; "fileProvider.uploadProgress.connecting" = "Подключение…"; +"fileProvider.uploadProgress.message" = "Текущий прогресс: %@\n\nЕсли вы заметите, что прогресс загрузки завис, вы можете повторить загрузку."; "fileProvider.uploadProgress.missing" = "Невозможно определить прогресс. Он может продолжаться в фоновом режиме."; "fileProvider.uploadProgress.title" = "Загрузка…"; "fileProvider.uploadProgress.missingDomainError" = "Домен не найден."; @@ -263,3 +267,4 @@ "webDAVAuthenticator.error.untrustedCertificate" = "Сертификат этого сервера ненадёжен. Возможно, вам придётся добавить это подключение WebDAV ещё раз."; "Retry Upload" = "Повторить загрузку"; +"Clear from Cache" = "Удалить из кэша"; diff --git a/SharedResources/sk.lproj/Localizable.strings b/SharedResources/sk.lproj/Localizable.strings index c77106c1e..cc5203b2e 100644 --- a/SharedResources/sk.lproj/Localizable.strings +++ b/SharedResources/sk.lproj/Localizable.strings @@ -10,6 +10,7 @@ "common.button.cancel" = "Zrušiť"; "common.button.change" = "Zmeniť"; "common.button.choose" = "Vybrať"; +"common.button.clear" = "Vymazať"; "common.button.close" = "Zavrieť"; "common.button.confirm" = "Potvrdiť"; "common.button.create" = "Vytvoriť"; @@ -97,6 +98,8 @@ "fileProvider.error.defaultLock.title" = "Odomknutie požadované"; "fileProvider.error.defaultLock.message" = "K prístupu a videniu obsahu Vášho trezora, musí byť odomknutý."; "fileProvider.error.unlockButton" = "Odomknúť"; +"fileProvider.clearFileFromCache.title" = "Vymazať súbor z Cache"; +"fileProvider.clearFileFromCache.message" = "Toto odstráni lokálny súbor iba z Vašeho zariadenia a neodstráni súbor v cloude."; "fileProvider.uploadProgress.connecting" = "Pripájanie…"; "fileProvider.uploadProgress.message" = "Aktuálny priebeh: %@\n\nV prípade, že pozorujete zaseknutý priebeh nahrávania, môžte zopakovať nahrávanie."; "fileProvider.uploadProgress.missing" = "Pokrok nebol spozorovaný. Môže však stále prebiehať na pozadí."; @@ -263,3 +266,4 @@ "webDAVAuthenticator.error.untrustedCertificate" = "Certifikát tohto servera nie je dôveryhodný. Môžte znovu-pridať toto WebDAV spojenie."; "Retry Upload" = "Opakovať nahratie"; +"Clear from Cache" = "Vymazať z Cache"; diff --git a/SharedResources/sv.lproj/Localizable.strings b/SharedResources/sv.lproj/Localizable.strings index 376548324..f923cc6ad 100644 --- a/SharedResources/sv.lproj/Localizable.strings +++ b/SharedResources/sv.lproj/Localizable.strings @@ -10,6 +10,7 @@ "common.button.cancel" = "Avbryt"; "common.button.change" = "Ändra"; "common.button.choose" = "Välj"; +"common.button.clear" = "Rensa"; "common.button.close" = "Stäng"; "common.button.confirm" = "Bekräfta"; "common.button.create" = "Skapa"; @@ -97,9 +98,11 @@ "fileProvider.error.defaultLock.title" = "Upplåsning krävs"; "fileProvider.error.defaultLock.message" = "För att komma åt och visa innehållet i ditt valv måste det låsas upp."; "fileProvider.error.unlockButton" = "Lås upp"; +"fileProvider.clearFileFromCache.title" = "Ta bort fil från buffert"; +"fileProvider.clearFileFromCache.message" = "Detta tar bara bort den lokala filen från din enhet och tar inte bort filen i molnet."; "fileProvider.uploadProgress.connecting" = "Ansluter…"; "fileProvider.uploadProgress.message" = "Status: %@\n\nOm du märker att uppladdningsprocessen har fastnat kan du försöka ladda upp den igen."; -"fileProvider.uploadProgress.missing" = "Processen kunde inte fastställas. Det kan fortfarande köras i bakgrunden."; +"fileProvider.uploadProgress.missing" = "Oklart om det fungerade. Processen kan fortfarande köras i bakgrunden."; "fileProvider.uploadProgress.title" = "Laddar upp…"; "fileProvider.uploadProgress.missingDomainError" = "Kunde inte hitta domänen."; @@ -264,3 +267,4 @@ "webDAVAuthenticator.error.untrustedCertificate" = "Certifikatet för denna server är inte betrott. Du kan behöva lägga till denna WebDAV-anslutning igen."; "Retry Upload" = "Ladda upp igen"; +"Clear from Cache" = "Ta bort fil från buffert"; diff --git a/SharedResources/sw-TZ.lproj/Localizable.strings b/SharedResources/sw-TZ.lproj/Localizable.strings index 68e1254a0..2863bbc16 100644 --- a/SharedResources/sw-TZ.lproj/Localizable.strings +++ b/SharedResources/sw-TZ.lproj/Localizable.strings @@ -10,6 +10,7 @@ "common.button.cancel" = "Katisha"; "common.button.change" = "Badilisha"; "common.button.choose" = "Chagua"; +"common.button.clear" = "Ondoa"; "common.button.close" = "Futa"; "common.button.confirm" = "Thibitisha"; "common.button.create" = "Unda"; @@ -97,6 +98,13 @@ "fileProvider.error.defaultLock.title" = "Fungua Inahitajika"; "fileProvider.error.defaultLock.message" = "Ili kufikia na kuonyesha yaliyomo kwenye vault yako, inapaswa kufunguliwa."; "fileProvider.error.unlockButton" = "Fungua"; +"fileProvider.clearFileFromCache.title" = "Ondoa Mafaili kutoka kwa Akiba"; +"fileProvider.clearFileFromCache.message" = "Hii huondoa tu faili ya ndani kutoka kwa kifaa chako na haifuti faili kwenye wingu."; +"fileProvider.uploadProgress.connecting" = "Kuunganisha…"; +"fileProvider.uploadProgress.message" = "Maendeleo ya sasa: %@\n\nlf Ikiwa unatambua kuwa maendeleo ya kupakia yamekwama, unaweza kujaribu tena upakiaji."; +"fileProvider.uploadProgress.missing" = "Maendeleo hayawezi kuamuliwa. Inaweza kuwa bado inafanya kazi kwa mandari nyuma."; +"fileProvider.uploadProgress.title" = "Inapakia…"; +"fileProvider.uploadProgress.missingDomainError" = "Haikuweza kupata kikoa."; "keepUnlocked.alert.title" = "Funga Kuba?"; "keepUnlocked.alert.message" = "Mabadiliko haya yanahitaji kuba yako kufungwa ili kuanza kutumika."; @@ -106,6 +114,7 @@ "keepUnlocked.footer.on" = "Kutumia chaguo lililoteuliwa, nakala ya ufunguo wako inahitaji kuhifadhiwa kwenye kiti cha vitufe cha iOS, mradi tu kuba imefunguliwa."; "keepUnlockedDuration.auto" = "Ruhusu iOS Iamue Kiotomatiki"; "keepUnlockedDuration.auto.shortDisplayName" = "Otomatiki"; +"keepUnlockedDuration.indefinite" = "Isiyo na kikomo"; "localFileSystemAuthentication.createNewVault.header" = "Katika skrini inayofuata, chagua eneo la kuhifadhi kwa kuba yako mpya."; "localFileSystemAuthentication.createNewVault.button" = "Teua Mahali pa Hifadhi"; @@ -172,7 +181,10 @@ "settings.rateApp" = "Programu ya Kiwango"; "settings.sendLogFile" = "Tuma Faili logi"; "settings.unlockFullVersion" = "Fungua Toleo Kamili"; + +"snapshots.fileprovider.file1" = "/Namba.za Uhasibu"; "snapshots.fileprovider.file2" = "/Uwasilishaji wa Mwisho.ufunguo"; +"snapshots.fileprovider.file3" = "/Njia ya Bidhaa.mov"; "snapshots.fileprovider.file4" = "/Pendekezo.docx"; "snapshots.fileprovider.file5" = "/Taarifa.pdf"; "snapshots.fileprovider.folder3" = "/Mradi wa Siri"; @@ -253,3 +265,6 @@ "webDAVAuthenticator.error.unsupportedProtocol" = "Seva haionekani kuwa inaoana na WebDAV. Tafadhali angalia ikiwa umetumia URL sahihi."; "webDAVAuthenticator.error.untrustedCertificate" = "Cheti cha seva hii hakiaminiki. Huenda ukalazimika kuongeza upya muunganisho huu wa WebDAV."; + +"Retry Upload" = "Jaribu upya Upakiaji"; +"Clear from Cache" = "Ondoa Mafaili kutoka kwa Akiba"; diff --git a/SharedResources/tr.lproj/Localizable.strings b/SharedResources/tr.lproj/Localizable.strings index b8f041ad1..c715084f3 100644 --- a/SharedResources/tr.lproj/Localizable.strings +++ b/SharedResources/tr.lproj/Localizable.strings @@ -10,6 +10,7 @@ "common.button.cancel" = "İptal"; "common.button.change" = "Değiştir"; "common.button.choose" = "Seç"; +"common.button.clear" = "Temizle"; "common.button.close" = "Kapat"; "common.button.confirm" = "Onayla"; "common.button.create" = "Oluştur"; @@ -97,6 +98,8 @@ "fileProvider.error.defaultLock.title" = "Kilit açma gerekli"; "fileProvider.error.defaultLock.message" = "Kasanızın içeriğine erişmek ve göstermek için kilidinin açılması gerekir."; "fileProvider.error.unlockButton" = "Kilidi Aç"; +"fileProvider.clearFileFromCache.title" = "Dosyayı Önbellekten Temizle"; +"fileProvider.clearFileFromCache.message" = "Bu yalnızca cihazınızdan yerel dosyayı kaldırır ve bulut içindeki dosyayı silmez."; "fileProvider.uploadProgress.connecting" = "Bağlanıyor…"; "fileProvider.uploadProgress.message" = "Mevcut Süreç: %@\n\nYükleme sürecinin takıldığını fark ederseniz, yeniden yüklemeyi deneyebilirsiniz."; "fileProvider.uploadProgress.missing" = "İlerleme saptanamadı. Hala arka planda çalışıyor olabilir."; @@ -264,3 +267,4 @@ "webDAVAuthenticator.error.untrustedCertificate" = "Bu sunucunun sertifikası güvenilir değil. Bu WebDAV bağlantısını yeniden eklemeniz gerekebilir."; "Retry Upload" = "Yüklemeyi yeniden dene"; +"Clear from Cache" = "Önbellekten Temizle"; diff --git a/SharedResources/zh-HK.lproj/Localizable.strings b/SharedResources/zh-HK.lproj/Localizable.strings index e69de29bb..24752fd1e 100644 --- a/SharedResources/zh-HK.lproj/Localizable.strings +++ b/SharedResources/zh-HK.lproj/Localizable.strings @@ -0,0 +1,271 @@ +/* + Localizable.strings + Cryptomator + + Copyright © 2021 Skymatic GmbH. All rights reserved. +*/ + +"common.alert.error.title" = "錯誤"; +"common.alert.attention.title" = "注意"; +"common.button.cancel" = "取消"; +"common.button.change" = "修改"; +"common.button.choose" = "選擇"; +"common.button.clear" = "清除"; +"common.button.close" = "關閉"; +"common.button.confirm" = "確認"; +"common.button.create" = "新增"; +"common.button.createFolder" = "建立資料夾"; +"common.button.done" = "完成"; +"common.button.download" = "下載"; +"common.button.edit" = "編輯"; +"common.button.enable" = "啟用"; +"common.button.next" = "繼續"; +"common.button.ok" = "確認"; +"common.button.remove" = "移除"; +"common.button.retry" = "重試"; +"common.button.signOut" = "登出"; +"common.button.verify" = "驗證"; +"common.cells.openInFilesApp" = "在「檔案」應用程式中開啟"; +"common.cells.password" = "密碼"; +"common.cells.url" = "網址"; +"common.cells.username" = "使用者名稱"; +"common.footer.learnMore" = "了解更多。"; + +"accountList.header.title" = "認證"; +"accountList.emptyList.message" = "點擊此處添加帳號"; +"accountList.signOut.alert.title" = "移除關聯的加密庫?"; +"accountList.signOut.alert.message" = "登出後,所有關聯的加密庫將從列表中移除。這並不會刪除任何加密數據。你可以再次登入並稍後重新加入加密庫。"; + +"addVault.title" = "新增加密庫"; +"addVault.createNewVault.title" = "建立新的加密庫"; +"addVault.createNewVault.purchase" = "建立新的加密庫需要 Cryptomator 完整版。"; +"addVault.createNewVault.setVaultName.header.title" = "為加密庫命名。"; +"addVault.createNewVault.setVaultName.cells.name" = "加密庫名稱"; +"addVault.createNewVault.setVaultName.error.emptyVaultName" = "加密庫名稱不可為空。"; +"addVault.createNewVault.chooseCloud.header" = "Cryptomator 應該把加密後的檔案儲存在哪裏?"; +"addVault.createNewVault.chooseFolder.error.vaultNameCollision" = "「%@」已經在此位置,請更加加密庫名稱或位置。"; +"addVault.createNewVault.detectedMasterkey.text" = "Cryptomator 在此檢測到現有的加密庫。\n如要建立新的加密庫,請返回並選擇其他資料夾。"; +"addVault.createNewVault.password.enterPassword.header" = "輸入新密碼。"; +"addVault.createNewVault.password.confirmPassword.header" = "確認新密碼。"; +"addVault.createNewVault.password.confirmPassword.alert.title" = "確認密碼?"; +"addVault.createNewVault.password.confirmPassword.alert.message" = "重要:如果你遺忘了密碼,你的數據將無法被恢復。"; +"addVault.createNewVault.password.error.emptyPassword" = "密碼不可為空。"; +"addVault.createNewVault.password.error.nonMatchingPasswords" = "密碼不符。"; +"addVault.createNewVault.password.error.tooShortPassword" = "密碼必須包含至少 8 個字符。"; +"addVault.createNewVault.progress" = "正在建立加密庫…"; +"addVault.openExistingVault.title" = "開啟現有的加密庫"; +"addVault.openExistingVault.chooseCloud.header" = "這加密檔案庫在甚麼地方?"; +"addVault.openExistingVault.detectedMasterkey.text" = "Cryptomator 在此檢測到加密庫「%@」。\n你想把這加密庫加到列表嗎?"; +"addVault.openExistingVault.detectedMasterkey.add" = "加入這加密庫"; +"addVault.openExistingVault.password.footer" = "輸入 「%@」 的密碼:"; +"addVault.openExistingVault.progress" = "正在加入加密庫…"; +"addVault.success.info" = "已成功加入加密庫「%@」。\n現在可以通過「檔案」程式瀏覽加密庫。"; +"addVault.success.footer" = "如果還沒有,請在「檔案」應用程式中啟用 Cryptomator。"; + +"biometryType.faceID" = "Face ID"; +"biometryType.touchID" = "Touch ID"; + +"changePassword.error.invalidOldPassword" = "當前的密碼不正確,請重試。"; +"changePassword.header.currentPassword.title" = "輸入目前的密碼。"; +"changePassword.header.newPassword.title" = "輸入新密碼。"; +"changePassword.header.newPasswordConfirmation.title" = "確認新密碼。"; +"changePassword.progress" = "正在更改密碼…"; + +"chooseFolder.emptyFolder.footer" = "資料夾為空"; +"chooseFolder.createNewFolder.header.title" = "為資料夾命名。"; +"chooseFolder.createNewFolder.cells.name" = "資料夾名稱"; +"chooseFolder.createNewFolder.error.emptyFolderName" = "資料夾名稱不得為空。"; +"chooseFolder.createNewFolder.progress" = "建立資料夾中…"; + +"cloudProvider.error.itemNotFound" = "找不到「%@」。"; +"cloudProvider.error.itemAlreadyExists" = "「%@」已經存在。"; +"cloudProvider.error.itemTypeMismatch" = "「%@」為非預期的文件類型。"; +"cloudProvider.error.parentFolderDoesNotExist" = "上一層資料夾「%@」不存在。"; +"cloudProvider.error.pageTokenInvalid" = "無法繼續取得資料夾的內容。"; +"cloudProvider.error.quotaInsufficient" = "儲存空間不足。"; +"cloudProvider.error.unauthorized" = "無法執行未授權的操作。"; +"cloudProvider.error.noInternetConnection" = "此操作需要連線至互聯網。"; + +"cloudProviderType.localFileSystem" = "其他儲存空間"; + +"fileProvider.onboarding.title" = "歡迎"; +"fileProvider.onboarding.info" = "感謝您選擇 Cryptomator 來保護你的文件。若要開始,請先前往主程式並加入加密庫。"; +"fileProvider.onboarding.button.openCryptomator" = "開啟 Cryptomator"; +"fileProvider.error.biometricalAuthCanceled.title" = "解鎖已取消"; +"fileProvider.error.biometricalAuthCanceled.message" = "以%@解鎖失敗,請重試。"; +"fileProvider.error.biometricalAuthWrongPassword.title" = "密碼錯誤"; +"fileProvider.error.biometricalAuthWrongPassword.message" = "保存在 %@ 的密碼錯誤,請重試並輸入你的密碼來重新啟用 %@ 。"; +"fileProvider.error.defaultLock.title" = "需要解鎖"; +"fileProvider.error.defaultLock.message" = "要存取加密庫和顯示其內容,需要先解鎖。"; +"fileProvider.error.unlockButton" = "解鎖"; +"fileProvider.clearFileFromCache.title" = "清除快取檔䅁"; +"fileProvider.clearFileFromCache.message" = "這只會從本機上刪除文件,而不會刪除雲端的文件。"; +"fileProvider.uploadProgress.connecting" = "連接中…"; +"fileProvider.uploadProgress.message" = "目前進度:%@\n\n如果你注意到上傳進度卡住,可以重試上傳。"; +"fileProvider.uploadProgress.missing" = "無法確定目前進度。它可能仍在背景運行。"; +"fileProvider.uploadProgress.title" = "上載中..."; +"fileProvider.uploadProgress.missingDomainError" = "找不到域名。"; + +"keepUnlocked.alert.title" = "鎖定加密庫?"; +"keepUnlocked.alert.message" = "此更改需要鎖定加密庫後才能生效。"; +"keepUnlocked.alert.confirm" = "確認並立即鎖定"; +"keepUnlocked.header" = "指定你希望此加密庫在空閒時保持解鎖狀態的時長。"; +"keepUnlocked.footer.auto" = "讓 iOS 決定意味着 Cryptomator 可能被隨時終止以釋放內存。這會自動鎖定加密檔案庫。"; +"keepUnlocked.footer.on" = "使用這個選項表示在加密檔案庫解鎖時,您密鑰的副本需要儲存在 iOS 鑰匙圈中。"; +"keepUnlockedDuration.auto" = "由 iOS 自動決定"; +"keepUnlockedDuration.auto.shortDisplayName" = "自動"; +"keepUnlockedDuration.indefinite" = "無期限"; + +"localFileSystemAuthentication.createNewVault.header" = "在下一個頁面,請選擇新加密庫的儲存位置。"; +"localFileSystemAuthentication.createNewVault.button" = "選取儲存位置"; +"localFileSystemAuthentication.createNewVault.error.detectedExistingVault" = "此位置已存在加密庫。 請以其他儲存位置重試。"; +"localFileSystemAuthentication.openExistingVault.header" = "在下一個頁面,請選擇已存在加密庫所在的文件夾。"; +"localFileSystemAuthentication.openExistingVault.button" = "選取加密庫的資料夾"; +"localFileSystemAuthentication.openExistingVault.error.noVaultFound" = "選擇的資料夾不是加密檔案庫。請以其他資料夾重試。"; +"localFileSystemAuthentication.info.footer" = "文件提供程式顯示為灰色的表示不支援「選擇資料夾」。這並非 Cryptomator 的限制。"; + +"maintenanceModeError.runningCloudTask" = "無法執行操作,因為此加密庫的其他背景操作必須要先完成。請稍後再試。"; + +"nameValidation.error.endsWithPeriod" = "您不能使用以句點(.)結尾的名稱。請選擇其他名稱。"; +"nameValidation.error.endsWithSpace" = "您不能使用以空格( )結尾的名稱。請選擇其他名稱。"; +"nameValidation.error.containsIllegalCharacter" = "您不能使用具有「%@」的名稱。請選擇其他名稱。"; + +"onboarding.title" = "歡迎"; +"onboarding.info" = "感謝選擇 Cryptomator 來保護您的文件。\n\n有了 Cryptomator,數據的鑰匙就在你手中。 + Cryptomator 可以快速輕鬆地加密您的數據。\n\n此應用程式和「檔案」應用程式完美結合。為此,請確保稍後在「檔案」應用程式中啟用 Cryptomator 以存取你的加密庫。"; +"onboarding.button.continue" = "繼續"; + +"purchase.beginFreeTrial.alert.title" = "試用開始"; +"purchase.expiredTrial" = "你的試用期已過。"; +"purchase.footer.privacyPolicy" = "私隱政策"; +"purchase.footer.termsOfUse" = "使用條款"; +"purchase.header.feature.familySharing" = "家庭共享"; +"purchase.header.feature.openSource" = "鼓勵開源程式開發者"; +"purchase.header.feature.writeAccess" = "寫入加密庫功能"; +"purchase.product.donateAndUpgrade" = "捐款與升級"; +"purchase.product.freeUpgrade" = "免費升級"; +"purchase.product.lifetimeLicense" = "終身授權"; +"purchase.product.lifetimeLicense.duration" = "一次性"; +"purchase.product.pricing.free" = "免費"; +"purchase.product.trial" = "三十日試用"; +"purchase.product.trial.expirationDate" = "到期日期: %@"; +"purchase.product.trial.duration" = "30 日"; +"purchase.product.yearlySubscription" = "年度訂閱"; +"purchase.product.yearlySubscription.duration" = "每年"; +"purchase.readOnlyMode.alert.title" = "唯讀模式"; +"purchase.readOnlyMode.alert.message" = "您可以稍後在設定中解鎖完整版 Cryptomator,而暫時以唯讀模式下繼續使用。"; +"purchase.restorePurchase.button" = "復原已購項目"; +"purchase.restorePurchase.validTrialFound.alert.title" = "繼續試用"; +"purchase.restorePurchase.validTrialFound.alert.message" = "你目前可以在時限內使用完整版的 Cryptomator。試用期將於 %@ 到期。之後,你的加密庫仍可在唯讀模式下取存。"; +"purchase.restorePurchase.fullVersionFound.alert.title" = "復原成功"; +"purchase.restorePurchase.fullVersionNotFound.alert.title" = "沒有完整版"; +"purchase.restorePurchase.fullVersionNotFound.alert.message" = "我們無法找到已購買並可以恢復的完整版本。請嘗試其他選項。"; +"purchase.restorePurchase.eligibleForUpgrade.alert.title" = "可以升級"; +"purchase.restorePurchase.eligibleForUpgrade.alert.message" = "你似乎正嘗試從舊版本的 Cryptomator 升級。在這情況下,請選擇「升級優惠」。"; +"purchase.retry.button" = "重試"; +"purchase.retry.footer" = "無法載入可購買產品。"; +"purchase.title" = "解鎖完整版"; +"purchase.unlockedFullVersion.message" = "您現在可以使用完整版的 Cryptomator。加密快樂!"; +"purchase.unlockedFullVersion.title" = "感謝"; +"purchase.error.unknown" = "由於未知原因,無法在 App Store 中購買,請稍後再試。\n\n如果此錯誤仍然存在,請嘗試重新啟動您的設備、或在 iOS 設定中登出並重新登入您的 Apple ID。"; + +"settings.title" = "設定"; +"settings.aboutCryptomator" = "關於 Cryptomator"; +"settings.aboutCryptomator.title" = "版本號%@(%@)"; +"settings.cacheSize" = "快取大小"; +"settings.clearCache" = "清理快取"; +"settings.cloudServices" = "雲端服務"; +"settings.contact" = "聯絡方式"; +"settings.debugMode" = "除錯模式"; +"settings.debugMode.alert.message" = "在此模式下,敏感數據可能會寫入您設備上的日誌文件(例如文件名和路徑)。密碼、 cookies 等被明確排除在外。\n\n請記住盡快停用除錯模式。"; +"settings.manageSubscriptions" = "管理訂閱"; +"settings.rateApp" = "為應用程式評分"; +"settings.sendLogFile" = "傳送日誌檔案"; +"settings.unlockFullVersion" = "解鎖完整版"; + +"snapshots.fileprovider.file1" = "/財務.numbers"; +"snapshots.fileprovider.file2" = "/最終演講.key"; +"snapshots.fileprovider.file3" = "/產品預告.mov"; +"snapshots.fileprovider.file4" = "/計劃書.docx"; +"snapshots.fileprovider.file5" = "/報告.pdf"; +"snapshots.fileprovider.folder3" = "/秘密計劃"; +"snapshots.fileprovider.folder2" = "/帳單"; +"snapshots.fileprovider.folder1" = "/證書"; +"snapshots.main.vault1" = "/工作"; +"snapshots.main.vault2" = "/家庭"; +"snapshots.main.vault3" = "/文件"; +"snapshots.main.vault4" = "/加州之旅"; + +"trialStatus.active" = "生效中"; +"trialStatus.expired" = "已過期"; + +"unlockVault.button.unlock" = "解鎖"; +"unlockVault.button.unlockVia" = "以%@解鎖"; +"unlockVault.password.footer" = "輸入 「%@」 的密碼:"; +"unlockVault.enableBiometricalUnlock.switch" = "啟用 %@"; +"unlockVault.enableBiometricalUnlock.footer" = "你可以通過 %@ 解鎖加密庫,而不使用密碼解鎖。"; +"unlockVault.evaluatePolicy.reason" = "解鎖你的加密庫"; +"unlockVault.progress" = "解鎖中…"; + +"untrustedTLSCertificate.title" = "無效的 TLS 憑證"; +"untrustedTLSCertificate.message" = "「%@」的 TLS 證書無效。您還是要信任它嗎?\n\nSHA-256: %@"; +"untrustedTLSCertificate.add" = "信任"; +"untrustedTLSCertificate.dismiss" = "不信任"; + +"upgrade.title" = "升級優惠"; +"upgrade.notEligible.alert.title" = "升級失敗"; +"upgrade.notEligible.alert.message" = "Cryptomator 無法檢測到您設備上安裝的舊版本。如果曾購買,請先從 App Store 重新下載並重試。"; +"upgrade.info" = "感謝您從第一版開始就信任 Cryptomator。作為忠實用戶,您可以獲得免費升級。"; + +"urlSession.error.httpError.401" = "用戶名或密碼錯誤。"; +"urlSession.error.httpError.403" = "沒有權限取得要求的資源。"; +"urlSession.error.httpError.404" = "找不到要求的資源。"; +"urlSession.error.httpError.405" = "請求方法不能被用於請求相應的資源。"; +"urlSession.error.httpError.409" = "請求存在衝突而無法處理。"; +"urlSession.error.httpError.412" = "資源存取被拒。"; +"urlSession.error.httpError.default" = "網絡連接失敗,狀態碼為 %ld 。"; +"urlSession.error.unexpectedResponse" = "出現了預期外的網絡錯誤。"; + +"vaultAccountManager.error.vaultAccountAlreadyExists" = "你已經添加了這個加密庫。"; + +"vaultDetail.button.changeVaultPassword" = "更改密碼"; +"vaultDetail.button.lock" = "立即鎖定"; +"vaultDetail.button.moveVault" = "移動"; +"vaultDetail.button.removeVault" = "從加密庫列表中移除"; +"vaultDetail.button.renameVault" = "重新命名"; +"vaultDetail.changePassword.footer" = "為加密庫選擇一個只有您知道的強密碼,並將其保管在安全的地方。"; +"vaultDetail.disabledBiometricalUnlock.footer" = "如果啟用了 %@,你的加密庫密碼便會儲存在 iOS 鑰匙圈中。"; +"vaultDetail.enabledBiometricalUnlock.footer" = "僅當 %@ 身份驗證失敗時才需要你的加密庫密碼。"; +"vaultDetail.info.footer.accessVault" = "以「檔案」應用程式存取加密庫。"; +"vaultDetail.info.footer.accountInfo" = "已經使用 %@ 登入 %@。"; +"vaultDetail.keepUnlocked.title" = "解鎖時長"; +"vaultDetail.keepUnlocked.footer.off" = "當 Cryptomator 被「檔案」應用程式終止時,便需要解鎖。"; +"vaultDetail.keepUnlocked.footer.limitedDuration" = "當您的加密庫空閒了 %@ 時便需要解鎖。"; +"vaultDetail.keepUnlocked.footer.unlimitedDuration" = "除非手動鎖定,否則無需解鎖。"; +"vaultDetail.locked.footer" = "你的加密庫已被鎖定。"; +"vaultDetail.moveVault.detectedMasterkey.text" = "Cryptomator 在此檢測到現有的加密庫。\n\n如要移動您的加密庫,請返回並選擇其他資料夾。"; +"vaultDetail.moveVault.progress" = "移動中…"; +"vaultDetail.removeVault.footer" = "這只會從列表中移除加密庫,而不會刪除任何加密文件。"; +"vaultDetail.renameVault.progress" = "重新命名中…"; +"vaultDetail.unlocked.footer" = "加密庫目前在「檔䅁」應用程式中解鎖。"; +"vaultDetail.unlockVault.footer" = "輸入「%@」的密碼以將其儲存在 iOS 鑰匙圈中並啟用 %@。"; + +"vaultList.header.title" = "加密庫"; +"vaultList.emptyList.message" = "點擊此處來加入加密庫"; +"vaultList.remove.alert.title" = "移除加密庫?"; +"vaultList.remove.alert.message" = "這只會從列表中移除加密庫,而不會刪除任何加密文件。您可以稍後再重新加入加密庫。"; + +"vaultProviderFactory.error.unsupportedVaultConfig" = "不支援目前加密庫的設定。請確保您運行的是最新版本的 Cryptomator。"; +"vaultProviderFactory.error.unsupportedVaultVersion" = "不支援 %ld 版本的加密庫。這加密庫是使用 Cryptomator 較舊或較新版本建立的。"; + +"webDAVAuthentication.progress" = "驗證中…"; +"webDAVAuthentication.httpConnection.alert.title" = "使用 HTTPS?"; +"webDAVAuthentication.httpConnection.alert.message" = "使用 HTTP 是不安全的。我們推薦使用 HTTPS 來取代。如果您瞭解風險,您可以使用 HTTP 繼續。"; +"webDAVAuthentication.httpConnection.change" = "更改為 HTTPS"; +"webDAVAuthentication.httpConnection.continue" = "保持 HTTP"; + +"webDAVAuthenticator.error.unsupportedProtocol" = "伺服器似乎不支援 WebDAV 。請檢查 URL 是否正確。"; +"webDAVAuthenticator.error.untrustedCertificate" = "此伺服器的證書不被信任。你可能需要重新添加此 WebDAV 連接。"; + +"Retry Upload" = "重新上載"; +"Clear from Cache" = "從快取中移除"; diff --git a/SharedResources/zh-Hans.lproj/Localizable.strings b/SharedResources/zh-Hans.lproj/Localizable.strings index fab4bdb55..aab518958 100644 --- a/SharedResources/zh-Hans.lproj/Localizable.strings +++ b/SharedResources/zh-Hans.lproj/Localizable.strings @@ -10,6 +10,7 @@ "common.button.cancel" = "取消"; "common.button.change" = "更改"; "common.button.choose" = "选择"; +"common.button.clear" = "清除"; "common.button.close" = "关闭"; "common.button.confirm" = "确认"; "common.button.create" = "新建"; @@ -97,6 +98,8 @@ "fileProvider.error.defaultLock.title" = "需要解锁"; "fileProvider.error.defaultLock.message" = "需解锁才能访问和显示您的保险库中的内容。"; "fileProvider.error.unlockButton" = "解锁"; +"fileProvider.clearFileFromCache.title" = "清除缓存文件"; +"fileProvider.clearFileFromCache.message" = "该操作仅会删除设备中的本地文件,不会删除云存储中的文件"; "fileProvider.uploadProgress.connecting" = "正在连接…"; "fileProvider.uploadProgress.message" = "当前进度:%@\n\n若发现上传进度卡住,请重试上传"; "fileProvider.uploadProgress.missing" = "无法确定上传进度,可能仍在后台运行"; @@ -264,3 +267,4 @@ "webDAVAuthenticator.error.untrustedCertificate" = "此服务器的证书不受信任,你可能需要重新添加此 WebDAV 连接"; "Retry Upload" = "重试上传"; +"Clear from Cache" = "清除缓存"; diff --git a/SharedResources/zh-Hant.lproj/Localizable.strings b/SharedResources/zh-Hant.lproj/Localizable.strings index 43aff619b..a1fe23f27 100644 --- a/SharedResources/zh-Hant.lproj/Localizable.strings +++ b/SharedResources/zh-Hant.lproj/Localizable.strings @@ -10,9 +10,10 @@ "common.button.cancel" = "取消"; "common.button.change" = "修改"; "common.button.choose" = "選擇"; +"common.button.clear" = "清除"; "common.button.close" = "關閉"; "common.button.confirm" = "確認"; -"common.button.create" = "新建"; +"common.button.create" = "新增"; "common.button.createFolder" = "建立資料夾"; "common.button.done" = "完成"; "common.button.download" = "下載"; @@ -27,12 +28,13 @@ "common.cells.openInFilesApp" = "在「檔案」應用程式中開啟"; "common.cells.password" = "密碼"; "common.cells.url" = "網址"; -"common.cells.username" = "帳號名稱"; +"common.cells.username" = "使用者名稱"; "common.footer.learnMore" = "了解更多。"; "accountList.header.title" = "認證"; "accountList.emptyList.message" = "點擊此處添加帳號"; "accountList.signOut.alert.title" = "移除關聯的加密檔案庫?"; +"accountList.signOut.alert.message" = "登出後,所有關聯的加密檔案庫將從列表中移除。這並不會刪除任何加密數據。您可以再次登入並稍後重新添加加密檔案庫。"; "addVault.title" = "新增加密檔案庫"; "addVault.createNewVault.title" = "新建加密檔案庫"; @@ -42,6 +44,7 @@ "addVault.createNewVault.setVaultName.error.emptyVaultName" = "加密檔案庫名稱不可留空。"; "addVault.createNewVault.chooseCloud.header" = "Cryptomator 應該將您加密後的檔案存放在哪裡?"; "addVault.createNewVault.chooseFolder.error.vaultNameCollision" = "“%@”已經在此位置,請更換加密檔案庫名或位置"; +"addVault.createNewVault.detectedMasterkey.text" = "Cryptomator 在此檢測到現有的加密檔案庫。\n如要建立新的加密檔案庫,請返回並選擇其他資料夾。"; "addVault.createNewVault.password.enterPassword.header" = "輸入新密碼."; "addVault.createNewVault.password.confirmPassword.header" = "確認新密碼."; "addVault.createNewVault.password.confirmPassword.alert.title" = "確認密碼?"; @@ -52,11 +55,12 @@ "addVault.createNewVault.progress" = "正在創建加密檔案庫……"; "addVault.openExistingVault.title" = "開啟現有加密檔案庫"; "addVault.openExistingVault.chooseCloud.header" = "加密檔案庫位於什麼地方?"; +"addVault.openExistingVault.detectedMasterkey.text" = "Cryptomator 在此檢測到加密檔案庫「%@」。\n您想把這加密檔案庫加到列表嗎?"; "addVault.openExistingVault.detectedMasterkey.add" = "添加這個加密檔案庫"; "addVault.openExistingVault.password.footer" = "輸入 「%@」 的密碼:"; "addVault.openExistingVault.progress" = "正在添加加密檔案庫……"; "addVault.success.info" = "成功添加加密檔案庫“%@”\n現在可以通過文件管理訪問加密檔案庫"; -"addVault.success.footer" = "如果尚未打開,請在文件應用程序中啟用 Cryptomator。"; +"addVault.success.footer" = "如果還沒有,請在「檔案」應用程式中啟用 Cryptomator。"; "biometryType.faceID" = "Face ID"; "biometryType.touchID" = "Touch ID"; @@ -77,44 +81,93 @@ "cloudProvider.error.itemAlreadyExists" = "「%@」已經存在。"; "cloudProvider.error.itemTypeMismatch" = "“%@”有一個非預期的文件類型"; "cloudProvider.error.parentFolderDoesNotExist" = "父文件夾“%@”不存在"; +"cloudProvider.error.pageTokenInvalid" = "無法繼續取得目錄的內容。"; "cloudProvider.error.quotaInsufficient" = "您的存儲已空間不足"; "cloudProvider.error.unauthorized" = "無法執行未授權的操作"; +"cloudProvider.error.noInternetConnection" = "此操作需要連線至互聯網。"; "cloudProviderType.localFileSystem" = "其他儲存空間"; "fileProvider.onboarding.title" = "歡迎"; "fileProvider.onboarding.info" = "感謝您選擇 Cryptomator 來保護您的文件。若要開始,請前往主應用程序並添加一個加密檔案庫"; "fileProvider.onboarding.button.openCryptomator" = "打開 Cryptomator"; -"fileProvider.error.biometricalAuthCanceled.title" = "解鎖取消"; +"fileProvider.error.biometricalAuthCanceled.title" = "解鎖已取消"; "fileProvider.error.biometricalAuthCanceled.message" = "通過“%@”解鎖失敗,請重試"; "fileProvider.error.biometricalAuthWrongPassword.title" = "密碼錯誤"; "fileProvider.error.biometricalAuthWrongPassword.message" = "保存在%@的密碼錯誤,請重試並輸入你的密碼來重新啟用%@"; "fileProvider.error.defaultLock.title" = "需要解鎖"; -"fileProvider.error.defaultLock.message" = "需要解鎖才能訪問和展示加密檔案庫"; +"fileProvider.error.defaultLock.message" = "要存取加密庫和顯示其內容,需要先解鎖。"; "fileProvider.error.unlockButton" = "解鎖"; +"fileProvider.clearFileFromCache.title" = "清除快取檔䅁"; +"fileProvider.clearFileFromCache.message" = "這只會從您的本機上刪除文件,而不會刪除雲端的文件。"; +"fileProvider.uploadProgress.connecting" = "連線中…"; +"fileProvider.uploadProgress.message" = "目前進度:%@\n\n如果您注意到上傳進度卡住,您可以重試上傳。"; +"fileProvider.uploadProgress.missing" = "無法確定目前進度。它可能仍在後台運行。"; +"fileProvider.uploadProgress.title" = "上傳中…"; +"fileProvider.uploadProgress.missingDomainError" = "找不到域名。"; -"keepUnlocked.alert.title" = "鎖定加密檔案庫?"; +"keepUnlocked.alert.title" = "鎖定加密檔案庫?"; "keepUnlocked.alert.message" = "此項更改需要鎖定加密檔案庫後才能生效"; -"keepUnlocked.alert.confirm" = "確認並上鎖"; +"keepUnlocked.alert.confirm" = "確認並立即鎖定"; "keepUnlocked.header" = "指定您希望此加密檔案庫在空閒時保持解鎖狀態的時間"; +"keepUnlocked.footer.auto" = "讓 iOS 決定意味着 Cryptomator 可能被隨時終止以釋放內存。這會自動鎖定加密檔案庫。"; +"keepUnlocked.footer.on" = "使用這個選項表示在加密檔案庫解鎖時,您密鑰的副本需要儲存在 iOS 鑰匙圈中。"; "keepUnlockedDuration.auto" = "由 iOS 自動決定"; "keepUnlockedDuration.auto.shortDisplayName" = "自動"; "keepUnlockedDuration.indefinite" = "無期限"; -"localFileSystemAuthentication.createNewVault.header" = "請在下一個屏幕選擇新加密檔案庫的存儲位置"; +"localFileSystemAuthentication.createNewVault.header" = "在下一個頁面,請選擇新加密庫的儲存位置。"; "localFileSystemAuthentication.createNewVault.button" = "選取儲存位置"; -"localFileSystemAuthentication.openExistingVault.header" = "請在下一屏中選擇已存在加密檔案庫所在的文件夾"; +"localFileSystemAuthentication.createNewVault.error.detectedExistingVault" = "此位置已存在加密檔案庫。 請以其他儲存位置重試。"; +"localFileSystemAuthentication.openExistingVault.header" = "在下一個頁面,請選擇已存在加密庫所在的文件夾。"; "localFileSystemAuthentication.openExistingVault.button" = "選取加密檔案庫的資料夾"; +"localFileSystemAuthentication.openExistingVault.error.noVaultFound" = "選擇的資料夾不是加密檔案庫。請以其他資料夾重試。"; +"localFileSystemAuthentication.info.footer" = "文件提供程式顯示為灰色的表示不支援「選擇資料夾」。這並非 Cryptomator 的限制。"; + +"maintenanceModeError.runningCloudTask" = "無法執行操作,因為此加密檔案庫的其他後台操作必須要先完成。請稍後再試。"; + +"nameValidation.error.endsWithPeriod" = "您不能使用以句點(.)結尾的名稱。請選擇其他名稱。"; +"nameValidation.error.endsWithSpace" = "您不能使用以空格( )結尾的名稱。請選擇其他名稱。"; +"nameValidation.error.containsIllegalCharacter" = "您不能使用具有「%@」的名稱。請選擇其他名稱。"; "onboarding.title" = "歡迎"; +"onboarding.info" = "感謝您選擇 Cryptomator 來保護您的文件。\n\n有了 Cryptomator,您數據的鑰匙就在您手中。 + Cryptomator 可以快速輕鬆地加密您的數據。\n\n此應用程式和「檔案」應用程式完美結合。為此,請確保稍後在「檔案」應用程式中啟用 Cryptomator 以存取您的加密檔案庫。"; "onboarding.button.continue" = "繼續"; + +"purchase.beginFreeTrial.alert.title" = "試用開始"; "purchase.expiredTrial" = "您的試用已過期"; "purchase.footer.privacyPolicy" = "隱私政策"; "purchase.footer.termsOfUse" = "使用條款"; "purchase.header.feature.familySharing" = "家庭共享"; +"purchase.header.feature.openSource" = "鼓勵開源程式開發者"; +"purchase.header.feature.writeAccess" = "寫入加密檔案庫功能"; +"purchase.product.donateAndUpgrade" = "捐款與升級"; +"purchase.product.freeUpgrade" = "免費升級"; +"purchase.product.lifetimeLicense" = "終身授權"; +"purchase.product.lifetimeLicense.duration" = "一次性"; "purchase.product.pricing.free" = "免費"; +"purchase.product.trial" = "30 天試用"; +"purchase.product.trial.expirationDate" = "到期日期: %@"; +"purchase.product.trial.duration" = "30 天"; +"purchase.product.yearlySubscription" = "年度訂閱"; +"purchase.product.yearlySubscription.duration" = "每年"; +"purchase.readOnlyMode.alert.title" = "唯讀模式"; +"purchase.readOnlyMode.alert.message" = "您可以稍後在設定中解鎖完整版 Cryptomator,而暫時以唯讀模式下繼續使用。"; +"purchase.restorePurchase.button" = "復原已購項目"; +"purchase.restorePurchase.validTrialFound.alert.title" = "繼續試用"; +"purchase.restorePurchase.validTrialFound.alert.message" = "您目前可以在時限內使用完整版的 Cryptomator。您的試用期將於 %@ 到期。之後,您的加密檔案庫仍可在唯讀模式下取存。"; +"purchase.restorePurchase.fullVersionFound.alert.title" = "復原成功"; +"purchase.restorePurchase.fullVersionNotFound.alert.title" = "沒有完整版"; +"purchase.restorePurchase.fullVersionNotFound.alert.message" = "我們無法找到已購買並可以恢復的完整版本。請嘗試其他選項。"; +"purchase.restorePurchase.eligibleForUpgrade.alert.title" = "可以升級"; +"purchase.restorePurchase.eligibleForUpgrade.alert.message" = "您似乎正嘗試從舊版本的 Cryptomator 升級。在這情況下,請選擇「升級優惠」。"; "purchase.retry.button" = "重試"; +"purchase.retry.footer" = "無法載入可購買產品。"; +"purchase.title" = "解鎖完整版"; +"purchase.unlockedFullVersion.message" = "您現在可以使用完整版的 Cryptomator。加密快樂!"; "purchase.unlockedFullVersion.title" = "感謝"; +"purchase.error.unknown" = "由於未知原因,無法在 App Store 中購買,請稍後再試。\n\n如果此錯誤仍然存在,請嘗試重新啟動您的設備、或在 iOS 設定中登出並重新登入您的 Apple ID。"; "settings.title" = "設定"; "settings.aboutCryptomator" = "關於 Cryptomator"; @@ -125,20 +178,53 @@ "settings.contact" = "聯絡方式"; "settings.debugMode" = "調試模式"; "settings.debugMode.alert.message" = "在此模式下,敏感數據可能會寫入您設備上的日誌文件(例如文件名和路徑)。 密碼、cookies 等被明確排除在外。\n\n請記住盡快禁用調試模式。"; +"settings.manageSubscriptions" = "管理訂閱"; "settings.rateApp" = "為應用程式評分"; +"settings.sendLogFile" = "傳送日誌檔案"; +"settings.unlockFullVersion" = "解鎖完整版"; + +"snapshots.fileprovider.file1" = "/財務.numbers"; +"snapshots.fileprovider.file2" = "/最終演講.key"; +"snapshots.fileprovider.file3" = "/產品預告.mov"; +"snapshots.fileprovider.file4" = "計劃書.docx"; +"snapshots.fileprovider.file5" = "報告.pdf"; +"snapshots.fileprovider.folder3" = "/秘密計劃"; +"snapshots.fileprovider.folder2" = "/帳單"; +"snapshots.fileprovider.folder1" = "/證書"; +"snapshots.main.vault1" = "/工作"; +"snapshots.main.vault2" = "/家庭"; +"snapshots.main.vault3" = "文件"; +"snapshots.main.vault4" = "加州之旅"; + +"trialStatus.active" = "生效中"; +"trialStatus.expired" = "已過期"; "unlockVault.button.unlock" = "解鎖"; "unlockVault.button.unlockVia" = "使用%@解鎖"; "unlockVault.password.footer" = "輸入 「%@」 的密碼:"; "unlockVault.enableBiometricalUnlock.switch" = "啓用 %@"; +"unlockVault.enableBiometricalUnlock.footer" = "您可以通過 %@ 解鎖加密檔案庫,而不使用密碼解鎖。"; "unlockVault.evaluatePolicy.reason" = "解鎖您的加密檔案庫"; "unlockVault.progress" = "正在解鎖……"; "untrustedTLSCertificate.title" = "無效的 TLS 憑證"; +"untrustedTLSCertificate.message" = "「%@」的 TLS 證書無效。您還是要信任它嗎?\n\nSHA-256: %@"; "untrustedTLSCertificate.add" = "信任"; "untrustedTLSCertificate.dismiss" = "不信任"; +"upgrade.title" = "升級優惠"; +"upgrade.notEligible.alert.title" = "升級失敗"; +"upgrade.notEligible.alert.message" = "Cryptomator 無法檢測到您設備上安裝的舊版本。如果曾購買,請先從 App Store 重新下載並重試。"; +"upgrade.info" = "感謝您從第一版開始就信任 Cryptomator。作為忠實用戶,您可以獲得免費升級。"; + "urlSession.error.httpError.401" = "用戶名或密碼錯誤"; +"urlSession.error.httpError.403" = "沒有權限取得要求的資源。"; +"urlSession.error.httpError.404" = "找不到要求的資源。"; +"urlSession.error.httpError.405" = "請求方法不能被用於請求相應的資源。"; +"urlSession.error.httpError.409" = "請求存在衝突而無法處理。"; +"urlSession.error.httpError.412" = "資源存取被拒。"; +"urlSession.error.httpError.default" = "網絡連接失敗,狀態碼為 %ld 。"; +"urlSession.error.unexpectedResponse" = "出現了預期外的網絡錯誤。"; "vaultAccountManager.error.vaultAccountAlreadyExists" = "您已經添加了這個加密檔案庫。"; @@ -147,16 +233,39 @@ "vaultDetail.button.moveVault" = "移動"; "vaultDetail.button.removeVault" = "從加密檔案庫列表中移除"; "vaultDetail.button.renameVault" = "重新命名"; +"vaultDetail.changePassword.footer" = "為您的加密檔案庫選擇一個只有您知道的強密碼,並將其保管在安全的地方。"; +"vaultDetail.disabledBiometricalUnlock.footer" = "如果您啟用 %@,您的加密檔案庫密碼便會儲存在 iOS 鑰匙圈中。"; +"vaultDetail.enabledBiometricalUnlock.footer" = "僅當 %@ 身份驗證失敗時才需要您的加密檔案庫密碼。"; +"vaultDetail.info.footer.accessVault" = "以「檔案」應用程式存取加密檔案庫。"; "vaultDetail.info.footer.accountInfo" = "已經使用 %@ 登入 %@。"; +"vaultDetail.keepUnlocked.title" = "解鎖時長"; +"vaultDetail.keepUnlocked.footer.off" = "當 Cryptomator 被「檔案」應用程式終止時,便需要解鎖。"; +"vaultDetail.keepUnlocked.footer.limitedDuration" = "當您的加密檔案庫空閒了 %@ 時便需要解鎖。"; +"vaultDetail.keepUnlocked.footer.unlimitedDuration" = "除非手動鎖定,否則無需解鎖。"; "vaultDetail.locked.footer" = "您的加密檔案庫當前已鎖定。"; +"vaultDetail.moveVault.detectedMasterkey.text" = "Cryptomator 在此檢測到現有的加密檔案庫。\n\n如要移動您的加密檔案庫,請返回並選擇其他資料夾。"; "vaultDetail.moveVault.progress" = "正在移動……"; +"vaultDetail.removeVault.footer" = "這只會從列表中移除加密檔案庫,而不會刪除任何加密文件。"; "vaultDetail.renameVault.progress" = "正在重命名……"; +"vaultDetail.unlocked.footer" = "您的加密檔案庫目前在「檔䅁」應用程式中解鎖。"; +"vaultDetail.unlockVault.footer" = "輸入「%@」的密碼以將其儲存在 iOS 鑰匙圈中並啟用 %@。"; "vaultList.header.title" = "加密檔案庫"; "vaultList.emptyList.message" = "點擊此處以添加加密檔案庫"; -"vaultList.remove.alert.title" = "移除加密檔案庫?"; +"vaultList.remove.alert.title" = "移除加密檔案庫?"; +"vaultList.remove.alert.message" = "這只會從列表中移除加密檔案庫,而不會刪除任何加密文件。您可以稍後再重新加入加密檔案庫。"; + +"vaultProviderFactory.error.unsupportedVaultConfig" = "不支援目前加密檔案庫的設定。請確保您運行的是最新版本的 Cryptomator。"; +"vaultProviderFactory.error.unsupportedVaultVersion" = "不支援 %ld 版本的加密檔案庫。這加密檔案庫是使用 Cryptomator 較舊或較新版本建立的。"; "webDAVAuthentication.progress" = "正在驗證……"; "webDAVAuthentication.httpConnection.alert.title" = "是否使用 HTTPS?"; "webDAVAuthentication.httpConnection.alert.message" = "使用 HTTP 是不安全的。我們推薦使用 HTTPS 來取代。如果您瞭解風險,您可以使用 HTTP 繼續。"; "webDAVAuthentication.httpConnection.change" = "更換為 HTTPS"; +"webDAVAuthentication.httpConnection.continue" = "保持 HTTP"; + +"webDAVAuthenticator.error.unsupportedProtocol" = "伺服器似乎不支援 WebDAV 。請檢查 URL 是否正確。"; +"webDAVAuthenticator.error.untrustedCertificate" = "此伺服器的證書不被信任。您可能需要重新添加此 WebDAV 連接。"; + +"Retry Upload" = "重新上傳"; +"Clear from Cache" = "從快取中移除"; From d8cf6e74939aacb960754213318ab7a75100ea02 Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Mon, 23 May 2022 16:35:51 +0200 Subject: [PATCH 17/18] Enabled Chinese (Hong Kong) translation [ci skip] --- Cryptomator.xcodeproj/project.pbxproj | 3 +++ .../xcshareddata/swiftpm/Package.resolved | 12 ++++++------ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/Cryptomator.xcodeproj/project.pbxproj b/Cryptomator.xcodeproj/project.pbxproj index 06af97605..de49f25fd 100644 --- a/Cryptomator.xcodeproj/project.pbxproj +++ b/Cryptomator.xcodeproj/project.pbxproj @@ -897,6 +897,7 @@ 74397A852832A09B00CB9410 /* sw-TZ */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "sw-TZ"; path = "sw-TZ.lproj/Localizable.strings"; sourceTree = ""; }; 7460FFED26FB6C100018BCC4 /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = pt.lproj/Localizable.strings; sourceTree = ""; }; 7460FFEE26FCC6FC0018BCC4 /* OnboardingNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingNavigationController.swift; sourceTree = ""; }; + 74626665283BD2D20070924B /* zh-HK */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-HK"; path = "zh-HK.lproj/Localizable.strings"; sourceTree = ""; }; 7469AD99266E26B0000DCD45 /* URL+Zip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Zip.swift"; sourceTree = ""; }; 747C35162762A3F500E4CA28 /* AttributedTextHeaderFooterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedTextHeaderFooterViewModel.swift; sourceTree = ""; }; 74833F9D27E4CCD800C1C5F0 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = nb.lproj/Localizable.strings; sourceTree = ""; }; @@ -2114,6 +2115,7 @@ tr, "zh-Hans", "zh-Hant", + "zh-HK", ); mainGroup = 4A5E5B202453119100BD6298; packageReferences = ( @@ -2795,6 +2797,7 @@ 74267A1C26A5799F004C61BC /* tr */, 74267A1D26A579A4004C61BC /* zh-Hans */, 74397A842832A05E00CB9410 /* zh-Hant */, + 74626665283BD2D20070924B /* zh-HK */, ); name = Localizable.strings; sourceTree = ""; diff --git a/Cryptomator.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Cryptomator.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index f81754ea3..95bffc2f9 100644 --- a/Cryptomator.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Cryptomator.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -87,8 +87,8 @@ "repositoryURL": "https://github.com/google/gtm-session-fetcher.git", "state": { "branch": null, - "revision": "bc6a19702ac76ac4e488b68148710eb815f9bc56", - "version": "1.7.0" + "revision": "4e9bbf2808b8fee444e84a48f5f3c12641987d3e", + "version": "1.7.2" } }, { @@ -96,8 +96,8 @@ "repositoryURL": "https://github.com/google/GTMAppAuth.git", "state": { "branch": null, - "revision": "40f4103fb52109032c05599a0c39ad43edbdf80a", - "version": "1.2.2" + "revision": "e803d09da0147fbf1bbb30e126c47ff43254e057", + "version": "1.2.3" } }, { @@ -114,8 +114,8 @@ "repositoryURL": "https://github.com/AzureAD/microsoft-authentication-library-for-objc.git", "state": { "branch": null, - "revision": "2825c3f72bf51f8be43df7177fe1c66370be2d0e", - "version": "1.2.0" + "revision": "e4202b9175d7b5566845b60e0ad233ed72b2783c", + "version": "1.2.1" } }, { From acc2e2cdece0934bd99832583dfe92ac747f7557 Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Mon, 23 May 2022 16:39:27 +0200 Subject: [PATCH 18/18] Updated release notes [ci skip] --- fastlane/changelog.txt | 2 +- fastlane/metadata/de-DE/release_notes.txt | 2 +- fastlane/metadata/en-US/release_notes.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/fastlane/changelog.txt b/fastlane/changelog.txt index f7a0dd9d3..54b015833 100644 --- a/fastlane/changelog.txt +++ b/fastlane/changelog.txt @@ -1,4 +1,4 @@ - Failed uploads are now actually shown with an error instead of being stuck in "Waiting" - Added "Retry Upload" action (#219) - Added "Clear from Cache" action (#149, #220) -- Added Traditional Chinese and Swahili (Tanzania) translations \ No newline at end of file +- Added Chinese (Traditional & Hong Kong) and Swahili (Tanzania) translations \ No newline at end of file diff --git a/fastlane/metadata/de-DE/release_notes.txt b/fastlane/metadata/de-DE/release_notes.txt index f63ce7318..fb0c2061c 100644 --- a/fastlane/metadata/de-DE/release_notes.txt +++ b/fastlane/metadata/de-DE/release_notes.txt @@ -1,4 +1,4 @@ - Fehlgeschlagene Uploads werden nun tatsächlich mit einer Fehlermeldung angezeigt, statt in „Warten“ zu hängen - Funktion "Upload erneut versuchen" hinzugefügt (#219) - Funktion "Aus Zwischenspeicher entfernen" hinzugefügt (#149, #220) -- Übersetzungen für Chinesisch (Langzeichen) und Swahili (Tansania) hinzugefügt \ No newline at end of file +- Übersetzungen für Chinesisch (Langzeichen & Hongkong) und Swahili (Tansania) hinzugefügt \ No newline at end of file diff --git a/fastlane/metadata/en-US/release_notes.txt b/fastlane/metadata/en-US/release_notes.txt index f7a0dd9d3..54b015833 100644 --- a/fastlane/metadata/en-US/release_notes.txt +++ b/fastlane/metadata/en-US/release_notes.txt @@ -1,4 +1,4 @@ - Failed uploads are now actually shown with an error instead of being stuck in "Waiting" - Added "Retry Upload" action (#219) - Added "Clear from Cache" action (#149, #220) -- Added Traditional Chinese and Swahili (Tanzania) translations \ No newline at end of file +- Added Chinese (Traditional & Hong Kong) and Swahili (Tanzania) translations \ No newline at end of file