1 // Copyright (c) 2011-2018 The Bitcoin Core developers
2 // Distributed under the MIT software license, see the accompanying
3 // file COPYING or http://www.opensource.org/licenses/mit-license.php.
4
5 #if defined(HAVE_CONFIG_H)
6 #include <config/bitcoin-config.h>
7 #endif
8
9 #include <fs.h>
10 #include <qt/intro.h>
11 #include <qt/forms/ui_intro.h>
12
13 #include <qt/guiconstants.h>
14 #include <qt/guiutil.h>
15
16 #include <interfaces/node.h>
17 #include <util/system.h>
18
19 #include <QFileDialog>
20 #include <QSettings>
21 #include <QMessageBox>
22
23 #include <cmath>
24
25 /* Total required space (in GB) depending on user choice (prune, not prune) */
26 static uint64_t requiredSpace;
27
28 /* Check free space asynchronously to prevent hanging the UI thread.
29
30 Up to one request to check a path is in flight to this thread; when the check()
31 function runs, the current path is requested from the associated Intro object.
32 The reply is sent back through a signal.
33
34 This ensures that no queue of checking requests is built up while the user is
35 still entering the path, and that always the most recently entered path is checked as
36 soon as the thread becomes available.
37 */
38 class FreespaceChecker : public QObject
39 {
40 Q_OBJECT
41
42 public:
43 explicit FreespaceChecker(Intro *intro);
44
45 enum Status {
46 ST_OK,
47 ST_ERROR
48 };
49
50 public Q_SLOTS:
51 void check();
52
53 Q_SIGNALS:
54 void reply(int status, const QString &message, quint64 available);
55
56 private:
57 Intro *intro;
58 };
59
60 #include <qt/intro.moc>
61
FreespaceChecker(Intro * _intro)62 FreespaceChecker::FreespaceChecker(Intro *_intro)
63 {
64 this->intro = _intro;
65 }
66
check()67 void FreespaceChecker::check()
68 {
69 QString dataDirStr = intro->getPathToCheck();
70 fs::path dataDir = GUIUtil::qstringToBoostPath(dataDirStr);
71 uint64_t freeBytesAvailable = 0;
72 int replyStatus = ST_OK;
73 QString replyMessage = tr("A new data directory will be created.");
74
75 /* Find first parent that exists, so that fs::space does not fail */
76 fs::path parentDir = dataDir;
77 fs::path parentDirOld = fs::path();
78 while(parentDir.has_parent_path() && !fs::exists(parentDir))
79 {
80 parentDir = parentDir.parent_path();
81
82 /* Check if we make any progress, break if not to prevent an infinite loop here */
83 if (parentDirOld == parentDir)
84 break;
85
86 parentDirOld = parentDir;
87 }
88
89 try {
90 freeBytesAvailable = fs::space(parentDir).available;
91 if(fs::exists(dataDir))
92 {
93 if(fs::is_directory(dataDir))
94 {
95 QString separator = "<code>" + QDir::toNativeSeparators("/") + tr("name") + "</code>";
96 replyStatus = ST_OK;
97 replyMessage = tr("Directory already exists. Add %1 if you intend to create a new directory here.").arg(separator);
98 } else {
99 replyStatus = ST_ERROR;
100 replyMessage = tr("Path already exists, and is not a directory.");
101 }
102 }
103 } catch (const fs::filesystem_error&)
104 {
105 /* Parent directory does not exist or is not accessible */
106 replyStatus = ST_ERROR;
107 replyMessage = tr("Cannot create data directory here.");
108 }
109 Q_EMIT reply(replyStatus, replyMessage, freeBytesAvailable);
110 }
111
112
Intro(QWidget * parent,uint64_t blockchain_size,uint64_t chain_state_size)113 Intro::Intro(QWidget *parent, uint64_t blockchain_size, uint64_t chain_state_size) :
114 QDialog(parent),
115 ui(new Ui::Intro),
116 thread(nullptr),
117 signalled(false),
118 m_blockchain_size(blockchain_size),
119 m_chain_state_size(chain_state_size)
120 {
121 ui->setupUi(this);
122 ui->welcomeLabel->setText(ui->welcomeLabel->text().arg(PACKAGE_NAME));
123 ui->storageLabel->setText(ui->storageLabel->text().arg(PACKAGE_NAME));
124
125 ui->lblExplanation1->setText(ui->lblExplanation1->text()
126 .arg(PACKAGE_NAME)
127 .arg(m_blockchain_size)
128 .arg(2011)
129 .arg(tr("Litecoin"))
130 );
131 ui->lblExplanation2->setText(ui->lblExplanation2->text().arg(PACKAGE_NAME));
132
133 uint64_t pruneTarget = std::max<int64_t>(0, gArgs.GetArg("-prune", 0));
134 requiredSpace = m_blockchain_size;
135 QString storageRequiresMsg = tr("At least %1 GB of data will be stored in this directory, and it will grow over time.");
136 if (pruneTarget) {
137 uint64_t prunedGBs = std::ceil(pruneTarget * 1024 * 1024.0 / GB_BYTES);
138 if (prunedGBs <= requiredSpace) {
139 requiredSpace = prunedGBs;
140 storageRequiresMsg = tr("Approximately %1 GB of data will be stored in this directory.");
141 }
142 ui->lblExplanation3->setVisible(true);
143 } else {
144 ui->lblExplanation3->setVisible(false);
145 }
146 requiredSpace += m_chain_state_size;
147 ui->sizeWarningLabel->setText(
148 tr("%1 will download and store a copy of the Litecoin block chain.").arg(PACKAGE_NAME) + " " +
149 storageRequiresMsg.arg(requiredSpace) + " " +
150 tr("The wallet will also be stored in this directory.")
151 );
152 startThread();
153 }
154
~Intro()155 Intro::~Intro()
156 {
157 delete ui;
158 /* Ensure thread is finished before it is deleted */
159 thread->quit();
160 thread->wait();
161 }
162
getDataDirectory()163 QString Intro::getDataDirectory()
164 {
165 return ui->dataDirectory->text();
166 }
167
setDataDirectory(const QString & dataDir)168 void Intro::setDataDirectory(const QString &dataDir)
169 {
170 ui->dataDirectory->setText(dataDir);
171 if(dataDir == getDefaultDataDirectory())
172 {
173 ui->dataDirDefault->setChecked(true);
174 ui->dataDirectory->setEnabled(false);
175 ui->ellipsisButton->setEnabled(false);
176 } else {
177 ui->dataDirCustom->setChecked(true);
178 ui->dataDirectory->setEnabled(true);
179 ui->ellipsisButton->setEnabled(true);
180 }
181 }
182
getDefaultDataDirectory()183 QString Intro::getDefaultDataDirectory()
184 {
185 return GUIUtil::boostPathToQString(GetDefaultDataDir());
186 }
187
pickDataDirectory(interfaces::Node & node)188 bool Intro::pickDataDirectory(interfaces::Node& node)
189 {
190 QSettings settings;
191 /* If data directory provided on command line, no need to look at settings
192 or show a picking dialog */
193 if(!gArgs.GetArg("-datadir", "").empty())
194 return true;
195 /* 1) Default data directory for operating system */
196 QString dataDir = getDefaultDataDirectory();
197 /* 2) Allow QSettings to override default dir */
198 dataDir = settings.value("strDataDir", dataDir).toString();
199
200 if(!fs::exists(GUIUtil::qstringToBoostPath(dataDir)) || gArgs.GetBoolArg("-choosedatadir", DEFAULT_CHOOSE_DATADIR) || settings.value("fReset", false).toBool() || gArgs.GetBoolArg("-resetguisettings", false))
201 {
202 /* Use selectParams here to guarantee Params() can be used by node interface */
203 try {
204 node.selectParams(gArgs.GetChainName());
205 } catch (const std::exception&) {
206 return false;
207 }
208
209 /* If current default data directory does not exist, let the user choose one */
210 Intro intro(0, node.getAssumedBlockchainSize(), node.getAssumedChainStateSize());
211 intro.setDataDirectory(dataDir);
212 intro.setWindowIcon(QIcon(":icons/bitcoin"));
213
214 while(true)
215 {
216 if(!intro.exec())
217 {
218 /* Cancel clicked */
219 return false;
220 }
221 dataDir = intro.getDataDirectory();
222 try {
223 if (TryCreateDirectories(GUIUtil::qstringToBoostPath(dataDir))) {
224 // If a new data directory has been created, make wallets subdirectory too
225 TryCreateDirectories(GUIUtil::qstringToBoostPath(dataDir) / "wallets");
226 }
227 break;
228 } catch (const fs::filesystem_error&) {
229 QMessageBox::critical(nullptr, PACKAGE_NAME,
230 tr("Error: Specified data directory \"%1\" cannot be created.").arg(dataDir));
231 /* fall through, back to choosing screen */
232 }
233 }
234
235 settings.setValue("strDataDir", dataDir);
236 settings.setValue("fReset", false);
237 }
238 /* Only override -datadir if different from the default, to make it possible to
239 * override -datadir in the bitcoin.conf file in the default data directory
240 * (to be consistent with bitcoind behavior)
241 */
242 if(dataDir != getDefaultDataDirectory()) {
243 node.softSetArg("-datadir", GUIUtil::qstringToBoostPath(dataDir).string()); // use OS locale for path setting
244 }
245 return true;
246 }
247
setStatus(int status,const QString & message,quint64 bytesAvailable)248 void Intro::setStatus(int status, const QString &message, quint64 bytesAvailable)
249 {
250 switch(status)
251 {
252 case FreespaceChecker::ST_OK:
253 ui->errorMessage->setText(message);
254 ui->errorMessage->setStyleSheet("");
255 break;
256 case FreespaceChecker::ST_ERROR:
257 ui->errorMessage->setText(tr("Error") + ": " + message);
258 ui->errorMessage->setStyleSheet("QLabel { color: #800000 }");
259 break;
260 }
261 /* Indicate number of bytes available */
262 if(status == FreespaceChecker::ST_ERROR)
263 {
264 ui->freeSpace->setText("");
265 } else {
266 QString freeString = tr("%n GB of free space available", "", bytesAvailable/GB_BYTES);
267 if(bytesAvailable < requiredSpace * GB_BYTES)
268 {
269 freeString += " " + tr("(of %n GB needed)", "", requiredSpace);
270 ui->freeSpace->setStyleSheet("QLabel { color: #800000 }");
271 } else {
272 ui->freeSpace->setStyleSheet("");
273 }
274 ui->freeSpace->setText(freeString + ".");
275 }
276 /* Don't allow confirm in ERROR state */
277 ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(status != FreespaceChecker::ST_ERROR);
278 }
279
on_dataDirectory_textChanged(const QString & dataDirStr)280 void Intro::on_dataDirectory_textChanged(const QString &dataDirStr)
281 {
282 /* Disable OK button until check result comes in */
283 ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
284 checkPath(dataDirStr);
285 }
286
on_ellipsisButton_clicked()287 void Intro::on_ellipsisButton_clicked()
288 {
289 QString dir = QDir::toNativeSeparators(QFileDialog::getExistingDirectory(nullptr, "Choose data directory", ui->dataDirectory->text()));
290 if(!dir.isEmpty())
291 ui->dataDirectory->setText(dir);
292 }
293
on_dataDirDefault_clicked()294 void Intro::on_dataDirDefault_clicked()
295 {
296 setDataDirectory(getDefaultDataDirectory());
297 }
298
on_dataDirCustom_clicked()299 void Intro::on_dataDirCustom_clicked()
300 {
301 ui->dataDirectory->setEnabled(true);
302 ui->ellipsisButton->setEnabled(true);
303 }
304
startThread()305 void Intro::startThread()
306 {
307 thread = new QThread(this);
308 FreespaceChecker *executor = new FreespaceChecker(this);
309 executor->moveToThread(thread);
310
311 connect(executor, &FreespaceChecker::reply, this, &Intro::setStatus);
312 connect(this, &Intro::requestCheck, executor, &FreespaceChecker::check);
313 /* make sure executor object is deleted in its own thread */
314 connect(thread, &QThread::finished, executor, &QObject::deleteLater);
315
316 thread->start();
317 }
318
checkPath(const QString & dataDir)319 void Intro::checkPath(const QString &dataDir)
320 {
321 mutex.lock();
322 pathToCheck = dataDir;
323 if(!signalled)
324 {
325 signalled = true;
326 Q_EMIT requestCheck();
327 }
328 mutex.unlock();
329 }
330
getPathToCheck()331 QString Intro::getPathToCheck()
332 {
333 QString retval;
334 mutex.lock();
335 retval = pathToCheck;
336 signalled = false; /* new request can be queued now */
337 mutex.unlock();
338 return retval;
339 }
340