From 03a868a6e3d4d6fd3f2b135b5d79ed35de4b78cd Mon Sep 17 00:00:00 2001 From: John Preston Date: Mon, 22 Mar 2021 16:32:40 +0400 Subject: [PATCH] Allow skipping stuck files in data export. Fixes #6423. --- Telegram/Resources/langs/lang.strings | 1 + .../SourceFiles/export/export_api_wrap.cpp | 58 +++++++++++++++---- Telegram/SourceFiles/export/export_api_wrap.h | 2 + .../SourceFiles/export/export_controller.cpp | 26 ++++++++- .../SourceFiles/export/export_controller.h | 2 + Telegram/SourceFiles/export/view/export.style | 3 +- .../export/view/export_view_content.cpp | 18 ++++-- .../export/view/export_view_content.h | 1 + .../view/export_view_panel_controller.cpp | 5 ++ .../export/view/export_view_progress.cpp | 47 ++++++++++++++- .../export/view/export_view_progress.h | 9 +++ 11 files changed, 150 insertions(+), 22 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 2e3f5284d1..99b4ff0847 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -2474,6 +2474,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_export_state_chats_list" = "Processing chats..."; "lng_export_state_chats" = "Chats"; "lng_export_state_ready_progress" = "{ready} / {total}"; +"lng_export_skip_file" = "Skip this file"; "lng_export_progress" = "You can close this window now. Please don't quit Telegram until the data export is completed."; "lng_export_stop" = "Stop"; "lng_export_sure_stop" = "Are you sure you want to stop exporting your data?\n\nIf you do, you'll need to start over."; diff --git a/Telegram/SourceFiles/export/export_api_wrap.cpp b/Telegram/SourceFiles/export/export_api_wrap.cpp index fa076a80fc..1a89a95681 100644 --- a/Telegram/SourceFiles/export/export_api_wrap.cpp +++ b/Telegram/SourceFiles/export/export_api_wrap.cpp @@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "mtproto/mtproto_response.h" #include "base/value_ordering.h" #include "base/bytes.h" +#include "base/openssl_help.h" #include #include @@ -182,6 +183,7 @@ struct ApiWrap::FileProcess { Fn progress; FnMut done; + uint64 randomId = 0; Data::FileLocation location; Data::FileOrigin origin; int offset = 0; @@ -192,6 +194,7 @@ struct ApiWrap::FileProcess { QByteArray bytes; }; std::deque requests; + mtpRequestId requestId = 0; }; struct ApiWrap::FileProgress { @@ -383,6 +386,7 @@ auto ApiWrap::fileRequest(const Data::FileLocation &location, int offset) { Expects(location.dcId != 0 || location.data.type() == mtpc_inputTakeoutFileLocation); Expects(_takeoutId.has_value()); + Expects(_fileProcess->requestId == 0); return std::move(_mtp.request(MTPInvokeWithTakeout( MTP_long(*_takeoutId), @@ -392,6 +396,7 @@ auto ApiWrap::fileRequest(const Data::FileLocation &location, int offset) { MTP_int(offset), MTP_int(kFileChunkSize)) )).fail([=](const MTP::Error &result) { + _fileProcess->requestId = 0; if (result.type() == qstr("TAKEOUT_FILE_EMPTY") && _otherDataProcess != nullptr) { filePartDone( @@ -853,6 +858,7 @@ bool ApiWrap::loadUserpicProgress(FileProgress progress) { < _userpicsProcess->slice->list.size())); return _userpicsProcess->fileProgress(DownloadProgress{ + _fileProcess->randomId, _fileProcess->relativePath, _userpicsProcess->fileIndex, progress.ready, @@ -1061,6 +1067,17 @@ void ApiWrap::finishExport(FnMut done) { )).done(std::move(done)).send(); } +void ApiWrap::skipFile(uint64 randomId) { + if (!_fileProcess || _fileProcess->randomId != randomId) { + return; + } + LOG(("Export Info: File skipped.")); + Assert(!_fileProcess->requests.empty()); + Assert(_fileProcess->requestId != 0); + _mtp.request(base::take(_fileProcess->requestId)).cancel(); + base::take(_fileProcess)->done(QString()); +} + void ApiWrap::cancelExportFast() { if (_takeoutId.has_value()) { const auto requestId = mainRequest(MTPaccount_FinishTakeoutSession( @@ -1591,10 +1608,11 @@ bool ApiWrap::loadMessageFileProgress(FileProgress progress) { && (_chatProcess->fileIndex < _chatProcess->slice->list.size())); return _chatProcess->fileProgress(DownloadProgress{ - _fileProcess->relativePath, - _chatProcess->fileIndex, - progress.ready, - progress.total }); + .randomId = _fileProcess->randomId, + .path = _fileProcess->relativePath, + .itemIndex = _chatProcess->fileIndex, + .ready = progress.ready, + .total = progress.total }); } void ApiWrap::loadMessageFileDone(const QString &relativePath) { @@ -1740,6 +1758,8 @@ void ApiWrap::loadFile( } loadFilePart(); + + Ensures(_fileProcess->requestId != 0); } auto ApiWrap::prepareFileProcess( @@ -1758,11 +1778,13 @@ auto ApiWrap::prepareFileProcess( result->location = file.location; result->size = file.size; result->origin = origin; + result->randomId = openssl::RandomValue(); return result; } void ApiWrap::loadFilePart() { if (!_fileProcess + || _fileProcess->requestId || _fileProcess->requests.size() >= kFileRequestsCount || (_fileProcess->size > 0 && _fileProcess->offset >= _fileProcess->size)) { @@ -1771,16 +1793,18 @@ void ApiWrap::loadFilePart() { const auto offset = _fileProcess->offset; _fileProcess->requests.push_back({ offset }); - fileRequest( + _fileProcess->requestId = fileRequest( _fileProcess->location, _fileProcess->offset ).done([=](const MTPupload_File &result) { + _fileProcess->requestId = 0; filePartDone(offset, result); }).send(); _fileProcess->offset += kFileChunkSize; if (_fileProcess->size > 0 && _fileProcess->requests.size() < kFileRequestsCount) { + // Only one request at a time supported right now. //const auto runner = _runner; //crl::on_main([=] { // QTimer::singleShot(kFileNextRequestDelay, [=] { @@ -1854,6 +1878,7 @@ void ApiWrap::filePartDone(int offset, const MTPupload_File &result) { void ApiWrap::filePartRefreshReference(int offset) { Expects(_fileProcess != nullptr); + Expects(_fileProcess->requestId == 0); const auto &origin = _fileProcess->origin; if (!origin.messageId) { @@ -1870,26 +1895,33 @@ void ApiWrap::filePartRefreshReference(int offset) { origin.peer.c_inputPeerChannelFromMessage().vpeer(), origin.peer.c_inputPeerChannelFromMessage().vmsg_id(), origin.peer.c_inputPeerChannelFromMessage().vchannel_id()); - mainRequest(MTPchannels_GetMessages( + _fileProcess->requestId = mainRequest(MTPchannels_GetMessages( channel, MTP_vector( 1, MTP_inputMessageID(MTP_int(origin.messageId))) )).fail([=](const MTP::Error &error) { + _fileProcess->requestId = 0; filePartUnavailable(); return true; }).done([=](const MTPmessages_Messages &result) { + _fileProcess->requestId = 0; filePartExtractReference(offset, result); }).send(); } else { - splitRequest(origin.split, MTPmessages_GetMessages( - MTP_vector( - 1, - MTP_inputMessageID(MTP_int(origin.messageId))) - )).fail([=](const MTP::Error &error) { + _fileProcess->requestId = splitRequest( + origin.split, + MTPmessages_GetMessages( + MTP_vector( + 1, + MTP_inputMessageID(MTP_int(origin.messageId))) + ) + ).fail([=](const MTP::Error &error) { + _fileProcess->requestId = 0; filePartUnavailable(); return true; }).done([=](const MTPmessages_Messages &result) { + _fileProcess->requestId = 0; filePartExtractReference(offset, result); }).send(); } @@ -1899,6 +1931,7 @@ void ApiWrap::filePartExtractReference( int offset, const MTPmessages_Messages &result) { Expects(_fileProcess != nullptr); + Expects(_fileProcess->requestId == 0); result.match([&](const MTPDmessages_messagesNotModified &data) { error("Unexpected messagesNotModified received."); @@ -1922,10 +1955,11 @@ void ApiWrap::filePartExtractReference( _fileProcess->location, message.thumb().file.location); if (refresh1 || refresh2) { - fileRequest( + _fileProcess->requestId = fileRequest( _fileProcess->location, offset ).done([=](const MTPupload_File &result) { + _fileProcess->requestId = 0; filePartDone(offset, result); }).send(); return; diff --git a/Telegram/SourceFiles/export/export_api_wrap.h b/Telegram/SourceFiles/export/export_api_wrap.h index f1b43cf619..7310e7a5b5 100644 --- a/Telegram/SourceFiles/export/export_api_wrap.h +++ b/Telegram/SourceFiles/export/export_api_wrap.h @@ -60,6 +60,7 @@ public: FnMut done); struct DownloadProgress { + uint64 randomId = 0; QString path; int itemIndex = 0; int ready = 0; @@ -83,6 +84,7 @@ public: FnMut done); void finishExport(FnMut done); + void skipFile(uint64 randomId); void cancelExportFast(); ~ApiWrap(); diff --git a/Telegram/SourceFiles/export/export_controller.cpp b/Telegram/SourceFiles/export/export_controller.cpp index fcef0bea2d..516c75cf73 100644 --- a/Telegram/SourceFiles/export/export_controller.cpp +++ b/Telegram/SourceFiles/export/export_controller.cpp @@ -51,12 +51,14 @@ public: void startExport( const Settings &settings, const Environment &environment); + void skipFile(uint64 randomId); void cancelExportFast(); private: using Step = ProcessingState::Step; using DownloadProgress = ApiWrap::DownloadProgress; + [[nodiscard]] bool stopped() const; void setState(State &&state); void ioError(const QString &path); bool ioCatchError(Output::Result result); @@ -166,8 +168,15 @@ rpl::producer ControllerObject::state() const { }); } +bool ControllerObject::stopped() const { + return v::is(_state) + || v::is(_state) + || v::is(_state) + || v::is(_state); +} + void ControllerObject::setState(State &&state) { - if (v::is(_state)) { + if (stopped()) { return; } _state = std::move(state); @@ -245,6 +254,13 @@ void ControllerObject::startExport( exportNext(); } +void ControllerObject::skipFile(uint64 randomId) { + if (stopped()) { + return; + } + _api.skipFile(randomId); +} + void ControllerObject::fillExportSteps() { using Type = Settings::Type; _steps.push_back(Step::Initializing); @@ -518,6 +534,7 @@ ProcessingState ControllerObject::stateUserpics( result.entityIndex = _userpicsWritten + progress.itemIndex; result.entityCount = std::max(_userpicsCount, result.entityIndex); result.bytesType = ProcessingState::FileType::Photo; + result.bytesRandomId = progress.randomId; if (!progress.path.isEmpty()) { const auto last = progress.path.lastIndexOf('/'); result.bytesName = progress.path.mid(last + 1); @@ -570,6 +587,7 @@ void ControllerObject::fillMessagesState( result.itemIndex = _messagesWritten + progress.itemIndex; result.itemCount = std::max(_messagesCount, result.itemIndex); result.bytesType = ProcessingState::FileType::File; // TODO + result.bytesRandomId = progress.randomId; if (!progress.path.isEmpty()) { const auto last = progress.path.lastIndexOf('/'); result.bytesName = progress.path.mid(last + 1); @@ -643,6 +661,12 @@ void Controller::startExport( }); } +void Controller::skipFile(uint64 randomId) { + _wrapped.with([=](Implementation &unwrapped) { + unwrapped.skipFile(randomId); + }); +} + void Controller::cancelExportFast() { LOG(("Export Info: Cancelled export.")); diff --git a/Telegram/SourceFiles/export/export_controller.h b/Telegram/SourceFiles/export/export_controller.h index b99c74d375..496c38c31f 100644 --- a/Telegram/SourceFiles/export/export_controller.h +++ b/Telegram/SourceFiles/export/export_controller.h @@ -74,6 +74,7 @@ struct ProcessingState { int itemIndex = 0; int itemCount = 0; + uint64 bytesRandomId = 0; FileType bytesType = FileType::None; QString bytesName; int bytesLoaded = 0; @@ -136,6 +137,7 @@ public: void startExport( const Settings &settings, const Environment &environment); + void skipFile(uint64 randomId); void cancelExportFast(); rpl::lifetime &lifetime(); diff --git a/Telegram/SourceFiles/export/view/export.style b/Telegram/SourceFiles/export/view/export.style index 4059cc3b16..4f74e8b548 100644 --- a/Telegram/SourceFiles/export/view/export.style +++ b/Telegram/SourceFiles/export/view/export.style @@ -50,7 +50,8 @@ exportErrorLabel: FlatLabel(boxLabel) { exportProgressDuration: 200; exportProgressRowHeight: 30px; -exportProgressRowPadding: margins(22px, 10px, 22px, 20px); +exportProgressRowPadding: margins(22px, 10px, 22px, 10px); +exportProgressRowSkip: 10px; exportProgressLabel: FlatLabel(boxLabel) { textFg: windowBoldFg; maxHeight: 20px; diff --git a/Telegram/SourceFiles/export/view/export_view_content.cpp b/Telegram/SourceFiles/export/view/export_view_content.cpp index 753294e461..2d38125257 100644 --- a/Telegram/SourceFiles/export/view/export_view_content.cpp +++ b/Telegram/SourceFiles/export/view/export_view_content.cpp @@ -26,8 +26,9 @@ Content ContentFromState( const QString &id, const QString &label, const QString &info, - float64 progress) { - result.rows.push_back({ id, label, info, progress }); + float64 progress, + uint64 randomId = 0) { + result.rows.push_back({ id, label, info, progress, randomId }); }; const auto pushMain = [&](const QString &label) { const auto info = (state.entityCount > 0) @@ -56,7 +57,10 @@ Content ContentFromState( : addPart(state.entityIndex, state.entityCount); push("main", label, info, doneProgress + addProgress); }; - const auto pushBytes = [&](const QString &id, const QString &label) { + const auto pushBytes = [&]( + const QString &id, + const QString &label, + uint64 randomId) { if (!state.bytesCount) { return; } @@ -64,7 +68,7 @@ Content ContentFromState( const auto info = Ui::FormatDownloadText( state.bytesLoaded, state.bytesCount); - push(id, label, info, progress); + push(id, label, info, progress, randomId); }; switch (state.step) { case Step::Initializing: @@ -80,7 +84,8 @@ Content ContentFromState( pushMain(tr::lng_export_state_userpics(tr::now)); pushBytes( "userpic" + QString::number(state.entityIndex), - state.bytesName); + state.bytesName, + state.bytesRandomId); break; case Step::Contacts: pushMain(tr::lng_export_option_contacts(tr::now)); @@ -117,7 +122,8 @@ Content ContentFromState( + QString::number(state.entityIndex) + '_' + QString::number(state.itemIndex)), - state.bytesName); + state.bytesName, + state.bytesRandomId); break; default: Unexpected("Step in ContentFromState."); } diff --git a/Telegram/SourceFiles/export/view/export_view_content.h b/Telegram/SourceFiles/export/view/export_view_content.h index 7e22b8bec5..551921cb47 100644 --- a/Telegram/SourceFiles/export/view/export_view_content.h +++ b/Telegram/SourceFiles/export/view/export_view_content.h @@ -22,6 +22,7 @@ struct Content { QString label; QString info; float64 progress = 0.; + uint64 randomId = 0; }; std::vector rows; diff --git a/Telegram/SourceFiles/export/view/export_view_panel_controller.cpp b/Telegram/SourceFiles/export/view/export_view_panel_controller.cpp index 9d5244fd4c..30c53872db 100644 --- a/Telegram/SourceFiles/export/view/export_view_panel_controller.cpp +++ b/Telegram/SourceFiles/export/view/export_view_panel_controller.cpp @@ -299,6 +299,11 @@ void PanelController::showProgress() { ContentFromState(_settings.get(), ProcessingState()) ) | rpl::then(progressState())); + progress->skipFileClicks( + ) | rpl::start_with_next([=](uint64 randomId) { + _process->skipFile(randomId); + }, progress->lifetime()); + progress->cancelClicks( ) | rpl::start_with_next([=] { stopWithConfirmation(); diff --git a/Telegram/SourceFiles/export/view/export_view_progress.cpp b/Telegram/SourceFiles/export/view/export_view_progress.cpp index 3fa3ede319..20cff5b7a2 100644 --- a/Telegram/SourceFiles/export/view/export_view_progress.cpp +++ b/Telegram/SourceFiles/export/view/export_view_progress.cpp @@ -18,6 +18,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Export { namespace View { +namespace { + +constexpr auto kShowSkipFileTimeout = 5 * crl::time(1000); + +} // namespace class ProgressWidget::Row : public Ui::RpWidget { public: @@ -235,13 +240,26 @@ ProgressWidget::ProgressWidget( QWidget *parent, rpl::producer content) : RpWidget(parent) -, _body(this) { +, _body(this) +, _fileShowSkipTimer([=] { _skipFile->show(anim::type::normal); }) { widthValue( ) | rpl::start_with_next([=](int width) { _body->resizeToWidth(width); _body->moveToLeft(0, 0); }, _body->lifetime()); + auto skipFileWrap = _body->add(object_ptr( + _body.data(), + st::defaultLinkButton.font->height + st::exportProgressRowSkip)); + _skipFile = base::make_unique_q>( + skipFileWrap, + object_ptr( + this, + tr::lng_export_skip_file(tr::now), + st::defaultLinkButton)); + _skipFile->hide(anim::type::instant); + _skipFile->moveToLeft(st::exportProgressRowPadding.left(), 0); + _about = _body->add( object_ptr( this, @@ -262,6 +280,11 @@ ProgressWidget::ProgressWidget( setupBottomButton(_cancel.get()); } +rpl::producer ProgressWidget::skipFileClicks() const { + return _skipFile->entity()->clicks( + ) | rpl::map([=] { return _fileRandomId; }); +} + rpl::producer<> ProgressWidget::cancelClicks() const { return _cancel ? (_cancel->clicks() | rpl::to_empty) @@ -294,14 +317,32 @@ void ProgressWidget::updateState(Content &&content) { if (index < _rows.size()) { _rows[index]->updateData(std::move(row)); } else { + if (index > 0) { + _body->insert( + index * 2 - 1, + object_ptr( + this, + st::exportProgressRowSkip)); + } _rows.push_back(_body->insert( - index, + index * 2, object_ptr(this, std::move(row)), st::exportProgressRowPadding)); _rows.back()->show(); } ++index; } + const auto fileRandomId = !content.rows.empty() + ? content.rows.back().randomId + : uint64(0); + if (_fileRandomId != fileRandomId) { + _fileShowSkipTimer.cancel(); + _skipFile->hide(anim::type::normal); + _fileRandomId = fileRandomId; + if (_fileRandomId) { + _fileShowSkipTimer.callOnce(kShowSkipFileTimeout); + } + } for (const auto count = _rows.size(); index != count; ++index) { _rows[index]->updateData(Content::Row()); } @@ -312,6 +353,8 @@ void ProgressWidget::updateState(Content &&content) { void ProgressWidget::showDone() { _cancel = nullptr; + _skipFile->hide(anim::type::instant); + _fileShowSkipTimer.cancel(); _about->setText(tr::lng_export_about_done(tr::now)); _done = base::make_unique_q( this, diff --git a/Telegram/SourceFiles/export/view/export_view_progress.h b/Telegram/SourceFiles/export/view/export_view_progress.h index f370c34c3e..a33e973169 100644 --- a/Telegram/SourceFiles/export/view/export_view_progress.h +++ b/Telegram/SourceFiles/export/view/export_view_progress.h @@ -10,11 +10,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/rp_widget.h" #include "export/view/export_view_content.h" #include "base/object_ptr.h" +#include "base/timer.h" namespace Ui { class VerticalLayout; class RoundButton; class FlatLabel; +class LinkButton; +template +class FadeWrap; } // namespace Ui namespace Export { @@ -26,6 +30,7 @@ public: QWidget *parent, rpl::producer content); + rpl::producer skipFileClicks() const; rpl::producer<> cancelClicks() const; rpl::producer<> doneClicks() const; @@ -42,11 +47,15 @@ private: object_ptr _body; std::vector> _rows; + base::unique_qptr> _skipFile; QPointer _about; base::unique_qptr _cancel; base::unique_qptr _done; rpl::event_stream<> _doneClicks; + uint64 _fileRandomId = 0; + base::Timer _fileShowSkipTimer; + }; } // namespace View