diff --git a/src/qt/qt_vmmanager_main.cpp b/src/qt/qt_vmmanager_main.cpp index b55e2be2a..e0a903c58 100644 --- a/src/qt/qt_vmmanager_main.cpp +++ b/src/qt/qt_vmmanager_main.cpp @@ -15,6 +15,8 @@ * Copyright 2024 cold-brewed */ +#include +#include #include #include #include @@ -23,12 +25,64 @@ #include #include #include +#include + +#include +#include #include "qt_vmmanager_main.hpp" #include "ui_qt_vmmanager_main.h" #include "qt_vmmanager_model.hpp" #include "qt_vmmanager_addmachine.hpp" +// https://stackoverflow.com/a/36460740 +bool copyPath(QString sourceDir, QString destinationDir, bool overWriteDirectory) +{ + QDir originDirectory(sourceDir); + + if (! originDirectory.exists()) + { + return false; + } + + QDir destinationDirectory(destinationDir); + + if(destinationDirectory.exists() && !overWriteDirectory) + { + return false; + } + else if(destinationDirectory.exists() && overWriteDirectory) + { + destinationDirectory.removeRecursively(); + } + + originDirectory.mkpath(destinationDir); + + foreach (QString directoryName, originDirectory.entryList(QDir::Dirs | \ + QDir::NoDotAndDotDot)) + { + QString destinationPath = destinationDir + "/" + directoryName; + originDirectory.mkpath(destinationPath); + copyPath(sourceDir + "/" + directoryName, destinationPath, overWriteDirectory); + } + + foreach (QString fileName, originDirectory.entryList(QDir::Files)) + { + QFile::copy(sourceDir + "/" + fileName, destinationDir + "/" + fileName); + } + + /*! Possible race-condition mitigation? */ + QDir finalDestination(destinationDir); + finalDestination.refresh(); + + if(finalDestination.exists()) + { + return true; + } + + return false; +} + VMManagerMain::VMManagerMain(QWidget *parent) : QWidget(parent), ui(new Ui::VMManagerMain), selected_sysconfig(new VMManagerSystem) { ui->setupUi(this); @@ -96,6 +150,131 @@ VMManagerMain::VMManagerMain(QWidget *parent) : }); setSystemIcon.setEnabled(!selected_sysconfig->window_obscured); + QAction cloneMachine(tr("C&lone...")); + contextMenu.addAction(&cloneMachine); + connect(&cloneMachine, &QAction::triggered, [this] { + QDialog dialog = QDialog(this); + auto layout = new QVBoxLayout(&dialog); + layout->setSizeConstraint(QLayout::SetFixedSize); + layout->addWidget(new QLabel(tr("Virtual machine \"%1\" (%2) will be cloned into:").arg(selected_sysconfig->displayName, selected_sysconfig->config_dir))); + QLineEdit* edit = new QLineEdit(&dialog); + layout->addWidget(edit); + QLabel* errLabel = new QLabel(&dialog); + layout->addWidget(errLabel); + errLabel->setVisible(false); + QDialogButtonBox* buttonBox = new QDialogButtonBox(&dialog); + buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + buttonBox->button(QDialogButtonBox::Ok)->setDisabled(true); + connect(buttonBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); + connect(buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); + layout->addWidget(buttonBox); + connect(edit, &QLineEdit::textChanged, this, [this, errLabel, buttonBox] (const QString& text) { + bool isSpaceOnly = true; +#ifdef Q_OS_WINDOWS + const char illegalChars[] = "<>:\"|?*\\/"; +#else + const char illegalChars[] = "\\/"; +#endif + for (const auto& curChar : text) { + for (int i = 0; i < sizeof(illegalChars) - 1; i++) { + if (illegalChars[i] == curChar) { + goto illegal_chars; + } + if (!curChar.isSpace()) { + isSpaceOnly = false; + } + } + } + errLabel->setVisible(false); + buttonBox->button(QDialogButtonBox::Ok)->setDisabled(isSpaceOnly || text.isEmpty()); + if (QDir((QString(vmm_path) + "/") + text).exists() && buttonBox->button(QDialogButtonBox::Ok)->isEnabled()) { + goto dir_already_exists; + } + return; +dir_already_exists: + errLabel->setText(tr("Directory %1 already exists").arg(QDir((QString(vmm_path) + "/") + text).canonicalPath())); + errLabel->setVisible(true); + buttonBox->button(QDialogButtonBox::Ok)->setDisabled(true); + return; +illegal_chars: + QString illegalCharsDisplay; + for (int i = 0; i < sizeof(illegalChars) - 1; i++) { + illegalCharsDisplay.push_back(illegalChars[i]); + illegalCharsDisplay.push_back(' '); + } + illegalCharsDisplay.chop(1); + errLabel->setText(tr("You cannot use the following characters in the name: %1").arg(illegalCharsDisplay)); + errLabel->setVisible(true); + buttonBox->button(QDialogButtonBox::Ok)->setDisabled(true); + return; + }); + + if (dialog.exec() > 0) { + std::atomic_bool finished{false}; + std::atomic_int errCode; + auto vmDir = QDir(vmm_path).canonicalPath(); + vmDir.append("/"); + vmDir.append(edit->text()); + vmDir.append("/"); + + if (!QDir(vmDir).mkpath(".")) { + QMessageBox::critical(this, tr("Clone"), tr("Failed to create directory for cloned VM"), QMessageBox::Ok); + return; + } + + QProgressDialog* progDialog = new QProgressDialog(this); + progDialog->setMaximum(0); + progDialog->setMinimum(0); + progDialog->setWindowFlags(progDialog->windowFlags() & ~Qt::WindowCloseButtonHint); + progDialog->setFixedSize(progDialog->sizeHint()); + progDialog->setMinimumDuration(0); + progDialog->setCancelButton(nullptr); + progDialog->setAutoClose(false); + progDialog->setAutoReset(false); + progDialog->setAttribute(Qt::WA_DeleteOnClose, true); + progDialog->setValue(0); + progDialog->show(); + QString srcPath = selected_sysconfig->config_dir; + QString dstPath = vmDir; + + std::thread copyThread([this, &finished, srcPath, dstPath, &errCode] { + errCode = copyPath(srcPath, dstPath, true); + finished = true; + }); + while (!finished) { + QApplication::processEvents(); + } + copyThread.join(); + progDialog->close(); + if (errCode) { + QDir(dstPath).removeRecursively(); + QMessageBox::critical(this, tr("Clone"), tr("Failed to clone VM: %1").arg(errCode), QMessageBox::Ok); + return; + } + + QFileInfo configFileInfo(vmDir + CONFIG_FILE); + if (configFileInfo.exists()) { + const auto current_index = ui->listView->currentIndex(); + vm_model->reload(this); + const auto created_object = vm_model->getIndexForConfigFile(configFileInfo); + if (created_object.row() < 0) { + // For some reason the index of the new object couldn't be determined. Fall back to the old index. + ui->listView->setCurrentIndex(current_index); + return; + } + auto added_system = vm_model->getConfigObjectForIndex(created_object); + added_system->setDisplayName(edit->text()); + // Get the index of the newly-created system and select it + const QModelIndex mapped_index = proxy_model->mapFromSource(created_object); + ui->listView->setCurrentIndex(mapped_index); + } else { + QDir(dstPath).removeRecursively(); + QMessageBox::critical(this, tr("Clone"), tr("Failed to clone VM for an unknown reason."), QMessageBox::Ok); + return; + } + } + }); + QAction killIcon(tr("&Kill")); contextMenu.addAction(&killIcon); connect(&killIcon, &QAction::triggered, [this, parent] {