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