diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 25abe7f5c6..4b6463f7b9 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -355,14 +355,21 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_settings_section_privacy" = "Privacy and Security"; "lng_local_storage_title" = "Local storage"; -"lng_settings_no_data_cached" = "No cached data found!"; -"lng_settings_images_cached#one" = "{count} image, {size}"; -"lng_settings_images_cached#other" = "{count} images, {size}"; -"lng_settings_audios_cached#one" = "{count} voice message, {size}"; -"lng_settings_audios_cached#other" = "{count} voice messages, {size}"; +"lng_local_storage_empty" = "No cached files"; +"lng_local_storage_image#one" = "{count} image"; +"lng_local_storage_image#other" = "{count} images"; +"lng_local_storage_sticker#one" = "{count} sticker"; +"lng_local_storage_sticker#other" = "{count} stickers"; +"lng_local_storage_voice#one" = "{count} voice message"; +"lng_local_storage_voice#other" = "{count} voice messages"; +"lng_local_storage_round#one" = "{count} video message"; +"lng_local_storage_round#other" = "{count} video messages"; +"lng_local_storage_animation#one" = "{count} animation"; +"lng_local_storage_animation#other" = "{count} animations"; +"lng_local_storage_summary" = "Summary"; +"lng_local_storage_clear_some" = "Clear"; "lng_local_storage_clear" = "Clear all"; "lng_local_storage_clearing" = "Clearing..."; -"lng_local_storage_cleared" = "Cleared!"; "lng_settings_section_advanced_settings" = "Advanced Settings"; "lng_settings_enable_night_theme" = "Enable night mode"; diff --git a/Telegram/SourceFiles/base/flat_map.h b/Telegram/SourceFiles/base/flat_map.h index 2c6a914be1..307f77edc2 100644 --- a/Telegram/SourceFiles/base/flat_map.h +++ b/Telegram/SourceFiles/base/flat_map.h @@ -307,6 +307,15 @@ public: }; + flat_multi_map() = default; + flat_multi_map(const flat_multi_map &other) = default; + flat_multi_map(flat_multi_map &&other) = default; + flat_multi_map &operator=(const flat_multi_map &other) { + auto copy = other; + return (*this = std::move(copy)); + } + flat_multi_map &operator=(flat_multi_map &&other) = default; + size_type size() const { return impl().size(); } diff --git a/Telegram/SourceFiles/base/flat_map_tests.cpp b/Telegram/SourceFiles/base/flat_map_tests.cpp index 8f1ddb18b4..b2fc8991f0 100644 --- a/Telegram/SourceFiles/base/flat_map_tests.cpp +++ b/Telegram/SourceFiles/base/flat_map_tests.cpp @@ -46,6 +46,27 @@ TEST_CASE("flat_maps should keep items sorted by key", "[flat_map]") { } } +TEST_CASE("simple flat_maps tests", "[flat_map]") { + SECTION("copy constructor") { + base::flat_map v; + v.emplace(0, "a"); + v.emplace(2, "b"); + auto u = v; + REQUIRE(u.size() == 2); + REQUIRE(u.find(0) == u.begin()); + REQUIRE(u.find(2) == u.end() - 1); + } + SECTION("assignment") { + base::flat_map v, u; + v.emplace(0, "a"); + v.emplace(2, "b"); + u = v; + REQUIRE(u.size() == 2); + REQUIRE(u.find(0) == u.begin()); + REQUIRE(u.find(2) == u.end() - 1); + } +} + TEST_CASE("flat_maps custom comparator", "[flat_map]") { base::flat_map v; v.emplace({ 0 }, "a"); diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style index 1483b41793..e10dc851a8 100644 --- a/Telegram/SourceFiles/boxes/boxes.style +++ b/Telegram/SourceFiles/boxes/boxes.style @@ -354,7 +354,27 @@ peerListBox: PeerList(defaultPeerList) { } } -localStorageBoxSkip: 10px; +localStorageRowHeight: 50px; +localStorageRowPadding: margins(23px, 5px, 23px, 5px); +localStorageRowTitle: FlatLabel(defaultFlatLabel) { + textFg: windowBoldFg; + maxHeight: 20px; + style: TextStyle(defaultTextStyle) { + font: font(14px semibold); + linkFont: font(14px semibold); + linkFontOver: font(14px semibold); + } +} +localStorageRowSize: FlatLabel(defaultFlatLabel) { + textFg: contactsStatusFg; + maxHeight: 20px; + style: TextStyle(defaultTextStyle) { + font: font(14px); + linkFont: font(14px); + linkFontOver: font(14px); + } +} +localStorageClear: defaultBoxButton; shareRowsTop: 12px; shareRowHeight: 108px; diff --git a/Telegram/SourceFiles/boxes/local_storage_box.cpp b/Telegram/SourceFiles/boxes/local_storage_box.cpp index 476a2d1650..2a7b6fb021 100644 --- a/Telegram/SourceFiles/boxes/local_storage_box.cpp +++ b/Telegram/SourceFiles/boxes/local_storage_box.cpp @@ -8,15 +8,185 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/local_storage_box.h" #include "styles/style_boxes.h" +#include "ui/wrap/vertical_layout.h" +#include "ui/wrap/slide_wrap.h" +#include "ui/widgets/labels.h" #include "ui/widgets/buttons.h" +#include "ui/effects/radial_animation.h" +#include "ui/widgets/shadow.h" #include "storage/localstorage.h" #include "lang/lang_keys.h" #include "mainwindow.h" #include "auth_session.h" #include "layout.h" -LocalStorageBox::LocalStorageBox(QWidget *parent) -: _clear(this, lang(lng_local_storage_clear), st::boxLinkButton) { +class LocalStorageBox::Row : public Ui::RpWidget { +public: + Row( + QWidget *parent, + Fn title, + Fn clear, + const Database::TaggedSummary &data); + + void update(const Database::TaggedSummary &data); + void toggleProgress(bool shown); + + rpl::producer<> clearRequests() const; + +protected: + int resizeGetHeight(int newWidth) override; + void paintEvent(QPaintEvent *e) override; + +private: + QString titleText(const Database::TaggedSummary &data) const; + QString sizeText(const Database::TaggedSummary &data) const; + void step_radial(TimeMs ms, bool timer); + + Fn _titleFactory; + object_ptr _title; + object_ptr _description; + object_ptr _clearing = { nullptr }; + object_ptr _clear; + std::unique_ptr _progress; + +}; + +LocalStorageBox::Row::Row( + QWidget *parent, + Fn title, + Fn clear, + const Database::TaggedSummary &data) +: RpWidget(parent) +, _titleFactory(std::move(title)) +, _title( + this, + titleText(data), + Ui::FlatLabel::InitType::Simple, + st::localStorageRowTitle) +, _description( + this, + sizeText(data), + Ui::FlatLabel::InitType::Simple, + st::localStorageRowSize) +, _clear(this, std::move(clear), st::localStorageClear) { + _clear->setVisible(data.count != 0); +} + +void LocalStorageBox::Row::update(const Database::TaggedSummary &data) { + if (data.count != 0) { + _title->setText(titleText(data)); + } + _description->setText(sizeText(data)); + _clear->setVisible(data.count != 0); +} + +void LocalStorageBox::Row::toggleProgress(bool shown) { + if (!shown) { + _progress = nullptr; + _description->show(); + _clearing.destroy(); + } else if (!_progress) { + _progress = std::make_unique( + animation(this, &Row::step_radial), + st::proxyCheckingAnimation); + _progress->start(); + _clearing = object_ptr( + this, + lang(lng_local_storage_clearing), + Ui::FlatLabel::InitType::Simple, + st::localStorageRowSize); + _clearing->show(); + _description->hide(); + resizeToWidth(width()); + RpWidget::update(); + } +} + +void LocalStorageBox::Row::step_radial(TimeMs ms, bool timer) { + if (timer) { + RpWidget::update(); + } +} + +rpl::producer<> LocalStorageBox::Row::clearRequests() const { + return _clear->clicks(); +} + +int LocalStorageBox::Row::resizeGetHeight(int newWidth) { + const auto height = st::localStorageRowHeight; + const auto padding = st::localStorageRowPadding; + const auto available = newWidth - padding.left() - padding.right(); + _title->resizeToWidth(available); + _description->resizeToWidth(available); + _title->moveToLeft(padding.left(), padding.top(), newWidth); + _description->moveToLeft( + padding.left(), + height - padding.bottom() - _description->height(), + newWidth); + if (_clearing) { + const auto progressShift = st::proxyCheckingPosition.x() + + st::proxyCheckingAnimation.size.width() + + st::proxyCheckingSkip; + _clearing->resizeToWidth(available - progressShift); + _clearing->moveToLeft( + padding.left(),// + progressShift, + _description->y(), + newWidth); + } + _clear->moveToRight( + st::boxButtonPadding.right(), + (height - _clear->height()) / 2, + newWidth); + return height; +} + +void LocalStorageBox::Row::paintEvent(QPaintEvent *e) { + if (!_progress || true) { + return; + } + Painter p(this); + + const auto padding = st::localStorageRowPadding; + const auto height = st::localStorageRowHeight; + const auto bottom = height - padding.bottom() - _description->height(); + _progress->step(crl::time()); + _progress->draw( + p, + { + st::proxyCheckingPosition.x() + padding.left(), + st::proxyCheckingPosition.y() + bottom + }, + width()); +} + +QString LocalStorageBox::Row::titleText(const Database::TaggedSummary &data) const { + return _titleFactory(data.count); +} + +QString LocalStorageBox::Row::sizeText(const Database::TaggedSummary &data) const { + return data.totalSize + ? formatSizeText(data.totalSize) + : lang(lng_local_storage_empty); +} + +LocalStorageBox::LocalStorageBox( + QWidget*, + not_null db, + CreateTag) +: _db(db) { +} + +void LocalStorageBox::Show(not_null db) { + auto shared = std::make_shared>( + Box(db, CreateTag())); + const auto weak = shared->data(); + db->statsOnMain( + ) | rpl::start_with_next([=](Database::Stats &&stats) { + weak->update(std::move(stats)); + if (auto &strong = *shared) { + Ui::show(std::move(strong)); + } + }, weak->lifetime()); } void LocalStorageBox::prepare() { @@ -24,27 +194,110 @@ void LocalStorageBox::prepare() { addButton(langFactory(lng_box_ok), [this] { closeBox(); }); - _clear->setClickedCallback([this] { clearStorage(); }); - - connect(App::wnd(), SIGNAL(tempDirCleared(int)), this, SLOT(onTempDirCleared(int))); - connect(App::wnd(), SIGNAL(tempDirClearFailed(int)), this, SLOT(onTempDirClearFailed(int))); - - subscribe(Auth().downloaderTaskFinished(), [this] { update(); }); - - updateControls(); - - checkLocalStoredCounts(); + setupControls(); } -void LocalStorageBox::updateControls() { - const auto rowsHeight = st::linkFont->height + st::localStorageBoxSkip; - _clear->setVisible(false); - setDimensions(st::boxWidth, st::localStorageBoxSkip + rowsHeight + _clear->height()); - _clear->moveToLeft(st::boxPadding.left(), st::localStorageBoxSkip + rowsHeight); - update(); +void LocalStorageBox::updateRow( + not_null*> row, + Database::TaggedSummary *data) { + const auto summary = (_rows.find(0)->second == row); + const auto shown = (data && data->count && data->totalSize) || summary; + if (shown) { + row->entity()->update(*data); + } + row->toggle(shown, anim::type::normal); } -void LocalStorageBox::checkLocalStoredCounts() { +void LocalStorageBox::update(Database::Stats &&stats) { + _stats = std::move(stats); + if (const auto i = _rows.find(0); i != end(_rows)) { + i->second->entity()->toggleProgress(_stats.clearing); + } + for (const auto &entry : _rows) { + if (entry.first) { + const auto i = _stats.tagged.find(entry.first); + updateRow( + entry.second, + (i != end(_stats.tagged)) ? &i->second : nullptr); + } else { + updateRow(entry.second, &_stats.full); + } + } +} + +void LocalStorageBox::clearByTag(uint8 tag) { + if (tag) { + _db->clearByTag(tag); + } else { + _db->clear(); + } +} + +void LocalStorageBox::setupControls() { + _content.create(this); + + const auto createRow = [&]( + uint8 tag, + Fn title, + Fn clear, + const Database::TaggedSummary &data) { + auto result = _content->add(object_ptr>( + _content, + object_ptr( + _content, + std::move(title), + std::move(clear), + data))); + const auto shown = (data.count && data.totalSize) || !tag; + result->toggle(shown, anim::type::instant); + result->entity()->clearRequests( + ) | rpl::start_with_next([=] { + clearByTag(tag); + }, result->lifetime()); + _rows.emplace(tag, result); + return result; + }; + auto tracker = Ui::MultiSlideTracker(); + const auto createTagRow = [&](uint8 tag, auto &&titleFactory) { + static const auto empty = Database::TaggedSummary(); + const auto i = _stats.tagged.find(tag); + const auto &data = (i != end(_stats.tagged)) ? i->second : empty; + auto factory = std::forward(titleFactory); + auto title = [factory = std::move(factory)](size_type count) { + return factory(lt_count, count); + }; + tracker.track(createRow( + tag, + std::move(title), + langFactory(lng_local_storage_clear_some), + data)); + }; + auto summaryTitle = [](size_type) { + return lang(lng_local_storage_summary); + }; + createRow( + 0, + std::move(summaryTitle), + langFactory(lng_local_storage_clear), + _stats.full); + const auto shadow = _content->add(object_ptr>( + _content, + object_ptr(_content), + st::localStorageRowPadding) + ); + createTagRow(Data::kImageCacheTag, lng_local_storage_image); + createTagRow(Data::kStickerCacheTag, lng_local_storage_sticker); + createTagRow(Data::kVoiceMessageCacheTag, lng_local_storage_voice); + createTagRow(Data::kVideoMessageCacheTag, lng_local_storage_round); + createTagRow(Data::kAnimationCacheTag, lng_local_storage_animation); + shadow->toggleOn( + std::move(tracker).atLeastOneShownValue() + ); + _content->resizeToWidth(st::boxWidth); + _content->heightValue( + ) | rpl::start_with_next([=](int height) { + setDimensions(st::boxWidth, height); + }, _content->lifetime()); } void LocalStorageBox::paintEvent(QPaintEvent *e) { @@ -54,40 +307,4 @@ void LocalStorageBox::paintEvent(QPaintEvent *e) { p.setFont(st::boxTextFont); p.setPen(st::windowFg); - checkLocalStoredCounts(); - auto top = st::localStorageBoxSkip; - p.drawTextLeft(st::boxPadding.left(), top, width(), lang(lng_settings_no_data_cached)); - top += st::boxTextFont->height + st::localStorageBoxSkip; - auto text = ([this]() -> QString { - switch (_state) { - case State::Clearing: return lang(lng_local_storage_clearing); - case State::Cleared: return lang(lng_local_storage_cleared); - case State::ClearFailed: return Lang::Hard::ClearPathFailed(); - } - return QString(); - })(); - if (!text.isEmpty()) { - p.drawTextLeft(st::boxPadding.left(), top, width(), text); - top += st::boxTextFont->height + st::localStorageBoxSkip; - } -} - -void LocalStorageBox::clearStorage() { - App::wnd()->tempDirDelete(Local::ClearManagerStorage); - _state = State::Clearing; - updateControls(); -} - -void LocalStorageBox::onTempDirCleared(int task) { - if (task & Local::ClearManagerStorage) { - _state = State::Cleared; - } - updateControls(); -} - -void LocalStorageBox::onTempDirClearFailed(int task) { - if (task & Local::ClearManagerStorage) { - _state = State::ClearFailed; - } - updateControls(); } diff --git a/Telegram/SourceFiles/boxes/local_storage_box.h b/Telegram/SourceFiles/boxes/local_storage_box.h index 0bd6c3b292..8978ba8281 100644 --- a/Telegram/SourceFiles/boxes/local_storage_box.h +++ b/Telegram/SourceFiles/boxes/local_storage_box.h @@ -8,20 +8,30 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "boxes/abstract_box.h" +#include "storage/cache/storage_cache_database.h" + +namespace Storage { +namespace Cache { +class Database; +} // namespace Cache +} // namespace Storage namespace Ui { -class LinkButton; +class VerticalLayout; +template +class SlideWrap; } // namespace Ui class LocalStorageBox : public BoxContent { - Q_OBJECT + struct CreateTag { + }; public: - LocalStorageBox(QWidget*); + using Database = Storage::Cache::Database; -private slots: - void onTempDirCleared(int task); - void onTempDirClearFailed(int task); + LocalStorageBox(QWidget*, not_null db, CreateTag); + + static void Show(not_null db); protected: void prepare() override; @@ -29,18 +39,19 @@ protected: void paintEvent(QPaintEvent *e) override; private: - void clearStorage(); - void updateControls(); - void checkLocalStoredCounts(); + class Row; - enum class State { - Normal, - Clearing, - Cleared, - ClearFailed, - }; - State _state = State::Normal; + void clearByTag(uint8 tag); + void update(Database::Stats &&stats); + void updateRow( + not_null*> row, + Database::TaggedSummary *data); + void setupControls(); - object_ptr _clear; + not_null _db; + Database::Stats _stats; + + object_ptr _content = { nullptr }; + base::flat_map*>> _rows; }; diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index d3ad605b06..8129215434 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "observer_peer.h" #include "auth_session.h" #include "apiwrap.h" +#include "messenger.h" #include "export/export_controller.h" #include "export/view/export_view_panel_controller.h" #include "window/notifications_manager.h" @@ -67,17 +68,19 @@ void UpdateImage(ImagePtr &old, ImagePtr now) { Session::Session(not_null session) : _session(session) -, _cache(Local::cachePath(), Local::cacheSettings()) +, _cache(Messenger::Instance().databases().get( + Local::cachePath(), + Local::cacheSettings())) , _groups(this) , _unmuteByFinishedTimer([=] { unmuteByFinished(); }) { - _cache.open(Local::cacheKey()); + _cache->open(Local::cacheKey()); setupContactViewsViewer(); setupChannelLeavingViewer(); } Storage::Cache::Database &Session::cache() { - return _cache; + return *_cache; } void Session::startExport(PeerData *peer) { diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h index 4c1991d567..8c9d49e0d8 100644 --- a/Telegram/SourceFiles/data/data_session.h +++ b/Telegram/SourceFiles/data/data_session.h @@ -7,7 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once -#include "storage/cache/storage_cache_database.h" +#include "storage/storage_databases.h" #include "chat_helpers/stickers.h" #include "dialogs/dialogs_key.h" #include "data/data_groups.h" @@ -523,7 +523,7 @@ private: not_null _session; - Storage::Cache::Database _cache; + Storage::DatabasePointer _cache; std::unique_ptr _export; std::unique_ptr _exportPanel; diff --git a/Telegram/SourceFiles/messenger.cpp b/Telegram/SourceFiles/messenger.cpp index d90cf6e64a..a7dd4cf638 100644 --- a/Telegram/SourceFiles/messenger.cpp +++ b/Telegram/SourceFiles/messenger.cpp @@ -30,6 +30,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "passport/passport_form_controller.h" #include "observer_peer.h" #include "storage/file_upload.h" +#include "storage/storage_databases.h" #include "mainwidget.h" #include "mediaview.h" #include "mtproto/dc_options.h" @@ -76,6 +77,7 @@ Messenger::Messenger(not_null launcher) : QObject() , _launcher(launcher) , _private(std::make_unique()) +, _databases(std::make_unique()) , _langpack(std::make_unique()) , _audio(std::make_unique()) , _logo(Window::LoadLogo()) diff --git a/Telegram/SourceFiles/messenger.h b/Telegram/SourceFiles/messenger.h index f3f000d637..b909b29362 100644 --- a/Telegram/SourceFiles/messenger.h +++ b/Telegram/SourceFiles/messenger.h @@ -19,6 +19,10 @@ class Translator; class MediaView; class BoxContent; +namespace Storage { +class Databases; +} // namespace Storage + namespace Core { class Launcher; } // namespace Core @@ -125,6 +129,11 @@ public: void suggestMainDcId(MTP::DcId mainDcId); void destroyStaleAuthorizationKeys(); + // Databases + Storage::Databases &databases() { + return *_databases; + } + // AuthSession component. AuthSession *authSession() { return _authSession.get(); @@ -249,6 +258,7 @@ private: QWidget _globalShortcutParent; + std::unique_ptr _databases; std::unique_ptr _window; std::unique_ptr _mediaView; std::unique_ptr _langpack; diff --git a/Telegram/SourceFiles/rpl/event_stream.h b/Telegram/SourceFiles/rpl/event_stream.h index e0f1aeb9fe..94177d4c1d 100644 --- a/Telegram/SourceFiles/rpl/event_stream.h +++ b/Telegram/SourceFiles/rpl/event_stream.h @@ -34,7 +34,7 @@ public: void fire_copy(const Value &value) const { return fire_forward(value); } -#if defined _MSC_VER && _MSC_VER >= 1914 +#if defined _MSC_VER && _MSC_VER >= 1914 && false producer events() const { #else // _MSC_VER >= 1914 auto events() const { @@ -65,6 +65,9 @@ public: auto events_starting_with_copy(const Value &value) const { return single(value) | then(events()); } + bool has_consumers() const { + return (_data != nullptr) && !_data->consumers.empty(); + } ~event_stream(); diff --git a/Telegram/SourceFiles/settings/settings_advanced_widget.cpp b/Telegram/SourceFiles/settings/settings_advanced_widget.cpp index 1b8b3d28a3..3d6f01d314 100644 --- a/Telegram/SourceFiles/settings/settings_advanced_widget.cpp +++ b/Telegram/SourceFiles/settings/settings_advanced_widget.cpp @@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/confirm_box.h" #include "boxes/about_box.h" #include "boxes/local_storage_box.h" +#include "data/data_session.h" #include "mainwindow.h" #include "ui/widgets/buttons.h" #include "ui/wrap/slide_wrap.h" @@ -85,7 +86,7 @@ void AdvancedWidget::checkNonDefaultTheme() { } void AdvancedWidget::onManageLocalStorage() { - Ui::show(Box()); + LocalStorageBox::Show(&Auth().data().cache()); } #ifndef TDESKTOP_DISABLE_NETWORK_PROXY diff --git a/Telegram/SourceFiles/storage/cache/storage_cache_database.cpp b/Telegram/SourceFiles/storage/cache/storage_cache_database.cpp index 75c98ccf33..fdfd318067 100644 --- a/Telegram/SourceFiles/storage/cache/storage_cache_database.cpp +++ b/Telegram/SourceFiles/storage/cache/storage_cache_database.cpp @@ -16,6 +16,12 @@ Database::Database(const QString &path, const Settings &settings) : _wrapped(path, settings) { } +void Database::reconfigure(const Settings &settings) { + _wrapped.with([settings](Implementation &unwrapped) mutable { + unwrapped.reconfigure(settings); + }); +} + void Database::open(EncryptionKey &&key, FnMut &&done) { _wrapped.with([ key = std::move(key), @@ -125,7 +131,7 @@ void Database::putIfEmpty( void Database::getWithTag( const Key &key, FnMut &&done) { - _wrapped.with([ + _wrapped.with([ key, done = std::move(done) ](Implementation &unwrapped) mutable { @@ -133,11 +139,9 @@ void Database::getWithTag( }); } -void Database::stats(FnMut &&done) { - _wrapped.with([ - done = std::move(done) - ](Implementation &unwrapped) mutable { - unwrapped.stats(std::move(done)); +auto Database::statsOnMain() const -> rpl::producer { + return _wrapped.producer_on_main([](const Implementation &unwrapped) { + return unwrapped.stats(); }); } @@ -149,6 +153,15 @@ void Database::clear(FnMut &&done) { }); } +void Database::clearByTag(uint8 tag, FnMut &&done) { + _wrapped.with([ + tag, + done = std::move(done) + ](Implementation &unwrapped) mutable { + unwrapped.clearByTag(tag, std::move(done)); + }); +} + Database::~Database() = default; } // namespace Cache diff --git a/Telegram/SourceFiles/storage/cache/storage_cache_database.h b/Telegram/SourceFiles/storage/cache/storage_cache_database.h index 46ff32c18b..3e9c4290f8 100644 --- a/Telegram/SourceFiles/storage/cache/storage_cache_database.h +++ b/Telegram/SourceFiles/storage/cache/storage_cache_database.h @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/basic_types.h" #include #include +#include #include namespace Storage { @@ -25,6 +26,8 @@ public: using Settings = details::Settings; Database(const QString &path, const Settings &settings); + void reconfigure(const Settings &settings); + void open(EncryptionKey &&key, FnMut &&done = nullptr); void close(FnMut &&done = nullptr); @@ -60,9 +63,11 @@ public: void getWithTag(const Key &key, FnMut &&done); using Stats = details::Stats; - void stats(FnMut &&done); + using TaggedSummary = details::TaggedSummary; + rpl::producer statsOnMain() const; void clear(FnMut &&done = nullptr); + void clearByTag(uint8 tag, FnMut &&done = nullptr); ~Database(); diff --git a/Telegram/SourceFiles/storage/cache/storage_cache_database_object.cpp b/Telegram/SourceFiles/storage/cache/storage_cache_database_object.cpp index 4d3b0024f7..caa2517df8 100644 --- a/Telegram/SourceFiles/storage/cache/storage_cache_database_object.cpp +++ b/Telegram/SourceFiles/storage/cache/storage_cache_database_object.cpp @@ -82,6 +82,18 @@ DatabaseObject::DatabaseObject( , _settings(settings) , _writeBundlesTimer(_weak, [=] { writeBundles(); checkCompactor(); }) , _pruneTimer(_weak, [=] { prune(); }) { + checkSettings(); +} + +void DatabaseObject::reconfigure(const Settings &settings) { + Expects(_key.empty()); + + _settings = settings; + checkSettings(); +} + +void DatabaseObject::checkSettings() { + Expects(_settings.staleRemoveChunk > 0); Expects(_settings.maxDataSize > 0 && _settings.maxDataSize < kDataSizeLimit); Expects(_settings.maxBundledRecords > 0 @@ -124,7 +136,9 @@ Error DatabaseObject::openSomeBinlog(EncryptionKey &&key) { case File::Result::LockFailed: return Error{ Error::Type::LockFailed, binlogPath(version) }; case File::Result::WrongKey: - return Error{ Error::Type::WrongKey, binlogPath(version) }; + return _settings.clearOnWrongKey + ? openNewBinlog(key) + : Error{ Error::Type::WrongKey, binlogPath(version) }; } Unexpected("Result from DatabaseObject::openBinlog."); } @@ -290,8 +304,8 @@ bool DatabaseObject::startDelayedPruning() { if (_settings.totalSizeLimit > 0 && _totalSize > _settings.totalSizeLimit) { return true; - } else if (_minimalEntryTime != 0 - && _minimalEntryTime <= before) { + } else if ((!_minimalEntryTime && !_map.empty()) + || _minimalEntryTime <= before) { return true; } return false; @@ -315,17 +329,77 @@ bool DatabaseObject::startDelayedPruning() { } void DatabaseObject::prune() { + if (!_stale.empty()) { + return; + } auto stale = base::flat_set(); auto staleTotalSize = int64(); - collectTimePrune(stale, staleTotalSize); - collectSizePrune(stale, staleTotalSize); + collectTimeStale(stale, staleTotalSize); + collectSizeStale(stale, staleTotalSize); + if (stale.size() <= _settings.staleRemoveChunk) { + clearStaleNow(stale); + } else { + _stale = ranges::view::all(stale) | ranges::to_vector; + startStaleClear(); + } +} + +void DatabaseObject::startStaleClear() { + // Report "Clearing..." status. + pushStats(); + clearStaleChunk(); +} + +void DatabaseObject::clearStaleNow(const base::flat_set &stale) { + if (stale.empty()) { + return; + } + + // Report "Clearing..." status. + _stale.push_back(stale.front()); + pushStats(); + for (const auto &key : stale) { remove(key, nullptr); } + + // Report correct status async. + _stale.clear(); optimize(); } -void DatabaseObject::collectTimePrune( +void DatabaseObject::clearStaleChunkDelayed() { + if (_clearingStale) { + return; + } + _clearingStale = true; + _weak.with([](DatabaseObject &that) { + if (base::take(that._clearingStale)) { + that.clearStaleChunk(); + } + }); +} + +void DatabaseObject::clearStaleChunk() { + if (_stale.empty()) { + return; + } + const auto stale = gsl::make_span(_stale); + const auto count = size_type(stale.size()); + const auto clear = std::min(count, _settings.staleRemoveChunk); + for (const auto &key : stale.subspan(count - clear)) { + remove(key, nullptr); + } + _stale.resize(count - clear); + if (_stale.empty()) { + base::take(_stale); + optimize(); + } else { + clearStaleChunkDelayed(); + } +} + +void DatabaseObject::collectTimeStale( base::flat_set &stale, int64 &staleTotalSize) { if (!_settings.totalTimeLimit) { @@ -351,7 +425,7 @@ void DatabaseObject::collectTimePrune( } } -void DatabaseObject::collectSizePrune( +void DatabaseObject::collectSizeStale( base::flat_set &stale, int64 &staleTotalSize) { const auto removeSize = (_settings.totalSizeLimit > 0) @@ -516,7 +590,9 @@ void DatabaseObject::setMapEntry(const Key &key, Entry &&entry) { ++_entriesWithMinimalTimeCount; } else if (already.useTime == _minimalEntryTime) { Assert(_entriesWithMinimalTimeCount > 0); - --_entriesWithMinimalTimeCount; + if (!--_entriesWithMinimalTimeCount) { + _minimalEntryTime = 0; + } } } already = std::move(entry); @@ -542,6 +618,25 @@ void DatabaseObject::updateStats(const Entry &was, const Entry &now) { summary.totalSize -= was.size; } } + pushStatsDelayed(); +} + +void DatabaseObject::pushStatsDelayed() { + if (_pushingStats) { + return; + } + _pushingStats = true; + _weak.with([](DatabaseObject &that) { + if (base::take(that._pushingStats)) { + that.pushStats(); + } + }); +} + +void DatabaseObject::pushStats() { + if (_stats.has_consumers()) { + _stats.fire(collectStats()); + } } void DatabaseObject::eraseMapEntry(const Map::const_iterator &i) { @@ -550,7 +645,9 @@ void DatabaseObject::eraseMapEntry(const Map::const_iterator &i) { updateStats(entry, Entry()); if (_minimalEntryTime != 0 && entry.useTime == _minimalEntryTime) { Assert(_entriesWithMinimalTimeCount > 0); - --_entriesWithMinimalTimeCount; + if (!--_entriesWithMinimalTimeCount) { + _minimalEntryTime = 0; + } } _map.erase(i); } @@ -629,14 +726,29 @@ void DatabaseObject::compactorFail() { void DatabaseObject::close(FnMut &&done) { if (_binlog.isOpen()) { writeBundles(); + _binlog.close(); } + invokeCallback(done); + clearState(); +} + +void DatabaseObject::clearState() { + _path = QString(); + _key = {}; + _map = {}; + _removing = {}; + _accessed = {}; + _time = {}; + _binlogExcessLength = 0; + _totalSize = 0; + _minimalEntryTime = 0; + _entriesWithMinimalTimeCount = 0; + _taggedStats = {}; + _pushingStats = false; + _writeBundlesTimer.cancel(); + _pruneTimer.cancel(); _cleaner = CleanerWrap(); _compactor = CompactorWrap(); - _binlog.close(); - _key = EncryptionKey(); - invokeCallback(done); - _map.clear(); - _binlogExcessLength = 0; } void DatabaseObject::put( @@ -648,6 +760,7 @@ void DatabaseObject::put( return; } _removing.erase(key); + _stale.erase(ranges::remove(_stale, key), end(_stale)); const auto checksum = CountChecksum(bytes::make_span(value.bytes)); const auto maybepath = writeKeyPlace(key, value, checksum); @@ -762,6 +875,7 @@ Error DatabaseObject::writeExistingPlaceGeneric( const Key &key, const Entry &entry) { record.key = key; + record.tag = entry.tag; record.size = ReadTo(entry.size); record.checksum = entry.checksum; if (const auto i = _map.find(key); i != end(_map)) { @@ -927,20 +1041,22 @@ void DatabaseObject::moveIfEmpty( invokeCallback(done, result); return; } + _removing.erase(to); + _stale.erase(ranges::remove(_stale, to), end(_stale)); invokeCallback(done, writeExistingPlace(to, entry)); } -void DatabaseObject::stats(FnMut &&done) { - auto result = _taggedStats; - auto zero = TaggedSummary(); - zero.count = _map.size(); - zero.totalSize = _totalSize; - for (const auto &summary : result) { - zero.count -= summary.second.count; - zero.totalSize -= summary.second.totalSize; - } - result[0] = zero; - invokeCallback(done, std::move(result)); +rpl::producer DatabaseObject::stats() const { + return _stats.events_starting_with(collectStats()); +} + +Stats DatabaseObject::collectStats() const { + auto result = Stats(); + result.tagged = _taggedStats; + result.full.count = _map.size(); + result.full.totalSize = _totalSize; + result.clearing = (_cleaner.object != nullptr) || !_stale.empty(); + return result; } void DatabaseObject::writeBundlesLazy() { @@ -1047,10 +1163,12 @@ void DatabaseObject::createCleaner() { _base, std::move(right), std::move(done)); + pushStatsDelayed(); } void DatabaseObject::cleanerDone(Error error) { _cleaner = CleanerWrap(); + pushStatsDelayed(); } void DatabaseObject::checkCompactor() { @@ -1079,12 +1197,33 @@ void DatabaseObject::checkCompactor() { } void DatabaseObject::clear(FnMut &&done) { - Expects(_key.empty()); - + auto key = std::move(_key); + if (!key.empty()) { + close(nullptr); + } const auto version = findAvailableVersion(); - invokeCallback( - done, - writeVersion(version) ? Error::NoError() : ioError(versionPath())); + if (!writeVersion(version)) { + invokeCallback(done, ioError(versionPath())); + return; + } + if (key.empty()) { + invokeCallback(done, Error::NoError()); + return; + } + open(std::move(key), std::move(done)); +} + +void DatabaseObject::clearByTag(uint8 tag, FnMut &&done) { + const auto hadStale = !_stale.empty(); + for (const auto &[key, entry] : _map) { + if (entry.tag == tag) { + _stale.push_back(key); + } + } + if (!hadStale) { + startStaleClear(); + } + invokeCallback(done, Error::NoError()); } auto DatabaseObject::getManyRaw(const std::vector &keys) const diff --git a/Telegram/SourceFiles/storage/cache/storage_cache_database_object.h b/Telegram/SourceFiles/storage/cache/storage_cache_database_object.h index f5eee3116d..bebed72591 100644 --- a/Telegram/SourceFiles/storage/cache/storage_cache_database_object.h +++ b/Telegram/SourceFiles/storage/cache/storage_cache_database_object.h @@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/bytes.h" #include "base/flat_set.h" #include +#include namespace Storage { namespace Cache { @@ -29,6 +30,7 @@ public: crl::weak_on_queue weak, const QString &path, const Settings &settings); + void reconfigure(const Settings &settings); void open(EncryptionKey &&key, FnMut &&done); void close(FnMut &&done); @@ -53,9 +55,10 @@ public: const Key &to, FnMut &&done); - void stats(FnMut &&done); + rpl::producer stats() const; void clear(FnMut &&done); + void clearByTag(uint8 tag, FnMut &&done); static QString BinlogFilename(); static QString CompactReadyFilename(); @@ -101,6 +104,7 @@ private: Error ioError(const QString &path) const; + void checkSettings(); QString computePath(Version version) const; QString binlogPath(Version version) const; QString binlogPath() const; @@ -146,15 +150,24 @@ private: uint64 countRelativeTime() const; EstimatedTimePoint countTimePoint() const; void applyTimePoint(EstimatedTimePoint time); + uint64 pruneBeforeTime() const; void prune(); - void collectTimePrune( + void collectTimeStale( base::flat_set &stale, int64 &staleTotalSize); - void collectSizePrune( + void collectSizeStale( base::flat_set &stale, int64 &staleTotalSize); + void startStaleClear(); + void clearStaleNow(const base::flat_set &stale); + void clearStaleChunkDelayed(); + void clearStaleChunk(); + void updateStats(const Entry &was, const Entry &now); + Stats collectStats() const; + void pushStatsDelayed(); + void pushStats(); void setMapEntry(const Key &key, Entry &&entry); void eraseMapEntry(const Map::const_iterator &i); @@ -197,15 +210,17 @@ private: void createCleaner(); void cleanerDone(Error error); + void clearState(); crl::weak_on_queue _weak; QString _base, _path; - const Settings _settings; + Settings _settings; EncryptionKey _key; File _binlog; Map _map; std::set _removing; std::set _accessed; + std::vector _stale; EstimatedTimePoint _time; @@ -214,7 +229,10 @@ private: uint64 _minimalEntryTime = 0; size_type _entriesWithMinimalTimeCount = 0; - Stats _taggedStats; + base::flat_map _taggedStats; + rpl::event_stream _stats; + bool _pushingStats = false; + bool _clearingStale = false; base::ConcurrentTimer _writeBundlesTimer; base::ConcurrentTimer _pruneTimer; diff --git a/Telegram/SourceFiles/storage/cache/storage_cache_types.h b/Telegram/SourceFiles/storage/cache/storage_cache_types.h index 04fe2e2d5f..582edcffef 100644 --- a/Telegram/SourceFiles/storage/cache/storage_cache_types.h +++ b/Telegram/SourceFiles/storage/cache/storage_cache_types.h @@ -56,6 +56,7 @@ struct Settings { size_type readBlockSize = 8 * 1024 * 1024; size_type maxDataSize = 10 * 1024 * 1024; crl::time_type writeBundleDelay = 15 * 60 * crl::time_type(1000); + size_type staleRemoveChunk = 256; int64 compactAfterExcess = 8 * 1024 * 1024; int64 compactAfterFullSize = 0; @@ -66,6 +67,8 @@ struct Settings { size_type totalTimeLimit = 30 * 86400; // One month in seconds. crl::time_type pruneTimeout = 5 * crl::time_type(1000); crl::time_type maxPruneCheckTimeout = 3600 * crl::time_type(1000); + + bool clearOnWrongKey = false; }; struct TaggedValue { @@ -80,7 +83,11 @@ struct TaggedSummary { size_type count = 0; size_type totalSize = 0; }; -using Stats = base::flat_map; +struct Stats { + TaggedSummary full; + base::flat_map tagged; + bool clearing = false; +}; using Version = int32; diff --git a/Telegram/SourceFiles/storage/localstorage.cpp b/Telegram/SourceFiles/storage/localstorage.cpp index a4354e27d8..c417fe17c8 100644 --- a/Telegram/SourceFiles/storage/localstorage.cpp +++ b/Telegram/SourceFiles/storage/localstorage.cpp @@ -2949,6 +2949,7 @@ QString cachePath() { Storage::Cache::Database::Settings cacheSettings() { auto result = Storage::Cache::Database::Settings(); + result.clearOnWrongKey = true; return result; } @@ -2958,7 +2959,6 @@ Storage::EncryptionKey cacheKey() { return Storage::EncryptionKey(bytes::make_vector(LocalKey->data())); } - class CountWaveformTask : public Task { public: CountWaveformTask(DocumentData *doc) diff --git a/Telegram/SourceFiles/storage/storage_databases.cpp b/Telegram/SourceFiles/storage/storage_databases.cpp new file mode 100644 index 0000000000..9f722f3933 --- /dev/null +++ b/Telegram/SourceFiles/storage/storage_databases.cpp @@ -0,0 +1,103 @@ +/* +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 "storage/storage_databases.h" + +#include "storage/cache/storage_cache_database.h" + +namespace Storage { + +DatabasePointer::DatabasePointer( + not_null owner, + const std::unique_ptr &value) +: _owner(owner) +, _value(value.get()) { +} + +DatabasePointer::DatabasePointer(DatabasePointer &&other) +: _value(base::take(other._value)) +, _owner(other._owner) { +} + +DatabasePointer &DatabasePointer::operator=(DatabasePointer &&other) { + if (this != &other) { + destroy(); + _owner = other._owner; + _value = base::take(other._value); + } + return *this; +} + +DatabasePointer::~DatabasePointer() { + destroy(); +} + +Cache::Database *DatabasePointer::get() const { + return _value; +} + +Cache::Database &DatabasePointer::operator*() const { + Expects(_value != nullptr); + + return *get(); +} + +Cache::Database *DatabasePointer::operator->() const { + Expects(_value != nullptr); + + return get(); +} + +DatabasePointer::operator bool() const { + return get() != nullptr; +} + +void DatabasePointer::destroy() { + if (const auto value = base::take(_value)) { + _owner->destroy(value); + } +} + +Databases::Kept::Kept(std::unique_ptr &&database) +: database(std::move(database)) { +} + +DatabasePointer Databases::get( + const QString &path, + const Cache::details::Settings &settings) { + if (const auto i = _map.find(path); i != end(_map)) { + auto &kept = i->second; + Assert(kept.destroying.alive()); + kept.destroying.kill(); + kept.database->reconfigure(settings); + return DatabasePointer(this, kept.database); + } + const auto [i, ok] = _map.emplace( + path, + std::make_unique(path, settings)); + return DatabasePointer(this, i->second.database); +} + +void Databases::destroy(Cache::Database *database) { + for (auto &[path, kept] : _map) { + if (kept.database.get() == database) { + Assert(!kept.destroying.alive()); + auto [first, second] = base::make_binary_guard(); + kept.destroying = std::move(first); + database->close([=, guard = std::move(second)]() mutable { + crl::on_main([=, guard = std::move(guard)]{ + if (!guard.alive()) { + return; + } + _map.erase(path); + }); + }); + } + } +} + +} // namespace Storage \ No newline at end of file diff --git a/Telegram/SourceFiles/storage/storage_databases.h b/Telegram/SourceFiles/storage/storage_databases.h new file mode 100644 index 0000000000..9d4832f4fc --- /dev/null +++ b/Telegram/SourceFiles/storage/storage_databases.h @@ -0,0 +1,71 @@ +/* +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 +*/ +#pragma once + +#include "storage/cache/storage_cache_database.h" +#include "base/binary_guard.h" + +namespace Storage { +namespace Cache { +namespace details { +struct Settings; +} // namespace details +class Database; +} // namespace Cache + +class Databases; + +class DatabasePointer { +public: + DatabasePointer(const DatabasePointer &other) = delete; + DatabasePointer(DatabasePointer &&other); + DatabasePointer &operator=(const DatabasePointer &other) = delete; + DatabasePointer &operator=(DatabasePointer &&other); + ~DatabasePointer(); + + Cache::Database *get() const; + Cache::Database &operator*() const; + Cache::Database *operator->() const; + explicit operator bool() const; + +private: + friend class Databases; + + DatabasePointer( + not_null owner, + const std::unique_ptr &value); + void destroy(); + + Cache::Database *_value = nullptr; + not_null _owner; + +}; + +class Databases { +public: + DatabasePointer get( + const QString &path, + const Cache::details::Settings &settings); + +private: + friend class DatabasePointer; + + struct Kept { + Kept(std::unique_ptr &&database); + + std::unique_ptr database; + base::binary_guard destroying; + }; + + void destroy(Cache::Database *database); + + std::map _map; + +}; + +} // namespace Storage diff --git a/Telegram/SourceFiles/ui/widgets/buttons.h b/Telegram/SourceFiles/ui/widgets/buttons.h index 28c331e7dd..f08301f578 100644 --- a/Telegram/SourceFiles/ui/widgets/buttons.h +++ b/Telegram/SourceFiles/ui/widgets/buttons.h @@ -100,7 +100,10 @@ private: class RoundButton : public RippleButton, private base::Subscriber { public: - RoundButton(QWidget *parent, Fn textFactory, const style::RoundButton &st); + RoundButton( + QWidget *parent, + Fn textFactory, + const style::RoundButton &st); void setText(Fn textFactory); diff --git a/Telegram/gyp/lib_storage.gyp b/Telegram/gyp/lib_storage.gyp index d00b7b7eca..d24a105d04 100644 --- a/Telegram/gyp/lib_storage.gyp +++ b/Telegram/gyp/lib_storage.gyp @@ -50,6 +50,8 @@ '<(submodules_loc)/xxHash', ], 'sources': [ + '<(src_loc)/storage/storage_databases.cpp', + '<(src_loc)/storage/storage_databases.h', '<(src_loc)/storage/storage_encryption.cpp', '<(src_loc)/storage/storage_encryption.h', '<(src_loc)/storage/storage_encrypted_file.cpp',