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