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 {
/// Restore / Backup
container.register(SFTPService.mock)
container.register(iCloudServiceMock() as iCloudInterface)
container.register(DropboxServiceMock() as DropboxInterface)
container.register(GoogleDriveServiceMock() as GoogleDriveInterface)
......@@ -121,6 +122,7 @@ struct DependencyRegistrator {
container.register(
BackupCoordinator(
sftpFactory: BackupSFTPController.init,
passphraseFactory: BackupPassphraseController.init(_:_:)
) as BackupCoordinating)
......@@ -161,9 +163,9 @@ struct DependencyRegistrator {
container.register(
RestoreCoordinator(
sftpFactory: SFTPController.init,
successFactory: RestoreSuccessController.init,
chatListFactory: ChatListController.init,
sftpFactory: RestoreSFTPController.init(_:),
restoreFactory: RestoreController.init(_:_:),
passphraseFactory: RestorePassphraseController.init(_:)
) as RestoreCoordinating)
......
......@@ -122,7 +122,7 @@ final class BackupConfigController: UIViewController {
screenView.sftpButton
.publisher(for: .touchUpInside)
.sink { [unowned self] in viewModel.didTapService(.sftp, self) }
.sink { [unowned self] in coordinator.toSFTP(from: self) }
.store(in: &cancellables)
screenView.iCloudButton
......
import HUD
import UIKit
import Combine
import DependencyInjection
public final class SFTPController: UIViewController {
lazy private var screenView = SFTPView()
public final class BackupSFTPController: UIViewController {
@Dependency private var hud: HUDType
private let viewModel = SFTPViewModel()
lazy private var screenView = BackupSFTPView()
private let viewModel = BackupSFTPViewModel()
private var cancellables = Set<AnyCancellable>()
public override func loadView() {
......@@ -29,6 +33,17 @@ public final class SFTPController: UIViewController {
}
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
.textPublisher
.receive(on: DispatchQueue.main)
......
......@@ -8,6 +8,8 @@ public protocol BackupCoordinating {
from: UIViewController
)
func toSFTP(from: UIViewController)
func toPassphrase(
from: UIViewController,
cancelClosure: @escaping EmptyClosure,
......@@ -16,24 +18,27 @@ public protocol BackupCoordinating {
}
public struct BackupCoordinator: BackupCoordinating {
var pushPresenter: Presenting = PushPresenter()
var bottomPresenter: Presenting = BottomPresenter()
var passphraseFactory: (
@escaping EmptyClosure,
@escaping StringClosure
) -> UIViewController
var sftpFactory: () -> UIViewController
var passphraseFactory: (@escaping EmptyClosure, @escaping StringClosure) -> UIViewController
public init(
passphraseFactory: @escaping (
@escaping EmptyClosure,
@escaping StringClosure
) -> UIViewController
sftpFactory: @escaping () -> UIViewController,
passphraseFactory: @escaping (@escaping EmptyClosure, @escaping StringClosure) -> UIViewController
) {
self.sftpFactory = sftpFactory
self.passphraseFactory = passphraseFactory
}
}
public extension BackupCoordinator {
func toSFTP(from parent: UIViewController) {
let screen = sftpFactory()
pushPresenter.present(screen, from: parent)
}
func toDrawer(
_ screen: UIViewController,
from parent: UIViewController
......
......@@ -152,9 +152,7 @@ extension BackupService {
}.store(in: &cancellables)
}
case .sftp:
if !sftpService.isAuthorized() {
// TODO
}
break
}
}
}
......@@ -209,7 +207,25 @@ extension BackupService {
}
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() {
......
import HUD
import Models
import Combine
import Foundation
import SFTPFeature
import DependencyInjection
struct SFTPViewState {
struct BackupSFTPViewState {
var host: String = ""
var username: String = ""
var password: String = ""
var isButtonEnabled: Bool = false
}
final class SFTPViewModel {
var statePublisher: AnyPublisher<SFTPViewState, Never> {
final class BackupSFTPViewModel {
@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()
}
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) {
stateSubject.value.host = string
......@@ -31,20 +47,16 @@ final class SFTPViewModel {
}
func didTapLogin() {
hudSubject.send(.on(nil))
let host = stateSubject.value.host
let username = stateSubject.value.username
let password = stateSubject.value.password
// do {
// let session = try SSH(host: stateSubject.value.host)
// try session.authenticate(
// username: stateSubject.value.username,
// password: stateSubject.value.password
// )
//
// let sftp = try session.openSftp()
// try sftp.download(remotePath: "", localURL: URL(string: "")!)
// } catch {
// print(error.localizedDescription)
// }
let authParams = SFTPAuthParams(host, username, password)
service.justAuthenticate(authParams)
hudSubject.send(.none)
popSubject.send(())
}
private func validate() {
......
......@@ -2,7 +2,7 @@ import UIKit
import Shared
import InputField
final class SFTPView: UIView {
final class BackupSFTPView: UIView {
let titleLabel = UILabel()
let subtitleLabel = UILabel()
let hostField = OutlinedInputField()
......
import HUD
import DrawerFeature
import Shared
import UIKit
import Combine
import DrawerFeature
import DependencyInjection
public final class RestoreListController: UIViewController {
......@@ -88,7 +88,7 @@ public final class RestoreListController: UIViewController {
screenView.sftpButton
.publisher(for: .touchUpInside)
.sink { [unowned self] in
coordinator.toSFTP(from: self)
coordinator.toSFTP(using: ndf, from: self)
}.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
import Presentation
public protocol RestoreCoordinating {
func toSFTP(from: UIViewController)
func toChats(from: UIViewController)
func toSuccess(from: UIViewController)
func toSFTP(using: String, from: UIViewController)
func toDrawer(_: UIViewController, from: UIViewController)
func toPassphrase(from: UIViewController, _: @escaping StringClosure)
func toRestore(using: String, with: RestoreSettings, from: UIViewController)
func toRestoreReplacing(using: String, with: RestoreSettings, from: UIViewController)
}
public struct RestoreCoordinator: RestoreCoordinating {
var pushPresenter: Presenting = PushPresenter()
var bottomPresenter: Presenting = BottomPresenter()
var replacePresenter: Presenting = ReplacePresenter()
var replaceLastPresenter: Presenting = ReplacePresenter(mode: .replaceLast)
var sftpFactory: () -> UIViewController
var successFactory: () -> UIViewController
var chatListFactory: () -> UIViewController
var sftpFactory: (String) -> UIViewController
var restoreFactory: (String, RestoreSettings) -> UIViewController
var passphraseFactory: (@escaping StringClosure) -> UIViewController
public init(
sftpFactory: @escaping () -> UIViewController,
successFactory: @escaping () -> UIViewController,
chatListFactory: @escaping () -> UIViewController,
sftpFactory: @escaping (String) -> UIViewController,
restoreFactory: @escaping (String, RestoreSettings) -> UIViewController,
passphraseFactory: @escaping (@escaping StringClosure) -> UIViewController
) {
......@@ -48,6 +50,15 @@ public extension RestoreCoordinator {
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) {
let screen = chatListFactory()
replacePresenter.present(screen, from: parent)
......@@ -70,8 +81,8 @@ public extension RestoreCoordinator {
bottomPresenter.present(screen, from: parent)
}
func toSFTP(from parent: UIViewController) {
let screen = sftpFactory()
func toSFTP(using ndf: String, from parent: UIViewController) {
let screen = sftpFactory(ndf)
pushPresenter.present(screen, from: parent)
}
}
......@@ -6,22 +6,24 @@ import Combine
import BackupFeature
import DependencyInjection
import SFTPFeature
import iCloudFeature
import DropboxFeature
import GoogleDriveFeature
final class RestoreListViewModel {
@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 hud: AnyPublisher<HUDStatus, Never> {
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 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
import BackupFeature
import DependencyInjection
import SFTPFeature
import iCloudFeature
import DropboxFeature
import GoogleDriveFeature
......@@ -39,7 +38,6 @@ 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
......
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 var isAuthorized: () -> Bool
public var downloadMetadata: (@escaping (String) -> Void) -> Void
public var fetch: (SFTPFetchParams) -> Void
public var justAuthenticate: (SFTPAuthParams) -> Void
}
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(
isAuthorized: {
/// If it has host/username/password on keychain
/// means its authorized, not that is working
///
true
},
downloadMetadata: { completion in
completion("MOCK")
fetch: { (authParams, completion) in
// 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.
Finish editing this message first!
Please register or to comment