diff --git a/Package.swift b/Package.swift index 617ab061294b661012d646258bf8cde4c3cf4aef..0e7bb614d299a3ed2eadd9464518af68ad563f9f 100644 --- a/Package.swift +++ b/Package.swift @@ -27,6 +27,7 @@ let package = Package( .library(name: "Integration", targets: ["Integration"]), .library(name: "ChatFeature", targets: ["ChatFeature"]), .library(name: "PushFeature", targets: ["PushFeature"]), + .library(name: "SFTPFeature", targets: ["SFTPFeature"]), .library(name: "CrashService", targets: ["CrashService"]), .library(name: "Presentation", targets: ["Presentation"]), .library(name: "BackupFeature", targets: ["BackupFeature"]), @@ -68,6 +69,7 @@ let package = Package( .package(url: "https://github.com/google/google-api-objectivec-client-for-rest", from: "1.6.0"), .package(url: "https://git.xx.network/elixxir/client-ios-db.git", .upToNextMajor(from: "1.0.5")), .package(url: "https://github.com/firebase/firebase-ios-sdk.git", .upToNextMajor(from: "8.10.0")), + .package(url: "https://github.com/darrarski/Shout.git", revision: "df5a662293f0ac15eeb4f2fd3ffd0c07b73d0de0"), .package(url: "https://github.com/pointfreeco/swift-composable-architecture.git",.upToNextMajor(from: "0.32.0")) ], targets: [ @@ -81,6 +83,7 @@ let package = Package( "ChatFeature", "MenuFeature", "PushFeature", + "SFTPFeature", "ToastFeature", "CrashService", "BackupFeature", @@ -208,6 +211,23 @@ let package = Package( ] ), + // MARK: - SFTPFeature + + .target( + name: "SFTPFeature", + dependencies: [ + "HUD", + "Shared", + "Keychain", + "InputField", + "DependencyInjection", + .product( + name: "Shout", + package: "Shout" + ) + ] + ), + // MARK: - GoogleDriveFeature .target( @@ -398,6 +418,7 @@ let package = Package( dependencies: [ "HUD", "Shared", + "SFTPFeature", "Integration", "Presentation", "iCloudFeature", @@ -612,6 +633,7 @@ let package = Package( "Shared", "Models", "InputField", + "SFTPFeature", "Presentation", "iCloudFeature", "DrawerFeature", diff --git a/Sources/App/AppDelegate.swift b/Sources/App/AppDelegate.swift index 3675b45050f041af457b16d9bc6105633dabab91..72a7f682477967599a823601b8315c46ed429ee6 100644 --- a/Sources/App/AppDelegate.swift +++ b/Sources/App/AppDelegate.swift @@ -1,8 +1,8 @@ import UIKit import BackgroundTasks -import XXModels import Theme +import XXModels import XXLogger import Defaults import Integration diff --git a/Sources/App/DependencyRegistrator.swift b/Sources/App/DependencyRegistrator.swift index 2a886995871ac2b82dd2fb0c5e01b1b0187a71bb..87b8e7dd9d903fbf11f686ac46ecb730fac4eef7 100644 --- a/Sources/App/DependencyRegistrator.swift +++ b/Sources/App/DependencyRegistrator.swift @@ -18,6 +18,7 @@ import Voxophone import Integration import Permissions import PushFeature +import SFTPFeature import CrashService import ToastFeature import iCloudFeature @@ -63,6 +64,7 @@ struct DependencyRegistrator { /// Restore / Backup + container.register(SFTPService.mock) container.register(iCloudServiceMock() as iCloudInterface) container.register(DropboxServiceMock() as DropboxInterface) container.register(GoogleDriveServiceMock() as GoogleDriveInterface) @@ -86,6 +88,7 @@ struct DependencyRegistrator { /// Restore / Backup + container.register(SFTPService.live) container.register(iCloudService() as iCloudInterface) container.register(DropboxService() as DropboxInterface) container.register(GoogleDriveService() as GoogleDriveInterface) diff --git a/Sources/BackupFeature/Controllers/BackupConfigController.swift b/Sources/BackupFeature/Controllers/BackupConfigController.swift index c61bd74d6c971c544087d7020db72ce16d11e763..a901e6b9668fc89df2cd1ae1184ca655fa8bdb1b 100644 --- a/Sources/BackupFeature/Controllers/BackupConfigController.swift +++ b/Sources/BackupFeature/Controllers/BackupConfigController.swift @@ -105,6 +105,11 @@ final class BackupConfigController: UIViewController { .sink { [unowned self] in viewModel.didToggleService(self, .dropbox, screenView.dropboxButton.switcherView.isOn) } .store(in: &cancellables) + screenView.sftpButton.switcherView + .publisher(for: .valueChanged) + .sink { [unowned self] in viewModel.didToggleService(self, .sftp, screenView.sftpButton.switcherView.isOn) } + .store(in: &cancellables) + screenView.iCloudButton.switcherView .publisher(for: .valueChanged) .sink { [unowned self] in viewModel.didToggleService(self, .icloud, screenView.iCloudButton.switcherView.isOn) } @@ -115,6 +120,11 @@ final class BackupConfigController: UIViewController { .sink { [unowned self] in viewModel.didTapService(.dropbox, self) } .store(in: &cancellables) + screenView.sftpButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in viewModel.didTapService(.sftp, self) } + .store(in: &cancellables) + screenView.iCloudButton .publisher(for: .touchUpInside) .sink { [unowned self] in viewModel.didTapService(.icloud, self) } @@ -128,16 +138,17 @@ final class BackupConfigController: UIViewController { case .none: break case .icloud: - serviceName = "iCloud" + serviceName = Localized.Backup.iCloud button = screenView.iCloudButton - case .dropbox: - serviceName = "Dropbox" + serviceName = Localized.Backup.dropbox button = screenView.dropboxButton - case .drive: - serviceName = "Google Drive" + serviceName = Localized.Backup.googleDrive button = screenView.googleDriveButton + case .sftp: + serviceName = Localized.Backup.sftp + button = screenView.sftpButton } screenView.enabledSubtitleLabel.text @@ -146,10 +157,12 @@ final class BackupConfigController: UIViewController { = Localized.Backup.Config.frequency(serviceName).uppercased() guard let button = button else { + screenView.sftpButton.isHidden = false screenView.iCloudButton.isHidden = false screenView.dropboxButton.isHidden = false screenView.googleDriveButton.isHidden = false + screenView.sftpButton.switcherView.isOn = false screenView.iCloudButton.switcherView.isOn = false screenView.dropboxButton.switcherView.isOn = false screenView.googleDriveButton.switcherView.isOn = false @@ -166,11 +179,13 @@ final class BackupConfigController: UIViewController { screenView.latestBackupDetailView.isHidden = false screenView.infrastructureDetailView.isHidden = false - [screenView.iCloudButton, screenView.dropboxButton, screenView.googleDriveButton] - .forEach { - $0.isHidden = $0 != button - $0.switcherView.isOn = $0 == button - } + [screenView.iCloudButton, + screenView.dropboxButton, + screenView.googleDriveButton, + screenView.sftpButton].forEach { + $0.isHidden = $0 != button + $0.switcherView.isOn = $0 == button + } } private func decorate(connectedServices: Set<CloudService>) { @@ -191,6 +206,12 @@ final class BackupConfigController: UIViewController { } else { screenView.googleDriveButton.showChevron() } + + if connectedServices.contains(.sftp) { + screenView.sftpButton.showSwitcher(enabled: false) + } else { + screenView.sftpButton.showChevron() + } } private func presentInfrastructureDrawer(wifiOnly: Bool) { diff --git a/Sources/BackupFeature/Controllers/BackupSetupController.swift b/Sources/BackupFeature/Controllers/BackupSetupController.swift index 49e05a2bd74ce9141e19320cfebbc33f989414f7..e22de517680d7ab5fc348b56cbb396ca80c4e091 100644 --- a/Sources/BackupFeature/Controllers/BackupSetupController.swift +++ b/Sources/BackupFeature/Controllers/BackupSetupController.swift @@ -37,5 +37,10 @@ final class BackupSetupController: UIViewController { .publisher(for: .touchUpInside) .sink { [unowned self] in viewModel.didTapService(.icloud, self) } .store(in: &cancellables) + + screenView.sftpButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in viewModel.didTapService(.sftp, self) } + .store(in: &cancellables) } } diff --git a/Sources/BackupFeature/Coordinator/BackupCoordinator.swift b/Sources/BackupFeature/Coordinator/BackupCoordinator.swift index 9dbd110e659f066dd25d5bda897ebccc14047748..a49b51bd28fe3a670efeba3c2c9cff8e34e31af6 100644 --- a/Sources/BackupFeature/Coordinator/BackupCoordinator.swift +++ b/Sources/BackupFeature/Coordinator/BackupCoordinator.swift @@ -18,16 +18,10 @@ public protocol BackupCoordinating { public struct BackupCoordinator: BackupCoordinating { var bottomPresenter: Presenting = BottomPresenter() - var passphraseFactory: ( - @escaping EmptyClosure, - @escaping StringClosure - ) -> UIViewController + var passphraseFactory: (@escaping EmptyClosure, @escaping StringClosure) -> UIViewController public init( - passphraseFactory: @escaping ( - @escaping EmptyClosure, - @escaping StringClosure - ) -> UIViewController + passphraseFactory: @escaping (@escaping EmptyClosure, @escaping StringClosure) -> UIViewController ) { self.passphraseFactory = passphraseFactory } diff --git a/Sources/BackupFeature/Service/BackupService.swift b/Sources/BackupFeature/Service/BackupService.swift index 8b82a8f27c40af85acd836182144d05c945008e1..626adba515d28c1a9969b067da742d07e94c8da5 100644 --- a/Sources/BackupFeature/Service/BackupService.swift +++ b/Sources/BackupFeature/Service/BackupService.swift @@ -2,6 +2,8 @@ import UIKit import Models import Combine import Defaults +import Keychain +import SFTPFeature import iCloudFeature import DropboxFeature import NetworkMonitor @@ -9,10 +11,12 @@ import GoogleDriveFeature import DependencyInjection public final class BackupService { + @Dependency private var sftpService: SFTPService @Dependency private var icloudService: iCloudInterface @Dependency private var dropboxService: DropboxInterface - @Dependency private var driveService: GoogleDriveInterface @Dependency private var networkManager: NetworkMonitoring + @Dependency private var keychainHandler: KeychainHandling + @Dependency private var driveService: GoogleDriveInterface @KeyObject(.backupSettings, defaultValue: Data()) private var storedSettings: Data @@ -149,6 +153,15 @@ extension BackupService { self.refreshBackups() }.store(in: &cancellables) } + case .sftp: + if !sftpService.isAuthorized() { + sftpService.authorizeFlow((screen, { [weak self] in + guard let self = self else { return } + screen.navigationController?.popViewController(animated: true) + self.refreshConnections() + self.refreshBackups() + })) + } } } } @@ -167,6 +180,12 @@ extension BackupService { settings.value.connectedServices.remove(.dropbox) } + if sftpService.isAuthorized() && !settings.value.connectedServices.contains(.sftp) { + settings.value.connectedServices.insert(.sftp) + } else if !sftpService.isAuthorized() && settings.value.connectedServices.contains(.sftp) { + settings.value.connectedServices.remove(.sftp) + } + driveService.isAuthorized { [weak settings] isAuthorized in guard let settings = settings else { return } @@ -196,6 +215,23 @@ extension BackupService { } } + if sftpService.isAuthorized() { + sftpService.fetchMetadata { [weak settings] in + guard let settings = settings else { return } + + guard let metadata = try? $0.get()?.backup else { + settings.value.backups[.sftp] = nil + return + } + + settings.value.backups[.sftp] = Backup( + id: metadata.id, + date: metadata.date, + size: metadata.size + ) + } + } + if dropboxService.isAuthorized() { dropboxService.downloadMetadata { [weak settings] in guard let settings = settings else { return } @@ -241,7 +277,7 @@ extension BackupService { .appendingPathComponent(UUID().uuidString) do { - try data.write(to: url) + try data.write(to: url, options: .atomic) } catch { print("Couldn't write to temp: \(error.localizedDescription)") return @@ -260,8 +296,6 @@ extension BackupService { case .failure(let error): print(error.localizedDescription) } - - // try? FileManager.default.removeItem(at: url) } case .icloud: icloudService.uploadBackup(url) { @@ -275,8 +309,6 @@ extension BackupService { case .failure(let error): print(error.localizedDescription) } - - // try? FileManager.default.removeItem(at: url) } case .dropbox: dropboxService.uploadBackup(url) { @@ -290,8 +322,15 @@ extension BackupService { case .failure(let error): print(error.localizedDescription) } - - // try? FileManager.default.removeItem(at: url) + } + case .sftp: + sftpService.uploadBackup(url: url) { + switch $0 { + case .success(let backup): + self.settings.value.backups[.sftp] = backup + case .failure(let error): + print(error.localizedDescription) + } } } } diff --git a/Sources/BackupFeature/Views/BackupConfigView.swift b/Sources/BackupFeature/Views/BackupConfigView.swift index 8c400b3ff9162f380548d0478f2356454910833e..1c65f58f7222f1c069284c717518904dc2e596ca 100644 --- a/Sources/BackupFeature/Views/BackupConfigView.swift +++ b/Sources/BackupFeature/Views/BackupConfigView.swift @@ -7,6 +7,7 @@ final class BackupConfigView: UIView { let actionView = BackupActionView() let stackView = UIStackView() + let sftpButton = BackupSwitcherButton() let iCloudButton = BackupSwitcherButton() let dropboxButton = BackupSwitcherButton() let googleDriveButton = BackupSwitcherButton() @@ -44,6 +45,9 @@ final class BackupConfigView: UIView { subtitleLabel.numberOfLines = 0 subtitleLabel.attributedText = attString + sftpButton.titleLabel.text = Localized.Backup.sftp + sftpButton.logoImageView.image = Asset.restoreSFTP.image + iCloudButton.titleLabel.text = Localized.Backup.iCloud iCloudButton.logoImageView.image = Asset.restoreIcloud.image @@ -65,6 +69,7 @@ final class BackupConfigView: UIView { stackView.addArrangedSubview(googleDriveButton) stackView.addArrangedSubview(iCloudButton) stackView.addArrangedSubview(dropboxButton) + stackView.addArrangedSubview(sftpButton) stackView.addArrangedSubview(enabledSubtitleView) stackView.addArrangedSubview(latestBackupDetailView) stackView.addArrangedSubview(frequencyDetailView) @@ -75,36 +80,36 @@ final class BackupConfigView: UIView { addSubview(actionView) addSubview(stackView) - titleLabel.snp.makeConstraints { make in - make.top.equalToSuperview().offset(15) - make.left.equalToSuperview().offset(38) - make.right.equalToSuperview().offset(-41) + titleLabel.snp.makeConstraints { + $0.top.equalToSuperview().offset(15) + $0.left.equalToSuperview().offset(38) + $0.right.equalToSuperview().offset(-41) } - enabledSubtitleLabel.snp.makeConstraints { make in - make.top.equalToSuperview().offset(-10) - make.left.equalToSuperview().offset(92) - make.right.equalToSuperview().offset(-48) - make.bottom.equalToSuperview() + enabledSubtitleLabel.snp.makeConstraints { + $0.top.equalToSuperview().offset(-10) + $0.left.equalToSuperview().offset(92) + $0.right.equalToSuperview().offset(-48) + $0.bottom.equalToSuperview() } - subtitleLabel.snp.makeConstraints { make in - make.top.equalTo(titleLabel.snp.bottom).offset(8) - make.left.equalToSuperview().offset(38) - make.right.equalToSuperview().offset(-41) + subtitleLabel.snp.makeConstraints { + $0.top.equalTo(titleLabel.snp.bottom).offset(8) + $0.left.equalToSuperview().offset(38) + $0.right.equalToSuperview().offset(-41) } - actionView.snp.makeConstraints { make in - make.top.equalTo(subtitleLabel.snp.bottom).offset(15) - make.left.equalToSuperview().offset(38) - make.right.equalToSuperview().offset(-38) + actionView.snp.makeConstraints { + $0.top.equalTo(subtitleLabel.snp.bottom).offset(15) + $0.left.equalToSuperview().offset(38) + $0.right.equalToSuperview().offset(-38) } - stackView.snp.makeConstraints { make in - make.top.equalTo(actionView.snp.bottom).offset(28) - make.left.equalToSuperview() - make.right.equalToSuperview() - make.bottom.lessThanOrEqualToSuperview() + stackView.snp.makeConstraints { + $0.top.equalTo(actionView.snp.bottom).offset(28) + $0.left.equalToSuperview() + $0.right.equalToSuperview() + $0.bottom.lessThanOrEqualToSuperview() } } diff --git a/Sources/BackupFeature/Views/BackupSetupView.swift b/Sources/BackupFeature/Views/BackupSetupView.swift index eeaad48195da765ac9833293ccd7f7f9a7673ca0..3e19d50034f4d03ba62d3e4d038d82d24196af89 100644 --- a/Sources/BackupFeature/Views/BackupSetupView.swift +++ b/Sources/BackupFeature/Views/BackupSetupView.swift @@ -6,6 +6,7 @@ final class BackupSetupView: UIView { let subtitleLabel = UILabel() let stackView = UIStackView() + let sftpButton = BackupSwitcherButton() let iCloudButton = BackupSwitcherButton() let dropboxButton = BackupSwitcherButton() let googleDriveButton = BackupSwitcherButton() @@ -60,32 +61,37 @@ final class BackupSetupView: UIView { googleDriveButton.logoImageView.image = Asset.restoreDrive.image googleDriveButton.showChevron() + sftpButton.titleLabel.text = Localized.Backup.sftp + sftpButton.logoImageView.image = Asset.restoreSFTP.image + sftpButton.showChevron() + stackView.axis = .vertical stackView.addArrangedSubview(googleDriveButton) stackView.addArrangedSubview(iCloudButton) stackView.addArrangedSubview(dropboxButton) + stackView.addArrangedSubview(sftpButton) addSubview(titleLabel) addSubview(subtitleLabel) addSubview(stackView) - titleLabel.snp.makeConstraints { make in - make.top.equalToSuperview().offset(15) - make.left.equalToSuperview().offset(38) - make.right.equalToSuperview().offset(-41) + titleLabel.snp.makeConstraints { + $0.top.equalToSuperview().offset(15) + $0.left.equalToSuperview().offset(38) + $0.right.equalToSuperview().offset(-41) } - subtitleLabel.snp.makeConstraints { make in - make.top.equalTo(titleLabel.snp.bottom).offset(8) - make.left.equalToSuperview().offset(38) - make.right.equalToSuperview().offset(-41) + subtitleLabel.snp.makeConstraints { + $0.top.equalTo(titleLabel.snp.bottom).offset(8) + $0.left.equalToSuperview().offset(38) + $0.right.equalToSuperview().offset(-41) } - stackView.snp.makeConstraints { make in - make.top.equalTo(subtitleLabel.snp.bottom).offset(28) - make.left.equalToSuperview() - make.right.equalToSuperview() - make.bottom.lessThanOrEqualToSuperview() + stackView.snp.makeConstraints { + $0.top.equalTo(subtitleLabel.snp.bottom).offset(28) + $0.left.equalToSuperview() + $0.right.equalToSuperview() + $0.bottom.lessThanOrEqualToSuperview() } } diff --git a/Sources/InputField/OutlinedInputField.swift b/Sources/InputField/OutlinedInputField.swift new file mode 100644 index 0000000000000000000000000000000000000000..3bb8ad3005167b9e9e99511243da912f7082d434 --- /dev/null +++ b/Sources/InputField/OutlinedInputField.swift @@ -0,0 +1,85 @@ +import UIKit +import Shared +import Combine + +public final class OutlinedInputField: UIView { + private let stackView = UIStackView() + private let textField = UITextField() + private let placeholderLabel = UILabel() + private let inputContainerView = UIView() + + private let secureInputButton = SecureInputButton() + + public var textPublisher: AnyPublisher<String, Never> { + textField.textPublisher + } + + public init() { + super.init(frame: .zero) + + layer.borderWidth = 1.0 + layer.cornerRadius = 4.0 + layer.masksToBounds = true + layer.borderColor = Asset.neutralWeak.color.cgColor + + textField.delegate = self + textField.backgroundColor = .clear + textField.textColor = Asset.neutralDark.color + placeholderLabel.textColor = Asset.neutralWeak.color + placeholderLabel.font = Fonts.Mulish.regular.font(size: 16.0) + + secureInputButton.button.addTarget(self, action: #selector(didTapRight), for: .touchUpInside) + + inputContainerView.addSubview(placeholderLabel) + inputContainerView.addSubview(textField) + + stackView.addArrangedSubview(inputContainerView) + stackView.addArrangedSubview(secureInputButton) + + addSubview(stackView) + + placeholderLabel.snp.makeConstraints { + $0.top.equalToSuperview().offset(15) + $0.left.equalToSuperview().offset(15) + $0.right.lessThanOrEqualToSuperview().offset(-15) + $0.bottom.equalToSuperview().offset(-18) + } + + textField.snp.makeConstraints { + $0.top.equalToSuperview().offset(15) + $0.left.equalToSuperview().offset(15) + $0.right.equalToSuperview().offset(-15) + $0.bottom.equalToSuperview().offset(-18) + } + + stackView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + } + + required init?(coder: NSCoder) { nil } + + public func setup(title: String, sensitive: Bool = false) { + placeholderLabel.text = title + textField.isSecureTextEntry = sensitive + secureInputButton.isHidden = !sensitive + } + + @objc private func didTapRight() { + textField.isSecureTextEntry.toggle() + secureInputButton.setSecure(textField.isSecureTextEntry) + } +} + +extension OutlinedInputField: UITextFieldDelegate { + public func textField( + _ textField: UITextField, + shouldChangeCharactersIn range: NSRange, + replacementString string: String + ) -> Bool { + placeholderLabel.alpha = (textField.text! as NSString) + .replacingCharacters(in: range, with: string) + .count > 0 ? 0.0 : 1.0 + return true + } +} diff --git a/Sources/InputField/SecureInputButton.swift b/Sources/InputField/SecureInputButton.swift new file mode 100644 index 0000000000000000000000000000000000000000..1f2e6b20751370755ec23b966c153ca5440c9f32 --- /dev/null +++ b/Sources/InputField/SecureInputButton.swift @@ -0,0 +1,31 @@ +import UIKit +import Shared + +final class SecureInputButton: UIView { + private(set) var button = UIButton() + private let color = Asset.neutralSecondaryAlternative.color + private lazy var openedImage = Asset.eyeOpen.image.withTintColor(color) + private lazy var closedImage = Asset.eyeClosed.image.withTintColor(color) + + init() { + super.init(frame: .zero) + + button.setContentCompressionResistancePriority(.required, for: .horizontal) + button.setImage(Asset.eyeClosed.image.withTintColor(color), for: .normal) + + addSubview(button) + + button.snp.makeConstraints { + $0.top.equalToSuperview() + $0.left.equalToSuperview().offset(10) + $0.right.equalToSuperview().offset(-10) + $0.bottom.equalToSuperview() + } + } + + required init?(coder: NSCoder) { nil } + + func setSecure(_ bool: Bool) { + button.setImage(bool ? closedImage : openedImage, for: .normal) + } +} diff --git a/Sources/Integration/Client.swift b/Sources/Integration/Client.swift index 7d53fb7efaf8ebe3b4f6485bc38dd101ce8960e5..2b488ef925bd1d5367b94585dc4959bf14b66ea6 100644 --- a/Sources/Integration/Client.swift +++ b/Sources/Integration/Client.swift @@ -89,7 +89,12 @@ public class Client { // } public func addJson(_ string: String) { - guard let backupManager = backupManager else { return } + guard let backupManager = backupManager else { + fatalError() + } + + print("^^^ Set params: \(string) to backup") + backupManager.addJson(string) } diff --git a/Sources/Integration/Implementations/Bindings.swift b/Sources/Integration/Implementations/Bindings.swift index 6aac71d4e5c02c661c719db62816662286f9c308..7c6feb9f10688bc0d34726aec2479c8174438484 100644 --- a/Sources/Integration/Implementations/Bindings.swift +++ b/Sources/Integration/Implementations/Bindings.swift @@ -499,11 +499,11 @@ extension BindingsClient: BindingsInterface { fatalError("Couldn't retrieve cert.") } - try! udb!.setAlternative( - "18.198.117.203:11420".data(using: .utf8), - cert: try! Data(contentsOf: URL(fileURLWithPath: certPath)), - contactFile: try! Data(contentsOf: URL(fileURLWithPath: contactFilePath)) - ) +// try! udb!.setAlternative( +// "18.198.117.203:11420".data(using: .utf8), +// cert: try! Data(contentsOf: URL(fileURLWithPath: certPath)), +// contactFile: try! Data(contentsOf: URL(fileURLWithPath: contactFilePath)) +// ) guard let error = error else { return udb! } throw error.friendly() @@ -525,11 +525,11 @@ extension BindingsClient: BindingsInterface { fatalError("Couldn't retrieve cert.") } - try! udb!.setAlternative( - "18.198.117.203:11420".data(using: .utf8), - cert: try! Data(contentsOf: URL(fileURLWithPath: certPath)), - contactFile: try! Data(contentsOf: URL(fileURLWithPath: contactFilePath)) - ) +// try! udb!.setAlternative( +// "18.198.117.203:11420".data(using: .utf8), +// cert: try! Data(contentsOf: URL(fileURLWithPath: certPath)), +// contactFile: try! Data(contentsOf: URL(fileURLWithPath: contactFilePath)) +// ) guard let error = error else { return udb! } throw error.friendly() diff --git a/Sources/Integration/Session/Session.swift b/Sources/Integration/Session/Session.swift index ae01b853ac2ac24d7e8427f3a868dcb18b34dcde..51234cabf4aa2f29f29bc41bfc4166cd5afa2ab9 100644 --- a/Sources/Integration/Session/Session.swift +++ b/Sources/Integration/Session/Session.swift @@ -356,6 +356,11 @@ public final class Session: SessionType { ).jsonFormat client.addJson(params) + + guard username!.isEmpty == false else { + fatalError("Tried to build a backup with my username but an empty string was set to it") + } + backupService.performBackupIfAutomaticIsEnabled() } @@ -381,7 +386,6 @@ public final class Session: SessionType { .store(in: &cancellables) client.backup - .throttle(for: .seconds(5), scheduler: DispatchQueue.main, latest: true) .sink { [unowned self] in backupService.updateBackup(data: $0) } .store(in: &cancellables) @@ -402,7 +406,6 @@ public final class Session: SessionType { if $0 == true { guard let passphrase = backupService.passphrase else { client.resumeBackup() - updateFactsOnBackup() return } @@ -416,10 +419,6 @@ public final class Session: SessionType { } .store(in: &cancellables) - networkMonitor.statusPublisher - .sink { print($0) } - .store(in: &cancellables) - client.messages .sink { [unowned self] in if var contact = try? dbManager.fetchContacts(.init(id: [$0.senderId])).first { diff --git a/Sources/Keychain/KeychainHandler.swift b/Sources/Keychain/KeychainHandler.swift index 23f248879a7f2275b0faddd9f4c08962d9ed6246..6ac0d645d6def33360ee5ed0d0ab1e973a0487fc 100644 --- a/Sources/Keychain/KeychainHandler.swift +++ b/Sources/Keychain/KeychainHandler.swift @@ -1,15 +1,24 @@ import Foundation import KeychainAccess +public enum KeychainSFTP: String { + case pwd + case host + case username +} + public protocol KeychainHandling { func clear() throws func getPassword() throws -> Data? func store(password pwd: Data) throws + + func get(key: KeychainSFTP) throws -> String? + func store(key: KeychainSFTP, value: String) throws } public struct KeychainHandler: KeychainHandling { - private let password = "password" private let keychain: Keychain + private let password = "password" public init() { self.keychain = Keychain(service: "XXM") @@ -26,4 +35,12 @@ public struct KeychainHandler: KeychainHandling { public func getPassword() throws -> Data? { try keychain.getData(password) } + + public func get(key: KeychainSFTP) throws -> String? { + try keychain.get(key.rawValue) + } + + public func store(key: KeychainSFTP, value: String) throws { + try keychain.set(value, key: key.rawValue) + } } diff --git a/Sources/Keychain/MockKeychainHandler.swift b/Sources/Keychain/MockKeychainHandler.swift index c1ff10dd802d5cd230cffbe25f20b744aa06126b..39d4a33ddb1e0e6d8a75d8b5a1ea980d8fb189bf 100644 --- a/Sources/Keychain/MockKeychainHandler.swift +++ b/Sources/Keychain/MockKeychainHandler.swift @@ -6,4 +6,6 @@ public struct MockKeychainHandler: KeychainHandling { public func clear() throws {} public func store(password pwd: Data) throws {} public func getPassword() throws -> Data? { Data() } + public func get(key: KeychainSFTP) throws -> String? { nil } + public func store(key: KeychainSFTP, value: String) throws {} } diff --git a/Sources/LaunchFeature/LaunchViewModel.swift b/Sources/LaunchFeature/LaunchViewModel.swift index 054432b4403e7177bf129c21680a58ac4abb6b12..4e1b25ea61eb4475c9849f1873482b8a6eec3b25 100644 --- a/Sources/LaunchFeature/LaunchViewModel.swift +++ b/Sources/LaunchFeature/LaunchViewModel.swift @@ -5,6 +5,7 @@ import Models import Combine import Defaults import XXModels +import Keychain import Foundation import Integration import Permissions @@ -31,6 +32,7 @@ final class LaunchViewModel { @Dependency private var network: XXNetworking @Dependency private var versionChecker: VersionChecker @Dependency private var dropboxService: DropboxInterface + @Dependency private var keychainHandler: KeychainHandling @Dependency private var permissionHandler: PermissionHandling @KeyObject(.username, defaultValue: nil) var username: String? @@ -90,6 +92,7 @@ final class LaunchViewModel { self.hudSubject.send(.none) self.routeSubject.send(.onboarding(ndf)) self.dropboxService.unlink() + try? self.keychainHandler.clear() return } @@ -98,6 +101,7 @@ final class LaunchViewModel { self.hudSubject.send(.none) self.routeSubject.send(.onboarding(ndf)) self.dropboxService.unlink() + try? self.keychainHandler.clear() return } diff --git a/Sources/Models/CloudService.swift b/Sources/Models/CloudService.swift index 5daba7238d74388dac591b63220dbbc8b1c9f911..d217dca853e6ecf22f119520d9805567f7ead3a5 100644 --- a/Sources/Models/CloudService.swift +++ b/Sources/Models/CloudService.swift @@ -2,4 +2,5 @@ public enum CloudService: Equatable, Codable { case drive case icloud case dropbox + case sftp } diff --git a/Sources/NetworkMonitor/MockNetworkMonitor.swift b/Sources/NetworkMonitor/MockNetworkMonitor.swift index 473eb593ece68fd213a4896abe9a74eb3c23d286..30bf846df9e36359ed80a7572a966e56091dce4d 100644 --- a/Sources/NetworkMonitor/MockNetworkMonitor.swift +++ b/Sources/NetworkMonitor/MockNetworkMonitor.swift @@ -32,11 +32,11 @@ public struct MockNetworkMonitor: NetworkMonitoring { statusRelay.send(status) if status == .available { - DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + DispatchQueue.main.asyncAfter(deadline: .now() + 10) { simulateOscilation(.internetNotAvailable) } } else if status == .internetNotAvailable { - DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + DispatchQueue.main.asyncAfter(deadline: .now() + 5) { simulateOscilation(.available) } } diff --git a/Sources/OnboardingFeature/Controllers/OnboardingUsernameController.swift b/Sources/OnboardingFeature/Controllers/OnboardingUsernameController.swift index 19fc945b63b2495f5823af11fb21de205f538d85..d25db4581f5c5dbd3083515b1828433494631e45 100644 --- a/Sources/OnboardingFeature/Controllers/OnboardingUsernameController.swift +++ b/Sources/OnboardingFeature/Controllers/OnboardingUsernameController.swift @@ -1,9 +1,9 @@ import HUD -import DrawerFeature import Theme import UIKit import Shared import Combine +import DrawerFeature import DependencyInjection import ScrollViewController diff --git a/Sources/RestoreFeature/Controllers/RestoreListController.swift b/Sources/RestoreFeature/Controllers/RestoreListController.swift index 041717b6dde907568db4872827957d487010d006..c73e94edec314484dc06307f84e9e528f66bcc2a 100644 --- a/Sources/RestoreFeature/Controllers/RestoreListController.swift +++ b/Sources/RestoreFeature/Controllers/RestoreListController.swift @@ -1,8 +1,8 @@ import HUD -import DrawerFeature -import Shared import UIKit +import Shared import Combine +import DrawerFeature import DependencyInjection public final class RestoreListController: UIViewController { @@ -51,15 +51,16 @@ public final class RestoreListController: UIViewController { } private func setupBindings() { - viewModel.hud + viewModel.hudPublisher .receive(on: DispatchQueue.main) .sink { [hud] in hud.update(with: $0) } .store(in: &cancellables) - viewModel.didFetchBackup + viewModel.backupPublisher .receive(on: DispatchQueue.main) - .sink { [unowned self] in coordinator.toRestore(using: ndf, with: $0, from: self) } - .store(in: &cancellables) + .sink { [unowned self] in + coordinator.toRestore(using: ndf, with: $0, from: self) + }.store(in: &cancellables) screenView.cancelButton .publisher(for: .touchUpInside) @@ -68,18 +69,27 @@ public final class RestoreListController: UIViewController { screenView.driveButton .publisher(for: .touchUpInside) - .sink { [unowned self] in viewModel.didTapCloud(.drive, from: self) } - .store(in: &cancellables) + .sink { [unowned self] in + viewModel.didTapCloud(.drive, from: self) + }.store(in: &cancellables) screenView.icloudButton .publisher(for: .touchUpInside) - .sink { [unowned self] in viewModel.didTapCloud(.icloud, from: self) } - .store(in: &cancellables) + .sink { [unowned self] in + viewModel.didTapCloud(.icloud, from: self) + }.store(in: &cancellables) screenView.dropboxButton .publisher(for: .touchUpInside) - .sink { [unowned self] in viewModel.didTapCloud(.dropbox, from: self) } - .store(in: &cancellables) + .sink { [unowned self] in + viewModel.didTapCloud(.dropbox, from: self) + }.store(in: &cancellables) + + screenView.sftpButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + viewModel.didTapCloud(.sftp, from: self) + }.store(in: &cancellables) } @objc private func didTapBack() { diff --git a/Sources/RestoreFeature/Service/MockRestoreService.swift b/Sources/RestoreFeature/Service/MockRestoreService.swift deleted file mode 100644 index 1a013434211580df1c99ac1fa3842ab0fc797d59..0000000000000000000000000000000000000000 --- a/Sources/RestoreFeature/Service/MockRestoreService.swift +++ /dev/null @@ -1,30 +0,0 @@ -import UIKit -import Models -import Combine -import Foundation -import GoogleDriveFeature -import DependencyInjection - -public struct RestoreServiceMock: RestoreServiceType { - public var inProgress: AnyPublisher<Void, Never> { - fatalError() - } - - public var settings: AnyPublisher<RestoreSettings, Never> { - fatalError() - } - - public init() {} - - public func didSelectBackup(at url: URL) {} - - public func authorize(service: CloudService, from: UIViewController) {} - - public func download( - from settings: RestoreSettings, - progress: @escaping RestoreProgress, - whenFinished: @escaping RestoreDownloadFinished - ) { - fatalError() - } -} diff --git a/Sources/RestoreFeature/Service/RestoreServiceType.swift b/Sources/RestoreFeature/Service/RestoreServiceType.swift deleted file mode 100644 index 78a32e9f6d9e199d78fa9c17a4a06b7ca1de51f3..0000000000000000000000000000000000000000 --- a/Sources/RestoreFeature/Service/RestoreServiceType.swift +++ /dev/null @@ -1,20 +0,0 @@ -import UIKit -import Models -import Combine - -public typealias RestoreProgress = (Float) -> Void -public typealias RestoreDownloadFinished = (Result<Data, Error>) -> Void - -public protocol RestoreServiceType { - var inProgress: AnyPublisher<Void, Never> { get } - - var settings: AnyPublisher<RestoreSettings, Never> { get } - - func authorize(service: CloudService, from: UIViewController) - - func download( - from settings: RestoreSettings, - progress: @escaping RestoreProgress, - whenFinished: @escaping RestoreDownloadFinished - ) -} diff --git a/Sources/RestoreFeature/ViewModels/RestoreListViewModel.swift b/Sources/RestoreFeature/ViewModels/RestoreListViewModel.swift index 3e91448aed07d65e2cdf650033be9d15be08fb4d..a4c2402e819c0576afdacc501c4550cd3656269f 100644 --- a/Sources/RestoreFeature/ViewModels/RestoreListViewModel.swift +++ b/Sources/RestoreFeature/ViewModels/RestoreListViewModel.swift @@ -6,20 +6,26 @@ import Combine import BackupFeature import DependencyInjection +import SFTPFeature import iCloudFeature import DropboxFeature import GoogleDriveFeature final class RestoreListViewModel { - @Dependency private var icloud: iCloudInterface - @Dependency private var dropbox: DropboxInterface - @Dependency private var drive: GoogleDriveInterface + @Dependency private var sftpService: SFTPService + @Dependency private var icloudService: iCloudInterface + @Dependency private var dropboxService: DropboxInterface + @Dependency private var googleDriveService: GoogleDriveInterface - var hud: AnyPublisher<HUDStatus, Never> { hudSubject.eraseToAnyPublisher() } - var didFetchBackup: AnyPublisher<RestoreSettings, Never> { backupSubject.eraseToAnyPublisher() } + var hudPublisher: AnyPublisher<HUDStatus, Never> { + hudSubject.eraseToAnyPublisher() + } - private var dropboxAuthCancellable: AnyCancellable? + var backupPublisher: AnyPublisher<RestoreSettings, Never> { + backupSubject.eraseToAnyPublisher() + } + private var dropboxAuthCancellable: AnyCancellable? private let hudSubject = PassthroughSubject<HUDStatus, Never>() private let backupSubject = PassthroughSubject<RestoreSettings, Never>() @@ -31,15 +37,43 @@ final class RestoreListViewModel { didRequestICloudAuthorization() case .dropbox: didRequestDropboxAuthorization(from: parent) + case .sftp: + didRequestSFTPAuthorization(from: parent) } } + private func didRequestSFTPAuthorization(from controller: UIViewController) { + let params = SFTPAuthorizationParams(controller, { [weak self] in + guard let self = self else { return } + controller.navigationController?.popViewController(animated: true) + + self.hudSubject.send(.on(nil)) + + self.sftpService.fetchMetadata{ result in + switch result { + case .success(let settings): + self.hudSubject.send(.none) + + if let settings = settings { + self.backupSubject.send(settings) + } else { + self.backupSubject.send(.init(cloudService: .sftp)) + } + case .failure(let error): + self.hudSubject.send(.error(.init(with: error))) + } + } + }) + + sftpService.authorizeFlow(params) + } + private func didRequestDriveAuthorization(from controller: UIViewController) { - drive.authorize(presenting: controller) { authResult in + googleDriveService.authorize(presenting: controller) { authResult in switch authResult { case .success: self.hudSubject.send(.on(nil)) - self.drive.downloadMetadata { downloadResult in + self.googleDriveService.downloadMetadata { downloadResult in switch downloadResult { case .success(let metadata): var backup: Backup? @@ -62,10 +96,10 @@ final class RestoreListViewModel { } private func didRequestICloudAuthorization() { - if icloud.isAuthorized() { + if icloudService.isAuthorized() { self.hudSubject.send(.on(nil)) - icloud.downloadMetadata { result in + icloudService.downloadMetadata { result in switch result { case .success(let metadata): var backup: Backup? @@ -83,12 +117,12 @@ final class RestoreListViewModel { } else { /// This could be an alert controller asking if user wants to enable/deeplink /// - icloud.openSettings() + icloudService.openSettings() } } private func didRequestDropboxAuthorization(from controller: UIViewController) { - dropboxAuthCancellable = dropbox.authorize(presenting: controller) + dropboxAuthCancellable = dropboxService.authorize(presenting: controller) .receive(on: DispatchQueue.main) .sink { [unowned self] authResult in switch authResult { @@ -96,7 +130,7 @@ final class RestoreListViewModel { guard bool == true else { return } self.hudSubject.send(.on(nil)) - dropbox.downloadMetadata { metadataResult in + dropboxService.downloadMetadata { metadataResult in switch metadataResult { case .success(let metadata): var backup: Backup? diff --git a/Sources/RestoreFeature/ViewModels/RestoreViewModel.swift b/Sources/RestoreFeature/ViewModels/RestoreViewModel.swift index 37a5bbe469665e83df59f9a5c3215c4223e426d5..bea37e5113ad33a547e5680e478a0ed7717db741 100644 --- a/Sources/RestoreFeature/ViewModels/RestoreViewModel.swift +++ b/Sources/RestoreFeature/ViewModels/RestoreViewModel.swift @@ -8,6 +8,7 @@ import Integration import BackupFeature import DependencyInjection +import SFTPFeature import iCloudFeature import DropboxFeature import GoogleDriveFeature @@ -38,13 +39,16 @@ extension RestorationStep: Equatable { } final class RestoreViewModel { + @Dependency private var sftpService: SFTPService @Dependency private var iCloudService: iCloudInterface @Dependency private var dropboxService: DropboxInterface @Dependency private var googleService: GoogleDriveInterface @KeyObject(.username, defaultValue: nil) var username: String? - var step: AnyPublisher<RestorationStep, Never> { stepRelay.eraseToAnyPublisher() } + var step: AnyPublisher<RestorationStep, Never> { + stepRelay.eraseToAnyPublisher() + } // TO REFACTOR: // @@ -80,6 +84,22 @@ final class RestoreViewModel { downloadBackupForDropbox(backup) case .icloud: downloadBackupForiCloud(backup) + case .sftp: + downloadBackupForSFTP(backup) + } + } + + private func downloadBackupForSFTP(_ backup: Backup) { + sftpService.downloadBackup(path: backup.id) { [weak self] in + guard let self = self else { return } + self.stepRelay.send(.downloading(backup.size, backup.size)) + + switch $0 { + case .success(let data): + self.continueRestoring(data: data) + case .failure(let error): + self.stepRelay.send(.failDownload(error)) + } } } diff --git a/Sources/RestoreFeature/Views/RestoreListView.swift b/Sources/RestoreFeature/Views/RestoreListView.swift index 2a688760bc68cd509775fa97d28a37538dc44d3a..a955173a742b7c0b94f95601f90634353ada327f 100644 --- a/Sources/RestoreFeature/Views/RestoreListView.swift +++ b/Sources/RestoreFeature/Views/RestoreListView.swift @@ -6,6 +6,7 @@ final class RestoreListView: UIView { let stackView = UIStackView() let firstSubtitleLabel = UILabel() let secondSubtitleLabel = UILabel() + let sftpButton = RowButton() let driveButton = RowButton() let icloudButton = RowButton() let dropboxButton = RowButton() @@ -34,6 +35,7 @@ final class RestoreListView: UIView { secondSubtitleLabel.numberOfLines = 0 secondSubtitleLabel.attributedText = attrString + sftpButton.setup(title: Localized.Backup.sftp, icon: Asset.restoreSFTP.image) icloudButton.setup(title: Localized.Backup.iCloud, icon: Asset.restoreIcloud.image) dropboxButton.setup(title: Localized.Backup.dropbox, icon: Asset.restoreDropbox.image) driveButton.setup(title: Localized.Backup.googleDrive, icon: Asset.restoreDrive.image) @@ -41,9 +43,11 @@ final class RestoreListView: UIView { cancelButton.set(style: .seeThrough, title: Localized.AccountRestore.List.cancel) stackView.axis = .vertical + stackView.distribution = .fillEqually stackView.addArrangedSubview(driveButton) stackView.addArrangedSubview(icloudButton) stackView.addArrangedSubview(dropboxButton) + stackView.addArrangedSubview(sftpButton) addSubview(titleLabel) addSubview(firstSubtitleLabel) @@ -51,35 +55,35 @@ final class RestoreListView: UIView { addSubview(stackView) addSubview(cancelButton) - titleLabel.snp.makeConstraints { make in - make.top.equalTo(safeAreaLayoutGuide).offset(15) - make.left.equalToSuperview().offset(38) - make.right.equalToSuperview().offset(-41) + titleLabel.snp.makeConstraints { + $0.top.equalTo(safeAreaLayoutGuide).offset(15) + $0.left.equalToSuperview().offset(38) + $0.right.equalToSuperview().offset(-41) } - firstSubtitleLabel.snp.makeConstraints { make in - make.top.equalTo(titleLabel.snp.bottom).offset(8) - make.left.equalToSuperview().offset(38) - make.right.equalToSuperview().offset(-41) + firstSubtitleLabel.snp.makeConstraints { + $0.top.equalTo(titleLabel.snp.bottom).offset(8) + $0.left.equalToSuperview().offset(38) + $0.right.equalToSuperview().offset(-41) } - secondSubtitleLabel.snp.makeConstraints { make in - make.top.equalTo(firstSubtitleLabel.snp.bottom).offset(8) - make.left.equalToSuperview().offset(38) - make.right.equalToSuperview().offset(-41) + secondSubtitleLabel.snp.makeConstraints { + $0.top.equalTo(firstSubtitleLabel.snp.bottom).offset(8) + $0.left.equalToSuperview().offset(38) + $0.right.equalToSuperview().offset(-41) } - stackView.snp.makeConstraints { make in - make.top.equalTo(secondSubtitleLabel.snp.bottom).offset(28) - make.left.equalToSuperview().offset(24) - make.right.equalToSuperview().offset(-24) + stackView.snp.makeConstraints { + $0.top.equalTo(secondSubtitleLabel.snp.bottom).offset(28) + $0.left.equalToSuperview().offset(24) + $0.right.equalToSuperview().offset(-24) } - cancelButton.snp.makeConstraints { make in - make.top.greaterThanOrEqualTo(stackView.snp.bottom).offset(20) - make.left.equalToSuperview().offset(40) - make.right.equalToSuperview().offset(-40) - make.bottom.equalTo(safeAreaLayoutGuide).offset(-50) + cancelButton.snp.makeConstraints { + $0.top.greaterThanOrEqualTo(stackView.snp.bottom).offset(20) + $0.left.equalToSuperview().offset(40) + $0.right.equalToSuperview().offset(-40) + $0.bottom.equalTo(safeAreaLayoutGuide).offset(-50) } } diff --git a/Sources/RestoreFeature/Views/RestoreView.swift b/Sources/RestoreFeature/Views/RestoreView.swift index e36e5d5b1dd97cb735ac84f9d1c5c56fdb19b5c3..ba2e643cafc4dcca652ac7daac8ad14d22f52d15 100644 --- a/Sources/RestoreFeature/Views/RestoreView.swift +++ b/Sources/RestoreFeature/Views/RestoreView.swift @@ -39,36 +39,36 @@ final class RestoreView: UIView { bottomStackView.addArrangedSubview(cancelButton) bottomStackView.addArrangedSubview(backButton) - titleLabel.snp.makeConstraints { make in - make.top.equalTo(safeAreaLayoutGuide).offset(20) - make.left.equalToSuperview().offset(38) - make.right.equalToSuperview().offset(-38) + titleLabel.snp.makeConstraints { + $0.top.equalTo(safeAreaLayoutGuide).offset(20) + $0.left.equalToSuperview().offset(38) + $0.right.equalToSuperview().offset(-38) } - subtitleLabel.snp.makeConstraints { make in - make.top.equalTo(titleLabel.snp.bottom).offset(20) - make.left.equalToSuperview().offset(38) - make.right.equalToSuperview().offset(-38) + subtitleLabel.snp.makeConstraints { + $0.top.equalTo(titleLabel.snp.bottom).offset(20) + $0.left.equalToSuperview().offset(38) + $0.right.equalToSuperview().offset(-38) } - detailsView.snp.makeConstraints { make in - make.top.equalTo(subtitleLabel.snp.bottom).offset(40) - make.left.equalToSuperview() - make.right.equalToSuperview() + detailsView.snp.makeConstraints { + $0.top.equalTo(subtitleLabel.snp.bottom).offset(40) + $0.left.equalToSuperview() + $0.right.equalToSuperview() } - progressView.snp.makeConstraints { make in - make.top.greaterThanOrEqualTo(detailsView.snp.bottom) - make.left.equalToSuperview() - make.right.equalToSuperview() - make.bottom.lessThanOrEqualTo(bottomStackView.snp.top) + progressView.snp.makeConstraints { + $0.top.greaterThanOrEqualTo(detailsView.snp.bottom) + $0.left.equalToSuperview() + $0.right.equalToSuperview() + $0.bottom.lessThanOrEqualTo(bottomStackView.snp.top) } - bottomStackView.snp.makeConstraints { make in - make.top.greaterThanOrEqualTo(detailsView.snp.bottom).offset(10) - make.left.equalToSuperview().offset(40) - make.right.equalToSuperview().offset(-40) - make.bottom.equalTo(safeAreaLayoutGuide).offset(-20) + bottomStackView.snp.makeConstraints { + $0.top.greaterThanOrEqualTo(detailsView.snp.bottom).offset(10) + $0.left.equalToSuperview().offset(40) + $0.right.equalToSuperview().offset(-40) + $0.bottom.equalTo(safeAreaLayoutGuide).offset(-20) } } @@ -151,6 +151,8 @@ private extension CloudService { return Localized.Backup.iCloud case .dropbox: return Localized.Backup.dropbox + case .sftp: + return Localized.Backup.sftp } } @@ -162,6 +164,8 @@ private extension CloudService { return Asset.restoreIcloud.image case .dropbox: return Asset.restoreDropbox.image + case .sftp: + return Asset.restoreSFTP.image } } } diff --git a/Sources/SFTPFeature/ActionHandlers/SFTPAuthenticator.swift b/Sources/SFTPFeature/ActionHandlers/SFTPAuthenticator.swift new file mode 100644 index 0000000000000000000000000000000000000000..389cdd46fc0c0136247e09f3dbd900ec265a7858 --- /dev/null +++ b/Sources/SFTPFeature/ActionHandlers/SFTPAuthenticator.swift @@ -0,0 +1,54 @@ +import Shout +import Socket +import Keychain +import Foundation +import DependencyInjection + +public struct SFTPAuthenticator { + public var authenticate: (String, String, String) throws -> Void + + public func callAsFunction(host: String, username: String, password: String) throws { + try authenticate(host, username, password) + } +} + +extension SFTPAuthenticator { + static let mock = SFTPAuthenticator { host, username, password in + print("^^^ Requested authentication on sftp service.") + print("^^^ Host: \(host)") + print("^^^ Username: \(username)") + print("^^^ Password: \(password)") + } + + static let live = SFTPAuthenticator { host, username, password in + do { + try SSH.connect( + host: host, + port: 22, + username: username, + authMethod: SSHPassword(password)) { ssh in + _ = try ssh.openSftp() + + let keychain = try DependencyInjection.Container.shared.resolve() as KeychainHandling + try keychain.store(key: .host, value: host) + try keychain.store(key: .pwd, value: password) + try keychain.store(key: .username, value: username) + } + } catch { + if let error = error as? SSHError { + print(error.kind) + print(error.message) + print(error.description) + } else if let error = error as? Socket.Error { + print(error.errorCode) + print(error.description) + print(error.errorReason) + print(error.localizedDescription) + } else { + print(error.localizedDescription) + } + + throw error + } + } +} diff --git a/Sources/SFTPFeature/ActionHandlers/SFTPDownloader.swift b/Sources/SFTPFeature/ActionHandlers/SFTPDownloader.swift new file mode 100644 index 0000000000000000000000000000000000000000..6a435df0051a2755f2fb73522f7be92a24c4dd77 --- /dev/null +++ b/Sources/SFTPFeature/ActionHandlers/SFTPDownloader.swift @@ -0,0 +1,56 @@ +import Shout +import Socket +import Keychain +import Foundation +import DependencyInjection + +public typealias SFTPDownloadResult = (Result<Data, Error>) -> Void + +public struct SFTPDownloader { + public var download: (String, @escaping SFTPDownloadResult) -> Void + + public func callAsFunction(path: String, completion: @escaping SFTPDownloadResult) { + download(path, completion) + } +} + +extension SFTPDownloader { + static let mock = SFTPDownloader { path, _ in + print("^^^ Requested backup download on sftp service.") + print("^^^ Path: \(path)") + } + + static let live = SFTPDownloader { path, completion in + DispatchQueue.global().async { + do { + let keychain = try DependencyInjection.Container.shared.resolve() as KeychainHandling + let host = try keychain.get(key: .host) + let password = try keychain.get(key: .pwd) + let username = try keychain.get(key: .username) + + let ssh = try SSH(host: host!, port: 22) + try ssh.authenticate(username: username!, password: password!) + let sftp = try ssh.openSftp() + + let localURL = FileManager.default + .containerURL(forSecurityApplicationGroupIdentifier: "group.elixxir.messenger")! + .appendingPathComponent("sftp") + + try sftp.download(remotePath: path, localURL: localURL) + + let data = try Data(contentsOf: localURL) + completion(.success(data)) + } catch { + completion(.failure(error)) + + if var error = error as? SSHError { + print(error.kind) + print(error.message) + print(error.description) + } else { + print(error.localizedDescription) + } + } + } + } +} diff --git a/Sources/SFTPFeature/ActionHandlers/SFTPFetcher.swift b/Sources/SFTPFeature/ActionHandlers/SFTPFetcher.swift new file mode 100644 index 0000000000000000000000000000000000000000..a27df80ffe8e9be6f41f09185e9962351c44cff9 --- /dev/null +++ b/Sources/SFTPFeature/ActionHandlers/SFTPFetcher.swift @@ -0,0 +1,68 @@ +import Shout +import Socket +import Models +import Keychain +import Foundation +import DependencyInjection + +public typealias SFTPFetchResult = (Result<RestoreSettings?, Error>) -> Void + +public struct SFTPFetcher { + public var fetch: (@escaping SFTPFetchResult) -> Void + + public func callAsFunction(completion: @escaping SFTPFetchResult) { + fetch(completion) + } +} + +extension SFTPFetcher { + static let mock = SFTPFetcher { _ in + print("^^^ Requested backup metadata on sftp service.") + } + + static let live = SFTPFetcher { completion in + DispatchQueue.global().async { + do { + let keychain = try DependencyInjection.Container.shared.resolve() as KeychainHandling + let host = try keychain.get(key: .host) + let password = try keychain.get(key: .pwd) + let username = try keychain.get(key: .username) + + let ssh = try SSH(host: host!, port: 22) + try ssh.authenticate(username: username!, password: password!) + let sftp = try ssh.openSftp() + + if let files = try? sftp.listFiles(in: "backup"), + let backup = files.filter({ file in file.0 == "backup.xxm" }).first { + completion(.success(.init( + backup: .init( + id: "backup/backup.xxm", + date: backup.value.lastModified, + size: Float(backup.value.size) + ), + cloudService: .sftp + ))) + + return + } + + completion(.success(nil)) + } catch { + if let error = error as? SSHError { + print(error.kind) + print(error.message) + print(error.description) + } else if let error = error as? Socket.Error { + print(error.errorCode) + print(error.description) + print(error.errorReason) + print(error.localizedDescription) + } else { + print(error.localizedDescription) + } + + completion(.failure(error)) + } + } + } +} diff --git a/Sources/SFTPFeature/ActionHandlers/SFTPUploader.swift b/Sources/SFTPFeature/ActionHandlers/SFTPUploader.swift new file mode 100644 index 0000000000000000000000000000000000000000..fee691d1b7e1669226fecfad6ecc37c97e64f9c5 --- /dev/null +++ b/Sources/SFTPFeature/ActionHandlers/SFTPUploader.swift @@ -0,0 +1,69 @@ +import Shout +import Socket +import Models +import Keychain +import Foundation +import DependencyInjection + +public typealias SFTPUploadResult = (Result<Backup, Error>) -> Void + +public struct SFTPUploader { + public var upload: (URL, @escaping SFTPUploadResult) -> Void + + public func callAsFunction(url: URL, completion: @escaping SFTPUploadResult) { + upload(url, completion) + } +} + +extension SFTPUploader { + static let mock = SFTPUploader( + upload: { url, _ in + print("^^^ Requested upload on sftp service") + print("^^^ URL path: \(url.path)") + } + ) + + static let live = SFTPUploader { url, completion in + DispatchQueue.global().async { + do { + let keychain = try DependencyInjection.Container.shared.resolve() as KeychainHandling + let host = try keychain.get(key: .host) + let password = try keychain.get(key: .pwd) + let username = try keychain.get(key: .username) + + let ssh = try SSH(host: host!, port: 22) + try ssh.authenticate(username: username!, password: password!) + let sftp = try ssh.openSftp() + + let data = try Data(contentsOf: url) + + if (try? sftp.listFiles(in: "backup")) == nil { + try sftp.createDirectory("backup") + } + + try sftp.upload(data: data, remotePath: "backup/backup.xxm") + + completion(.success(.init( + id: "backup/backup.xxm", + date: Date(), + size: Float(data.count) + ))) + } catch { + if let error = error as? SSHError { + print(error.kind) + print(error.message) + print(error.description) + } else if let error = error as? Socket.Error { + print(error.errorCode) + print(error.description) + print(error.errorReason) + print(error.localizedDescription) + } else { + print(error.localizedDescription) + } + + completion(.failure(error)) + } + } + } +} diff --git a/Sources/SFTPFeature/SFTPController.swift b/Sources/SFTPFeature/SFTPController.swift new file mode 100644 index 0000000000000000000000000000000000000000..0a8c8d38ed8dbd9d802d58962b468b11f4c57431 --- /dev/null +++ b/Sources/SFTPFeature/SFTPController.swift @@ -0,0 +1,96 @@ +import HUD +import UIKit +import Combine +import DependencyInjection +import ScrollViewController + +public final class SFTPController: UIViewController { + @Dependency private var hud: HUDType + + lazy private var screenView = SFTPView() + lazy private var scrollViewController = ScrollViewController() + + private let completion: () -> Void + private let viewModel = SFTPViewModel() + private var cancellables = Set<AnyCancellable>() + + public init(_ completion: @escaping () -> Void) { + self.completion = completion + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { nil } + + public override func viewDidLoad() { + super.viewDidLoad() + setupScrollView() + setupNavigationBar() + setupBindings() + } + + private func setupScrollView() { + scrollViewController.scrollView.backgroundColor = .white + + addChild(scrollViewController) + view.addSubview(scrollViewController.view) + scrollViewController.view.snp.makeConstraints { $0.edges.equalToSuperview() } + scrollViewController.didMove(toParent: self) + scrollViewController.contentView = screenView + } + + private func setupNavigationBar() { + navigationItem.backButtonTitle = "" + + let back = UIButton.back() + back.addTarget(self, action: #selector(didTapBack), for: .touchUpInside) + + navigationItem.leftBarButtonItem = UIBarButtonItem( + customView: UIStackView(arrangedSubviews: [back]) + ) + } + + private func setupBindings() { + viewModel.hudPublisher + .receive(on: DispatchQueue.main) + .sink { [hud] in hud.update(with: $0) } + .store(in: &cancellables) + + viewModel.authPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in completion() } + .store(in: &cancellables) + + screenView.hostField + .textPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in viewModel.didEnterHost($0) } + .store(in: &cancellables) + + screenView.usernameField + .textPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in viewModel.didEnterUsername($0) } + .store(in: &cancellables) + + screenView.passwordField + .textPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in viewModel.didEnterPassword($0) } + .store(in: &cancellables) + + viewModel.statePublisher + .receive(on: DispatchQueue.main) + .map(\.isButtonEnabled) + .sink { [unowned self] in screenView.loginButton.isEnabled = $0 } + .store(in: &cancellables) + + screenView.loginButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in viewModel.didTapLogin() } + .store(in: &cancellables) + } + + @objc private func didTapBack() { + navigationController?.popViewController(animated: true) + } +} diff --git a/Sources/SFTPFeature/SFTPService.swift b/Sources/SFTPFeature/SFTPService.swift new file mode 100644 index 0000000000000000000000000000000000000000..f1a908564df810ed2aeaa1b4af358b367253f84b --- /dev/null +++ b/Sources/SFTPFeature/SFTPService.swift @@ -0,0 +1,47 @@ +import UIKit +import Keychain +import Presentation +import DependencyInjection + +public typealias SFTPAuthorizationParams = (UIViewController, () -> Void) + +public struct SFTPService { + public var isAuthorized: () -> Bool + public var fetchMetadata: SFTPFetcher + public var uploadBackup: SFTPUploader + public var authorizeFlow: (SFTPAuthorizationParams) -> Void + public var authenticate: SFTPAuthenticator + public var downloadBackup: SFTPDownloader +} + +public extension SFTPService { + static var mock = SFTPService( + isAuthorized: { true }, + fetchMetadata: .mock, + uploadBackup: .mock, + authorizeFlow: { (_, completion) in completion() }, + authenticate: .mock, + downloadBackup: .mock + ) + + static var live = SFTPService( + isAuthorized: { + if let keychain = try? DependencyInjection.Container.shared.resolve() as KeychainHandling, + let pwd = try? keychain.get(key: .pwd), + let host = try? keychain.get(key: .host), + let username = try? keychain.get(key: .username) { + return true + } + + return false + }, + fetchMetadata: .live, + uploadBackup: .live , + authorizeFlow: { controller, completion in + var pushPresenter: Presenting = PushPresenter() + pushPresenter.present(SFTPController(completion), from: controller) + }, + authenticate: .live, + downloadBackup: .live + ) +} diff --git a/Sources/SFTPFeature/SFTPView.swift b/Sources/SFTPFeature/SFTPView.swift new file mode 100644 index 0000000000000000000000000000000000000000..3e62e4caeb0eca188fd1dad4db740545f0b3f045 --- /dev/null +++ b/Sources/SFTPFeature/SFTPView.swift @@ -0,0 +1,76 @@ +import UIKit +import Shared +import InputField + +final class SFTPView: UIView { + let titleLabel = UILabel() + let subtitleLabel = UILabel() + let hostField = OutlinedInputField() + let usernameField = OutlinedInputField() + let passwordField = OutlinedInputField() + let loginButton = CapsuleButton() + let stackView = UIStackView() + + init() { + super.init(frame: .zero) + backgroundColor = Asset.neutralWhite.color + + titleLabel.textColor = Asset.neutralDark.color + titleLabel.text = Localized.AccountRestore.Sftp.title + titleLabel.font = Fonts.Mulish.bold.font(size: 24.0) + + let paragraph = NSMutableParagraphStyle() + paragraph.alignment = .left + paragraph.lineHeightMultiple = 1.15 + + let attString = NSAttributedString( + string: Localized.AccountRestore.Sftp.subtitle, + attributes: [ + .foregroundColor: Asset.neutralBody.color, + .font: Fonts.Mulish.regular.font(size: 16.0) as Any, + .paragraphStyle: paragraph + ]) + + subtitleLabel.numberOfLines = 0 + subtitleLabel.attributedText = attString + + hostField.setup(title: Localized.AccountRestore.Sftp.host) + usernameField.setup(title: Localized.AccountRestore.Sftp.username) + passwordField.setup(title: Localized.AccountRestore.Sftp.password, sensitive: true) + + loginButton.set(style: .brandColored, title: Localized.AccountRestore.Sftp.login) + + stackView.spacing = 30 + stackView.axis = .vertical + stackView.distribution = .fillEqually + stackView.addArrangedSubview(hostField) + stackView.addArrangedSubview(usernameField) + stackView.addArrangedSubview(passwordField) + stackView.addArrangedSubview(loginButton) + + addSubview(titleLabel) + addSubview(subtitleLabel) + addSubview(stackView) + + titleLabel.snp.makeConstraints { + $0.top.equalTo(safeAreaLayoutGuide).offset(15) + $0.left.equalToSuperview().offset(38) + $0.right.equalToSuperview().offset(-41) + } + + subtitleLabel.snp.makeConstraints { + $0.top.equalTo(titleLabel.snp.bottom).offset(8) + $0.left.equalToSuperview().offset(38) + $0.right.equalToSuperview().offset(-41) + } + + stackView.snp.makeConstraints { + $0.top.equalTo(subtitleLabel.snp.bottom).offset(28) + $0.left.equalToSuperview().offset(38) + $0.right.equalToSuperview().offset(-38) + $0.bottom.lessThanOrEqualToSuperview() + } + } + + required init?(coder: NSCoder) { nil } +} diff --git a/Sources/SFTPFeature/SFTPViewModel.swift b/Sources/SFTPFeature/SFTPViewModel.swift new file mode 100644 index 0000000000000000000000000000000000000000..dcf397a1da1c474681001b87df0d148ae26b2706 --- /dev/null +++ b/Sources/SFTPFeature/SFTPViewModel.swift @@ -0,0 +1,77 @@ +import HUD +import Combine +import Foundation +import DependencyInjection + +struct SFTPViewState { + var host: String = "" + var username: String = "" + var password: String = "" + var isButtonEnabled: Bool = false +} + +final class SFTPViewModel { + @Dependency private var service: SFTPService + + var hudPublisher: AnyPublisher<HUDStatus, Never> { + hudSubject.eraseToAnyPublisher() + } + + var statePublisher: AnyPublisher<SFTPViewState, Never> { + stateSubject.eraseToAnyPublisher() + } + + var authPublisher: AnyPublisher<Void, Never> { + authSubject.eraseToAnyPublisher() + } + + private let authSubject = PassthroughSubject<Void, Never>() + private let hudSubject = CurrentValueSubject<HUDStatus, Never>(.none) + private let stateSubject = CurrentValueSubject<SFTPViewState, Never>(.init()) + + func didEnterHost(_ string: String) { + stateSubject.value.host = string + validate() + } + + func didEnterUsername(_ string: String) { + stateSubject.value.username = string + validate() + } + + func didEnterPassword(_ string: String) { + stateSubject.value.password = string + validate() + } + + func didTapLogin() { + hudSubject.send(.on(nil)) + + let host = stateSubject.value.host + let username = stateSubject.value.username + let password = stateSubject.value.password + + DispatchQueue.global().async { [weak self] in + guard let self = self else { return } + do { + try self.service.authenticate( + host: host, + username: username, + password: password + ) + + self.hudSubject.send(.none) + self.authSubject.send(()) + } catch { + self.hudSubject.send(.error(.init(with: error))) + } + } + } + + private func validate() { + stateSubject.value.isButtonEnabled = + !stateSubject.value.host.isEmpty && + !stateSubject.value.username.isEmpty && + !stateSubject.value.password.isEmpty + } +} diff --git a/Sources/Shared/AutoGenerated/Assets.swift b/Sources/Shared/AutoGenerated/Assets.swift index 0f3b9d9280e9df670205e4f58eb7adc194456358..6755de526a369e8245365d38a47984ea96a02e42 100644 --- a/Sources/Shared/AutoGenerated/Assets.swift +++ b/Sources/Shared/AutoGenerated/Assets.swift @@ -97,6 +97,7 @@ public enum Asset { public static let requestsTabReceived = ImageAsset(name: "requests_tab_received") public static let requestsTabSent = ImageAsset(name: "requests_tab_sent") public static let requestsVerificationFailed = ImageAsset(name: "requests_verification_failed") + public static let restoreSFTP = ImageAsset(name: "restore_SFTP") public static let restoreDrive = ImageAsset(name: "restore_drive") public static let restoreDropbox = ImageAsset(name: "restore_dropbox") public static let restoreIcloud = ImageAsset(name: "restore_icloud") diff --git a/Sources/Shared/AutoGenerated/Strings.swift b/Sources/Shared/AutoGenerated/Strings.swift index a7b424f76629a0d52e217855b372e1a332da30c3..9f9fbfb27622937b8f189ffb9327c60ea9a681d0 100644 --- a/Sources/Shared/AutoGenerated/Strings.swift +++ b/Sources/Shared/AutoGenerated/Strings.swift @@ -211,6 +211,20 @@ public enum Localized { /// Backup not found public static let title = Localized.tr("Localizable", "accountRestore.notFound.title") } + public enum Sftp { + /// Host + public static let host = Localized.tr("Localizable", "accountRestore.sftp.host") + /// Login + public static let login = Localized.tr("Localizable", "accountRestore.sftp.login") + /// Password + public static let password = Localized.tr("Localizable", "accountRestore.sftp.password") + /// Login to your server. Your credentials will be automatically and securley saved locally on your device. + public static let subtitle = Localized.tr("Localizable", "accountRestore.sftp.subtitle") + /// Login to your SFTP + public static let title = Localized.tr("Localizable", "accountRestore.sftp.title") + /// Username + public static let username = Localized.tr("Localizable", "accountRestore.sftp.username") + } public enum Success { /// You now have access to all your contacts. public static let subtitle = Localized.tr("Localizable", "accountRestore.success.subtitle") @@ -236,6 +250,8 @@ public enum Localized { public static let header = Localized.tr("Localizable", "backup.header") /// iCloud public static let iCloud = Localized.tr("Localizable", "backup.iCloud") + /// SFTP + public static let sftp = Localized.tr("Localizable", "backup.SFTP") /// Back up your account to a cloud storage service, you can restore it along with only your contacts when you reinstall xx Messenger on another device. public static let subtitle = Localized.tr("Localizable", "backup.subtitle") public enum Config { diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_SFTP.imageset/Contents.json b/Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_SFTP.imageset/Contents.json new file mode 100644 index 0000000000000000000000000000000000000000..5c2f805eb3317d819659b5d73f14b6a1b249804f --- /dev/null +++ b/Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_SFTP.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Icon.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_SFTP.imageset/Icon.pdf b/Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_SFTP.imageset/Icon.pdf new file mode 100644 index 0000000000000000000000000000000000000000..08fcf11943d754dfc4e7427d290def530dc7dbcb --- /dev/null +++ b/Sources/Shared/Resources/Assets.xcassets/AssetsRestore/restore_SFTP.imageset/Icon.pdf @@ -0,0 +1,447 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 15.426392 33.381592 cm +0.693500 0.711750 0.730000 scn +23.717567 7.403564 m +1.429677 7.403564 l +0.639939 7.403564 0.000000 6.763626 0.000000 5.973887 c +0.000000 0.000000 l +25.147245 0.000000 l +25.147245 5.973887 l +25.147245 6.763626 24.507305 7.403564 23.717567 7.403564 c +h +3.436188 1.708138 m +2.466323 1.708138 l +2.293242 1.708138 2.152940 1.848440 2.152940 2.021521 c +2.152940 2.194602 2.293242 2.334904 2.466323 2.334904 c +3.436188 2.334904 l +3.609268 2.334904 3.749571 2.194602 3.749571 2.021521 c +3.749265 1.848440 3.608962 1.708138 3.436188 1.708138 c +h +3.436188 3.388399 m +2.466323 3.388399 l +2.293242 3.388399 2.152940 3.528702 2.152940 3.701782 c +2.152940 3.874863 2.293242 4.015165 2.466323 4.015165 c +3.436188 4.015165 l +3.609268 4.015165 3.749571 3.874863 3.749571 3.701782 c +3.749265 3.528702 3.608962 3.388399 3.436188 3.388399 c +h +3.436188 5.069273 m +2.466323 5.069273 l +2.293242 5.069273 2.152940 5.209575 2.152940 5.382656 c +2.152940 5.555737 2.293242 5.696039 2.466323 5.696039 c +3.436188 5.696039 l +3.609268 5.696039 3.749571 5.555737 3.749571 5.382656 c +3.749571 5.209575 3.608962 5.069273 3.436188 5.069273 c +h +6.515186 1.708138 m +5.545321 1.708138 l +5.372241 1.708138 5.231938 1.848440 5.231938 2.021521 c +5.231938 2.194602 5.372241 2.334904 5.545321 2.334904 c +6.515186 2.334904 l +6.688267 2.334904 6.828569 2.194602 6.828569 2.021521 c +6.828263 1.848440 6.687960 1.708138 6.515186 1.708138 c +h +6.515186 3.388399 m +5.545321 3.388399 l +5.372241 3.388399 5.231938 3.528702 5.231938 3.701782 c +5.231938 3.874863 5.372241 4.015165 5.545321 4.015165 c +6.515186 4.015165 l +6.688267 4.015165 6.828569 3.874863 6.828569 3.701782 c +6.828263 3.528702 6.687960 3.388399 6.515186 3.388399 c +h +6.515186 5.069273 m +5.545321 5.069273 l +5.372241 5.069273 5.231938 5.209575 5.231938 5.382656 c +5.231938 5.555737 5.372241 5.696039 5.545321 5.696039 c +6.515186 5.696039 l +6.688267 5.696039 6.828569 5.555737 6.828569 5.382656 c +6.828569 5.209575 6.687960 5.069273 6.515186 5.069273 c +h +9.594184 1.708138 m +8.624320 1.708138 l +8.451240 1.708138 8.310936 1.848440 8.310936 2.021521 c +8.310936 2.194602 8.451240 2.334904 8.624320 2.334904 c +9.594184 2.334904 l +9.767264 2.334904 9.907569 2.194602 9.907569 2.021521 c +9.907262 1.848440 9.766958 1.708138 9.594184 1.708138 c +h +9.594184 3.388399 m +8.624320 3.388399 l +8.451240 3.388399 8.310936 3.528702 8.310936 3.701782 c +8.310936 3.874863 8.451240 4.015165 8.624320 4.015165 c +9.594184 4.015165 l +9.767264 4.015165 9.907569 3.874863 9.907569 3.701782 c +9.907262 3.528702 9.766958 3.388399 9.594184 3.388399 c +h +9.594184 5.069273 m +8.624320 5.069273 l +8.451240 5.069273 8.310936 5.209575 8.310936 5.382656 c +8.310936 5.555737 8.451240 5.696039 8.624320 5.696039 c +9.594184 5.696039 l +9.767264 5.696039 9.907569 5.555737 9.907569 5.382656 c +9.907569 5.209575 9.766958 5.069273 9.594184 5.069273 c +h +12.673183 1.708138 m +11.703625 1.708138 l +11.530544 1.708138 11.390241 1.848440 11.390241 2.021521 c +11.390241 2.194602 11.530544 2.334904 11.703625 2.334904 c +12.673183 2.334904 l +12.846264 2.334904 12.986566 2.194602 12.986566 2.021521 c +12.986259 1.848440 12.845958 1.708138 12.673183 1.708138 c +h +12.673183 3.388399 m +11.703625 3.388399 l +11.530544 3.388399 11.390241 3.528702 11.390241 3.701782 c +11.390241 3.874863 11.530544 4.015165 11.703625 4.015165 c +12.673183 4.015165 l +12.846264 4.015165 12.986566 3.874863 12.986566 3.701782 c +12.986259 3.528702 12.845958 3.388399 12.673183 3.388399 c +h +12.673183 5.069273 m +11.703625 5.069273 l +11.530544 5.069273 11.390241 5.209575 11.390241 5.382656 c +11.390241 5.555737 11.530544 5.696039 11.703625 5.696039 c +12.673183 5.696039 l +12.846264 5.696039 12.986566 5.555737 12.986566 5.382656 c +12.986566 5.209575 12.845958 5.069273 12.673183 5.069273 c +h +16.798935 2.525446 m +16.149193 2.525446 15.622601 3.052040 15.622601 3.701782 c +15.622601 4.351524 16.149193 4.878118 16.798935 4.878118 c +17.448677 4.878118 17.975273 4.351524 17.975273 3.701782 c +17.975273 3.052040 17.448677 2.525446 16.798935 2.525446 c +h +21.504585 2.525446 m +20.854843 2.525446 20.328251 3.052040 20.328251 3.701782 c +20.328251 4.351524 20.854843 4.878118 21.504585 4.878118 c +22.154327 4.878118 22.680923 4.351524 22.680923 3.701782 c +22.680923 3.052040 22.154327 2.525446 21.504585 2.525446 c +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 15.426392 23.298096 cm +0.693500 0.711750 0.730000 scn +0.000000 7.403564 m +0.000000 0.000000 l +25.147245 0.000000 l +25.147245 7.403564 l +0.000000 7.403564 l +h +3.436188 1.707832 m +2.466323 1.707832 l +2.293242 1.707832 2.152940 1.847827 2.152940 2.021214 c +2.152940 2.194602 2.293242 2.334598 2.466323 2.334598 c +3.436188 2.334598 l +3.609268 2.334598 3.749571 2.194602 3.749571 2.021214 c +3.749571 1.847827 3.608962 1.707832 3.436188 1.707832 c +h +3.436188 3.388399 m +2.466323 3.388399 l +2.293242 3.388399 2.152940 3.528702 2.152940 3.701782 c +2.152940 3.874863 2.293242 4.015165 2.466323 4.015165 c +3.436188 4.015165 l +3.609268 4.015165 3.749571 3.874863 3.749571 3.701782 c +3.749265 3.528702 3.608962 3.388399 3.436188 3.388399 c +h +3.436188 5.068967 m +2.466323 5.068967 l +2.293242 5.068967 2.152940 5.209269 2.152940 5.382350 c +2.152940 5.555430 2.293242 5.695733 2.466323 5.695733 c +3.436188 5.695733 l +3.609268 5.695733 3.749571 5.555430 3.749571 5.382350 c +3.749265 5.209269 3.608962 5.068967 3.436188 5.068967 c +h +6.515186 1.707832 m +5.545321 1.707832 l +5.372241 1.707832 5.231938 1.847827 5.231938 2.021214 c +5.231938 2.194602 5.372241 2.334598 5.545321 2.334598 c +6.515186 2.334598 l +6.688267 2.334598 6.828569 2.194602 6.828569 2.021214 c +6.828569 1.847827 6.687960 1.707832 6.515186 1.707832 c +h +6.515186 3.388399 m +5.545321 3.388399 l +5.372241 3.388399 5.231938 3.528702 5.231938 3.701782 c +5.231938 3.874863 5.372241 4.015165 5.545321 4.015165 c +6.515186 4.015165 l +6.688267 4.015165 6.828569 3.874863 6.828569 3.701782 c +6.828263 3.528702 6.687960 3.388399 6.515186 3.388399 c +h +6.515186 5.068967 m +5.545321 5.068967 l +5.372241 5.068967 5.231938 5.209269 5.231938 5.382350 c +5.231938 5.555430 5.372241 5.695733 5.545321 5.695733 c +6.515186 5.695733 l +6.688267 5.695733 6.828569 5.555430 6.828569 5.382350 c +6.828263 5.209269 6.687960 5.068967 6.515186 5.068967 c +h +9.594184 1.707832 m +8.624320 1.707832 l +8.451240 1.707832 8.310936 1.847827 8.310936 2.021214 c +8.310936 2.194602 8.451240 2.334598 8.624320 2.334598 c +9.594184 2.334598 l +9.767264 2.334598 9.907569 2.194602 9.907569 2.021214 c +9.907569 1.847827 9.766958 1.707832 9.594184 1.707832 c +h +9.594184 3.388399 m +8.624320 3.388399 l +8.451240 3.388399 8.310936 3.528702 8.310936 3.701782 c +8.310936 3.874863 8.451240 4.015165 8.624320 4.015165 c +9.594184 4.015165 l +9.767264 4.015165 9.907569 3.874863 9.907569 3.701782 c +9.907262 3.528702 9.766958 3.388399 9.594184 3.388399 c +h +9.594184 5.068967 m +8.624320 5.068967 l +8.451240 5.068967 8.310936 5.209269 8.310936 5.382350 c +8.310936 5.555430 8.451240 5.695733 8.624320 5.695733 c +9.594184 5.695733 l +9.767264 5.695733 9.907569 5.555430 9.907569 5.382350 c +9.907262 5.209269 9.766958 5.068967 9.594184 5.068967 c +h +12.673183 1.707832 m +11.703625 1.707832 l +11.530544 1.707832 11.390241 1.847827 11.390241 2.021214 c +11.390241 2.194602 11.530544 2.334598 11.703625 2.334598 c +12.673183 2.334598 l +12.846264 2.334598 12.986566 2.194602 12.986566 2.021214 c +12.986566 1.847827 12.845958 1.707832 12.673183 1.707832 c +h +12.673183 3.388399 m +11.703625 3.388399 l +11.530544 3.388399 11.390241 3.528702 11.390241 3.701782 c +11.390241 3.874863 11.530544 4.015165 11.703625 4.015165 c +12.673183 4.015165 l +12.846264 4.015165 12.986566 3.874863 12.986566 3.701782 c +12.986259 3.528702 12.845958 3.388399 12.673183 3.388399 c +h +12.673183 5.068967 m +11.703625 5.068967 l +11.530544 5.068967 11.390241 5.209269 11.390241 5.382350 c +11.390241 5.555430 11.530544 5.695733 11.703625 5.695733 c +12.673183 5.695733 l +12.846264 5.695733 12.986566 5.555430 12.986566 5.382350 c +12.986259 5.209269 12.845958 5.068967 12.673183 5.068967 c +h +16.798935 2.525446 m +16.149193 2.525446 15.622601 3.052040 15.622601 3.701782 c +15.622601 4.351524 16.149193 4.878119 16.798935 4.878119 c +17.448677 4.878119 17.975273 4.351524 17.975273 3.701782 c +17.975273 3.052040 17.448677 2.525446 16.798935 2.525446 c +h +21.504585 2.525446 m +20.854843 2.525446 20.328251 3.052040 20.328251 3.701782 c +20.328251 4.351524 20.854843 4.878119 21.504585 4.878119 c +22.154327 4.878119 22.680923 4.351524 22.680923 3.701782 c +22.680923 3.052040 22.154327 2.525446 21.504585 2.525446 c +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 15.426392 13.214844 cm +0.693500 0.711750 0.730000 scn +0.000000 7.403564 m +0.000000 1.429677 l +0.000000 0.639939 0.639939 0.000000 1.429677 0.000000 c +23.717876 0.000000 l +24.507307 0.000000 25.147552 0.639939 25.147552 1.429677 c +25.147552 7.403564 l +0.000000 7.403564 l +h +3.436188 1.707830 m +2.466323 1.707830 l +2.293243 1.707830 2.152940 1.847827 2.152940 2.021214 c +2.152940 2.194295 2.293243 2.334599 2.466323 2.334599 c +3.436188 2.334599 l +3.609269 2.334599 3.749571 2.194602 3.749571 2.021214 c +3.749265 1.848134 3.608963 1.707830 3.436188 1.707830 c +h +3.436188 3.388398 m +2.466323 3.388398 l +2.293243 3.388398 2.152940 3.528395 2.152940 3.701782 c +2.152940 3.875169 2.293243 4.015166 2.466323 4.015166 c +3.436188 4.015166 l +3.609269 4.015166 3.749571 3.875169 3.749571 3.701782 c +3.749571 3.528395 3.608963 3.388398 3.436188 3.388398 c +h +3.436188 5.068966 m +2.466323 5.068966 l +2.293243 5.068966 2.152940 5.208962 2.152940 5.382350 c +2.152940 5.555430 2.293243 5.695734 2.466323 5.695734 c +3.436188 5.695734 l +3.609269 5.695734 3.749571 5.555737 3.749571 5.382350 c +3.749265 5.209269 3.608963 5.068966 3.436188 5.068966 c +h +6.515187 1.707830 m +5.545322 1.707830 l +5.372241 1.707830 5.231939 1.847827 5.231939 2.021214 c +5.231939 2.194295 5.372241 2.334599 5.545322 2.334599 c +6.515187 2.334599 l +6.688267 2.334599 6.828570 2.194602 6.828570 2.021214 c +6.828264 1.848134 6.687961 1.707830 6.515187 1.707830 c +h +6.515187 3.388398 m +5.545322 3.388398 l +5.372241 3.388398 5.231939 3.528395 5.231939 3.701782 c +5.231939 3.875169 5.372241 4.015166 5.545322 4.015166 c +6.515187 4.015166 l +6.688267 4.015166 6.828570 3.875169 6.828570 3.701782 c +6.828570 3.528395 6.687961 3.388398 6.515187 3.388398 c +h +6.515187 5.068966 m +5.545322 5.068966 l +5.372241 5.068966 5.231939 5.208962 5.231939 5.382350 c +5.231939 5.555430 5.372241 5.695734 5.545322 5.695734 c +6.515187 5.695734 l +6.688267 5.695734 6.828570 5.555737 6.828570 5.382350 c +6.828264 5.209269 6.687961 5.068966 6.515187 5.068966 c +h +9.594185 1.707830 m +8.624321 1.707830 l +8.451241 1.707830 8.310937 1.847827 8.310937 2.021214 c +8.310937 2.194295 8.451241 2.334599 8.624321 2.334599 c +9.594185 2.334599 l +9.767265 2.334599 9.907570 2.194602 9.907570 2.021214 c +9.907263 1.848134 9.766959 1.707830 9.594185 1.707830 c +h +9.594185 3.388398 m +8.624321 3.388398 l +8.451241 3.388398 8.310937 3.528395 8.310937 3.701782 c +8.310937 3.875169 8.451241 4.015166 8.624321 4.015166 c +9.594185 4.015166 l +9.767265 4.015166 9.907570 3.875169 9.907570 3.701782 c +9.907570 3.528395 9.766959 3.388398 9.594185 3.388398 c +h +9.594185 5.068966 m +8.624321 5.068966 l +8.451241 5.068966 8.310937 5.208962 8.310937 5.382350 c +8.310937 5.555430 8.451241 5.695734 8.624321 5.695734 c +9.594185 5.695734 l +9.767265 5.695734 9.907570 5.555737 9.907570 5.382350 c +9.907263 5.209269 9.766959 5.068966 9.594185 5.068966 c +h +12.673184 1.707830 m +11.703626 1.707830 l +11.530545 1.707830 11.390242 1.847827 11.390242 2.021214 c +11.390242 2.194295 11.530545 2.334599 11.703626 2.334599 c +12.673184 2.334599 l +12.846266 2.334599 12.986567 2.194602 12.986567 2.021214 c +12.986260 1.848134 12.845959 1.707830 12.673184 1.707830 c +h +12.673184 3.388398 m +11.703626 3.388398 l +11.530545 3.388398 11.390242 3.528395 11.390242 3.701782 c +11.390242 3.875169 11.530545 4.015166 11.703626 4.015166 c +12.673184 4.015166 l +12.846266 4.015166 12.986567 3.875169 12.986567 3.701782 c +12.986567 3.528395 12.845959 3.388398 12.673184 3.388398 c +h +12.673184 5.068966 m +11.703626 5.068966 l +11.530545 5.068966 11.390242 5.208962 11.390242 5.382350 c +11.390242 5.555430 11.530545 5.695734 11.703626 5.695734 c +12.673184 5.695734 l +12.846266 5.695734 12.986567 5.555737 12.986567 5.382350 c +12.986260 5.209269 12.845959 5.068966 12.673184 5.068966 c +h +16.798937 2.525447 m +16.149195 2.525447 15.622602 3.052040 15.622602 3.701782 c +15.622602 4.351524 16.149195 4.878119 16.798937 4.878119 c +17.448679 4.878119 17.975275 4.351524 17.975275 3.701782 c +17.975275 3.052040 17.448679 2.525447 16.798937 2.525447 c +h +21.504587 2.525447 m +20.854845 2.525447 20.328253 3.052040 20.328253 3.701782 c +20.328253 4.351524 20.854845 4.878119 21.504587 4.878119 c +22.154329 4.878119 22.680925 4.351524 22.680925 3.701782 c +22.680925 3.052040 22.154329 2.525447 21.504587 2.525447 c +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 17.400757 21.498291 cm +0.693500 0.711750 0.730000 scn +21.201620 0.918945 m +0.000000 0.918945 l +0.000000 -0.000067 l +21.201620 -0.000067 l +21.201620 0.918945 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 17.400757 31.582764 cm +0.693500 0.711750 0.730000 scn +21.201620 0.918945 m +0.000000 0.918945 l +0.000000 -0.000067 l +21.201620 -0.000067 l +21.201620 0.918945 l +h +f +n +Q + +endstream +endobj + +3 0 obj + 13265 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 56.000000 56.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000013355 00000 n +0000013379 00000 n +0000013552 00000 n +0000013626 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +13685 +%%EOF \ No newline at end of file diff --git a/Sources/Shared/Resources/en.lproj/Localizable.strings b/Sources/Shared/Resources/en.lproj/Localizable.strings index 7c8da3fd206e97854424e16b94dbbc91a22b9d5f..3aa07cedf9576e85634d4d61ec701efcee5229c5 100644 --- a/Sources/Shared/Resources/en.lproj/Localizable.strings +++ b/Sources/Shared/Resources/en.lproj/Localizable.strings @@ -629,6 +629,8 @@ = "Dropbox"; "backup.googleDrive" = "Google Drive"; +"backup.SFTP" += "SFTP"; // Settings - Delete Account @@ -838,6 +840,19 @@ "accountRestore.list.cancel" = "Cancel"; +"accountRestore.sftp.title" += "Login to your SFTP"; +"accountRestore.sftp.subtitle" += "Login to your server. Your credentials will be automatically and securely saved locally on your device."; +"accountRestore.sftp.host" += "Host"; +"accountRestore.sftp.username" += "Username"; +"accountRestore.sftp.password" += "Password"; +"accountRestore.sftp.login" += "Login"; + "accountRestore.header" = "Account restore"; "accountRestore.found.title" diff --git a/client-ios.xcworkspace/xcshareddata/swiftpm/Package.resolved b/client-ios.xcworkspace/xcshareddata/swiftpm/Package.resolved index eeb145af40bba398a5526019333b5de39606557e..102be628eff74bf62db32030a74c7eed314ba3f3 100644 --- a/client-ios.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/client-ios.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -27,6 +27,15 @@ "version" : "1.4.0" } }, + { + "identity" : "bluesocket", + "kind" : "remoteSourceControl", + "location" : "https://github.com/IBM-Swift/BlueSocket.git", + "state" : { + "revision" : "c9894fd117457f1d006575fbfb2fdfd6f79eac03", + "version" : "1.0.200" + } + }, { "identity" : "boringssl-swiftpm", "kind" : "remoteSourceControl", @@ -207,6 +216,15 @@ "version" : "1.22.2" } }, + { + "identity" : "libssh2prebuild", + "kind" : "remoteSourceControl", + "location" : "https://github.com/DimaRU/Libssh2Prebuild.git", + "state" : { + "branch" : "1.10.0+OpenSSL_1_1_1o", + "revision" : "a91bcf205a6cbc84144f840c44145656abbd266a" + } + }, { "identity" : "nanopb", "kind" : "remoteSourceControl", @@ -261,6 +279,14 @@ "version" : "1.2.0" } }, + { + "identity" : "shout", + "kind" : "remoteSourceControl", + "location" : "https://github.com/darrarski/Shout.git", + "state" : { + "revision" : "df5a662293f0ac15eeb4f2fd3ffd0c07b73d0de0" + } + }, { "identity" : "snapkit", "kind" : "remoteSourceControl",