Merge pull request #5919 from Cacodemon345/vmm-cloning
Implement VM cloning option
This commit is contained in:
@@ -15,6 +15,8 @@
|
||||
* Copyright 2024 cold-brewed
|
||||
*/
|
||||
|
||||
#include <QDirIterator>
|
||||
#include <QLabel>
|
||||
#include <QAbstractListModel>
|
||||
#include <QCompleter>
|
||||
#include <QDebug>
|
||||
@@ -23,12 +25,64 @@
|
||||
#include <QMessageBox>
|
||||
#include <QStringListModel>
|
||||
#include <QTimer>
|
||||
#include <QProgressDialog>
|
||||
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
|
||||
#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] {
|
||||
|
||||
Reference in New Issue
Block a user