1208 lines
33 KiB
C++
1208 lines
33 KiB
C++
/*
|
|
This file is part of Telegram Desktop,
|
|
the official desktop application for the Telegram messaging service.
|
|
|
|
For license and copyright information please follow this link:
|
|
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|
*/
|
|
#include "data/data_download_manager.h"
|
|
|
|
#include "data/data_session.h"
|
|
#include "data/data_photo.h"
|
|
#include "data/data_document.h"
|
|
#include "data/data_document_media.h"
|
|
#include "data/data_web_page.h"
|
|
#include "data/data_changes.h"
|
|
#include "data/data_user.h"
|
|
#include "data/data_channel.h"
|
|
#include "data/data_file_origin.h"
|
|
#include "base/unixtime.h"
|
|
#include "base/random.h"
|
|
#include "main/main_session.h"
|
|
#include "main/main_account.h"
|
|
#include "lang/lang_keys.h"
|
|
#include "storage/storage_account.h"
|
|
#include "history/history.h"
|
|
#include "history/history_item.h"
|
|
#include "history/history_item_helpers.h"
|
|
#include "core/application.h"
|
|
#include "core/mime_type.h"
|
|
#include "ui/controls/download_bar.h"
|
|
#include "ui/text/format_song_document_name.h"
|
|
#include "ui/layers/generic_box.h"
|
|
#include "storage/serialize_common.h"
|
|
#include "window/window_controller.h"
|
|
#include "window/window_session_controller.h"
|
|
#include "apiwrap.h"
|
|
#include "styles/style_layers.h"
|
|
|
|
namespace Data {
|
|
namespace {
|
|
|
|
constexpr auto kClearLoadingTimeout = 5 * crl::time(1000);
|
|
constexpr auto kMaxFileSize = 4000 * int64(1024 * 1024);
|
|
constexpr auto kMaxResolvePerAttempt = 100;
|
|
|
|
constexpr auto ByItem = [](const auto &entry) {
|
|
if constexpr (std::is_same_v<decltype(entry), const DownloadingId&>) {
|
|
return entry.object.item;
|
|
} else {
|
|
const auto resolved = entry.object.get();
|
|
return resolved ? resolved->item.get() : nullptr;
|
|
}
|
|
};
|
|
|
|
constexpr auto ByDocument = [](const auto &entry) {
|
|
return entry.object.document;
|
|
};
|
|
|
|
[[nodiscard]] uint64 PeerAccessHash(not_null<PeerData*> peer) {
|
|
if (const auto user = peer->asUser()) {
|
|
return user->accessHash();
|
|
} else if (const auto channel = peer->asChannel()) {
|
|
return channel->access;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
[[nodiscard]] bool ItemContainsMedia(const DownloadObject &object) {
|
|
if (const auto photo = object.photo) {
|
|
if (const auto media = object.item->media()) {
|
|
if (const auto page = media->webpage()) {
|
|
if (page->photo == photo) {
|
|
return true;
|
|
}
|
|
for (const auto &item : page->collage.items) {
|
|
if (const auto v = std::get_if<PhotoData*>(&item)) {
|
|
if ((*v) == photo) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
return (media->photo() == photo);
|
|
}
|
|
}
|
|
} else if (const auto document = object.document) {
|
|
if (const auto media = object.item->media()) {
|
|
if (const auto page = media->webpage()) {
|
|
if (page->document == document) {
|
|
return true;
|
|
}
|
|
for (const auto &item : page->collage.items) {
|
|
if (const auto v = std::get_if<DocumentData*>(&item)) {
|
|
if ((*v) == document) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
return (media->document() == document);
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
struct DocumentDescriptor {
|
|
uint64 sessionUniqueId = 0;
|
|
DocumentId documentId = 0;
|
|
FullMsgId itemId;
|
|
};
|
|
|
|
} // namespace
|
|
|
|
struct DownloadManager::DeleteFilesDescriptor {
|
|
base::flat_set<not_null<Main::Session*>> sessions;
|
|
base::flat_map<QString, DocumentDescriptor> files;
|
|
};
|
|
|
|
DownloadManager::DownloadManager()
|
|
: _clearLoadingTimer([=] { clearLoading(); }) {
|
|
}
|
|
|
|
DownloadManager::~DownloadManager() = default;
|
|
|
|
void DownloadManager::trackSession(not_null<Main::Session*> session) {
|
|
auto &data = _sessions.emplace(session, SessionData()).first->second;
|
|
data.downloaded = deserialize(session);
|
|
data.resolveNeeded = data.downloaded.size();
|
|
|
|
session->data().documentLoadProgress(
|
|
) | rpl::filter([=](not_null<DocumentData*> document) {
|
|
return _loadingDocuments.contains(document);
|
|
}) | rpl::start_with_next([=](not_null<DocumentData*> document) {
|
|
check(document);
|
|
}, data.lifetime);
|
|
|
|
session->data().itemLayoutChanged(
|
|
) | rpl::filter([=](not_null<const HistoryItem*> item) {
|
|
return _loading.contains(item);
|
|
}) | rpl::start_with_next([=](not_null<const HistoryItem*> item) {
|
|
check(item);
|
|
}, data.lifetime);
|
|
|
|
session->data().itemViewRefreshRequest(
|
|
) | rpl::start_with_next([=](not_null<HistoryItem*> item) {
|
|
changed(item);
|
|
}, data.lifetime);
|
|
|
|
session->changes().messageUpdates(
|
|
MessageUpdate::Flag::Destroyed
|
|
) | rpl::start_with_next([=](const MessageUpdate &update) {
|
|
removed(update.item);
|
|
}, data.lifetime);
|
|
|
|
session->account().sessionChanges(
|
|
) | rpl::filter(
|
|
rpl::mappers::_1 != session
|
|
) | rpl::take(1) | rpl::start_with_next([=] {
|
|
untrack(session);
|
|
}, data.lifetime);
|
|
}
|
|
|
|
void DownloadManager::itemVisibilitiesUpdated(
|
|
not_null<Main::Session*> session) {
|
|
const auto i = _sessions.find(session);
|
|
if (i == end(_sessions)
|
|
|| i->second.downloading.empty()
|
|
|| !i->second.downloading.front().hiddenByView) {
|
|
return;
|
|
}
|
|
for (const auto &id : i->second.downloading) {
|
|
if (!id.done
|
|
&& !session->data().queryItemVisibility(id.object.item)) {
|
|
for (auto &id : i->second.downloading) {
|
|
id.hiddenByView = false;
|
|
}
|
|
_loadingListChanges.fire({});
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
int64 DownloadManager::computeNextStartDate() {
|
|
const auto now = base::unixtime::now();
|
|
if (_lastStartedBase != now) {
|
|
_lastStartedBase = now;
|
|
_lastStartedAdded = 0;
|
|
} else {
|
|
++_lastStartedAdded;
|
|
}
|
|
return int64(_lastStartedBase) * 1000 + _lastStartedAdded;
|
|
}
|
|
|
|
void DownloadManager::addLoading(DownloadObject object) {
|
|
Expects(object.item != nullptr);
|
|
Expects(object.document != nullptr);
|
|
|
|
const auto item = object.item;
|
|
auto &data = sessionData(item);
|
|
|
|
const auto already = ranges::find(data.downloading, item, ByItem);
|
|
if (already != end(data.downloading)) {
|
|
const auto document = already->object.document;
|
|
const auto photo = already->object.photo;
|
|
if (document == object.document && photo == object.photo) {
|
|
check(item);
|
|
return;
|
|
}
|
|
remove(data, already);
|
|
}
|
|
|
|
const auto size = object.document->size;
|
|
const auto path = object.document->loadingFilePath();
|
|
if (path.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
const auto shownExists = !data.downloading.empty()
|
|
&& !data.downloading.front().hiddenByView;
|
|
data.downloading.push_back({
|
|
.object = object,
|
|
.started = computeNextStartDate(),
|
|
.path = path,
|
|
.total = size,
|
|
.hiddenByView = (!shownExists
|
|
&& item->history()->owner().queryItemVisibility(item)),
|
|
});
|
|
_loading.emplace(item);
|
|
_loadingDocuments.emplace(object.document);
|
|
_loadingProgress = DownloadProgress{
|
|
.ready = _loadingProgress.current().ready,
|
|
.total = _loadingProgress.current().total + size,
|
|
};
|
|
_loadingListChanges.fire({});
|
|
_clearLoadingTimer.cancel();
|
|
|
|
check(item);
|
|
}
|
|
|
|
void DownloadManager::check(not_null<const HistoryItem*> item) {
|
|
auto &data = sessionData(item);
|
|
const auto i = ranges::find(data.downloading, item, ByItem);
|
|
Assert(i != end(data.downloading));
|
|
check(data, i);
|
|
}
|
|
|
|
void DownloadManager::check(not_null<DocumentData*> document) {
|
|
auto &data = sessionData(document);
|
|
const auto i = ranges::find(
|
|
data.downloading,
|
|
document.get(),
|
|
ByDocument);
|
|
Assert(i != end(data.downloading));
|
|
check(data, i);
|
|
}
|
|
|
|
void DownloadManager::check(
|
|
SessionData &data,
|
|
std::vector<DownloadingId>::iterator i) {
|
|
auto &entry = *i;
|
|
|
|
if (!ItemContainsMedia(entry.object)) {
|
|
cancel(data, i);
|
|
return;
|
|
}
|
|
const auto document = entry.object.document;
|
|
|
|
// Load with progress only documents for now.
|
|
Assert(document != nullptr);
|
|
|
|
const auto path = document->filepath(true);
|
|
if (!path.isEmpty()) {
|
|
if (_loading.contains(entry.object.item)) {
|
|
addLoaded(entry.object, path, entry.started);
|
|
}
|
|
} else if (!document->loading()) {
|
|
remove(data, i);
|
|
} else {
|
|
const auto totalChange = document->size - entry.total;
|
|
const auto readyChange = document->loadOffset() - entry.ready;
|
|
if (!readyChange && !totalChange) {
|
|
return;
|
|
}
|
|
entry.ready += readyChange;
|
|
entry.total += totalChange;
|
|
_loadingProgress = DownloadProgress{
|
|
.ready = _loadingProgress.current().ready + readyChange,
|
|
.total = _loadingProgress.current().total + totalChange,
|
|
};
|
|
}
|
|
}
|
|
|
|
void DownloadManager::addLoaded(
|
|
DownloadObject object,
|
|
const QString &path,
|
|
DownloadDate started) {
|
|
Expects(object.item != nullptr);
|
|
Expects(object.document || object.photo);
|
|
|
|
const auto size = QFileInfo(path).size();
|
|
if (size <= 0 || size > kMaxFileSize) {
|
|
return;
|
|
}
|
|
|
|
const auto item = object.item;
|
|
auto &data = sessionData(item);
|
|
|
|
const auto id = object.document
|
|
? DownloadId{ object.document->id, DownloadType::Document }
|
|
: DownloadId{ object.photo->id, DownloadType::Photo };
|
|
data.downloaded.push_back({
|
|
.download = id,
|
|
.started = started,
|
|
.path = path,
|
|
.size = size,
|
|
.itemId = item->fullId(),
|
|
.peerAccessHash = PeerAccessHash(item->history()->peer),
|
|
.object = std::make_unique<DownloadObject>(object),
|
|
});
|
|
_loaded.emplace(item);
|
|
_loadedAdded.fire(&data.downloaded.back());
|
|
|
|
writePostponed(&item->history()->session());
|
|
|
|
const auto i = ranges::find(data.downloading, item, ByItem);
|
|
if (i != end(data.downloading)) {
|
|
auto &entry = *i;
|
|
const auto document = entry.object.document;
|
|
if (document) {
|
|
_loadingDocuments.remove(document);
|
|
}
|
|
const auto j = _loading.find(entry.object.item);
|
|
if (j == end(_loading)) {
|
|
return;
|
|
}
|
|
const auto totalChange = document->size - entry.total;
|
|
const auto readyChange = document->size - entry.ready;
|
|
entry.ready += readyChange;
|
|
entry.total += totalChange;
|
|
entry.done = true;
|
|
_loading.erase(j);
|
|
_loadingDone.emplace(entry.object.item);
|
|
_loadingProgress = DownloadProgress{
|
|
.ready = _loadingProgress.current().ready + readyChange,
|
|
.total = _loadingProgress.current().total + totalChange,
|
|
};
|
|
_loadingListChanges.fire({});
|
|
if (_loading.empty()) {
|
|
_clearLoadingTimer.callOnce(kClearLoadingTimeout);
|
|
}
|
|
}
|
|
}
|
|
|
|
void DownloadManager::clearIfFinished() {
|
|
if (_clearLoadingTimer.isActive()) {
|
|
_clearLoadingTimer.cancel();
|
|
clearLoading();
|
|
}
|
|
}
|
|
|
|
void DownloadManager::deleteFiles(const std::vector<GlobalMsgId> &ids) {
|
|
auto descriptor = DeleteFilesDescriptor();
|
|
for (const auto &id : ids) {
|
|
if (const auto item = MessageByGlobalId(id)) {
|
|
const auto session = &item->history()->session();
|
|
const auto i = _sessions.find(session);
|
|
if (i == end(_sessions)) {
|
|
continue;
|
|
}
|
|
auto &data = i->second;
|
|
const auto j = ranges::find(
|
|
data.downloading,
|
|
not_null{ item },
|
|
ByItem);
|
|
if (j != end(data.downloading)) {
|
|
cancel(data, j);
|
|
}
|
|
|
|
const auto k = ranges::find(data.downloaded, item, ByItem);
|
|
if (k != end(data.downloaded)) {
|
|
const auto document = k->object->document;
|
|
descriptor.files.emplace(k->path, DocumentDescriptor{
|
|
.sessionUniqueId = id.sessionUniqueId,
|
|
.documentId = document ? document->id : DocumentId(),
|
|
.itemId = id.itemId,
|
|
});
|
|
_loaded.remove(item);
|
|
_generated.remove(item);
|
|
if (document) {
|
|
_generatedDocuments.remove(document);
|
|
}
|
|
data.downloaded.erase(k);
|
|
_loadedRemoved.fire_copy(item);
|
|
|
|
descriptor.sessions.emplace(session);
|
|
}
|
|
}
|
|
}
|
|
finishFilesDelete(std::move(descriptor));
|
|
}
|
|
|
|
void DownloadManager::deleteAll() {
|
|
auto descriptor = DeleteFilesDescriptor();
|
|
for (auto &[session, data] : _sessions) {
|
|
if (!data.downloaded.empty()) {
|
|
descriptor.sessions.emplace(session);
|
|
} else if (data.downloading.empty()) {
|
|
continue;
|
|
}
|
|
const auto sessionUniqueId = session->uniqueId();
|
|
while (!data.downloading.empty()) {
|
|
cancel(data, data.downloading.end() - 1);
|
|
}
|
|
for (auto &id : base::take(data.downloaded)) {
|
|
const auto object = id.object.get();
|
|
const auto document = object ? object->document : nullptr;
|
|
descriptor.files.emplace(id.path, DocumentDescriptor{
|
|
.sessionUniqueId = sessionUniqueId,
|
|
.documentId = document ? document->id : DocumentId(),
|
|
.itemId = id.itemId,
|
|
});
|
|
if (document) {
|
|
_generatedDocuments.remove(document);
|
|
}
|
|
if (const auto item = object ? object->item.get() : nullptr) {
|
|
_loaded.remove(item);
|
|
_generated.remove(item);
|
|
_loadedRemoved.fire_copy(item);
|
|
}
|
|
}
|
|
}
|
|
for (const auto &session : descriptor.sessions) {
|
|
writePostponed(session);
|
|
}
|
|
finishFilesDelete(std::move(descriptor));
|
|
}
|
|
|
|
void DownloadManager::finishFilesDelete(DeleteFilesDescriptor &&descriptor) {
|
|
for (const auto &session : descriptor.sessions) {
|
|
writePostponed(session);
|
|
}
|
|
crl::async([files = std::move(descriptor.files)]{
|
|
for (const auto &file : files) {
|
|
QFile(file.first).remove();
|
|
crl::on_main([descriptor = file.second] {
|
|
if (const auto session = SessionByUniqueId(
|
|
descriptor.sessionUniqueId)) {
|
|
if (const auto id = descriptor.documentId) {
|
|
[[maybe_unused]] const auto location
|
|
= session->data().document(id)->location(true);
|
|
}
|
|
const auto itemId = descriptor.itemId;
|
|
if (const auto item = session->data().message(itemId)) {
|
|
session->data().requestItemRepaint(item);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
bool DownloadManager::loadedHasNonCloudFile() const {
|
|
for (const auto &[session, data] : _sessions) {
|
|
for (const auto &id : data.downloaded) {
|
|
if (const auto object = id.object.get()) {
|
|
if (!object->item->isHistoryEntry()) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
auto DownloadManager::loadingList() const
|
|
-> ranges::any_view<const DownloadingId*, ranges::category::input> {
|
|
return ranges::views::all(
|
|
_sessions
|
|
) | ranges::views::transform([=](const auto &pair) {
|
|
return ranges::views::all(
|
|
pair.second.downloading
|
|
) | ranges::views::transform([](const DownloadingId &id) {
|
|
return &id;
|
|
});
|
|
}) | ranges::views::join;
|
|
}
|
|
|
|
DownloadProgress DownloadManager::loadingProgress() const {
|
|
return _loadingProgress.current();
|
|
}
|
|
|
|
rpl::producer<> DownloadManager::loadingListChanges() const {
|
|
return _loadingListChanges.events();
|
|
}
|
|
|
|
auto DownloadManager::loadingProgressValue() const
|
|
-> rpl::producer<DownloadProgress> {
|
|
return _loadingProgress.value();
|
|
}
|
|
|
|
bool DownloadManager::loadingInProgress(Main::Session *onlyInSession) const {
|
|
return lookupLoadingItem(onlyInSession) != nullptr;
|
|
}
|
|
|
|
HistoryItem *DownloadManager::lookupLoadingItem(
|
|
Main::Session *onlyInSession) const {
|
|
constexpr auto find = [](const SessionData &data) {
|
|
constexpr auto proj = &DownloadingId::done;
|
|
const auto i = ranges::find(data.downloading, false, proj);
|
|
return (i != end(data.downloading)) ? i->object.item.get() : nullptr;
|
|
};
|
|
if (onlyInSession) {
|
|
const auto i = _sessions.find(onlyInSession);
|
|
return (i != end(_sessions)) ? find(i->second) : nullptr;
|
|
} else {
|
|
for (const auto &[session, data] : _sessions) {
|
|
if (const auto result = find(data)) {
|
|
return result;
|
|
}
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
void DownloadManager::loadingStopWithConfirmation(
|
|
Fn<void()> callback,
|
|
Main::Session *onlyInSession) {
|
|
const auto item = lookupLoadingItem(onlyInSession);
|
|
if (!item) {
|
|
return;
|
|
}
|
|
const auto window = Core::App().windowFor(
|
|
&item->history()->session().account());
|
|
if (!window) {
|
|
return;
|
|
}
|
|
const auto weak = base::make_weak(&item->history()->session());
|
|
const auto id = item->fullId();
|
|
auto box = Box([=](not_null<Ui::GenericBox*> box) {
|
|
box->addRow(
|
|
object_ptr<Ui::FlatLabel>(
|
|
box.get(),
|
|
tr::lng_download_sure_stop(),
|
|
st::boxLabel),
|
|
st::boxPadding + QMargins(0, 0, 0, st::boxPadding.bottom()));
|
|
box->setStyle(st::defaultBox);
|
|
box->addButton(tr::lng_selected_upload_stop(), [=] {
|
|
box->closeBox();
|
|
|
|
if (!onlyInSession || weak.get()) {
|
|
loadingStop(onlyInSession);
|
|
}
|
|
if (callback) {
|
|
callback();
|
|
}
|
|
}, st::attentionBoxButton);
|
|
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
|
box->addLeftButton(tr::lng_upload_show_file(), [=] {
|
|
box->closeBox();
|
|
|
|
if (const auto strong = weak.get()) {
|
|
if (const auto item = strong->data().message(id)) {
|
|
if (const auto window = strong->tryResolveWindow()) {
|
|
window->showMessage(item);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
});
|
|
window->show(std::move(box));
|
|
window->activate();
|
|
}
|
|
|
|
void DownloadManager::loadingStop(Main::Session *onlyInSession) {
|
|
const auto stopInSession = [&](SessionData &data) {
|
|
while (!data.downloading.empty()) {
|
|
cancel(data, data.downloading.end() - 1);
|
|
}
|
|
};
|
|
if (onlyInSession) {
|
|
const auto i = _sessions.find(onlyInSession);
|
|
if (i != end(_sessions)) {
|
|
stopInSession(i->second);
|
|
}
|
|
} else {
|
|
for (auto &[session, data] : _sessions) {
|
|
stopInSession(data);
|
|
}
|
|
}
|
|
}
|
|
|
|
void DownloadManager::clearLoading() {
|
|
Expects(_loading.empty());
|
|
|
|
for (auto &[session, data] : _sessions) {
|
|
while (!data.downloading.empty()) {
|
|
remove(data, data.downloading.end() - 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
auto DownloadManager::loadedList()
|
|
-> ranges::any_view<const DownloadedId*, ranges::category::input> {
|
|
for (auto &[session, data] : _sessions) {
|
|
resolve(session, data);
|
|
}
|
|
return ranges::views::all(
|
|
_sessions
|
|
) | ranges::views::transform([=](const auto &pair) {
|
|
return ranges::views::all(
|
|
pair.second.downloaded
|
|
) | ranges::views::filter([](const DownloadedId &id) {
|
|
return (id.object != nullptr);
|
|
}) | ranges::views::transform([](const DownloadedId &id) {
|
|
return &id;
|
|
});
|
|
}) | ranges::views::join;
|
|
}
|
|
|
|
rpl::producer<> DownloadManager::loadedResolveDone() const {
|
|
using namespace rpl::mappers;
|
|
return _loadedResolveDone.value() | rpl::filter(_1) | rpl::to_empty;
|
|
}
|
|
|
|
void DownloadManager::resolve(
|
|
not_null<Main::Session*> session,
|
|
SessionData &data) {
|
|
const auto guard = gsl::finally([&] {
|
|
checkFullResolveDone();
|
|
});
|
|
if (data.resolveSentTotal >= data.resolveNeeded
|
|
|| data.resolveSentTotal >= kMaxResolvePerAttempt) {
|
|
return;
|
|
}
|
|
struct Prepared {
|
|
uint64 peerAccessHash = 0;
|
|
QVector<MTPInputMessage> ids;
|
|
};
|
|
auto &owner = session->data();
|
|
auto prepared = base::flat_map<PeerId, Prepared>();
|
|
auto last = begin(data.downloaded);
|
|
auto from = last + (data.resolveNeeded - data.resolveSentTotal);
|
|
for (auto i = from; i != last;) {
|
|
auto &id = *--i;
|
|
const auto msgId = id.itemId.msg;
|
|
const auto info = QFileInfo(id.path);
|
|
if (!info.exists() || info.size() != id.size) {
|
|
// Mark as deleted.
|
|
id.path = QString();
|
|
} else if (!owner.message(id.itemId) && IsServerMsgId(msgId)) {
|
|
const auto groupByPeer = peerIsChannel(id.itemId.peer)
|
|
? id.itemId.peer
|
|
: session->userPeerId();
|
|
auto &perPeer = prepared[groupByPeer];
|
|
if (peerIsChannel(id.itemId.peer) && !perPeer.peerAccessHash) {
|
|
perPeer.peerAccessHash = id.peerAccessHash;
|
|
}
|
|
perPeer.ids.push_back(MTP_inputMessageID(MTP_int(msgId.bare)));
|
|
}
|
|
if (++data.resolveSentTotal >= kMaxResolvePerAttempt) {
|
|
break;
|
|
}
|
|
}
|
|
const auto check = [=] {
|
|
auto &data = sessionData(session);
|
|
if (!data.resolveSentRequests) {
|
|
resolveRequestsFinished(session, data);
|
|
}
|
|
};
|
|
const auto requestFinished = [=] {
|
|
--sessionData(session).resolveSentRequests;
|
|
check();
|
|
};
|
|
for (auto &[peer, perPeer] : prepared) {
|
|
if (const auto channelId = peerToChannel(peer)) {
|
|
session->api().request(MTPchannels_GetMessages(
|
|
MTP_inputChannel(
|
|
MTP_long(channelId.bare),
|
|
MTP_long(perPeer.peerAccessHash)),
|
|
MTP_vector<MTPInputMessage>(perPeer.ids)
|
|
)).done([=](const MTPmessages_Messages &result) {
|
|
session->data().processExistingMessages(
|
|
session->data().channelLoaded(channelId),
|
|
result);
|
|
requestFinished();
|
|
}).fail(requestFinished).send();
|
|
} else {
|
|
session->api().request(MTPmessages_GetMessages(
|
|
MTP_vector<MTPInputMessage>(perPeer.ids)
|
|
)).done([=](const MTPmessages_Messages &result) {
|
|
session->data().processExistingMessages(nullptr, result);
|
|
requestFinished();
|
|
}).fail(requestFinished).send();
|
|
}
|
|
}
|
|
data.resolveSentRequests += prepared.size();
|
|
check();
|
|
}
|
|
|
|
void DownloadManager::resolveRequestsFinished(
|
|
not_null<Main::Session*> session,
|
|
SessionData &data) {
|
|
auto &owner = session->data();
|
|
for (; data.resolveSentTotal > 0; --data.resolveSentTotal) {
|
|
const auto i = begin(data.downloaded) + (--data.resolveNeeded);
|
|
if (i->path.isEmpty()) {
|
|
data.downloaded.erase(i);
|
|
continue;
|
|
}
|
|
const auto item = owner.message(i->itemId);
|
|
const auto media = item ? item->media() : nullptr;
|
|
const auto document = media ? media->document() : nullptr;
|
|
const auto photo = media ? media->photo() : nullptr;
|
|
if (i->download.type == DownloadType::Document
|
|
&& (!document || document->id != i->download.objectId)) {
|
|
generateEntry(session, *i);
|
|
} else if (i->download.type == DownloadType::Photo
|
|
&& (!photo || photo->id != i->download.objectId)) {
|
|
generateEntry(session, *i);
|
|
} else {
|
|
i->object = std::make_unique<DownloadObject>(DownloadObject{
|
|
.item = item,
|
|
.document = document,
|
|
.photo = photo,
|
|
});
|
|
_loaded.emplace(item);
|
|
}
|
|
_loadedAdded.fire(&*i);
|
|
}
|
|
crl::on_main(session, [=] {
|
|
resolve(session, sessionData(session));
|
|
});
|
|
}
|
|
|
|
void DownloadManager::checkFullResolveDone() {
|
|
if (_loadedResolveDone.current()) {
|
|
return;
|
|
}
|
|
for (const auto &[session, data] : _sessions) {
|
|
if (data.resolveSentTotal < data.resolveNeeded
|
|
|| data.resolveSentRequests > 0) {
|
|
return;
|
|
}
|
|
}
|
|
_loadedResolveDone = true;
|
|
}
|
|
|
|
void DownloadManager::generateEntry(
|
|
not_null<Main::Session*> session,
|
|
DownloadedId &id) {
|
|
Expects(!id.object);
|
|
|
|
const auto info = QFileInfo(id.path);
|
|
const auto document = session->data().document(
|
|
base::RandomValue<DocumentId>(),
|
|
0, // accessHash
|
|
QByteArray(), // fileReference
|
|
TimeId(id.started / 1000),
|
|
QVector<MTPDocumentAttribute>(
|
|
1,
|
|
MTP_documentAttributeFilename(
|
|
MTP_string(info.fileName()))),
|
|
Core::MimeTypeForFile(info).name(),
|
|
InlineImageLocation(), // inlineThumbnail
|
|
ImageWithLocation(), // thumbnail
|
|
ImageWithLocation(), // videoThumbnail
|
|
false, // isPremiumSticker
|
|
0, // dc
|
|
id.size);
|
|
document->setLocation(Core::FileLocation(info));
|
|
_generatedDocuments.emplace(document);
|
|
|
|
id.object = std::make_unique<DownloadObject>(DownloadObject{
|
|
.item = generateFakeItem(document),
|
|
.document = document,
|
|
});
|
|
_loaded.emplace(id.object->item);
|
|
}
|
|
|
|
auto DownloadManager::loadedAdded() const
|
|
-> rpl::producer<not_null<const DownloadedId*>> {
|
|
return _loadedAdded.events();
|
|
}
|
|
|
|
auto DownloadManager::loadedRemoved() const
|
|
-> rpl::producer<not_null<const HistoryItem*>> {
|
|
return _loadedRemoved.events();
|
|
}
|
|
|
|
void DownloadManager::remove(
|
|
SessionData &data,
|
|
std::vector<DownloadingId>::iterator i) {
|
|
const auto now = DownloadProgress{
|
|
.ready = _loadingProgress.current().ready - i->ready,
|
|
.total = _loadingProgress.current().total - i->total,
|
|
};
|
|
_loading.remove(i->object.item);
|
|
_loadingDone.remove(i->object.item);
|
|
if (const auto document = i->object.document) {
|
|
_loadingDocuments.remove(document);
|
|
}
|
|
data.downloading.erase(i);
|
|
_loadingListChanges.fire({});
|
|
_loadingProgress = now;
|
|
if (_loading.empty() && !_loadingDone.empty()) {
|
|
_clearLoadingTimer.callOnce(kClearLoadingTimeout);
|
|
}
|
|
}
|
|
|
|
void DownloadManager::cancel(
|
|
SessionData &data,
|
|
std::vector<DownloadingId>::iterator i) {
|
|
const auto object = i->object;
|
|
const auto item = object.item;
|
|
remove(data, i);
|
|
if (!item->isAdminLogEntry()) {
|
|
if (const auto document = object.document) {
|
|
document->cancel();
|
|
} else if (const auto photo = object.photo) {
|
|
photo->cancel();
|
|
}
|
|
}
|
|
}
|
|
|
|
void DownloadManager::changed(not_null<const HistoryItem*> item) {
|
|
if (_loaded.contains(item)) {
|
|
auto &data = sessionData(item);
|
|
const auto i = ranges::find(data.downloaded, item.get(), ByItem);
|
|
Assert(i != end(data.downloaded));
|
|
|
|
const auto media = item->media();
|
|
const auto photo = media ? media->photo() : nullptr;
|
|
const auto document = media ? media->document() : nullptr;
|
|
if (i->object->photo != photo || i->object->document != document) {
|
|
detach(*i);
|
|
}
|
|
}
|
|
if (_loading.contains(item) || _loadingDone.contains(item)) {
|
|
check(item);
|
|
}
|
|
}
|
|
|
|
void DownloadManager::removed(not_null<const HistoryItem*> item) {
|
|
if (_loaded.contains(item)) {
|
|
auto &data = sessionData(item);
|
|
const auto i = ranges::find(data.downloaded, item.get(), ByItem);
|
|
Assert(i != end(data.downloaded));
|
|
detach(*i);
|
|
}
|
|
if (_loading.contains(item) || _loadingDone.contains(item)) {
|
|
auto &data = sessionData(item);
|
|
const auto i = ranges::find(data.downloading, item, ByItem);
|
|
Assert(i != end(data.downloading));
|
|
|
|
// We don't want to download files without messages.
|
|
// For example, there is no way to refresh a file reference for them.
|
|
//entry.object.item = nullptr;
|
|
cancel(data, i);
|
|
}
|
|
}
|
|
|
|
not_null<HistoryItem*> DownloadManager::regenerateItem(
|
|
const DownloadObject &previous) {
|
|
return generateItem(previous.item, previous.document, previous.photo);
|
|
}
|
|
|
|
not_null<HistoryItem*> DownloadManager::generateFakeItem(
|
|
not_null<DocumentData*> document) {
|
|
return generateItem(nullptr, document, nullptr);
|
|
}
|
|
|
|
not_null<HistoryItem*> DownloadManager::generateItem(
|
|
HistoryItem *previousItem,
|
|
DocumentData *document,
|
|
PhotoData *photo) {
|
|
Expects(document || photo);
|
|
|
|
const auto session = document
|
|
? &document->session()
|
|
: &photo->session();
|
|
const auto fromId = previousItem
|
|
? previousItem->from()->id
|
|
: session->userPeerId();
|
|
const auto history = previousItem
|
|
? previousItem->history()
|
|
: session->data().history(session->user());
|
|
const auto flags = MessageFlag::FakeHistoryItem;
|
|
const auto replyTo = FullReplyTo();
|
|
const auto viaBotId = UserId();
|
|
const auto date = base::unixtime::now();
|
|
const auto caption = TextWithEntities();
|
|
const auto make = [&](const auto media) {
|
|
return history->makeMessage(
|
|
history->nextNonHistoryEntryId(),
|
|
flags,
|
|
replyTo,
|
|
viaBotId,
|
|
date,
|
|
fromId,
|
|
QString(),
|
|
media,
|
|
caption,
|
|
HistoryMessageMarkupData());
|
|
};
|
|
const auto result = document ? make(document) : make(photo);
|
|
_generated.emplace(result);
|
|
return result;
|
|
}
|
|
|
|
void DownloadManager::detach(DownloadedId &id) {
|
|
Expects(id.object != nullptr);
|
|
Expects(_loaded.contains(id.object->item));
|
|
Expects(!_generated.contains(id.object->item));
|
|
|
|
// Maybe generate new document?
|
|
const auto was = id.object->item;
|
|
const auto now = regenerateItem(*id.object);
|
|
_loaded.remove(was);
|
|
_loaded.emplace(now);
|
|
id.object->item = now;
|
|
|
|
_loadedRemoved.fire_copy(was);
|
|
_loadedAdded.fire_copy(&id);
|
|
}
|
|
|
|
DownloadManager::SessionData &DownloadManager::sessionData(
|
|
not_null<Main::Session*> session) {
|
|
const auto i = _sessions.find(session);
|
|
Assert(i != end(_sessions));
|
|
return i->second;
|
|
}
|
|
|
|
const DownloadManager::SessionData &DownloadManager::sessionData(
|
|
not_null<Main::Session*> session) const {
|
|
const auto i = _sessions.find(session);
|
|
Assert(i != end(_sessions));
|
|
return i->second;
|
|
}
|
|
|
|
DownloadManager::SessionData &DownloadManager::sessionData(
|
|
not_null<const HistoryItem*> item) {
|
|
return sessionData(&item->history()->session());
|
|
}
|
|
|
|
DownloadManager::SessionData &DownloadManager::sessionData(
|
|
not_null<DocumentData*> document) {
|
|
return sessionData(&document->session());
|
|
}
|
|
|
|
void DownloadManager::writePostponed(not_null<Main::Session*> session) {
|
|
session->account().local().updateDownloads(serializator(session));
|
|
}
|
|
|
|
Fn<std::optional<QByteArray>()> DownloadManager::serializator(
|
|
not_null<Main::Session*> session) const {
|
|
return [this, weak = base::make_weak(session)]()
|
|
-> std::optional<QByteArray> {
|
|
const auto strong = weak.get();
|
|
if (!strong) {
|
|
return std::nullopt;
|
|
} else if (!_sessions.contains(strong)) {
|
|
return QByteArray();
|
|
}
|
|
auto result = QByteArray();
|
|
const auto &data = sessionData(strong);
|
|
const auto count = data.downloaded.size();
|
|
const auto constant = sizeof(quint64) // download.objectId
|
|
+ sizeof(qint32) // download.type
|
|
+ sizeof(qint64) // started
|
|
+ sizeof(quint32) // size
|
|
+ sizeof(quint64) // itemId.peer
|
|
+ sizeof(qint64) // itemId.msg
|
|
+ sizeof(quint64); // peerAccessHash
|
|
auto size = sizeof(qint32) // count
|
|
+ count * constant;
|
|
for (const auto &id : data.downloaded) {
|
|
size += Serialize::stringSize(id.path);
|
|
}
|
|
result.reserve(size);
|
|
|
|
auto stream = QDataStream(&result, QIODevice::WriteOnly);
|
|
stream.setVersion(QDataStream::Qt_5_1);
|
|
stream << qint32(count);
|
|
for (const auto &id : data.downloaded) {
|
|
stream
|
|
<< quint64(id.download.objectId)
|
|
<< qint32(id.download.type)
|
|
<< qint64(id.started)
|
|
// FileSize: Right now any file size fits 32 bit.
|
|
<< quint32(id.size)
|
|
<< quint64(id.itemId.peer.value)
|
|
<< qint64(id.itemId.msg.bare)
|
|
<< quint64(id.peerAccessHash)
|
|
<< id.path;
|
|
}
|
|
stream.device()->close();
|
|
|
|
return result;
|
|
};
|
|
}
|
|
|
|
std::vector<DownloadedId> DownloadManager::deserialize(
|
|
not_null<Main::Session*> session) const {
|
|
const auto serialized = session->account().local().downloadsSerialized();
|
|
if (serialized.isEmpty()) {
|
|
return {};
|
|
}
|
|
|
|
QDataStream stream(serialized);
|
|
stream.setVersion(QDataStream::Qt_5_1);
|
|
|
|
auto count = qint32();
|
|
stream >> count;
|
|
if (stream.status() != QDataStream::Ok || count <= 0 || count > 99'999) {
|
|
return {};
|
|
}
|
|
auto result = std::vector<DownloadedId>();
|
|
result.reserve(count);
|
|
for (auto i = 0; i != count; ++i) {
|
|
auto downloadObjectId = quint64();
|
|
auto uncheckedDownloadType = qint32();
|
|
auto started = qint64();
|
|
// FileSize: Right now any file size fits 32 bit.
|
|
auto size = quint32();
|
|
auto itemIdPeer = quint64();
|
|
auto itemIdMsg = qint64();
|
|
auto peerAccessHash = quint64();
|
|
auto path = QString();
|
|
stream
|
|
>> downloadObjectId
|
|
>> uncheckedDownloadType
|
|
>> started
|
|
>> size
|
|
>> itemIdPeer
|
|
>> itemIdMsg
|
|
>> peerAccessHash
|
|
>> path;
|
|
const auto downloadType = DownloadType(uncheckedDownloadType);
|
|
if (stream.status() != QDataStream::Ok
|
|
|| path.isEmpty()
|
|
|| size <= 0
|
|
|| size > kMaxFileSize
|
|
|| (downloadType != DownloadType::Document
|
|
&& downloadType != DownloadType::Photo)) {
|
|
return {};
|
|
}
|
|
result.push_back({
|
|
.download = {
|
|
.objectId = downloadObjectId,
|
|
.type = downloadType,
|
|
},
|
|
.started = started,
|
|
.path = path,
|
|
.size = int64(size),
|
|
.itemId = { PeerId(itemIdPeer), MsgId(itemIdMsg) },
|
|
.peerAccessHash = peerAccessHash,
|
|
});
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void DownloadManager::untrack(not_null<Main::Session*> session) {
|
|
const auto i = _sessions.find(session);
|
|
Assert(i != end(_sessions));
|
|
|
|
for (const auto &entry : i->second.downloaded) {
|
|
if (const auto resolved = entry.object.get()) {
|
|
const auto item = resolved->item;
|
|
_loaded.remove(item);
|
|
_generated.remove(item);
|
|
if (const auto document = resolved->document) {
|
|
_generatedDocuments.remove(document);
|
|
}
|
|
}
|
|
}
|
|
while (!i->second.downloading.empty()) {
|
|
remove(i->second, i->second.downloading.end() - 1);
|
|
}
|
|
_sessions.erase(i);
|
|
}
|
|
|
|
rpl::producer<Ui::DownloadBarProgress> MakeDownloadBarProgress() {
|
|
return Core::App().downloadManager().loadingProgressValue(
|
|
) | rpl::map([=](const DownloadProgress &progress) {
|
|
return Ui::DownloadBarProgress{
|
|
.ready = progress.ready,
|
|
.total = progress.total,
|
|
};
|
|
});
|
|
}
|
|
|
|
rpl::producer<Ui::DownloadBarContent> MakeDownloadBarContent() {
|
|
return [](auto consumer) {
|
|
auto lifetime = rpl::lifetime();
|
|
|
|
struct State {
|
|
DocumentData *document = nullptr;
|
|
std::shared_ptr<Data::DocumentMedia> media;
|
|
rpl::lifetime downloadTaskLifetime;
|
|
QImage thumbnail;
|
|
|
|
base::has_weak_ptr guard;
|
|
bool scheduled = false;
|
|
Fn<void()> push;
|
|
};
|
|
|
|
const auto state = lifetime.make_state<State>();
|
|
auto &manager = Core::App().downloadManager();
|
|
|
|
const auto resolveThumbnailRecursive = [=](auto &&self) -> bool {
|
|
if (state->document && !state->document->hasThumbnail()) {
|
|
state->media = nullptr;
|
|
}
|
|
if (!state->media) {
|
|
state->downloadTaskLifetime.destroy();
|
|
if (!state->thumbnail.isNull()) {
|
|
return false;
|
|
}
|
|
state->thumbnail = QImage();
|
|
return true;
|
|
}
|
|
if (const auto image = state->media->thumbnail()) {
|
|
state->thumbnail = image->original();
|
|
state->downloadTaskLifetime.destroy();
|
|
state->media = nullptr;
|
|
return true;
|
|
} else if (const auto embed = state->media->thumbnailInline()) {
|
|
if (!state->thumbnail.isNull()) {
|
|
return false;
|
|
}
|
|
state->thumbnail = Images::Prepare(embed->original(), 0, {
|
|
.options = Images::Option::Blur,
|
|
});
|
|
}
|
|
state->document->session().downloaderTaskFinished(
|
|
) | rpl::filter([=] {
|
|
return self(self);
|
|
}) | rpl::start_with_next(
|
|
state->push,
|
|
state->downloadTaskLifetime);
|
|
return !state->thumbnail.isNull();
|
|
};
|
|
const auto resolveThumbnail = [=] {
|
|
return resolveThumbnailRecursive(resolveThumbnailRecursive);
|
|
};
|
|
|
|
const auto notify = [=, &manager] {
|
|
auto content = Ui::DownloadBarContent();
|
|
auto single = (const Data::DownloadObject*) nullptr;
|
|
for (const auto id : manager.loadingList()) {
|
|
if (id->hiddenByView) {
|
|
break;
|
|
}
|
|
if (!single) {
|
|
single = &id->object;
|
|
}
|
|
++content.count;
|
|
if (id->done) {
|
|
++content.done;
|
|
}
|
|
}
|
|
if (content.count == 1) {
|
|
const auto document = single->document;
|
|
const auto thumbnailed = (single->item
|
|
&& document->hasThumbnail())
|
|
? document
|
|
: nullptr;
|
|
if (state->document != thumbnailed) {
|
|
state->document = thumbnailed;
|
|
state->media = thumbnailed
|
|
? thumbnailed->createMediaView()
|
|
: nullptr;
|
|
if (const auto raw = state->media.get()) {
|
|
raw->thumbnailWanted(single->item->fullId());
|
|
}
|
|
state->thumbnail = QImage();
|
|
resolveThumbnail();
|
|
}
|
|
content.singleName = Ui::Text::FormatDownloadsName(
|
|
document);
|
|
content.singleThumbnail = state->thumbnail;
|
|
}
|
|
consumer.put_next(std::move(content));
|
|
};
|
|
state->push = [=] {
|
|
if (state->scheduled) {
|
|
return;
|
|
}
|
|
state->scheduled = true;
|
|
Ui::PostponeCall(&state->guard, [=] {
|
|
state->scheduled = false;
|
|
notify();
|
|
});
|
|
};
|
|
|
|
manager.loadingListChanges(
|
|
) | rpl::filter([=] {
|
|
return !state->scheduled;
|
|
}) | rpl::start_with_next(state->push, lifetime);
|
|
|
|
notify();
|
|
return lifetime;
|
|
};
|
|
}
|
|
|
|
} // namespace Data
|