diff --git a/src/qt/introdialog.cpp b/src/qt/introdialog.cpp index c8d72fe..9165a70 100644 --- a/src/qt/introdialog.cpp +++ b/src/qt/introdialog.cpp @@ -16,6 +16,8 @@ #include +#include + IntroDialog::IntroDialog(QWidget *parent) : QDialog(parent) { @@ -190,6 +192,24 @@ bool IntroDialog::pickDataDirectory() settings.setValue("strDataDir", dataDir); } + // Check for pending data directory migration + if (settings.value("fPendingDataDirMigration", false).toBool()) { + QString oldDir = settings.value("strDataDirPrevious", "").toString(); + if (!oldDir.isEmpty() && oldDir != dataDir) { + if (!migrateDataDirectory(oldDir, dataDir)) { + // Migration failed - revert to old directory + QMessageBox::warning(0, "Triangles", + QString("Data directory migration failed.\nContinuing with the previous directory:\n%1") + .arg(oldDir)); + dataDir = oldDir; + settings.setValue("strDataDir", oldDir); + } + } + // Clear migration state regardless + settings.remove("strDataDirPrevious"); + settings.setValue("fPendingDataDirMigration", false); + } + // If the saved path is the default, don't set -datadir (let normal defaults work) QString defaultDir = QString::fromStdString(GetDefaultDataDir().string()); if (dataDir != defaultDir) { @@ -275,3 +295,145 @@ bool IntroDialog::pickDataDirectory() return true; } + +static void copyDirectoryRecursive(const boost::filesystem::path& src, + const boost::filesystem::path& dst) +{ + namespace fs = boost::filesystem; + fs::create_directories(dst); + for (fs::directory_iterator it(src), end; it != end; ++it) { + fs::path dstChild = dst / it->path().filename(); + if (fs::is_directory(it->path())) { + copyDirectoryRecursive(it->path(), dstChild); + } else { + fs::copy_file(it->path(), dstChild, fs::copy_option::overwrite_if_exists); + } + } +} + +bool IntroDialog::migrateDataDirectory(const QString& oldPath, const QString& newPath) +{ + namespace fs = boost::filesystem; + + fs::path srcDir(oldPath.toStdString()); + fs::path dstDir(newPath.toStdString()); + + if (!fs::exists(srcDir) || !fs::is_directory(srcDir)) + return false; + + // Create destination directory + try { + fs::create_directories(dstDir); + } catch (const fs::filesystem_error& e) { + printf("Migration: Cannot create destination directory: %s\n", e.what()); + return false; + } + + // Check free space + try { + quint64 srcSize = 0; + for (fs::recursive_directory_iterator it(srcDir), end; it != end; ++it) { + if (fs::is_regular_file(*it)) + srcSize += fs::file_size(*it); + } + fs::space_info si = fs::space(dstDir); + if (si.available < srcSize + (50 * 1024 * 1024)) { // 50MB headroom + printf("Migration: Insufficient disk space. Need %llu, have %llu\n", + (unsigned long long)srcSize, (unsigned long long)si.available); + return false; + } + } catch (const fs::filesystem_error& e) { + printf("Migration: Cannot check disk space: %s\n", e.what()); + return false; + } + + // Files/directories to skip during copy + static const std::set skipFiles = { + ".lock", + "debug.log", + "db.log", + }; + + // Show progress dialog + QProgressDialog progress("Moving data directory...", QString(), 0, 0, 0); + progress.setWindowTitle("Triangles - Data Migration"); + progress.setWindowModality(Qt::ApplicationModal); + progress.setMinimumDuration(0); + progress.setCancelButton(0); + progress.show(); + QApplication::processEvents(); + + // Phase 1: Copy wallet.dat FIRST (most critical file) + fs::path walletSrc = srcDir / "wallet.dat"; + fs::path walletDst = dstDir / "wallet.dat"; + if (fs::exists(walletSrc)) { + progress.setLabelText("Copying wallet.dat..."); + QApplication::processEvents(); + try { + // Copy to temp name first, then rename for atomicity + fs::path walletTmp = dstDir / "wallet.dat.migrating"; + fs::copy_file(walletSrc, walletTmp, fs::copy_option::overwrite_if_exists); + + // Verify copy by checking file size + if (fs::file_size(walletTmp) != fs::file_size(walletSrc)) { + fs::remove(walletTmp); + printf("Migration: wallet.dat copy size mismatch!\n"); + return false; + } + + // Rename into place + if (fs::exists(walletDst)) + fs::remove(walletDst); + fs::rename(walletTmp, walletDst); + } catch (const fs::filesystem_error& e) { + printf("Migration: Failed to copy wallet.dat: %s\n", e.what()); + return false; // Abort - wallet is critical + } + } + + // Phase 2: Copy everything else + int filesCopied = 0; + try { + for (fs::directory_iterator it(srcDir), end; it != end; ++it) { + std::string filename = it->path().filename().string(); + + // Skip special files + if (skipFiles.count(filename)) + continue; + + // Skip wallet.dat (already copied) + if (filename == "wallet.dat") + continue; + + fs::path dst = dstDir / filename; + + progress.setLabelText(QString("Copying %1...").arg(QString::fromStdString(filename))); + QApplication::processEvents(); + + if (fs::is_directory(it->path())) { + copyDirectoryRecursive(it->path(), dst); + } else { + fs::copy_file(it->path(), dst, fs::copy_option::overwrite_if_exists); + } + filesCopied++; + } + } catch (const fs::filesystem_error& e) { + // Non-wallet copy failure: log but don't abort + // Chain data can be re-synced; wallet was already safely copied + printf("Migration: Warning: failed to copy some files: %s\n", e.what()); + } + + // Phase 3: Rename old wallet.dat as safety backup (don't delete old dir) + try { + if (fs::exists(walletSrc)) { + fs::rename(walletSrc, srcDir / "wallet.dat.bak-migrated"); + } + } catch (...) { + // Not critical + } + + progress.close(); + printf("Migration: Successfully copied %d items from %s to %s\n", + filesCopied, srcDir.string().c_str(), dstDir.string().c_str()); + return true; +} diff --git a/src/qt/introdialog.h b/src/qt/introdialog.h index 672ddcd..f7d193e 100644 --- a/src/qt/introdialog.h +++ b/src/qt/introdialog.h @@ -25,6 +25,13 @@ public: */ static bool pickDataDirectory(); + /** + * Migrate data directory contents from oldPath to newPath. + * Returns true on success, false on failure. + * Shows a progress dialog during the copy. + */ + static bool migrateDataDirectory(const QString& oldPath, const QString& newPath); + private slots: void on_browseButton_clicked(); void on_defaultRadio_toggled(bool checked); diff --git a/src/qt/optionsdialog.cpp b/src/qt/optionsdialog.cpp index a841663..8b512df 100644 --- a/src/qt/optionsdialog.cpp +++ b/src/qt/optionsdialog.cpp @@ -7,12 +7,21 @@ #include "optionsmodel.h" #include "dialog_move_handler.h" +#include "init.h" +#include "util.h" + +#include + #include +#include +#include #include #include #include +#include #include #include +#include OptionsDialog::OptionsDialog(QWidget *parent) : QDialog(parent), @@ -21,12 +30,54 @@ OptionsDialog::OptionsDialog(QWidget *parent) : mapper(0), fRestartWarningDisplayed_Proxy(false), fRestartWarningDisplayed_Lang(false), - fProxyIpValid(true) + fProxyIpValid(true), + dataDirPath(0), + dataDirFreeSpaceLabel(0) { ui->setupUi(this); setWindowFlags(Qt::CustomizeWindowHint | Qt::FramelessWindowHint | Qt::Window); ui->wCaption->installEventFilter(new DialogMoveHandler(this)); + /* Data Directory section in Main tab */ + m_currentDataDir = QString::fromStdString(GetDataDir(false).string()); + m_pendingDataDir.clear(); + + QGroupBox *groupDataDir = new QGroupBox(tr("Data Directory"), this); + groupDataDir->setStyleSheet( + "QGroupBox { border: 1px solid #61280E; margin-top: 8px; padding-top: 16px; color: #f26522; }" + "QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 3px; }"); + + QVBoxLayout *dataDirLayout = new QVBoxLayout(groupDataDir); + + QHBoxLayout *dataDirPathLayout = new QHBoxLayout(); + dataDirPath = new QLineEdit(m_currentDataDir, groupDataDir); + dataDirPath->setReadOnly(true); + dataDirPath->setStyleSheet("QLineEdit { background-color: #1c1c1c; border: 1px solid #f26522; color: #f26522; padding: 2px; }"); + + QPushButton *dataDirBrowseButton = new QPushButton(tr("Browse..."), groupDataDir); + dataDirBrowseButton->setStyleSheet( + "QPushButton { background-color: #000; color: #f26522; border: 1px solid #f26522; padding: 2px 12px; min-height: 20px; }" + "QPushButton:hover { background-color: #61280E; }" + "QPushButton:pressed:flat { color: #000; background-color: #f26522; }"); + + dataDirPathLayout->addWidget(dataDirPath); + dataDirPathLayout->addWidget(dataDirBrowseButton); + dataDirLayout->addLayout(dataDirPathLayout); + + dataDirFreeSpaceLabel = new QLabel(groupDataDir); + dataDirFreeSpaceLabel->setStyleSheet("color: #999; font-size: 11px;"); + dataDirLayout->addWidget(dataDirFreeSpaceLabel); + + // Insert into Main tab layout, before the vertical spacer (last item) + QVBoxLayout *mainTabLayout = qobject_cast(ui->tabWidget->widget(0)->layout()); + if (mainTabLayout) { + int spacerIndex = mainTabLayout->count() - 1; // vertical spacer is last + mainTabLayout->insertWidget(spacerIndex, groupDataDir); + } + + connect(dataDirBrowseButton, SIGNAL(clicked()), this, SLOT(on_dataDirBrowseButton_clicked())); + updateDataDirFreeSpace(); + /* Network elements init */ #ifndef USE_UPNP ui->mapPortUpnp->setEnabled(false); @@ -188,6 +239,8 @@ void OptionsDialog::setSaveButtonState(bool fState) void OptionsDialog::on_okButton_clicked() { mapper->submit(); + if (handleDataDirChange()) + return; // restart flow handles closing accept(); } @@ -199,6 +252,7 @@ void OptionsDialog::on_cancelButton_clicked() void OptionsDialog::on_applyButton_clicked() { mapper->submit(); + handleDataDirChange(); disableApplyButton(); } @@ -303,3 +357,150 @@ bool OptionsDialog::eventFilter(QObject *object, QEvent *event) } return QDialog::eventFilter(object, event); } + +void OptionsDialog::on_dataDirBrowseButton_clicked() +{ + QString dir = QFileDialog::getExistingDirectory( + this, tr("Choose data directory"), m_currentDataDir); + if (!dir.isEmpty() && dir != m_currentDataDir) + { + m_pendingDataDir = dir; + dataDirPath->setText(dir); + updateDataDirFreeSpace(); + enableApplyButton(); + } +} + +void OptionsDialog::updateDataDirFreeSpace() +{ + namespace fs = boost::filesystem; + QString path = dataDirPath->text(); + fs::path fsPath(path.toStdString()); + try { + while (!fsPath.empty() && !fs::exists(fsPath)) + fsPath = fsPath.parent_path(); + if (!fsPath.empty()) { + fs::space_info si = fs::space(fsPath); + double freeGB = (double)si.available / (1024.0 * 1024.0 * 1024.0); + dataDirFreeSpaceLabel->setText( + tr("Free space: %1 GB").arg(QString::number(freeGB, 'f', 2))); + } else { + dataDirFreeSpaceLabel->setText(tr("Cannot determine free space")); + } + } catch (const fs::filesystem_error &) { + dataDirFreeSpaceLabel->setText(tr("Cannot determine free space")); + } +} + +quint64 OptionsDialog::calculateDirSize(const QString& path) +{ + namespace fs = boost::filesystem; + quint64 totalSize = 0; + try { + for (fs::recursive_directory_iterator it(path.toStdString()), end; it != end; ++it) { + if (fs::is_regular_file(*it)) + totalSize += fs::file_size(*it); + } + } catch (...) {} + return totalSize; +} + +bool OptionsDialog::handleDataDirChange() +{ + if (m_pendingDataDir.isEmpty() || m_pendingDataDir == m_currentDataDir) + return false; + + namespace fs = boost::filesystem; + fs::path destPath(m_pendingDataDir.toStdString()); + + // Check destination is writable + try { + fs::create_directories(destPath); + } catch (const fs::filesystem_error& e) { + QMessageBox::critical(this, tr("Error"), + tr("Cannot create directory: %1").arg(QString::fromStdString(e.what()))); + m_pendingDataDir.clear(); + dataDirPath->setText(m_currentDataDir); + updateDataDirFreeSpace(); + return false; + } + + // Check free space vs current data dir size + quint64 dataDirSize = calculateDirSize(m_currentDataDir); + try { + fs::space_info si = fs::space(destPath); + quint64 required = dataDirSize + (dataDirSize / 10); // 10% headroom + if (si.available < required) { + QMessageBox::critical(this, tr("Insufficient Space"), + tr("The destination has %1 MB free but the data directory requires approximately %2 MB.") + .arg(si.available / (1024*1024)) + .arg(required / (1024*1024))); + m_pendingDataDir.clear(); + dataDirPath->setText(m_currentDataDir); + updateDataDirFreeSpace(); + return false; + } + } catch (const fs::filesystem_error&) { + // If we can't check space, proceed anyway + } + + // Save migration state to QSettings + QSettings settings; + settings.setValue("strDataDirPrevious", m_currentDataDir); + settings.setValue("strDataDir", m_pendingDataDir); + settings.setValue("fPendingDataDirMigration", true); + + // Ask about restart + QMessageBox msgBox(this); + msgBox.setWindowFlags(Qt::FramelessWindowHint); + msgBox.setWindowTitle(tr("Data Directory Changed")); + msgBox.setText(tr("The data directory will be moved from:\n%1\n\nTo:\n%2\n\n" + "This will happen when the wallet restarts.") + .arg(m_currentDataDir).arg(m_pendingDataDir)); + msgBox.setIcon(QMessageBox::Information); + msgBox.setIconPixmap(QPixmap(":/msgbox/information")); + msgBox.setStyleSheet("QMessageBox { border: 2px solid #f26522; background-color: #000; color: #f26522; }"); + + QPushButton *restartBtn = msgBox.addButton(tr("Restart Now"), QMessageBox::AcceptRole); + QPushButton *laterBtn = msgBox.addButton(tr("Later"), QMessageBox::RejectRole); + + QString btnStyle = + "QPushButton { background-color: #000; color: #f26522; border: 1px solid #f26522; " + "min-width: 120px; max-width: 120px; max-height: 20px; min-height: 20px; }" + "QPushButton:hover { background-color: #61280E; }" + "QPushButton:pressed:flat { color: #000; background-color: #f26522; }"; + restartBtn->setStyleSheet(btnStyle); + laterBtn->setStyleSheet(btnStyle); + + msgBox.exec(); + + if (msgBox.clickedButton() == restartBtn) { + performRestart(); + return true; + } + return false; +} + +void OptionsDialog::performRestart() +{ + // Launch a new instance of ourselves + QString exePath = QApplication::applicationFilePath(); + QStringList args = QApplication::arguments(); + args.removeFirst(); // remove argv[0] + + // Remove any existing -datadir argument so the new instance + // reads strDataDir from QSettings and performs migration + QMutableStringListIterator it(args); + while (it.hasNext()) { + QString arg = it.next(); + if (arg.startsWith("-datadir") || arg.startsWith("/datadir")) + it.remove(); + } + + // Start new process detached so it survives our shutdown + QProcess::startDetached(exePath, args); + + // Close dialog and trigger wallet shutdown + accept(); + StartShutdown(); +} diff --git a/src/qt/optionsdialog.h b/src/qt/optionsdialog.h index 2a58457..7eb457d 100644 --- a/src/qt/optionsdialog.h +++ b/src/qt/optionsdialog.h @@ -3,6 +3,9 @@ #include +class QLineEdit; +class QLabel; + namespace Ui { class OptionsDialog; } @@ -45,17 +48,29 @@ private slots: void updateDisplayUnit(); void handleProxyIpValid(QValidatedLineEdit *object, bool fState); void applyTorDefaults(bool enabled); + void on_dataDirBrowseButton_clicked(); + void updateDataDirFreeSpace(); signals: void proxyIpValid(QValidatedLineEdit *object, bool fValid); private: + bool handleDataDirChange(); + void performRestart(); + quint64 calculateDirSize(const QString& path); + Ui::OptionsDialog *ui; OptionsModel *model; MonitoredDataMapper *mapper; bool fRestartWarningDisplayed_Proxy; bool fRestartWarningDisplayed_Lang; bool fProxyIpValid; + + // Data directory widgets (built programmatically) + QLineEdit *dataDirPath; + QLabel *dataDirFreeSpaceLabel; + QString m_currentDataDir; + QString m_pendingDataDir; }; #endif // OPTIONSDIALOG_H