2 * Bittorrent Client using Qt and libtorrent.
3 * Copyright (C) 2015-2022 Vladimir Golovnev <glassez@yandex.ru>
5 * This program is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU General Public License
7 * as published by the Free Software Foundation; either version 2
8 * of the License, or (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 * In addition, as a special exception, the copyright holders give permission to
20 * link this program with the OpenSSL project's "OpenSSL" library (or with
21 * modified versions of it that use the same license as the "OpenSSL" library),
22 * and distribute the linked executables. You must obey the GNU General Public
23 * License in all respects for all of the code used other than "OpenSSL". If you
24 * modify file(s), you may extend this exception to your version of the file(s),
25 * but you are not obligated to do so. If you do not wish to do so, delete this
26 * exception statement from your version.
29 #include "bencoderesumedatastorage.h"
31 #include <libtorrent/bdecode.hpp>
32 #include <libtorrent/entry.hpp>
33 #include <libtorrent/read_resume_data.hpp>
34 #include <libtorrent/torrent_info.hpp>
35 #include <libtorrent/write_resume_data.hpp>
40 #include <QRegularExpression>
43 #include "base/exceptions.h"
44 #include "base/global.h"
45 #include "base/logger.h"
46 #include "base/preferences.h"
47 #include "base/profile.h"
48 #include "base/tagset.h"
49 #include "base/utils/fs.h"
50 #include "base/utils/io.h"
51 #include "base/utils/sslkey.h"
52 #include "base/utils/string.h"
54 #include "loadtorrentparams.h"
58 class BencodeResumeDataStorage::Worker final
: public QObject
60 Q_DISABLE_COPY_MOVE(Worker
)
63 explicit Worker(const Path
&resumeDataDir
);
65 void store(const TorrentID
&id
, const LoadTorrentParams
&resumeData
) const;
66 void remove(const TorrentID
&id
) const;
67 void storeQueue(const QList
<TorrentID
> &queue
) const;
70 const Path m_resumeDataDir
;
76 const char KEY_SSL_CERTIFICATE
[] = "qBt-sslCertificate";
77 const char KEY_SSL_PRIVATE_KEY
[] = "qBt-sslPrivateKey";
78 const char KEY_SSL_DH_PARAMS
[] = "qBt-sslDhParams";
80 template <typename LTStr
>
81 QString
fromLTString(const LTStr
&str
)
83 return QString::fromUtf8(str
.data(), static_cast<qsizetype
>(str
.size()));
86 template <typename LTStr
>
87 QByteArray
toByteArray(const LTStr
&str
)
89 return {str
.data(), static_cast<qsizetype
>(str
.size())};
92 using ListType
= lt::entry::list_type
;
94 ListType
setToEntryList(const TagSet
&input
)
97 entryList
.reserve(input
.size());
98 for (const Tag
&setValue
: input
)
99 entryList
.emplace_back(setValue
.toString().toStdString());
104 BitTorrent::BencodeResumeDataStorage::BencodeResumeDataStorage(const Path
&path
, QObject
*parent
)
105 : ResumeDataStorage(path
, parent
)
106 , m_ioThread
{new QThread
}
107 , m_asyncWorker
{new Worker(path
)}
109 Q_ASSERT(path
.isAbsolute());
111 if (!path
.exists() && !Utils::Fs::mkpath(path
))
113 throw RuntimeError(tr("Cannot create torrent resume folder: \"%1\"")
114 .arg(path
.toString()));
117 const QRegularExpression filenamePattern
{u
"^([A-Fa-f0-9]{40})\\.fastresume$"_s
};
118 const QStringList filenames
= QDir(path
.data()).entryList({u
"*.fastresume"_s
}, QDir::Files
);
120 m_registeredTorrents
.reserve(filenames
.size());
121 for (const QString
&filename
: filenames
)
123 const QRegularExpressionMatch rxMatch
= filenamePattern
.match(filename
);
124 if (rxMatch
.hasMatch())
125 m_registeredTorrents
.append(TorrentID::fromString(rxMatch
.captured(1)));
128 loadQueue(path
/ Path(u
"queue"_s
));
130 qDebug() << "Registered torrents count: " << m_registeredTorrents
.size();
132 m_asyncWorker
->moveToThread(m_ioThread
.get());
133 connect(m_ioThread
.get(), &QThread::finished
, m_asyncWorker
, &QObject::deleteLater
);
137 QList
<BitTorrent::TorrentID
> BitTorrent::BencodeResumeDataStorage::registeredTorrents() const
139 return m_registeredTorrents
;
142 BitTorrent::LoadResumeDataResult
BitTorrent::BencodeResumeDataStorage::load(const TorrentID
&id
) const
144 const QString idString
= id
.toString();
145 const Path fastresumePath
= path() / Path(idString
+ u
".fastresume");
146 const Path torrentFilePath
= path() / Path(idString
+ u
".torrent");
147 const qint64 torrentSizeLimit
= Preferences::instance()->getTorrentFileSizeLimit();
149 const auto resumeDataReadResult
= Utils::IO::readFile(fastresumePath
, torrentSizeLimit
);
150 if (!resumeDataReadResult
)
151 return nonstd::make_unexpected(resumeDataReadResult
.error().message
);
153 const auto metadataReadResult
= Utils::IO::readFile(torrentFilePath
, torrentSizeLimit
);
154 if (!metadataReadResult
)
156 if (metadataReadResult
.error().status
!= Utils::IO::ReadError::NotExist
)
157 return nonstd::make_unexpected(metadataReadResult
.error().message
);
160 const QByteArray data
= resumeDataReadResult
.value();
161 const QByteArray metadata
= metadataReadResult
.value_or(QByteArray());
162 return loadTorrentResumeData(data
, metadata
);
165 void BitTorrent::BencodeResumeDataStorage::doLoadAll() const
167 qDebug() << "Loading torrents count: " << m_registeredTorrents
.size();
169 emit
const_cast<BencodeResumeDataStorage
*>(this)->loadStarted(m_registeredTorrents
);
171 for (const TorrentID
&torrentID
: asConst(m_registeredTorrents
))
172 onResumeDataLoaded(torrentID
, load(torrentID
));
174 emit
const_cast<BencodeResumeDataStorage
*>(this)->loadFinished();
177 void BitTorrent::BencodeResumeDataStorage::loadQueue(const Path
&queueFilename
)
179 const int lineMaxLength
= 48;
181 QFile queueFile
{queueFilename
.data()};
182 if (!queueFile
.exists())
185 if (!queueFile
.open(QFile::ReadOnly
))
187 LogMsg(tr("Couldn't load torrents queue: %1").arg(queueFile
.errorString()), Log::WARNING
);
191 const QRegularExpression hashPattern
{u
"^([A-Fa-f0-9]{40})$"_s
};
195 const auto line
= QString::fromLatin1(queueFile
.readLine(lineMaxLength
).trimmed());
199 const QRegularExpressionMatch rxMatch
= hashPattern
.match(line
);
200 if (rxMatch
.hasMatch())
202 const auto torrentID
= BitTorrent::TorrentID::fromString(rxMatch
.captured(1));
203 const int pos
= m_registeredTorrents
.indexOf(torrentID
, start
);
206 std::swap(m_registeredTorrents
[start
], m_registeredTorrents
[pos
]);
213 BitTorrent::LoadResumeDataResult
BitTorrent::BencodeResumeDataStorage::loadTorrentResumeData(const QByteArray
&data
, const QByteArray
&metadata
) const
215 const auto *pref
= Preferences::instance();
218 const lt::bdecode_node resumeDataRoot
= lt::bdecode(data
, ec
219 , nullptr, pref
->getBdecodeDepthLimit(), pref
->getBdecodeTokenLimit());
221 return nonstd::make_unexpected(tr("Cannot parse resume data: %1").arg(QString::fromStdString(ec
.message())));
223 if (resumeDataRoot
.type() != lt::bdecode_node::dict_t
)
224 return nonstd::make_unexpected(tr("Cannot parse resume data: invalid format"));
226 LoadTorrentParams torrentParams
;
227 torrentParams
.category
= fromLTString(resumeDataRoot
.dict_find_string_value("qBt-category"));
228 torrentParams
.name
= fromLTString(resumeDataRoot
.dict_find_string_value("qBt-name"));
229 torrentParams
.hasFinishedStatus
= resumeDataRoot
.dict_find_int_value("qBt-seedStatus");
230 torrentParams
.firstLastPiecePriority
= resumeDataRoot
.dict_find_int_value("qBt-firstLastPiecePriority");
231 torrentParams
.seedingTimeLimit
= resumeDataRoot
.dict_find_int_value("qBt-seedingTimeLimit", Torrent::USE_GLOBAL_SEEDING_TIME
);
232 torrentParams
.inactiveSeedingTimeLimit
= resumeDataRoot
.dict_find_int_value("qBt-inactiveSeedingTimeLimit", Torrent::USE_GLOBAL_INACTIVE_SEEDING_TIME
);
233 torrentParams
.shareLimitAction
= Utils::String::toEnum(
234 fromLTString(resumeDataRoot
.dict_find_string_value("qBt-shareLimitAction")), ShareLimitAction::Default
);
236 torrentParams
.savePath
= Profile::instance()->fromPortablePath(
237 Path(fromLTString(resumeDataRoot
.dict_find_string_value("qBt-savePath"))));
238 torrentParams
.useAutoTMM
= torrentParams
.savePath
.isEmpty();
239 if (!torrentParams
.useAutoTMM
)
241 torrentParams
.downloadPath
= Profile::instance()->fromPortablePath(
242 Path(fromLTString(resumeDataRoot
.dict_find_string_value("qBt-downloadPath"))));
245 // TODO: The following code is deprecated. Replace with the commented one after several releases in 4.4.x.
246 // === BEGIN DEPRECATED CODE === //
247 const lt::bdecode_node contentLayoutNode
= resumeDataRoot
.dict_find("qBt-contentLayout");
248 if (contentLayoutNode
.type() == lt::bdecode_node::string_t
)
250 const QString contentLayoutStr
= fromLTString(contentLayoutNode
.string_value());
251 torrentParams
.contentLayout
= Utils::String::toEnum(contentLayoutStr
, TorrentContentLayout::Original
);
255 const bool hasRootFolder
= resumeDataRoot
.dict_find_int_value("qBt-hasRootFolder");
256 torrentParams
.contentLayout
= (hasRootFolder
? TorrentContentLayout::Original
: TorrentContentLayout::NoSubfolder
);
258 // === END DEPRECATED CODE === //
259 // === BEGIN REPLACEMENT CODE === //
260 // torrentParams.contentLayout = Utils::String::parse(
261 // fromLTString(root.dict_find_string_value("qBt-contentLayout")), TorrentContentLayout::Default);
262 // === END REPLACEMENT CODE === //
264 torrentParams
.stopCondition
= Utils::String::toEnum(
265 fromLTString(resumeDataRoot
.dict_find_string_value("qBt-stopCondition")), Torrent::StopCondition::None
);
266 torrentParams
.sslParameters
=
268 .certificate
= QSslCertificate(toByteArray(resumeDataRoot
.dict_find_string_value(KEY_SSL_CERTIFICATE
))),
269 .privateKey
= Utils::SSLKey::load(toByteArray(resumeDataRoot
.dict_find_string_value(KEY_SSL_PRIVATE_KEY
))),
270 .dhParams
= toByteArray(resumeDataRoot
.dict_find_string_value(KEY_SSL_DH_PARAMS
))
273 const lt::string_view ratioLimitString
= resumeDataRoot
.dict_find_string_value("qBt-ratioLimit");
274 if (ratioLimitString
.empty())
275 torrentParams
.ratioLimit
= resumeDataRoot
.dict_find_int_value("qBt-ratioLimit", Torrent::USE_GLOBAL_RATIO
* 1000) / 1000.0;
277 torrentParams
.ratioLimit
= fromLTString(ratioLimitString
).toDouble();
279 const lt::bdecode_node tagsNode
= resumeDataRoot
.dict_find("qBt-tags");
280 if (tagsNode
.type() == lt::bdecode_node::list_t
)
282 for (int i
= 0; i
< tagsNode
.list_size(); ++i
)
284 const Tag tag
{fromLTString(tagsNode
.list_string_value_at(i
))};
285 torrentParams
.tags
.insert(tag
);
289 lt::add_torrent_params
&p
= torrentParams
.ltAddTorrentParams
;
291 p
= lt::read_resume_data(resumeDataRoot
, ec
);
293 if (!metadata
.isEmpty())
295 const auto *pref
= Preferences::instance();
296 const lt::bdecode_node torentInfoRoot
= lt::bdecode(metadata
, ec
297 , nullptr, pref
->getBdecodeDepthLimit(), pref
->getBdecodeTokenLimit());
299 return nonstd::make_unexpected(tr("Cannot parse torrent info: %1").arg(QString::fromStdString(ec
.message())));
301 if (torentInfoRoot
.type() != lt::bdecode_node::dict_t
)
302 return nonstd::make_unexpected(tr("Cannot parse torrent info: invalid format"));
304 const auto torrentInfo
= std::make_shared
<lt::torrent_info
>(torentInfoRoot
, ec
);
306 return nonstd::make_unexpected(tr("Cannot parse torrent info: %1").arg(QString::fromStdString(ec
.message())));
310 #ifdef QBT_USES_LIBTORRENT2
311 if (((p
.info_hashes
.has_v1() && (p
.info_hashes
.v1
!= p
.ti
->info_hashes().v1
))
312 || (p
.info_hashes
.has_v2() && (p
.info_hashes
.v2
!= p
.ti
->info_hashes().v2
))))
314 if (!p
.info_hash
.is_all_zeros() && (p
.info_hash
!= p
.ti
->info_hash()))
317 return nonstd::make_unexpected(tr("Mismatching info-hash detected in resume data"));
321 p
.save_path
= Profile::instance()->fromPortablePath(
322 Path(fromLTString(p
.save_path
))).toString().toStdString();
324 torrentParams
.stopped
= (p
.flags
& lt::torrent_flags::paused
) && !(p
.flags
& lt::torrent_flags::auto_managed
);
325 torrentParams
.operatingMode
= (p
.flags
& lt::torrent_flags::paused
) || (p
.flags
& lt::torrent_flags::auto_managed
)
326 ? TorrentOperatingMode::AutoManaged
: TorrentOperatingMode::Forced
;
328 if (p
.flags
& lt::torrent_flags::stop_when_ready
)
330 p
.flags
&= ~lt::torrent_flags::stop_when_ready
;
331 torrentParams
.stopCondition
= Torrent::StopCondition::FilesChecked
;
334 const bool hasMetadata
= (p
.ti
&& p
.ti
->is_valid());
335 if (!hasMetadata
&& !resumeDataRoot
.dict_find("info-hash"))
336 return nonstd::make_unexpected(tr("Resume data is invalid: neither metadata nor info-hash was found"));
338 return torrentParams
;
341 void BitTorrent::BencodeResumeDataStorage::store(const TorrentID
&id
, const LoadTorrentParams
&resumeData
) const
343 QMetaObject::invokeMethod(m_asyncWorker
, [this, id
, resumeData
]()
345 m_asyncWorker
->store(id
, resumeData
);
349 void BitTorrent::BencodeResumeDataStorage::remove(const TorrentID
&id
) const
351 QMetaObject::invokeMethod(m_asyncWorker
, [this, id
]()
353 m_asyncWorker
->remove(id
);
357 void BitTorrent::BencodeResumeDataStorage::storeQueue(const QList
<TorrentID
> &queue
) const
359 QMetaObject::invokeMethod(m_asyncWorker
, [this, queue
]()
361 m_asyncWorker
->storeQueue(queue
);
365 BitTorrent::BencodeResumeDataStorage::Worker::Worker(const Path
&resumeDataDir
)
366 : m_resumeDataDir
{resumeDataDir
}
370 void BitTorrent::BencodeResumeDataStorage::Worker::store(const TorrentID
&id
, const LoadTorrentParams
&resumeData
) const
372 // We need to adjust native libtorrent resume data
373 lt::add_torrent_params p
= resumeData
.ltAddTorrentParams
;
374 p
.save_path
= Profile::instance()->toPortablePath(Path(p
.save_path
))
375 .toString().toStdString();
376 if (resumeData
.stopped
)
378 p
.flags
|= lt::torrent_flags::paused
;
379 p
.flags
&= ~lt::torrent_flags::auto_managed
;
383 // Torrent can be actually "running" but temporarily "paused" to perform some
384 // service jobs behind the scenes so we need to restore it as "running"
385 if (resumeData
.operatingMode
== BitTorrent::TorrentOperatingMode::AutoManaged
)
387 p
.flags
|= lt::torrent_flags::auto_managed
;
391 p
.flags
&= ~lt::torrent_flags::paused
;
392 p
.flags
&= ~lt::torrent_flags::auto_managed
;
396 lt::entry data
= lt::write_resume_data(p
);
398 // metadata is stored in separate .torrent file
401 lt::entry::dictionary_type
&dataDict
= data
.dict();
402 lt::entry metadata
{lt::entry::dictionary_t
};
403 lt::entry::dictionary_type
&metadataDict
= metadata
.dict();
404 metadataDict
.insert(dataDict
.extract("info"));
405 metadataDict
.insert(dataDict
.extract("creation date"));
406 metadataDict
.insert(dataDict
.extract("created by"));
407 metadataDict
.insert(dataDict
.extract("comment"));
409 const Path torrentFilepath
= m_resumeDataDir
/ Path(u
"%1.torrent"_s
.arg(id
.toString()));
410 const nonstd::expected
<void, QString
> result
= Utils::IO::saveToFile(torrentFilepath
, metadata
);
413 LogMsg(tr("Couldn't save torrent metadata to '%1'. Error: %2.")
414 .arg(torrentFilepath
.toString(), result
.error()), Log::CRITICAL
);
419 data
["qBt-ratioLimit"] = static_cast<int>(resumeData
.ratioLimit
* 1000);
420 data
["qBt-seedingTimeLimit"] = resumeData
.seedingTimeLimit
;
421 data
["qBt-inactiveSeedingTimeLimit"] = resumeData
.inactiveSeedingTimeLimit
;
422 data
["qBt-shareLimitAction"] = Utils::String::fromEnum(resumeData
.shareLimitAction
).toStdString();
424 data
["qBt-category"] = resumeData
.category
.toStdString();
425 data
["qBt-tags"] = setToEntryList(resumeData
.tags
);
426 data
["qBt-name"] = resumeData
.name
.toStdString();
427 data
["qBt-seedStatus"] = resumeData
.hasFinishedStatus
;
428 data
["qBt-contentLayout"] = Utils::String::fromEnum(resumeData
.contentLayout
).toStdString();
429 data
["qBt-firstLastPiecePriority"] = resumeData
.firstLastPiecePriority
;
430 data
["qBt-stopCondition"] = Utils::String::fromEnum(resumeData
.stopCondition
).toStdString();
432 if (!resumeData
.sslParameters
.certificate
.isNull())
433 data
[KEY_SSL_CERTIFICATE
] = resumeData
.sslParameters
.certificate
.toPem().toStdString();
434 if (!resumeData
.sslParameters
.privateKey
.isNull())
435 data
[KEY_SSL_PRIVATE_KEY
] = resumeData
.sslParameters
.privateKey
.toPem().toStdString();
436 if (!resumeData
.sslParameters
.dhParams
.isEmpty())
437 data
[KEY_SSL_DH_PARAMS
] = resumeData
.sslParameters
.dhParams
.toStdString();
439 if (!resumeData
.useAutoTMM
)
441 data
["qBt-savePath"] = Profile::instance()->toPortablePath(resumeData
.savePath
).data().toStdString();
442 data
["qBt-downloadPath"] = Profile::instance()->toPortablePath(resumeData
.downloadPath
).data().toStdString();
445 const Path resumeFilepath
= m_resumeDataDir
/ Path(u
"%1.fastresume"_s
.arg(id
.toString()));
446 const nonstd::expected
<void, QString
> result
= Utils::IO::saveToFile(resumeFilepath
, data
);
449 LogMsg(tr("Couldn't save torrent resume data to '%1'. Error: %2.")
450 .arg(resumeFilepath
.toString(), result
.error()), Log::CRITICAL
);
454 void BitTorrent::BencodeResumeDataStorage::Worker::remove(const TorrentID
&id
) const
456 const Path resumeFilename
{u
"%1.fastresume"_s
.arg(id
.toString())};
457 Utils::Fs::removeFile(m_resumeDataDir
/ resumeFilename
);
459 const Path torrentFilename
{u
"%1.torrent"_s
.arg(id
.toString())};
460 Utils::Fs::removeFile(m_resumeDataDir
/ torrentFilename
);
463 void BitTorrent::BencodeResumeDataStorage::Worker::storeQueue(const QList
<TorrentID
> &queue
) const
466 data
.reserve(((BitTorrent::TorrentID::length() * 2) + 1) * queue
.size());
467 for (const BitTorrent::TorrentID
&torrentID
: queue
)
468 data
+= (torrentID
.toString().toLatin1() + '\n');
470 const Path filepath
= m_resumeDataDir
/ Path(u
"queue"_s
);
471 const nonstd::expected
<void, QString
> result
= Utils::IO::saveToFile(filepath
, data
);
474 LogMsg(tr("Couldn't save data to '%1'. Error: %2")
475 .arg(filepath
.toString(), result
.error()), Log::CRITICAL
);