core, frontend: Better progress reporter

A new "quick" decryptor is implemented. This is not really much faster (not slower either) but provides the benefit of being able to report progress on a single file. The frontend is updated accordingly to support this feature.
This commit is contained in:
zhupengfei
2019-09-12 22:08:37 +08:00
parent ccffd51904
commit d55af0108e
14 changed files with 541 additions and 54 deletions
-2
View File
@@ -49,7 +49,6 @@ Make sure the SD card is properly recognized and shows up as a disk.
* The `System Data` group contains important data that is necessary for your imported games to run. You should definitely import the contents there, if they do not exist yet.
1. After you've finished your selection, click `OK`. You should now see a progress dialog; wait a while until your contents are imported.
* The time will depend on how big your contents are, as well as your CPU processing power and (mainly) disk I/O speeds.
* The progress is only updated every time a content has been *fully* imported. If you see it stall for a long time, it just means that a very large content is being processed. Just be more patient and do not worry. You can also check the disk usage in `Task Manager`.
### What to do next
@@ -66,7 +65,6 @@ If you have any game cartidges, and would like to dump them as well, visit [this
* UI improvements
* Better error messages
* Beautiful icons
* Better progress indicator
* Bug fixes
* Clear all the `TODO`s in the code
* Wireless transfer (probably FTP?)
+1
View File
@@ -13,6 +13,7 @@ add_library(common STATIC
string_util.cpp
string_util.h
swap.h
thread.h
)
target_link_libraries(common PUBLIC fmt)
+95
View File
@@ -0,0 +1,95 @@
// Copyright 2013 Dolphin Emulator Project / 2014 Citra Emulator Project
// 2019 threeSD Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <chrono>
#include <condition_variable>
#include <cstddef>
#include <mutex>
#include <thread>
namespace Common {
class Event {
public:
void Set() {
std::lock_guard lk{mutex};
if (!is_set) {
is_set = true;
condvar.notify_one();
}
}
void Wait() {
std::unique_lock lk{mutex};
condvar.wait(lk, [&] { return is_set; });
is_set = false;
}
template <class Duration>
bool WaitFor(const std::chrono::duration<Duration>& time) {
std::unique_lock lk(mutex);
if (!condvar.wait_for(lk, time, [this] { return is_set; }))
return false;
is_set = false;
return true;
}
template <class Clock, class Duration>
bool WaitUntil(const std::chrono::time_point<Clock, Duration>& time) {
std::unique_lock lk{mutex};
if (!condvar.wait_until(lk, time, [this] { return is_set; }))
return false;
is_set = false;
return true;
}
void Reset() {
std::unique_lock lk{mutex};
// no other action required, since wait loops on the predicate and any lingering signal will
// get cleared on the first iteration
is_set = false;
}
private:
bool is_set = false;
std::condition_variable condvar;
std::mutex mutex;
};
class Barrier {
public:
explicit Barrier(std::size_t count_) : count(count_) {}
/// Blocks until all "count" threads have called Sync()
void Sync() {
std::unique_lock lk{mutex};
const std::size_t current_generation = generation;
if (++waiting == count) {
generation++;
waiting = 0;
condvar.notify_all();
} else {
condvar.wait(lk,
[this, current_generation] { return current_generation != generation; });
}
}
std::size_t Generation() const {
std::unique_lock lk(mutex);
return generation;
}
private:
std::condition_variable condvar;
mutable std::mutex mutex;
std::size_t count;
std::size_t waiting = 0;
std::size_t generation = 0; // Incremented once each time the barrier is used
};
} // namespace Common
+2
View File
@@ -11,6 +11,8 @@ add_library(core STATIC
key/arithmetic128.h
key/key.cpp
key/key.h
quick_decryptor.cpp
quick_decryptor.h
)
target_link_libraries(core PRIVATE common cryptopp)
+9 -22
View File
@@ -17,7 +17,9 @@
namespace Core {
SDMCDecryptor::SDMCDecryptor(const std::string& root_folder_) : root_folder(root_folder_) {
SDMCDecryptor::SDMCDecryptor(const std::string& root_folder_)
: root_folder(root_folder_), quick_decryptor(root_folder) {
ASSERT_MSG(Key::IsNormalKeyAvailable(Key::SDKey),
"SD Key must be available in order to decrypt");
@@ -47,28 +49,13 @@ std::array<u8, 16> GetFileCTR(const std::string& path) {
}
} // namespace
bool SDMCDecryptor::DecryptAndWriteFile(const std::string& source,
const std::string& destination) const {
auto ctr = GetFileCTR(source);
auto key = Key::GetNormalKey(Key::SDKey);
CryptoPP::CTR_Mode<CryptoPP::AES>::Decryption aes;
aes.SetKeyWithIV(key.data(), key.size(), ctr.data());
bool SDMCDecryptor::DecryptAndWriteFile(const std::string& source, const std::string& destination,
const QuickDecryptor::ProgressCallback& callback) {
return quick_decryptor.DecryptAndWriteFile(source, destination, callback);
}
if (!FileUtil::CreateFullPath(destination)) {
return false;
}
std::string absolute_source = root_folder + source;
try {
CryptoPP::FileSource(absolute_source.c_str(), true,
new CryptoPP::StreamTransformationFilter(
aes, new CryptoPP::FileSink(destination.c_str(), true)),
true);
} catch (CryptoPP::Exception& e) {
LOG_ERROR(Core, "Error decrypting and writing file: {}", e.what());
return false;
}
return true;
void SDMCDecryptor::Abort() {
quick_decryptor.Abort();
}
std::vector<u8> SDMCDecryptor::DecryptFile(const std::string& source) const {
+9 -1
View File
@@ -7,6 +7,7 @@
#include <string>
#include <vector>
#include "common/common_types.h"
#include "core/quick_decryptor.h"
namespace Core {
@@ -22,11 +23,17 @@ public:
/**
* Decrypts a file from the SD card and writes it into another file.
* This function blocks, but can be aborted with the Abort() function (would return false)
*
* @param source Path to the file relative to the root folder, starting with "/".
* @param destination Path to the destination file.
* @return true on success, false otherwise
*/
bool DecryptAndWriteFile(const std::string& source, const std::string& destination) const;
bool DecryptAndWriteFile(const std::string& source, const std::string& destination,
const QuickDecryptor::ProgressCallback& callback = [](std::size_t,
std::size_t) {});
void Abort();
/**
* Decrypts a file and reads it into a vector.
@@ -36,6 +43,7 @@ public:
private:
std::string root_folder;
QuickDecryptor quick_decryptor;
};
} // namespace Core
+86 -12
View File
@@ -53,29 +53,34 @@ bool SDMCImporter::IsGood() const {
return is_good;
}
bool SDMCImporter::ImportContent(const ContentSpecifier& specifier) {
void SDMCImporter::Abort() {
decryptor->Abort();
}
bool SDMCImporter::ImportContent(const ContentSpecifier& specifier,
const ProgressCallback& callback) {
switch (specifier.type) {
case ContentType::Application:
case ContentType::Update:
case ContentType::DLC:
return ImportTitle(specifier.id);
return ImportTitle(specifier.id, callback);
case ContentType::Savegame:
return ImportSavegame(specifier.id);
return ImportSavegame(specifier.id, callback);
case ContentType::Extdata:
return ImportExtdata(specifier.id);
return ImportExtdata(specifier.id, callback);
case ContentType::Sysdata:
return ImportSysdata(specifier.id);
return ImportSysdata(specifier.id, callback);
default:
UNREACHABLE();
}
}
bool SDMCImporter::ImportTitle(u64 id) {
bool SDMCImporter::ImportTitle(u64 id, const ProgressCallback& callback) {
const auto path = fmt::format("title/{:08x}/{:08x}/content/", (id >> 32), (id & 0xFFFFFFFF));
return FileUtil::ForeachDirectoryEntry(
nullptr, config.sdmc_path + path,
[this, &path](u64* /*num_entries_out*/, const std::string& directory,
const std::string& virtual_name) {
[this, &path, callback](u64* /*num_entries_out*/, const std::string& directory,
const std::string& virtual_name) {
if (FileUtil::IsDirectory(directory + virtual_name)) {
return true;
}
@@ -84,11 +89,12 @@ bool SDMCImporter::ImportTitle(u64 id) {
FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir) +
"Nintendo "
"3DS/00000000000000000000000000000000/00000000000000000000000000000000/" +
path + virtual_name);
path + virtual_name,
callback);
});
}
bool SDMCImporter::ImportSavegame(u64 id) {
bool SDMCImporter::ImportSavegame(u64 id, [[maybe_unused]] const ProgressCallback& callback) {
const auto path = fmt::format("title/{:08x}/{:08x}/data/", (id >> 32), (id & 0xFFFFFFFF));
DataContainer container(decryptor->DecryptFile(fmt::format("/{}00000001.sav", path)));
@@ -106,7 +112,7 @@ bool SDMCImporter::ImportSavegame(u64 id) {
"Nintendo 3DS/00000000000000000000000000000000/00000000000000000000000000000000/" + path);
}
bool SDMCImporter::ImportExtdata(u64 id) {
bool SDMCImporter::ImportExtdata(u64 id, [[maybe_unused]] const ProgressCallback& callback) {
const auto path = fmt::format("extdata/{:08x}/{:08x}/", (id >> 32), (id & 0xFFFFFFFF));
SDExtdata extdata("/" + path, *decryptor);
if (!extdata.IsGood()) {
@@ -118,7 +124,7 @@ bool SDMCImporter::ImportExtdata(u64 id) {
"Nintendo 3DS/00000000000000000000000000000000/00000000000000000000000000000000/" + path);
}
bool SDMCImporter::ImportSysdata(u64 id) {
bool SDMCImporter::ImportSysdata(u64 id, [[maybe_unused]] const ProgressCallback& callback) {
switch (id) {
case 0: { // boot9.bin
const auto target_path = FileUtil::GetUserPath(FileUtil::UserPath::SysDataDir) + BOOTROM9;
@@ -330,6 +336,74 @@ void SDMCImporter::ListSysdata(std::vector<ContentSpecifier>& out) const {
} while (0);
}
void SDMCImporter::DeleteContent(const ContentSpecifier& specifier) {
switch (specifier.type) {
case ContentType::Application:
case ContentType::Update:
case ContentType::DLC:
return DeleteTitle(specifier.id);
case ContentType::Savegame:
return DeleteSavegame(specifier.id);
case ContentType::Extdata:
return DeleteExtdata(specifier.id);
case ContentType::Sysdata:
return DeleteSysdata(specifier.id);
default:
UNREACHABLE();
}
}
void SDMCImporter::DeleteTitle(u64 id) const {
FileUtil::DeleteDirRecursively(fmt::format(
"{}Nintendo "
"3DS/00000000000000000000000000000000/00000000000000000000000000000000/title/{:08x}/{:08x}/"
"content/",
FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir), (id >> 32), (id & 0xFFFFFFFF)));
}
void SDMCImporter::DeleteSavegame(u64 id) const {
FileUtil::DeleteDirRecursively(fmt::format(
"{}Nintendo "
"3DS/00000000000000000000000000000000/00000000000000000000000000000000/title/{:08x}/{:08x}/"
"data/",
FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir), (id >> 32), (id & 0xFFFFFFFF)));
}
void SDMCImporter::DeleteExtdata(u64 id) const {
FileUtil::DeleteDirRecursively(fmt::format(
"{}Nintendo "
"3DS/00000000000000000000000000000000/00000000000000000000000000000000/extdata/{:08x}/"
"{:08x}/",
FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir), (id >> 32), (id & 0xFFFFFFFF)));
}
void SDMCImporter::DeleteSysdata(u64 id) const {
switch (id) {
case 0: { // boot9.bin
FileUtil::Delete(FileUtil::GetUserPath(FileUtil::UserPath::SysDataDir) + BOOTROM9);
}
case 1: { // safe mode firm
const bool is_new_3ds = FileUtil::Exists(config.safe_mode_firm_path + "new/");
const auto target_path =
fmt::format("{}00000000000000000000000000000000/title/00040138/{}/",
FileUtil::GetUserPath(FileUtil::UserPath::NANDDir),
(is_new_3ds ? "20000003" : "00000003"));
FileUtil::DeleteDirRecursively(target_path);
}
case 2: { // seed db
FileUtil::Delete(FileUtil::GetUserPath(FileUtil::UserPath::SysDataDir) + SEED_DB);
}
case 3: { // secret sector
FileUtil::Delete(FileUtil::GetUserPath(FileUtil::UserPath::SysDataDir) + SECRET_SECTOR);
}
case 4: { // aes_keys.txt
FileUtil::Delete(FileUtil::GetUserPath(FileUtil::UserPath::SysDataDir) + AES_KEYS);
}
default:
UNREACHABLE_MSG("Unexpected sysdata id {}", id);
}
}
std::vector<Config> LoadPresetConfig(std::string mount_point) {
if (mount_point.back() != '/' && mount_point.back() != '\\') {
mount_point += '/';
+26 -6
View File
@@ -61,6 +61,9 @@ struct Config {
class SDMCImporter {
public:
/// (current_size, total_size)
using ProgressCallback = std::function<void(std::size_t, std::size_t)>;
/**
* Initializes the importer.
* @param root_folder Path to the "Nintendo 3DS/<ID0>/<ID1>" folder.
@@ -70,10 +73,23 @@ public:
~SDMCImporter();
/**
* Dumps a specific content by its specifier.
* Aborts a specific content by its specifier.
* Blocks, but can be aborted on another thread if needed.
* @return true on success, false otherwise
*/
bool ImportContent(const ContentSpecifier& specifier);
bool ImportContent(const ContentSpecifier& specifier,
const ProgressCallback& callback = [](std::size_t, std::size_t) {});
/**
* Deletes/Cleans up a content. Used for deleting contents that have
* not been fully imported.
*/
void DeleteContent(const ContentSpecifier& specifier);
/**
* Aborts current importing.
*/
void Abort();
/**
* Gets a list of dumpable content specifiers.
@@ -87,13 +103,17 @@ public:
private:
bool Init();
bool ImportTitle(u64 id);
bool ImportSavegame(u64 id);
bool ImportExtdata(u64 id);
bool ImportSysdata(u64 id);
bool ImportTitle(u64 id, const ProgressCallback& callback);
bool ImportSavegame(u64 id, const ProgressCallback& callback);
bool ImportExtdata(u64 id, const ProgressCallback& callback);
bool ImportSysdata(u64 id, const ProgressCallback& callback);
void ListTitle(std::vector<ContentSpecifier>& out) const;
void ListExtdata(std::vector<ContentSpecifier>& out) const;
void ListSysdata(std::vector<ContentSpecifier>& out) const;
void DeleteTitle(u64 id) const;
void DeleteSavegame(u64 id) const;
void DeleteExtdata(u64 id) const;
void DeleteSysdata(u64 id) const;
bool is_good{};
Config config;
+204
View File
@@ -0,0 +1,204 @@
// Copyright 2019 threeSD Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <vector>
#include <cryptopp/aes.h>
#include <cryptopp/files.h>
#include <cryptopp/filters.h>
#include <cryptopp/modes.h>
#include <cryptopp/sha.h>
#include "common/assert.h"
#include "common/file_util.h"
#include "common/string_util.h"
#include "core/key/key.h"
#include "core/quick_decryptor.h"
namespace Core {
QuickDecryptor::QuickDecryptor(const std::string& root_folder_) : root_folder(root_folder_) {
ASSERT_MSG(Key::IsNormalKeyAvailable(Key::SDKey),
"SD Key must be available in order to decrypt");
if (root_folder.back() == '/' || root_folder.back() == '\\') {
// Remove '/' or '\' character at the end as we will add them back when combining path
root_folder.erase(root_folder.size() - 1);
}
}
QuickDecryptor::~QuickDecryptor() = default;
namespace {
std::array<u8, 16> GetFileCTR(const std::string& path) {
auto path_utf16 = Common::UTF8ToUTF16(path);
std::vector<u8> path_data(path_utf16.size() * 2 + 2, 0); // Add the '\0' character
std::memcpy(path_data.data(), path_utf16.data(), path_utf16.size() * 2);
CryptoPP::SHA256 sha;
std::array<u8, CryptoPP::SHA256::DIGESTSIZE> hash;
sha.CalculateDigest(hash.data(), path_data.data(), path_data.size());
std::array<u8, 16> ctr;
for (int i = 0; i < 16; i++) {
ctr[i] = hash[i] ^ hash[16 + i];
}
return ctr;
}
} // namespace
bool QuickDecryptor::DecryptAndWriteFile(const std::string& source_,
const std::string& destination_,
const ProgressCallback& callback_) {
if (is_running) {
LOG_ERROR(Core, "Decryptor is running");
return false;
}
if (!FileUtil::CreateFullPath(destination_)) {
LOG_ERROR(Core, "Could not create path {}", destination_);
return false;
}
for (auto& event : data_read_event) {
event.Reset();
}
for (auto& event : data_decrypted_event) {
event.Reset();
}
for (auto& event : data_written_event) {
event.Reset();
}
completion_event.Reset();
source = source_;
destination = destination_;
callback = callback_;
total_size = FileUtil::GetSize(root_folder + source);
if (total_size == 0) {
LOG_ERROR(Core, "Could not open file {}", root_folder + source);
return false;
}
is_good = is_running = true;
read_thread = std::make_unique<std::thread>(&QuickDecryptor::DataReadLoop, this);
decrypt_thread = std::make_unique<std::thread>(&QuickDecryptor::DataDecryptLoop, this);
write_thread = std::make_unique<std::thread>(&QuickDecryptor::DataWriteLoop, this);
completion_event.Wait();
is_running = false;
read_thread->join();
decrypt_thread->join();
write_thread->join();
bool ret = is_good;
is_good = true;
return ret;
}
void QuickDecryptor::DataReadLoop() {
std::size_t current_buffer = 0;
bool is_first_run = true;
FileUtil::IOFile file(root_folder + source, "rb");
if (!file) {
is_good = false;
completion_event.Set();
return;
}
std::size_t file_size = total_size;
while (is_running && file_size > 0) {
if (is_first_run) {
if (current_buffer == buffers.size() - 1) {
is_first_run = false;
}
} else {
data_written_event[current_buffer].Wait();
}
const auto bytes_to_read = std::min(BufferSize, file_size);
if (file.ReadBytes(buffers[current_buffer].data(), bytes_to_read) != bytes_to_read) {
is_good = false;
completion_event.Set();
return;
}
file_size -= bytes_to_read;
data_read_event[current_buffer].Set();
current_buffer = (current_buffer + 1) % buffers.size();
}
}
void QuickDecryptor::DataDecryptLoop() {
auto ctr = GetFileCTR(source);
auto key = Key::GetNormalKey(Key::SDKey);
CryptoPP::CTR_Mode<CryptoPP::AES>::Decryption aes;
aes.SetKeyWithIV(key.data(), key.size(), ctr.data());
std::size_t current_buffer = 0;
std::size_t file_size = total_size;
while (is_running && file_size > 0) {
data_read_event[current_buffer].Wait();
const auto bytes_to_process = std::min(BufferSize, file_size);
aes.ProcessData(buffers[current_buffer].data(), buffers[current_buffer].data(),
bytes_to_process);
file_size -= bytes_to_process;
data_decrypted_event[current_buffer].Set();
current_buffer = (current_buffer + 1) % buffers.size();
}
}
void QuickDecryptor::DataWriteLoop() {
std::size_t current_buffer = 0;
FileUtil::IOFile file(destination, "wb");
if (!file) {
is_good = false;
completion_event.Set();
return;
}
std::size_t file_size = total_size;
std::size_t iteration = 0;
/// The number of iterations each progress report covers. 32 * 16K = 512K
constexpr std::size_t ProgressReportFreq = 32;
while (is_running && file_size > 0) {
iteration++;
if (iteration % ProgressReportFreq == 0) {
callback(iteration * BufferSize, total_size);
}
data_decrypted_event[current_buffer].Wait();
const auto bytes_to_write = std::min(BufferSize, file_size);
if (file.WriteBytes(buffers[current_buffer].data(), bytes_to_write) != bytes_to_write) {
is_good = false;
completion_event.Set();
return;
}
file_size -= bytes_to_write;
data_written_event[current_buffer].Set();
current_buffer = (current_buffer + 1) % buffers.size();
}
completion_event.Set();
}
void QuickDecryptor::Abort() {
if (is_running.exchange(false)) {
is_good = false;
completion_event.Set();
}
}
} // namespace Core
+62
View File
@@ -0,0 +1,62 @@
// Copyright 2019 threeSD Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <array>
#include <atomic>
#include <memory>
#include <string>
#include "common/common_types.h"
#include "common/thread.h"
namespace Core {
class QuickDecryptor {
public:
/// (current_size, total_size)
using ProgressCallback = std::function<void(std::size_t, std::size_t)>;
/**
* Initializes the decryptor.
* @param root_folder Path to the "Nintendo 3DS/<ID0>/<ID1>" folder.
*/
explicit QuickDecryptor(const std::string& root_folder);
~QuickDecryptor();
bool DecryptAndWriteFile(const std::string& source, const std::string& destination,
const ProgressCallback& callback = [](std::size_t, std::size_t) {});
void DataReadLoop();
void DataDecryptLoop();
void DataWriteLoop();
void Abort();
private:
static constexpr std::size_t BufferSize = 16 * 1024; // 16 KB
std::string root_folder;
std::string source;
std::string destination;
std::size_t total_size{};
std::array<std::array<u8, BufferSize>, 3> buffers;
std::array<Common::Event, 3> data_read_event;
std::array<Common::Event, 3> data_decrypted_event;
std::array<Common::Event, 3> data_written_event;
std::unique_ptr<std::thread> read_thread;
std::unique_ptr<std::thread> decrypt_thread;
std::unique_ptr<std::thread> write_thread;
ProgressCallback callback;
Common::Event completion_event;
bool is_good{true};
std::atomic_bool is_running{false};
};
} // namespace Core
+20 -6
View File
@@ -266,18 +266,32 @@ void ImportDialog::StartImporting() {
auto* dialog = new QProgressDialog(tr("Initializing..."), tr("Cancel"), 0,
static_cast<int>(total_size / multiplier), this);
dialog->setWindowModality(Qt::WindowModal);
dialog->setMinimumDuration(0);
dialog->setValue(0);
auto* job = new ImportJob(this, importer, std::move(to_import));
connect(job, &ImportJob::ProgressUpdated, this,
[dialog, multiplier, total_count](u64 size_imported, u64 count,
Core::ContentSpecifier next_content) {
connect(job, &ImportJob::NextContent, this,
[this, dialog, multiplier, total_count](u64 size_imported, u64 count,
Core::ContentSpecifier next_content) {
dialog->setValue(static_cast<int>(size_imported / multiplier));
dialog->setLabelText(tr("Importing %1 (%2/%3)...")
.arg(GetContentName(next_content))
dialog->setLabelText(tr("(%1/%2) Importing %3...")
.arg(count)
.arg(total_count));
.arg(total_count)
.arg(GetContentName(next_content)));
current_content = next_content;
current_count = count;
});
connect(job, &ImportJob::ProgressUpdated, this,
[this, dialog, multiplier, total_count](u64 total_size_imported,
u64 current_size_imported) {
dialog->setValue(static_cast<int>(total_size_imported / multiplier));
dialog->setLabelText(tr("(%1/%2) Importing %3 (%4/%5)...")
.arg(current_count)
.arg(total_count)
.arg(GetContentName(current_content))
.arg(ReadableByteSize(current_size_imported))
.arg(ReadableByteSize(current_content.maximum_size)));
});
connect(job, &ImportJob::ErrorOccured, this,
[this, dialog](Core::ContentSpecifier current_content) {
+4
View File
@@ -41,4 +41,8 @@ private:
// HACK: To tell whether the checkbox state change is a programmatic trigger
// TODO: Is there a more elegant way of doing the same?
bool program_trigger = false;
// TODO: Why this won't work as locals?
Core::ContentSpecifier current_content = {};
u64 current_count = 0;
};
+12 -4
View File
@@ -15,10 +15,17 @@ ImportJob::~ImportJob() = default;
void ImportJob::run() {
u64 size_imported = 0, count = 0;
for (const auto& content : contents) {
emit ProgressUpdated(size_imported, count + 1, content);
if (!importer.ImportContent(content)) {
emit ErrorOccured(content);
return;
emit NextContent(size_imported, count + 1, content);
const auto callback = [this, size_imported](std::size_t current_size,
std::size_t /*total_size*/) {
emit ProgressUpdated(size_imported + current_size, current_size);
};
if (!importer.ImportContent(content, callback)) {
importer.DeleteContent(content);
if (!cancelled) {
emit ErrorOccured(content);
return;
}
}
count++;
size_imported += content.maximum_size;
@@ -32,4 +39,5 @@ void ImportJob::run() {
void ImportJob::Cancel() {
cancelled.store(true);
importer.Abort();
}
+11 -1
View File
@@ -20,7 +20,17 @@ public:
void Cancel();
signals:
void ProgressUpdated(u64 size_imported, u64 count, Core::ContentSpecifier next_content);
/**
* Called when progress is updated on the current content.
* @param total_size_imported Total imported size taking all previous contents into
* consideration.
* @param current_size_imported Imported size of the current content.
*/
void ProgressUpdated(u64 total_size_imported, u64 current_size_imported);
/// Dumping of a content has been finished, go on to the next. Called at start as well.
void NextContent(u64 size_imported, u64 count, Core::ContentSpecifier next_content);
void Completed();
void ErrorOccured(Core::ContentSpecifier current_content);