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