Skip to content
Snippets Groups Projects
Commit add5f413 authored by Bruno Muniz's avatar Bruno Muniz :apple:
Browse files

Using SFTP on both backup and restore flows

parent 08fe999a
No related branches found
No related tags found
2 merge requests!54Releasing 1.1.4,!42Adding SFTP as a service to backup/restore
Showing
with 432 additions and 35 deletions
...@@ -64,6 +64,7 @@ struct DependencyRegistrator { ...@@ -64,6 +64,7 @@ struct DependencyRegistrator {
/// Restore / Backup /// Restore / Backup
container.register(SFTPService.mock)
container.register(iCloudServiceMock() as iCloudInterface) container.register(iCloudServiceMock() as iCloudInterface)
container.register(DropboxServiceMock() as DropboxInterface) container.register(DropboxServiceMock() as DropboxInterface)
container.register(GoogleDriveServiceMock() as GoogleDriveInterface) container.register(GoogleDriveServiceMock() as GoogleDriveInterface)
...@@ -121,6 +122,7 @@ struct DependencyRegistrator { ...@@ -121,6 +122,7 @@ struct DependencyRegistrator {
container.register( container.register(
BackupCoordinator( BackupCoordinator(
sftpFactory: BackupSFTPController.init,
passphraseFactory: BackupPassphraseController.init(_:_:) passphraseFactory: BackupPassphraseController.init(_:_:)
) as BackupCoordinating) ) as BackupCoordinating)
...@@ -161,9 +163,9 @@ struct DependencyRegistrator { ...@@ -161,9 +163,9 @@ struct DependencyRegistrator {
container.register( container.register(
RestoreCoordinator( RestoreCoordinator(
sftpFactory: SFTPController.init,
successFactory: RestoreSuccessController.init, successFactory: RestoreSuccessController.init,
chatListFactory: ChatListController.init, chatListFactory: ChatListController.init,
sftpFactory: RestoreSFTPController.init(_:),
restoreFactory: RestoreController.init(_:_:), restoreFactory: RestoreController.init(_:_:),
passphraseFactory: RestorePassphraseController.init(_:) passphraseFactory: RestorePassphraseController.init(_:)
) as RestoreCoordinating) ) as RestoreCoordinating)
......
...@@ -122,7 +122,7 @@ final class BackupConfigController: UIViewController { ...@@ -122,7 +122,7 @@ final class BackupConfigController: UIViewController {
screenView.sftpButton screenView.sftpButton
.publisher(for: .touchUpInside) .publisher(for: .touchUpInside)
.sink { [unowned self] in viewModel.didTapService(.sftp, self) } .sink { [unowned self] in coordinator.toSFTP(from: self) }
.store(in: &cancellables) .store(in: &cancellables)
screenView.iCloudButton screenView.iCloudButton
......
import HUD
import UIKit import UIKit
import Combine import Combine
import DependencyInjection
public final class SFTPController: UIViewController { public final class BackupSFTPController: UIViewController {
lazy private var screenView = SFTPView() @Dependency private var hud: HUDType
private let viewModel = SFTPViewModel() lazy private var screenView = BackupSFTPView()
private let viewModel = BackupSFTPViewModel()
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
public override func loadView() { public override func loadView() {
...@@ -29,6 +33,17 @@ public final class SFTPController: UIViewController { ...@@ -29,6 +33,17 @@ public final class SFTPController: UIViewController {
} }
private func setupBindings() { private func setupBindings() {
viewModel.hudPublisher
.receive(on: DispatchQueue.main)
.sink { [hud] in hud.update(with: $0) }
.store(in: &cancellables)
viewModel.popPublisher
.receive(on: DispatchQueue.main)
.sink { [unowned self] in
navigationController?.popViewController(animated: true)
}.store(in: &cancellables)
screenView.hostField screenView.hostField
.textPublisher .textPublisher
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
......
...@@ -8,6 +8,8 @@ public protocol BackupCoordinating { ...@@ -8,6 +8,8 @@ public protocol BackupCoordinating {
from: UIViewController from: UIViewController
) )
func toSFTP(from: UIViewController)
func toPassphrase( func toPassphrase(
from: UIViewController, from: UIViewController,
cancelClosure: @escaping EmptyClosure, cancelClosure: @escaping EmptyClosure,
...@@ -16,24 +18,27 @@ public protocol BackupCoordinating { ...@@ -16,24 +18,27 @@ public protocol BackupCoordinating {
} }
public struct BackupCoordinator: BackupCoordinating { public struct BackupCoordinator: BackupCoordinating {
var pushPresenter: Presenting = PushPresenter()
var bottomPresenter: Presenting = BottomPresenter() var bottomPresenter: Presenting = BottomPresenter()
var passphraseFactory: ( var sftpFactory: () -> UIViewController
@escaping EmptyClosure, var passphraseFactory: (@escaping EmptyClosure, @escaping StringClosure) -> UIViewController
@escaping StringClosure
) -> UIViewController
public init( public init(
passphraseFactory: @escaping ( sftpFactory: @escaping () -> UIViewController,
@escaping EmptyClosure, passphraseFactory: @escaping (@escaping EmptyClosure, @escaping StringClosure) -> UIViewController
@escaping StringClosure
) -> UIViewController
) { ) {
self.sftpFactory = sftpFactory
self.passphraseFactory = passphraseFactory self.passphraseFactory = passphraseFactory
} }
} }
public extension BackupCoordinator { public extension BackupCoordinator {
func toSFTP(from parent: UIViewController) {
let screen = sftpFactory()
pushPresenter.present(screen, from: parent)
}
func toDrawer( func toDrawer(
_ screen: UIViewController, _ screen: UIViewController,
from parent: UIViewController from parent: UIViewController
......
...@@ -152,9 +152,7 @@ extension BackupService { ...@@ -152,9 +152,7 @@ extension BackupService {
}.store(in: &cancellables) }.store(in: &cancellables)
} }
case .sftp: case .sftp:
if !sftpService.isAuthorized() { break
// TODO
}
} }
} }
} }
...@@ -209,7 +207,25 @@ extension BackupService { ...@@ -209,7 +207,25 @@ extension BackupService {
} }
if sftpService.isAuthorized() { if sftpService.isAuthorized() {
// TODO let host = ""
let username = ""
let password = ""
let completion: SFTPFetchResult = { result in
switch result {
case .success(let settings):
if let settings = settings {
print("")
} else {
print("")
}
case .failure(let error):
print(error.localizedDescription)
}
}
let authParams = SFTPAuthParams(host, username, password)
sftpService.fetch((authParams, completion))
} }
if dropboxService.isAuthorized() { if dropboxService.isAuthorized() {
......
import HUD
import Models
import Combine import Combine
import Foundation import Foundation
import SFTPFeature
import DependencyInjection
struct SFTPViewState { struct BackupSFTPViewState {
var host: String = "" var host: String = ""
var username: String = "" var username: String = ""
var password: String = "" var password: String = ""
var isButtonEnabled: Bool = false var isButtonEnabled: Bool = false
} }
final class SFTPViewModel { final class BackupSFTPViewModel {
var statePublisher: AnyPublisher<SFTPViewState, Never> { @Dependency private var service: SFTPService
var hudPublisher: AnyPublisher<HUDStatus, Never> {
hudSubject.eraseToAnyPublisher()
}
var popPublisher: AnyPublisher<Void, Never> {
popSubject.eraseToAnyPublisher()
}
var statePublisher: AnyPublisher<BackupSFTPViewState, Never> {
stateSubject.eraseToAnyPublisher() stateSubject.eraseToAnyPublisher()
} }
private let stateSubject = CurrentValueSubject<SFTPViewState, Never>(.init()) private let popSubject = PassthroughSubject<Void, Never>()
private let hudSubject = CurrentValueSubject<HUDStatus, Never>(.none)
private let stateSubject = CurrentValueSubject<BackupSFTPViewState, Never>(.init())
func didEnterHost(_ string: String) { func didEnterHost(_ string: String) {
stateSubject.value.host = string stateSubject.value.host = string
...@@ -31,20 +47,16 @@ final class SFTPViewModel { ...@@ -31,20 +47,16 @@ final class SFTPViewModel {
} }
func didTapLogin() { func didTapLogin() {
hudSubject.send(.on(nil))
let host = stateSubject.value.host
let username = stateSubject.value.username
let password = stateSubject.value.password
// do { let authParams = SFTPAuthParams(host, username, password)
// let session = try SSH(host: stateSubject.value.host) service.justAuthenticate(authParams)
// try session.authenticate( hudSubject.send(.none)
// username: stateSubject.value.username, popSubject.send(())
// password: stateSubject.value.password
// )
//
// let sftp = try session.openSftp()
// try sftp.download(remotePath: "", localURL: URL(string: "")!)
// } catch {
// print(error.localizedDescription)
// }
} }
private func validate() { private func validate() {
......
...@@ -2,7 +2,7 @@ import UIKit ...@@ -2,7 +2,7 @@ import UIKit
import Shared import Shared
import InputField import InputField
final class SFTPView: UIView { final class BackupSFTPView: UIView {
let titleLabel = UILabel() let titleLabel = UILabel()
let subtitleLabel = UILabel() let subtitleLabel = UILabel()
let hostField = OutlinedInputField() let hostField = OutlinedInputField()
......
import HUD import HUD
import DrawerFeature
import Shared import Shared
import UIKit import UIKit
import Combine import Combine
import DrawerFeature
import DependencyInjection import DependencyInjection
public final class RestoreListController: UIViewController { public final class RestoreListController: UIViewController {
...@@ -88,7 +88,7 @@ public final class RestoreListController: UIViewController { ...@@ -88,7 +88,7 @@ public final class RestoreListController: UIViewController {
screenView.sftpButton screenView.sftpButton
.publisher(for: .touchUpInside) .publisher(for: .touchUpInside)
.sink { [unowned self] in .sink { [unowned self] in
coordinator.toSFTP(from: self) coordinator.toSFTP(using: ndf, from: self)
}.store(in: &cancellables) }.store(in: &cancellables)
} }
......
import HUD
import UIKit
import Combine
import DependencyInjection
public final class RestoreSFTPController: UIViewController {
@Dependency private var hud: HUDType
@Dependency private var coordinator: RestoreCoordinating
lazy private var screenView = RestoreSFTPView()
private let ndf: String
private let viewModel = RestoreSFTPViewModel()
private var cancellables = Set<AnyCancellable>()
public override func loadView() {
view = screenView
}
public init(_ ndf: String) {
self.ndf = ndf
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) { nil }
public override func viewDidLoad() {
super.viewDidLoad()
setupNavigationBar()
setupBindings()
}
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.backupPublisher
.receive(on: DispatchQueue.main)
.sink { [unowned self] in
coordinator.toRestoreReplacing(using: ndf, with: $0, from: self)
}.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)
}
}
...@@ -4,29 +4,31 @@ import Shared ...@@ -4,29 +4,31 @@ import Shared
import Presentation import Presentation
public protocol RestoreCoordinating { public protocol RestoreCoordinating {
func toSFTP(from: UIViewController)
func toChats(from: UIViewController) func toChats(from: UIViewController)
func toSuccess(from: UIViewController) func toSuccess(from: UIViewController)
func toSFTP(using: String, from: UIViewController)
func toDrawer(_: UIViewController, from: UIViewController) func toDrawer(_: UIViewController, from: UIViewController)
func toPassphrase(from: UIViewController, _: @escaping StringClosure) func toPassphrase(from: UIViewController, _: @escaping StringClosure)
func toRestore(using: String, with: RestoreSettings, from: UIViewController) func toRestore(using: String, with: RestoreSettings, from: UIViewController)
func toRestoreReplacing(using: String, with: RestoreSettings, from: UIViewController)
} }
public struct RestoreCoordinator: RestoreCoordinating { public struct RestoreCoordinator: RestoreCoordinating {
var pushPresenter: Presenting = PushPresenter() var pushPresenter: Presenting = PushPresenter()
var bottomPresenter: Presenting = BottomPresenter() var bottomPresenter: Presenting = BottomPresenter()
var replacePresenter: Presenting = ReplacePresenter() var replacePresenter: Presenting = ReplacePresenter()
var replaceLastPresenter: Presenting = ReplacePresenter(mode: .replaceLast)
var sftpFactory: () -> UIViewController
var successFactory: () -> UIViewController var successFactory: () -> UIViewController
var chatListFactory: () -> UIViewController var chatListFactory: () -> UIViewController
var sftpFactory: (String) -> UIViewController
var restoreFactory: (String, RestoreSettings) -> UIViewController var restoreFactory: (String, RestoreSettings) -> UIViewController
var passphraseFactory: (@escaping StringClosure) -> UIViewController var passphraseFactory: (@escaping StringClosure) -> UIViewController
public init( public init(
sftpFactory: @escaping () -> UIViewController,
successFactory: @escaping () -> UIViewController, successFactory: @escaping () -> UIViewController,
chatListFactory: @escaping () -> UIViewController, chatListFactory: @escaping () -> UIViewController,
sftpFactory: @escaping (String) -> UIViewController,
restoreFactory: @escaping (String, RestoreSettings) -> UIViewController, restoreFactory: @escaping (String, RestoreSettings) -> UIViewController,
passphraseFactory: @escaping (@escaping StringClosure) -> UIViewController passphraseFactory: @escaping (@escaping StringClosure) -> UIViewController
) { ) {
...@@ -48,6 +50,15 @@ public extension RestoreCoordinator { ...@@ -48,6 +50,15 @@ public extension RestoreCoordinator {
pushPresenter.present(screen, from: parent) pushPresenter.present(screen, from: parent)
} }
func toRestoreReplacing(
using ndf: String,
with settings: RestoreSettings,
from parent: UIViewController
) {
let screen = restoreFactory(ndf, settings)
replaceLastPresenter.present(screen, from: parent)
}
func toChats(from parent: UIViewController) { func toChats(from parent: UIViewController) {
let screen = chatListFactory() let screen = chatListFactory()
replacePresenter.present(screen, from: parent) replacePresenter.present(screen, from: parent)
...@@ -70,8 +81,8 @@ public extension RestoreCoordinator { ...@@ -70,8 +81,8 @@ public extension RestoreCoordinator {
bottomPresenter.present(screen, from: parent) bottomPresenter.present(screen, from: parent)
} }
func toSFTP(from parent: UIViewController) { func toSFTP(using ndf: String, from parent: UIViewController) {
let screen = sftpFactory() let screen = sftpFactory(ndf)
pushPresenter.present(screen, from: parent) pushPresenter.present(screen, from: parent)
} }
} }
...@@ -6,22 +6,24 @@ import Combine ...@@ -6,22 +6,24 @@ import Combine
import BackupFeature import BackupFeature
import DependencyInjection import DependencyInjection
import SFTPFeature
import iCloudFeature import iCloudFeature
import DropboxFeature import DropboxFeature
import GoogleDriveFeature import GoogleDriveFeature
final class RestoreListViewModel { final class RestoreListViewModel {
@Dependency private var sftpService: SFTPService
@Dependency private var icloudService: iCloudInterface @Dependency private var icloudService: iCloudInterface
@Dependency private var dropboxService: DropboxInterface @Dependency private var dropboxService: DropboxInterface
@Dependency private var googleDriveService: GoogleDriveInterface @Dependency private var googleDriveService: GoogleDriveInterface
var hud: AnyPublisher<HUDStatus, Never> { hudSubject.eraseToAnyPublisher() } var hud: AnyPublisher<HUDStatus, Never> {
var didFetchBackup: AnyPublisher<RestoreSettings, Never> { backupSubject.eraseToAnyPublisher() } hudSubject.eraseToAnyPublisher()
}
private var dropboxAuthCancellable: AnyCancellable? var didFetchBackup: AnyPublisher<RestoreSettings, Never> {
backupSubject.eraseToAnyPublisher()
}
private var dropboxAuthCancellable: AnyCancellable?
private let hudSubject = PassthroughSubject<HUDStatus, Never>() private let hudSubject = PassthroughSubject<HUDStatus, Never>()
private let backupSubject = PassthroughSubject<RestoreSettings, Never>() private let backupSubject = PassthroughSubject<RestoreSettings, Never>()
......
import HUD
import Models
import Combine
import Foundation
import SFTPFeature
import DependencyInjection
struct RestoreSFTPViewState {
var host: String = ""
var username: String = ""
var password: String = ""
var isButtonEnabled: Bool = false
}
final class RestoreSFTPViewModel {
@Dependency private var service: SFTPService
var hudPublisher: AnyPublisher<HUDStatus, Never> {
hudSubject.eraseToAnyPublisher()
}
var backupPublisher: AnyPublisher<RestoreSettings, Never> {
backupSubject.eraseToAnyPublisher()
}
var statePublisher: AnyPublisher<RestoreSFTPViewState, Never> {
stateSubject.eraseToAnyPublisher()
}
private let hudSubject = CurrentValueSubject<HUDStatus, Never>(.none)
private let backupSubject = PassthroughSubject<RestoreSettings, Never>()
private let stateSubject = CurrentValueSubject<RestoreSFTPViewState, 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
let completion: SFTPFetchResult = { result in
switch result {
case .success(let backup):
self.hudSubject.send(.none)
if let backup = backup {
self.backupSubject.send(backup)
} else {
self.backupSubject.send(.init(cloudService: .sftp))
}
case .failure(let error):
self.hudSubject.send(.error(.init(with: error)))
}
}
let authParams = SFTPAuthParams(host, username, password)
service.fetch((authParams, completion))
}
private func validate() {
stateSubject.value.isButtonEnabled =
!stateSubject.value.host.isEmpty &&
!stateSubject.value.username.isEmpty &&
!stateSubject.value.password.isEmpty
}
}
...@@ -8,7 +8,6 @@ import Integration ...@@ -8,7 +8,6 @@ import Integration
import BackupFeature import BackupFeature
import DependencyInjection import DependencyInjection
import SFTPFeature
import iCloudFeature import iCloudFeature
import DropboxFeature import DropboxFeature
import GoogleDriveFeature import GoogleDriveFeature
...@@ -39,7 +38,6 @@ extension RestorationStep: Equatable { ...@@ -39,7 +38,6 @@ extension RestorationStep: Equatable {
} }
final class RestoreViewModel { final class RestoreViewModel {
@Dependency private var sftpService: SFTPService
@Dependency private var iCloudService: iCloudInterface @Dependency private var iCloudService: iCloudInterface
@Dependency private var dropboxService: DropboxInterface @Dependency private var dropboxService: DropboxInterface
@Dependency private var googleService: GoogleDriveInterface @Dependency private var googleService: GoogleDriveInterface
......
import UIKit
import Shared
import InputField
final class RestoreSFTPView: 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 }
}
import Models
import Foundation
public typealias SFTPAuthParams = (String, String, String)
public typealias SFTPFetchResult = (Result<RestoreSettings?, Error>) -> Void
public typealias SFTPFetchParams = (SFTPAuthParams, SFTPFetchResult)
public struct SFTPService { public struct SFTPService {
public var isAuthorized: () -> Bool public var isAuthorized: () -> Bool
public var downloadMetadata: (@escaping (String) -> Void) -> Void public var fetch: (SFTPFetchParams) -> Void
public var justAuthenticate: (SFTPAuthParams) -> Void
} }
public extension SFTPService { public extension SFTPService {
static var mock = SFTPService(
isAuthorized: {
false
},
fetch: { (authParams, completion) in
print("^^^ RestoreSFTP Host: \(authParams.0)")
print("^^^ RestoreSFTP Username: \(authParams.1)")
print("^^^ RestoreSFTP Password: \(authParams.2)")
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
completion(.success(.init(
backup: .init(id: "ASDF", date: Date.distantPast, size: 100_000_000),
cloudService: .sftp
)))
}
},
justAuthenticate: { host, username, password in
// TODO: Store these params on the keychain
})
static var live = SFTPService( static var live = SFTPService(
isAuthorized: { isAuthorized: {
/// If it has host/username/password on keychain
/// means its authorized, not that is working
///
true true
}, },
downloadMetadata: { completion in fetch: { (authParams, completion) in
completion("MOCK") // TODO: Store host/username/password on keychain
},
justAuthenticate: { host, username, password in
// TODO: Store host/username/password on keychain
} }
) )
} }
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment