From e4e7ae068baa6edd004dbbd8c9a935194217b5e9 Mon Sep 17 00:00:00 2001 From: Kiryl Kaveryn <kirylkaveryn@gmail.com> Date: Sat, 13 Jul 2024 17:55:53 +0400 Subject: [PATCH 1/2] [bookmarks] delete category files using the `.deleted` extension instead of full deleting Signed-off-by: Kiryl Kaveryn <kirylkaveryn@gmail.com> --- map/bookmark_helpers.cpp | 21 ++++++--- map/bookmark_helpers.hpp | 2 + map/bookmark_manager.cpp | 92 +++++++++++++++++++++++++++++++++++++--- map/bookmark_manager.hpp | 17 ++++++-- 4 files changed, 117 insertions(+), 15 deletions(-) diff --git a/map/bookmark_helpers.cpp b/map/bookmark_helpers.cpp index 5b70a46a60..a5579bb3ef 100644 --- a/map/bookmark_helpers.cpp +++ b/map/bookmark_helpers.cpp @@ -252,17 +252,19 @@ std::string GenerateUniqueFileName(std::string const & path, std::string name, s name.resize(name.size() - ext.size()); size_t counter = 1; - std::string suffix, res; + std::string suffix, resultFilePath, deletedFilePath; do { - res = name; - res = base::JoinPath(path, res.append(suffix).append(ext)); - if (!Platform::IsFileExistsByFullPath(res)) + resultFilePath = name; + deletedFilePath = name; + resultFilePath = base::JoinPath(path, resultFilePath.append(suffix).append(ext)); + deletedFilePath = base::JoinPath(path, deletedFilePath.append(suffix).append(ext).append(kDeletedExtension)); + if (!Platform::IsFileExistsByFullPath(resultFilePath) && !Platform::IsFileExistsByFullPath(deletedFilePath)) break; suffix = strings::to_string(counter++); } while (true); - return res; + return resultFilePath; } std::string GenerateValidAndUniqueFilePathForKML(std::string const & fileName) @@ -283,6 +285,15 @@ std::string GenerateValidAndUniqueFilePathForGPX(std::string const & fileName) return GenerateUniqueFileName(GetBookmarksDirectory(), std::move(filePath), kGpxExtension); } +std::string GenerateValidAndUniqueDeletedFilePath(std::string const & fileName) +{ + std::string filePath = RemoveInvalidSymbols(fileName); + if (filePath.empty()) + filePath = kDefaultBookmarksFileName; + + return GenerateUniqueFileName(GetBookmarksDirectory(), std::move(filePath), kDeletedExtension); +} + std::string const kDefaultBookmarksFileName = "Bookmarks"; // Populate empty category & track names based on file name: assign file name to category name, diff --git a/map/bookmark_helpers.hpp b/map/bookmark_helpers.hpp index fdff3078e8..3c18912a5c 100644 --- a/map/bookmark_helpers.hpp +++ b/map/bookmark_helpers.hpp @@ -69,6 +69,7 @@ std::string_view constexpr kKmzExtension = ".kmz"; std::string_view constexpr kKmlExtension = ".kml"; std::string_view constexpr kKmbExtension = ".kmb"; std::string_view constexpr kGpxExtension = ".gpx"; +std::string_view constexpr kDeletedExtension = ".deleted"; extern std::string const kDefaultBookmarksFileName; enum class KmlFileType @@ -96,6 +97,7 @@ std::string RemoveInvalidSymbols(std::string const & name); std::string GenerateUniqueFileName(const std::string & path, std::string name, std::string_view ext = kKmlExtension); std::string GenerateValidAndUniqueFilePathForKML(std::string const & fileName); std::string GenerateValidAndUniqueFilePathForGPX(std::string const & fileName); +std::string GenerateValidAndUniqueDeletedFilePath(std::string const & fileName); /// @} /// @name SerDes helpers. diff --git a/map/bookmark_manager.cpp b/map/bookmark_manager.cpp index ab42e98000..3d5aa2e189 100644 --- a/map/bookmark_manager.cpp +++ b/map/bookmark_manager.cpp @@ -27,6 +27,10 @@ #include "base/stl_helpers.hpp" #include "base/string_utils.hpp" +#ifndef _MSC_VER +#include <sys/time.h> +#endif + #include <algorithm> #include <chrono> #include <iomanip> @@ -404,6 +408,59 @@ void BookmarkManager::ResetRecentlyDeletedBookmark() m_recentlyDeletedBookmark.reset(); } +bool BookmarkManager::HasRecentlyDeletedCategories() const +{ + Platform::FilesList files; + Platform::GetFilesByExt(GetBookmarksDirectory(), kDeletedExtension, files); + return !files.empty(); +} + +BookmarkManager::KMLDataCollectionPtr BookmarkManager::GetRecentlyDeletedCategories() +{ + auto collection = LoadBookmarks(GetBookmarksDirectory(), kDeletedExtension, KmlFileType::Text, + [](kml::FileData const &) + { + return true; + }); + return collection; +} + +bool BookmarkManager::IsRecentlyDeletedCategory(const std::string &filePath) const +{ + return base::GetFileExtension(filePath) == kDeletedExtension; +} + +void BookmarkManager::RecoverRecentlyDeletedCategoriesAtPaths(std::vector<std::string> const & deletedFilePaths) +{ + CHECK_THREAD_CHECKER(m_threadChecker, ()); + GetPlatform().RunTask(Platform::Thread::File, [this, deletedFilePaths]() + { + for (auto const & deletedFilePath : deletedFilePaths) + { + CHECK(IsRecentlyDeletedCategory(deletedFilePath), ("The category at path", deletedFilePath, " should be 'deleted'.")); + ASSERT(Platform::IsFileExistsByFullPath(deletedFilePath), ("Deleted file should exist to be recovered.", deletedFilePath)); + auto filePath = base::FilenameWithoutExt(deletedFilePath); + if (Platform::IsFileExistsByFullPath(filePath)) + filePath = GenerateValidAndUniqueFilePathForKML(base::GetNameFromFullPathWithoutExt(deletedFilePath)); + UpdateLastModifiedTime(deletedFilePath); + base::MoveFileX(deletedFilePath, filePath); + GetPlatform().RunTask(Platform::Thread::Gui, [this, filePath]() { + ReloadBookmark(filePath); + }); + } + }); +} + +void BookmarkManager::DeleteRecentlyDeletedCategoriesAtPaths(std::vector<std::string> const & deletedFilePaths) +{ + CHECK_THREAD_CHECKER(m_threadChecker, ()); + for (auto const & deletedFilePath : deletedFilePaths) + { + CHECK(IsRecentlyDeletedCategory(deletedFilePath), ("The category at path", deletedFilePath, " should be 'deleted'.")); + FileWriter::DeleteFileX(deletedFilePath); + } +} + void BookmarkManager::DetachUserMark(kml::MarkId bmId, kml::MarkGroupId catId) { GetGroup(catId)->DetachUserMark(bmId); @@ -2056,10 +2113,10 @@ void BookmarkManager::LoadBookmarkRoutine(std::string const & filePath, bool isT kmlData = LoadKmlFile(fileToLoad, KmlFileType::Text); else if (ext == kGpxExtension) kmlData = LoadKmlFile(fileToLoad, KmlFileType::Gpx); + else if (ext == kDeletedExtension) + kmlData = nullptr; else - { ASSERT(false, ("Unsupported bookmarks extension", ext)); - } base::DeleteFileX(fileToLoad); @@ -2102,6 +2159,8 @@ void BookmarkManager::ReloadBookmarkRoutine(std::string const & filePath) kmlData = LoadKmlFile(filePath, KmlFileType::Text); else if (ext == kGpxExtension) kmlData = LoadKmlFile(filePath, KmlFileType::Gpx); + else if (ext == kDeletedExtension) + kmlData = nullptr; else ASSERT(false, ("Unsupported bookmarks extension", ext)); @@ -2485,7 +2544,7 @@ void BookmarkManager::CheckAndResetLastIds() idStorage.ResetTrackId(); } -bool BookmarkManager::DeleteBmCategory(kml::MarkGroupId groupId, bool deleteFile) +bool BookmarkManager::DeleteBmCategory(kml::MarkGroupId groupId, bool permanently) { CHECK_THREAD_CHECKER(m_threadChecker, ()); auto it = m_categories.find(groupId); @@ -2495,8 +2554,15 @@ bool BookmarkManager::DeleteBmCategory(kml::MarkGroupId groupId, bool deleteFile ClearGroup(groupId); m_changesTracker.OnDeleteGroup(groupId); - if (deleteFile) - FileWriter::DeleteFileX(it->second->GetFileName()); + auto const filePath = it->second->GetFileName(); + if (permanently) + FileWriter::DeleteFileX(filePath); + else + { + UpdateLastModifiedTime(filePath); + auto const deletedFilePath = GenerateValidAndUniqueDeletedFilePath(base::FileNameFromFullPath(filePath)); + base::MoveFileX(filePath, deletedFilePath); + } DeleteCompilations(it->second->GetCategoryData().m_compilationIds); m_categories.erase(it); @@ -2504,6 +2570,18 @@ bool BookmarkManager::DeleteBmCategory(kml::MarkGroupId groupId, bool deleteFile return true; } +void BookmarkManager::UpdateLastModifiedTime(const std::string & filePath) +{ + struct timeval tv[2]; + gettimeofday(&tv[0], NULL); + tv[1] = tv[0]; + #ifdef _MSC_VER + _utime(filePath.c_str(), nullptr); + #else + utimes(filePath.c_str(), nullptr); + #endif +} + namespace { class BestUserMarkFinder @@ -3573,9 +3651,9 @@ void BookmarkManager::EditSession::SetCategoryCustomProperty(kml::MarkGroupId ca m_bmManager.SetCategoryCustomProperty(categoryId, key, value); } -bool BookmarkManager::EditSession::DeleteBmCategory(kml::MarkGroupId groupId) +bool BookmarkManager::EditSession::DeleteBmCategory(kml::MarkGroupId groupId, bool permanently) { - return m_bmManager.DeleteBmCategory(groupId, true); + return m_bmManager.DeleteBmCategory(groupId, permanently); } void BookmarkManager::EditSession::NotifyChanges() diff --git a/map/bookmark_manager.hpp b/map/bookmark_manager.hpp index cd1bb9c4e4..52bdf80e9d 100644 --- a/map/bookmark_manager.hpp +++ b/map/bookmark_manager.hpp @@ -161,8 +161,11 @@ public: void SetCategoryAccessRules(kml::MarkGroupId categoryId, kml::AccessRules accessRules); void SetCategoryCustomProperty(kml::MarkGroupId categoryId, std::string const & key, std::string const & value); - bool DeleteBmCategory(kml::MarkGroupId groupId); - + + /// Removes the category from the list of categories and deletes the related file. + /// - Parameters: + /// - permanently: If true (default value), the file will be removed from the disk. If false, the file will be set as deleted. + bool DeleteBmCategory(kml::MarkGroupId groupId, bool permanently = true); void NotifyChanges(); private: @@ -378,6 +381,13 @@ public: bool HasRecentlyDeletedBookmark() const { return m_recentlyDeletedBookmark.operator bool(); }; void ResetRecentlyDeletedBookmark(); + bool HasRecentlyDeletedCategories() const; + BookmarkManager::KMLDataCollectionPtr GetRecentlyDeletedCategories(); + bool IsRecentlyDeletedCategory(std::string const & filePath) const; + + void RecoverRecentlyDeletedCategoriesAtPaths(std::vector<std::string> const & filePaths); + void DeleteRecentlyDeletedCategoriesAtPaths(std::vector<std::string> const & filePaths); + // Used for LoadBookmarks() and unit tests only. Does *not* update last modified time. void CreateCategories(KMLDataCollection && dataCollection, bool autoSave = false); @@ -581,8 +591,9 @@ private: void SetCategoryTags(kml::MarkGroupId categoryId, std::vector<std::string> const & tags); void SetCategoryAccessRules(kml::MarkGroupId categoryId, kml::AccessRules accessRules); void SetCategoryCustomProperty(kml::MarkGroupId categoryId, std::string const & key, std::string const & value); - bool DeleteBmCategory(kml::MarkGroupId groupId, bool deleteFile); + bool DeleteBmCategory(kml::MarkGroupId groupId, bool permanently); void ClearCategories(); + void UpdateLastModifiedTime(const std::string & filePath); void MoveBookmark(kml::MarkId bmID, kml::MarkGroupId curGroupID, kml::MarkGroupId newGroupID); void UpdateBookmark(kml::MarkId bmId, kml::BookmarkData const & bm); -- 2.45.3 From d75a35a7f52fe4a3d84eefc39f154ede68abd457 Mon Sep 17 00:00:00 2001 From: Kiryl Kaveryn <kirylkaveryn@gmail.com> Date: Tue, 16 Jul 2024 17:26:20 +0400 Subject: [PATCH 2/2] [bookmarks] [tests] unit tests for the `recently deleted` Signed-off-by: Kiryl Kaveryn <kirylkaveryn@gmail.com> --- map/map_tests/bookmarks_test.cpp | 43 ++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/map/map_tests/bookmarks_test.cpp b/map/map_tests/bookmarks_test.cpp index e201c9c312..72b42eba4d 100644 --- a/map/map_tests/bookmarks_test.cpp +++ b/map/map_tests/bookmarks_test.cpp @@ -1530,4 +1530,47 @@ UNIT_CLASS_TEST(Runner, Bookmarks_BrokenFile) auto kmlData = LoadKmlFile(fileName, KmlFileType::Binary); TEST(kmlData == nullptr, ()); } + +UNIT_CLASS_TEST(Runner, Bookmarks_RecentlyDeleted) +{ + BookmarkManager bmManager(BM_CALLBACKS); + bmManager.EnableTestMode(true); + auto const dir = GetBookmarksDirectory(); + bool const delDirOnExit = Platform::MkDir(dir) == Platform::ERR_OK; + SCOPE_GUARD(dirDeleter, [&](){ if (delDirOnExit) (void)Platform::RmDir(dir); }); + + std::string const filePath = base::JoinPath(dir, "file" + std::string{kKmlExtension}); + BookmarkManager::KMLDataCollection kmlDataCollection; + kmlDataCollection.emplace_back(filePath, LoadKmlData(MemReader(kmlString, std::strlen(kmlString)), KmlFileType::Text)); + + FileWriter w(filePath); + w.Write(kmlDataCollection.data(), kmlDataCollection.size()); + + TEST(kmlDataCollection.back().second, ()); + bmManager.CreateCategories(std::move(kmlDataCollection)); + TEST_EQUAL(bmManager.GetBmGroupsCount(), 1, ()); + + auto const groupId = bmManager.GetUnsortedBmGroupsIdList().front(); + + bmManager.GetEditSession().DeleteBmCategory(groupId, false /* permanently */); + TEST_EQUAL(bmManager.GetBmGroupsCount(), 0, ()); + + auto const deletedCategories = bmManager.GetRecentlyDeletedCategories(); + TEST_EQUAL(deletedCategories->size(), 1, ()); + TEST(bmManager.HasRecentlyDeletedCategories(), ()); + + auto const & deletedCategory = deletedCategories->front(); + TEST_EQUAL(base::GetFileExtension(deletedCategory.first), std::string{kDeletedExtension}, ()); + + auto const deletedFilePath = filePath + std::string{kDeletedExtension}; + TEST_EQUAL(deletedCategory.first, deletedFilePath, ()); + + bmManager.DeleteRecentlyDeletedCategoriesAtPaths({ deletedFilePath }); + TEST_EQUAL(bmManager.GetBmGroupsCount(), 0, ()); + TEST_EQUAL(bmManager.GetRecentlyDeletedCategories()->size(), 0, ()); + TEST(!bmManager.HasRecentlyDeletedCategories(), ()); + + TEST(!Platform::IsFileExistsByFullPath(filePath), ()); + TEST(!Platform::IsFileExistsByFullPath(deletedFilePath), ()); +} } // namespace bookmarks_test -- 2.45.3