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