Compare commits

...

4 Commits

Author SHA1 Message Date
sami7777 c74c92c542 Fix Boost filesystem API for modern Boost (copy_options)
Build All Platforms / test-linux-unit (push) Waiting to run
Build All Platforms / build-windows-qt (push) Waiting to run
Build All Platforms / build-windows-daemon (push) Waiting to run
Build All Platforms / build-linux-qt (push) Waiting to run
Build All Platforms / build-linux-daemon (push) Waiting to run
Build All Platforms / build-macos (push) Waiting to run
Build All Platforms / release (push) Blocked by required conditions
copy_option::overwrite_if_exists was removed in Boost 1.90+,
replaced with copy_options::overwrite_existing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 12:49:17 -07:00
sami7777 97ae675f0a Bump version to v5.4.3
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 12:40:33 -07:00
sami7777 b0b591364f Raise MAX_MONEY from 222222 to 2222222
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 12:38:22 -07:00
sami7777 bf257858a4 Add data directory change feature to Options dialog
Adds a "Data Directory" section to Options > Main tab that lets users
browse for a new data directory. On confirmation, files are automatically
migrated to the new location on restart (wallet.dat copied first with
atomic rename for safety). Supports "Restart Now" or "Later" workflow.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 12:38:15 -07:00
6 changed files with 388 additions and 3 deletions
+1 -1
View File
@@ -8,7 +8,7 @@
// These need to be macros, as version.cpp's and triangles-qt.rc's voodoo requires it
#define CLIENT_VERSION_MAJOR 5
#define CLIENT_VERSION_MINOR 4
#define CLIENT_VERSION_REVISION 2
#define CLIENT_VERSION_REVISION 3
#define CLIENT_VERSION_BUILD 0
// Converts the parameter X to a string after macro replacement on X has been performed.
+1 -1
View File
@@ -40,7 +40,7 @@ static const unsigned int MAX_ORPHAN_BLOCKS_IBD = 4000;
static const unsigned int MAX_INV_SZ = 50000;
static const int64_t MIN_TX_FEE = (1 * CENT) / 100;
static const int64_t MIN_RELAY_TX_FEE = (1 * CENT) / 100;
static const int64_t MAX_MONEY = 222222 * COIN;
static const int64_t MAX_MONEY = 2222222 * COIN;
static const int64_t COIN_YEAR_REWARD = 33 * CENT; // 33% per year
static const int64_t MAX_TRI_PROOF_OF_STAKE = 0.33 * COIN;
static const int MODIFIER_INTERVAL_SWITCH = 1;
+162
View File
@@ -16,6 +16,8 @@
#include <boost/filesystem.hpp>
#include <set>
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_options::overwrite_existing);
}
}
}
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<std::string> 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_options::overwrite_existing);
// 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_options::overwrite_existing);
}
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;
}
+7
View File
@@ -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);
+202 -1
View File
@@ -7,12 +7,21 @@
#include "optionsmodel.h"
#include "dialog_move_handler.h"
#include "init.h"
#include "util.h"
#include <boost/filesystem.hpp>
#include <QDir>
#include <QFileDialog>
#include <QGroupBox>
#include <QIntValidator>
#include <QLocale>
#include <QMessageBox>
#include <QProcess>
#include <QRegExp>
#include <QRegExpValidator>
#include <QSettings>
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<QVBoxLayout*>(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();
}
+15
View File
@@ -3,6 +3,9 @@
#include <QDialog>
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