mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-06-22 02:01:08 +07:00
40abac8725
backup operation so ssh_scp_new() doesn't fail on a reused stale session (reproduces as 'Failed to open channel for scp' right after restore) - Call resetConnection() at entry of createBackup() and uploadBackup() - Replace recursive findStackView() in PageSetupWizardEasy with upward parent traversal (depth+push check) to avoid JS stack overflow on iOS when component tree is large (71 VPN managers in test)
505 lines
19 KiB
QML
505 lines
19 KiB
QML
import QtQuick
|
|
import QtQuick.Controls
|
|
import QtQuick.Layouts
|
|
|
|
import SortFilterProxyModel 0.2
|
|
|
|
import PageEnum 1.0
|
|
import ContainerProps 1.0
|
|
import ProtocolProps 1.0
|
|
import Style 1.0
|
|
|
|
import "./"
|
|
import "../Controls2"
|
|
import "../Config"
|
|
|
|
PageType {
|
|
id: root
|
|
objectName: "pageSetupWizardEasy"
|
|
|
|
property bool isEasySetup: true
|
|
property bool isRestoreFromBackup: false
|
|
property string backupFilePath: ""
|
|
property string restoreHostname: ""
|
|
property string restoreUsername: ""
|
|
property string restoreSecretData: ""
|
|
property bool waitingForServerToAdd: false
|
|
|
|
// For installing containers from backup
|
|
property var containersToInstall: []
|
|
property int currentContainerIndex: 0
|
|
property bool isInstallingContainers: false
|
|
|
|
// Connections for ServersBackupController
|
|
Connections {
|
|
target: ServersBackupController
|
|
|
|
function onReadyForRestore(backupFilePath, hostname, username, secretData, serverIp, fileName) {
|
|
console.log("onReadyForRestore received from C++")
|
|
console.log(" backupFilePath:", backupFilePath)
|
|
console.log(" hostname:", hostname)
|
|
console.log(" serverIp:", serverIp)
|
|
console.log(" fileName:", fileName)
|
|
|
|
// Scan backup to determine containers (C++ already did this, but needed for QML)
|
|
var foundContainers = ServersBackupController.scanBackupForContainers(backupFilePath)
|
|
console.log("Found containers:", foundContainers)
|
|
|
|
if (foundContainers.length === 0) {
|
|
PageController.showErrorMessage(qsTr("No containers found in backup file"))
|
|
root.isRestoreFromBackup = false
|
|
return
|
|
}
|
|
|
|
root.containersToInstall = foundContainers
|
|
root.currentContainerIndex = 0
|
|
|
|
// Now add empty server with these credentials
|
|
InstallController.setProcessedServerCredentials(hostname, username, secretData)
|
|
|
|
// Set waiting flag
|
|
root.waitingForServerToAdd = true
|
|
|
|
console.log("Backup scanned, adding server...")
|
|
// Add server (asynchronously)
|
|
InstallController.addEmptyServer()
|
|
|
|
// Further execution will happen in onInstallServerFinished
|
|
}
|
|
}
|
|
|
|
// Connections for tracking server addition
|
|
Connections {
|
|
target: InstallController
|
|
|
|
function onInstallServerFinished(finishedMessage) {
|
|
if (root.waitingForServerToAdd && root.isRestoreFromBackup && root.backupFilePath.length > 0) {
|
|
console.log("Server added successfully, now installing containers from backup...")
|
|
root.waitingForServerToAdd = false
|
|
|
|
// Start installing containers
|
|
root.isInstallingContainers = true
|
|
installNextContainer()
|
|
}
|
|
}
|
|
|
|
function onInstallContainerFinished(finishedMessage, isServiceInstall) {
|
|
if (root.isInstallingContainers) {
|
|
console.log("Container installed:", finishedMessage)
|
|
|
|
// Move to next container
|
|
root.currentContainerIndex++
|
|
|
|
if (root.currentContainerIndex < root.containersToInstall.length) {
|
|
// Install next container
|
|
installNextContainer()
|
|
} else {
|
|
// All containers installed, now do restore
|
|
console.log("All containers installed, starting restore...")
|
|
root.isInstallingContainers = false
|
|
|
|
// IMPORTANT: Turn off busy indicator before navigation
|
|
PageController.showBusyIndicator(false)
|
|
|
|
// Start navigation to restore mode selection page
|
|
navigationTimer.start()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Function to install next container from list
|
|
function installNextContainer() {
|
|
if (root.currentContainerIndex >= root.containersToInstall.length) {
|
|
return
|
|
}
|
|
|
|
var containerName = root.containersToInstall[root.currentContainerIndex]
|
|
console.log("Installing container:", containerName, "(", root.currentContainerIndex + 1, "/", root.containersToInstall.length, ")")
|
|
|
|
// Convert container name to DockerContainer enum
|
|
var dockerContainer = ContainerProps.containerFromString(containerName)
|
|
|
|
if (dockerContainer === 0) { // None
|
|
console.log("Unknown container:", containerName, "skipping...")
|
|
root.currentContainerIndex++
|
|
installNextContainer()
|
|
return
|
|
}
|
|
|
|
// Get default settings for container
|
|
var defaultProtocol = ContainerProps.defaultProtocol(dockerContainer)
|
|
var defaultPort = InstallController.getPortForInstall(defaultProtocol)
|
|
var defaultTransport = InstallController.defaultTransportProto(defaultProtocol)
|
|
|
|
// Set server index
|
|
var serverIdx = ServersModel.getServersCount() - 1
|
|
ServersModel.processedIndex = serverIdx
|
|
var serverId = ServersUiController.getServerId(serverIdx)
|
|
|
|
// Show loading indicator with message
|
|
PageController.showBusyIndicator(true)
|
|
PageController.showNotificationMessage(qsTr("Installing %1 (%2/%3)...")
|
|
.arg(containerName)
|
|
.arg(root.currentContainerIndex + 1)
|
|
.arg(root.containersToInstall.length))
|
|
|
|
// Ensure credentials are set
|
|
InstallController.setProcessedServerCredentials(root.restoreHostname, root.restoreUsername, root.restoreSecretData)
|
|
|
|
// Install container
|
|
console.log("Installing container:", containerName, "serverId:", serverId)
|
|
ContainersModel.setProcessedContainerIndex(dockerContainer)
|
|
InstallController.install(dockerContainer, defaultPort, defaultTransport, serverId)
|
|
}
|
|
|
|
// Timer for navigating to restore mode selection page after file selection
|
|
Timer {
|
|
id: navigationTimer
|
|
interval: 500
|
|
repeat: false
|
|
onTriggered: {
|
|
if (root.backupFilePath.length > 0 && root.isRestoreFromBackup) {
|
|
console.log("Navigation timer triggered, going to restore mode page")
|
|
console.log("Credentials available:", root.restoreHostname, root.restoreUsername, root.restoreSecretData.length > 0 ? "***" : "EMPTY")
|
|
|
|
// Get filename
|
|
var fileName = SystemController.getFileNameFromPath(root.backupFilePath)
|
|
if (!fileName || fileName === undefined || fileName.length === 0) {
|
|
var fallbackName = root.backupFilePath.split('/').pop()
|
|
fileName = (fallbackName && fallbackName.length > 0) ? fallbackName : qsTr("backup.tgz")
|
|
}
|
|
fileName = String(fileName)
|
|
|
|
// Extract IP address from filename
|
|
var serverIp = ""
|
|
var ipMatch = fileName.match(/^([\d_]+)\s*-/)
|
|
if (ipMatch && ipMatch.length > 1) {
|
|
serverIp = ipMatch[1].replace(/_/g, ".")
|
|
}
|
|
if (!serverIp || serverIp.length === 0) {
|
|
serverIp = root.restoreHostname
|
|
}
|
|
|
|
var serverName = root.restoreHostname
|
|
if (!serverName || serverName.length === 0) {
|
|
serverName = qsTr("RestoredServer")
|
|
}
|
|
|
|
// Navigate to installation page
|
|
PageController.goToPage(PageEnum.PageSetupWizardInstalling)
|
|
|
|
// Immediately find StackView and navigate to restore page
|
|
// Server already added, as we waited for onInstallServerFinished
|
|
Qt.callLater(function() {
|
|
var pagePath = "qrc:/ui/qml/Pages2/PageSettingsServerRestoreMode.qml"
|
|
|
|
// Traverse upward from root to find the containing StackView.
|
|
// StackView has both `push` function and `depth` property.
|
|
// This avoids a recursive downward search that causes stack overflow
|
|
// on iOS when the component tree is large (many VPN managers).
|
|
var stackView = root.parent
|
|
while (stackView) {
|
|
if (typeof stackView.push === "function" && stackView.hasOwnProperty("depth")) {
|
|
break
|
|
}
|
|
stackView = stackView.parent
|
|
}
|
|
|
|
if (stackView) {
|
|
console.log("Found StackView, pushing restore mode page")
|
|
stackView.push(pagePath, {
|
|
"backupFilePath": root.backupFilePath,
|
|
"backupFileName": fileName,
|
|
"serverName": "",
|
|
"serverIp": serverIp,
|
|
"isFromSetupWizard": true,
|
|
"wizardHostname": root.restoreHostname,
|
|
"wizardUsername": root.restoreUsername,
|
|
"wizardSecretData": root.restoreSecretData
|
|
})
|
|
} else {
|
|
console.error("Could not find StackView")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
SortFilterProxyModel {
|
|
id: proxyContainersModel
|
|
|
|
sourceModel: ContainersModel
|
|
filters: [
|
|
ValueFilter {
|
|
roleName: "isEasySetupContainer"
|
|
value: true
|
|
}
|
|
]
|
|
sorters: RoleSorter {
|
|
roleName: "easySetupOrder"
|
|
sortOrder: Qt.DescendingOrder
|
|
}
|
|
}
|
|
|
|
BackButtonType {
|
|
id: backButton
|
|
|
|
anchors.top: parent.top
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.topMargin: 20 + PageController.safeAreaTopMargin
|
|
|
|
onActiveFocusChanged: {
|
|
if(backButton.enabled && backButton.activeFocus) {
|
|
listView.positionViewAtBeginning()
|
|
}
|
|
}
|
|
}
|
|
|
|
ButtonGroup {
|
|
id: buttonGroup
|
|
}
|
|
|
|
ListViewType {
|
|
id: listView
|
|
|
|
property int dockerContainer
|
|
property int containerDefaultPort
|
|
property int containerDefaultTransportProto
|
|
|
|
anchors.top: backButton.bottom
|
|
anchors.bottom: parent.bottom
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
|
|
spacing: 16
|
|
|
|
header: ColumnLayout {
|
|
width: listView.width
|
|
|
|
spacing: 16
|
|
|
|
BaseHeaderType {
|
|
id: header
|
|
|
|
Layout.fillWidth: true
|
|
Layout.rightMargin: 16
|
|
Layout.leftMargin: 16
|
|
Layout.bottomMargin: 16
|
|
|
|
headerTextMaximumLineCount: 10
|
|
|
|
headerText: qsTr("Choose Installation Type")
|
|
}
|
|
}
|
|
|
|
model: proxyContainersModel
|
|
currentIndex: 0
|
|
|
|
delegate: ColumnLayout {
|
|
width: listView.width
|
|
|
|
CardType {
|
|
id: card
|
|
|
|
Layout.fillWidth: true
|
|
Layout.rightMargin: 16
|
|
Layout.leftMargin: 16
|
|
Layout.bottomMargin: 16
|
|
|
|
headerText: easySetupHeader
|
|
bodyText: easySetupDescription
|
|
|
|
ButtonGroup.group: buttonGroup
|
|
|
|
onClicked: function() {
|
|
isEasySetup = true
|
|
checked = true
|
|
var defaultContainerProto = ContainerProps.defaultProtocol(dockerContainer)
|
|
|
|
listView.dockerContainer = dockerContainer
|
|
listView.containerDefaultPort = InstallController.getPortForInstall(defaultContainerProto)
|
|
listView.containerDefaultTransportProto = InstallController.defaultTransportProto(defaultContainerProto)
|
|
}
|
|
|
|
Keys.onReturnPressed: this.clicked()
|
|
Keys.onEnterPressed: this.clicked()
|
|
}
|
|
}
|
|
|
|
footer: ColumnLayout {
|
|
width: listView.width
|
|
spacing: 16
|
|
|
|
DividerType {
|
|
Layout.fillWidth: true
|
|
Layout.leftMargin: 16
|
|
Layout.rightMargin: 16
|
|
}
|
|
|
|
CardType {
|
|
Layout.fillWidth: true
|
|
Layout.leftMargin: 16
|
|
Layout.rightMargin: 16
|
|
|
|
headerText: qsTr("Manual")
|
|
bodyText: qsTr("Choose a VPN protocol")
|
|
|
|
ButtonGroup.group: buttonGroup
|
|
|
|
onClicked: function() {
|
|
isEasySetup = false
|
|
checked = true
|
|
}
|
|
|
|
Keys.onEnterPressed: this.clicked()
|
|
Keys.onReturnPressed: this.clicked()
|
|
}
|
|
|
|
DividerType {
|
|
Layout.fillWidth: true
|
|
Layout.leftMargin: 16
|
|
Layout.rightMargin: 16
|
|
}
|
|
|
|
CardType {
|
|
Layout.fillWidth: true
|
|
Layout.leftMargin: 16
|
|
Layout.rightMargin: 16
|
|
|
|
headerText: qsTr("Restore from backup")
|
|
bodyText: qsTr("Restoration of VPN protocols, services, all server settings and users")
|
|
|
|
ButtonGroup.group: buttonGroup
|
|
|
|
onClicked: function() {
|
|
|
|
var filter = GC.isMobile() ? "*.gz *.tgz *.tar.gz" : "Backup files (*.tar.gz *.backup *.tgz *.gz)"
|
|
var localPath = SystemController.getFileName(
|
|
qsTr("Select Backup to Restore"),
|
|
filter,
|
|
"",
|
|
false,
|
|
""
|
|
)
|
|
|
|
console.log("Selected file path:", localPath)
|
|
|
|
if (!localPath || localPath.length === 0) {
|
|
console.log("No file selected")
|
|
return
|
|
}
|
|
|
|
// Save backup file path
|
|
root.backupFilePath = localPath
|
|
root.isRestoreFromBackup = true
|
|
|
|
// Get credentials from PageSetupWizardCredentials via StackView search
|
|
var credentialsPage = null
|
|
var item = root
|
|
|
|
// Find StackView
|
|
while (item && !item.hasOwnProperty("depth")) {
|
|
item = item.parent
|
|
}
|
|
|
|
// If found StackView, search for PageSetupWizardCredentials in its history
|
|
if (item && item.depth > 0) {
|
|
for (var i = 0; i < item.depth; i++) {
|
|
var page = item.get(i)
|
|
if (page && page.hasOwnProperty("savedHostname")) {
|
|
credentialsPage = page
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if (credentialsPage && credentialsPage.savedHostname.length > 0) {
|
|
root.restoreHostname = credentialsPage.savedHostname
|
|
root.restoreUsername = credentialsPage.savedUsername
|
|
root.restoreSecretData = credentialsPage.savedSecretData
|
|
console.log("Got credentials from PageSetupWizardCredentials:", root.restoreHostname, root.restoreUsername)
|
|
|
|
// Call C++ method to prepare restore
|
|
// It will scan backup and send readyForRestore signal
|
|
ServersBackupController.prepareRestoreFromBackup(localPath, root.restoreHostname, root.restoreUsername, root.restoreSecretData)
|
|
} else {
|
|
console.log("WARNING: No credentials found")
|
|
return
|
|
}
|
|
}
|
|
|
|
Keys.onEnterPressed: this.clicked()
|
|
Keys.onReturnPressed: this.clicked()
|
|
}
|
|
|
|
BasicButtonType {
|
|
id: continueButton
|
|
|
|
Layout.fillWidth: true
|
|
Layout.leftMargin: 16
|
|
Layout.rightMargin: 16
|
|
|
|
text: qsTr("Continue")
|
|
|
|
clickedFunc: function() {
|
|
if (root.isEasySetup) {
|
|
ServersUiController.processedContainerIndex = listView.dockerContainer
|
|
PageController.goToPage(PageEnum.PageSetupWizardInstalling)
|
|
InstallController.install(listView.dockerContainer,
|
|
listView.containerDefaultPort,
|
|
listView.containerDefaultTransportProto,
|
|
ServersUiController.processedServerId)
|
|
} else {
|
|
PageController.goToPage(PageEnum.PageSetupWizardProtocols)
|
|
}
|
|
}
|
|
}
|
|
|
|
BasicButtonType {
|
|
id: setupLaterButton
|
|
|
|
Layout.fillWidth: true
|
|
Layout.topMargin: 8
|
|
Layout.bottomMargin: 24
|
|
Layout.leftMargin: 16
|
|
Layout.rightMargin: 16
|
|
|
|
defaultColor: AmneziaStyle.color.transparent
|
|
hoveredColor: AmneziaStyle.color.translucentWhite
|
|
pressedColor: AmneziaStyle.color.sheerWhite
|
|
disabledColor: AmneziaStyle.color.mutedGray
|
|
textColor: AmneziaStyle.color.paleGray
|
|
borderWidth: 1
|
|
|
|
visible: {
|
|
if (PageController.isTriggeredByConnectButton()) {
|
|
PageController.setTriggeredByConnectButton(false)
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
text: qsTr("Skip setup")
|
|
|
|
clickedFunc: function() {
|
|
PageController.goToPage(PageEnum.PageSetupWizardInstalling)
|
|
InstallController.addEmptyServer()
|
|
}
|
|
}
|
|
}
|
|
|
|
Component.onCompleted: {
|
|
var item = listView.itemAtIndex(listView.currentIndex)
|
|
if (item !== null) {
|
|
var button = item.children[0]
|
|
button.checked = true
|
|
button.clicked()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|