Implemented preferences_manager and helpers

This commit is contained in:
Timothy Hutchins
2023-08-07 13:58:39 -05:00
parent 21d3dfca4e
commit e6f52fba98
6 changed files with 974 additions and 0 deletions
@@ -0,0 +1,282 @@
// Copyright 2021-2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "internal/platform/implementation/linux/preferences_manager.h"
#include <filesystem> // NOLINT(build/c++17)
#include <memory>
#include <optional>
#include <ostream>
#include <string>
#include <vector>
#include "absl/strings/string_view.h"
#include "nlohmann/json.hpp"
#include "nlohmann/json_fwd.hpp"
#include "internal/platform/implementation/linux/preferences_repository.h"
#include "internal/platform/logging.h"
namespace nearby {
namespace linux {
namespace {
using json = ::nlohmann::json;
} // namespace
PreferencesManager::PreferencesManager(absl::string_view file_path)
: api::PreferencesManager(file_path) {
std::optional<std::filesystem::path> path =
nearby::api::ImplementationPlatform::CreateDeviceInfo()
->GetLocalAppDataPath();
if (!path.has_value()) {
path = std::filesystem::temp_directory_path();
}
std::filesystem::path full_path = *path / std::string(file_path);
preferences_repository_ =
std::make_unique<PreferencesRepository>(full_path.string());
value_ = preferences_repository_->LoadPreferences();
}
bool PreferencesManager::Set(absl::string_view key, const json& value) {
absl::MutexLock lock(&mutex_);
return SetValue(key, value);
}
bool PreferencesManager::SetBoolean(absl::string_view key, bool value) {
absl::MutexLock lock(&mutex_);
return SetValue(key, value);
}
bool PreferencesManager::SetInteger(absl::string_view key, int value) {
absl::MutexLock lock(&mutex_);
return SetValue(key, value);
}
bool PreferencesManager::SetInt64(absl::string_view key, int64_t value) {
absl::MutexLock lock(&mutex_);
return SetValue(key, value);
}
bool PreferencesManager::SetString(absl::string_view key,
absl::string_view value) {
absl::MutexLock lock(&mutex_);
return SetValue(key, absl::StrCat(value));
}
bool PreferencesManager::SetBooleanArray(absl::string_view key,
absl::Span<const bool> value) {
absl::MutexLock lock(&mutex_);
return SetArrayValue(key, value);
}
bool PreferencesManager::SetIntegerArray(absl::string_view key,
absl::Span<const int> value) {
absl::MutexLock lock(&mutex_);
return SetArrayValue(key, value);
}
bool PreferencesManager::SetInt64Array(absl::string_view key,
absl::Span<const int64_t> value) {
absl::MutexLock lock(&mutex_);
return SetArrayValue(key, value);
}
bool PreferencesManager::SetStringArray(absl::string_view key,
absl::Span<const std::string> value) {
absl::MutexLock lock(&mutex_);
return SetArrayValue(key, value);
}
bool PreferencesManager::SetTime(absl::string_view key, absl::Time value) {
// Save time as nanos
absl::MutexLock lock(&mutex_);
int64_t tt = absl::ToUnixNanos(value);
if (value_[absl::StrCat(key)] == tt) {
return false;
}
value_[absl::StrCat(key)] = tt;
return Commit();
}
// Get JSON value.
json PreferencesManager::Get(absl::string_view key,
const json& default_value) const {
absl::MutexLock lock(&mutex_);
return GetValue(key, default_value);
}
bool PreferencesManager::GetBoolean(absl::string_view key,
bool default_value) const {
absl::MutexLock lock(&mutex_);
return GetValue(key, default_value);
}
int PreferencesManager::GetInteger(absl::string_view key,
int default_value) const {
absl::MutexLock lock(&mutex_);
return GetValue(key, default_value);
}
int64_t PreferencesManager::GetInt64(absl::string_view key,
int64_t default_value) const {
absl::MutexLock lock(&mutex_);
return GetValue(key, default_value);
}
std::string PreferencesManager::GetString(
absl::string_view key, const std::string& default_value) const {
absl::MutexLock lock(&mutex_);
return GetValue(key, default_value);
}
std::vector<bool> PreferencesManager::GetBooleanArray(
absl::string_view key, absl::Span<const bool> default_value) const {
absl::MutexLock lock(&mutex_);
return GetArrayValue(key, default_value);
}
std::vector<int> PreferencesManager::GetIntegerArray(
absl::string_view key, absl::Span<const int> default_value) const {
absl::MutexLock lock(&mutex_);
return GetArrayValue(key, default_value);
}
std::vector<int64_t> PreferencesManager::GetInt64Array(
absl::string_view key, absl::Span<const int64_t> default_value) const {
absl::MutexLock lock(&mutex_);
return GetArrayValue(key, default_value);
}
std::vector<std::string> PreferencesManager::GetStringArray(
absl::string_view key, absl::Span<const std::string> default_value) const {
absl::MutexLock lock(&mutex_);
return GetArrayValue(key, default_value);
}
absl::Time PreferencesManager::GetTime(absl::string_view key,
absl::Time default_value) const {
absl::MutexLock lock(&mutex_);
auto result = value_.find(absl::StrCat(key));
if (result == value_.end()) {
return default_value;
}
return absl::FromUnixNanos(result->get<int64_t>());
}
// Removes preferences
void PreferencesManager::Remove(absl::string_view key) {
absl::MutexLock lock(&mutex_);
value_.erase(absl::StrCat(key));
}
// Private methods
// Writes data to storage.
bool PreferencesManager::Commit() {
if (!preferences_repository_->SavePreferences(value_)) {
NEARBY_LOGS(ERROR) << "Failed to save preference." << std::endl;
return false;
}
return true;
}
bool PreferencesManager::SetValue(absl::string_view key, const json& value) {
if (!value_.is_object()) {
NEARBY_LOGS(ERROR) << "Preferences is no longer an object! value_="
<< value_.dump(4);
value_ = json::object();
}
if (value_[absl::StrCat(key)] == value) {
return false;
}
value_[absl::StrCat(key)] = value;
return Commit();
}
template <typename T>
T PreferencesManager::GetValue(absl::string_view key,
const T& default_value) const {
if (!value_.is_object()) {
NEARBY_LOGS(ERROR) << "Preferences is no longer an object! value_="
<< value_.dump(4);
return default_value;
}
auto it = value_.find(absl::StrCat(key));
if (it == value_.end()) {
return default_value;
}
return it->get<T>();
}
template <typename T>
bool PreferencesManager::SetArrayValue(absl::string_view key,
absl::Span<const T> value) {
if (!value_.is_object()) {
NEARBY_LOGS(ERROR) << "Preferences is no longer an object! value_="
<< value_.dump(4);
value_ = json::object();
}
json array_value = json::array();
for (const T& item_value : value) {
array_value.push_back(item_value);
}
if (value_[absl::StrCat(key)] == array_value) {
return false;
}
value_[absl::StrCat(key)] = array_value;
return Commit();
}
template <typename T>
std::vector<T> PreferencesManager::GetArrayValue(
absl::string_view key, absl::Span<const T> default_value) const {
std::vector<T> result;
if (!value_.is_object()) {
NEARBY_LOGS(ERROR) << "Preferences is no longer an object! value_="
<< value_.dump(4);
for (const T& value : default_value) {
result.push_back(value);
}
return result;
}
auto array_value = value_.find(absl::StrCat(key));
if (array_value == value_.end() || !array_value->is_array()) {
for (const T& value : default_value) {
result.push_back(value);
}
return result;
}
auto it = array_value->begin();
while (it != array_value->end()) {
result.push_back(it->get<T>());
++it;
}
return result;
}
} // namespace linux
} // namespace nearby
@@ -0,0 +1,141 @@
// Copyright 2021-2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#ifndef PLATFORM_IMPLEMENTATION_LINUX_PREFERENCES_MANAGER_H_
#define PLATFORM_IMPLEMENTATION_LINUX_PREFERENCES_MANAGER_H_
#include <stdint.h>
#include <memory>
#include <string>
#include <vector>
#include "absl/base/thread_annotations.h"
#include "absl/strings/string_view.h"
#include "absl/synchronization/mutex.h"
#include "absl/time/time.h"
#include "absl/types/span.h"
#include "nlohmann/json.hpp"
#include "nlohmann/json_fwd.hpp"
#include "internal/platform/implementation/preferences_manager.h"
#include "internal/platform/implementation/linux/preferences_repository.h"
namespace nearby {
namespace linux {
// Sets and gets preference settings from the application.
// Preferences are persistent storage for application settings, it is key/value
// based settings. Application components can observe the interested preference
// change by the observer.
class PreferencesManager : public api::PreferencesManager {
public:
explicit PreferencesManager(absl::string_view path);
// Sets values
bool Set(absl::string_view key, const nlohmann::json& value) override
ABSL_LOCKS_EXCLUDED(mutex_);
bool SetBoolean(absl::string_view key, bool value) override
ABSL_LOCKS_EXCLUDED(mutex_);
bool SetInteger(absl::string_view key, int value) override
ABSL_LOCKS_EXCLUDED(mutex_);
bool SetInt64(absl::string_view key, int64_t value) override
ABSL_LOCKS_EXCLUDED(mutex_);
bool SetString(absl::string_view key, absl::string_view value) override
ABSL_LOCKS_EXCLUDED(mutex_);
bool SetBooleanArray(absl::string_view key,
absl::Span<const bool> value) override
ABSL_LOCKS_EXCLUDED(mutex_);
bool SetIntegerArray(absl::string_view key,
absl::Span<const int> value) override
ABSL_LOCKS_EXCLUDED(mutex_);
bool SetInt64Array(absl::string_view key,
absl::Span<const int64_t> value) override
ABSL_LOCKS_EXCLUDED(mutex_);
bool SetStringArray(absl::string_view key,
absl::Span<const std::string> value) override
ABSL_LOCKS_EXCLUDED(mutex_);
bool SetTime(absl::string_view key, absl::Time value) override
ABSL_LOCKS_EXCLUDED(mutex_);
// Gets values
nlohmann::json Get(absl::string_view key,
const nlohmann::json& default_value) const override
ABSL_LOCKS_EXCLUDED(mutex_);
bool GetBoolean(absl::string_view key, bool default_value) const override
ABSL_LOCKS_EXCLUDED(mutex_);
int GetInteger(absl::string_view key, int default_value) const override
ABSL_LOCKS_EXCLUDED(mutex_);
int64_t GetInt64(absl::string_view key, int64_t default_value) const override
ABSL_LOCKS_EXCLUDED(mutex_);
std::string GetString(absl::string_view key,
const std::string& default_value) const override
ABSL_LOCKS_EXCLUDED(mutex_);
std::vector<bool> GetBooleanArray(absl::string_view key,
absl::Span<const bool> default_value)
const override ABSL_LOCKS_EXCLUDED(mutex_);
std::vector<int> GetIntegerArray(
absl::string_view key, absl::Span<const int> default_value) const override
ABSL_LOCKS_EXCLUDED(mutex_);
std::vector<int64_t> GetInt64Array(absl::string_view key,
absl::Span<const int64_t> default_value)
const override ABSL_LOCKS_EXCLUDED(mutex_);
std::vector<std::string> GetStringArray(
absl::string_view key,
absl::Span<const std::string> default_value) const override
ABSL_LOCKS_EXCLUDED(mutex_);
absl::Time GetTime(absl::string_view key,
absl::Time default_value) const override
ABSL_LOCKS_EXCLUDED(mutex_);
// Removes preferences
void Remove(absl::string_view key) override ABSL_LOCKS_EXCLUDED(mutex_);
private:
// Writes data to storage.
bool Commit() ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
bool SetValue(absl::string_view key, const nlohmann::json& value)
ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
template <typename T>
T GetValue(absl::string_view key, const T& default_value) const
ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
template <typename T>
bool SetArrayValue(absl::string_view key, absl::Span<const T> value)
ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
template <typename T>
std::vector<T> GetArrayValue(absl::string_view key,
absl::Span<const T> default_value) const
ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
nlohmann::json value_ ABSL_GUARDED_BY(mutex_);
std::unique_ptr<PreferencesRepository> preferences_repository_
ABSL_GUARDED_BY(mutex_);
mutable absl::Mutex mutex_;
};
} // namespace linux
} // namespace nearby
#endif // PLATFORM_IMPLEMENTATION_LINUX_PREFERENCES_MANAGER_H_
@@ -0,0 +1,189 @@
// Copyright 2021-2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "internal/platform/implementation/linux/preferences_manager.h"
#include <stdint.h>
#include <codecvt>
#include <filesystem> // NOLINT(build/c++17)
#include <fstream>
#include <locale>
#include <ostream>
#include <string>
#include <vector>
#include "gtest/gtest.h"
#include "absl/strings/string_view.h"
#include "absl/time/clock.h"
#include "absl/time/time.h"
#include "absl/types/span.h"
#include "nlohmann/json.hpp"
#include "nlohmann/json_fwd.hpp"
#include "internal/platform/logging.h"
namespace nearby {
namespace linux {
namespace {
using json = ::nlohmann::json;
constexpr absl::Duration kTimeOut = absl::Milliseconds(200);
constexpr char kPreferencesFilePath[] = "Google/Nearby/Sharing";
} // namespace
TEST(PreferencesManager, CorruptedConfigFile) {
std::filesystem::path settingsPath =
std::filesystem::temp_directory_path();
std::ofstream output_stream{settingsPath / "preferences.json"};
output_stream << "CORRUPTED" << std::endl;
NEARBY_LOGS(INFO) << "Loading preferences from: " << settingsPath.string();
EXPECT_EQ(PreferencesManager(settingsPath.string()).GetInteger("data", 100),
100);
}
TEST(PreferencesManager, ValidConfigFile) {
std::filesystem::path settingsPath =
std::filesystem::temp_directory_path();
std::ofstream output_stream{settingsPath / "preferences.json"};
output_stream << "{\"data\":8, \"name\": \"Valid\"}" << std::endl;
output_stream.close();
NEARBY_LOGS(INFO) << "Loading preferences from: " << settingsPath.string();
EXPECT_EQ(PreferencesManager(settingsPath.string()).GetInteger("data", 100),
8);
}
TEST(PreferencesManager, SetAndGetBoolean) {
std::string bool_key = "bool_key";
PreferencesManager pm(kPreferencesFilePath);
EXPECT_TRUE(pm.GetBoolean(bool_key, true));
pm.SetBoolean(bool_key, true);
EXPECT_TRUE(pm.GetBoolean(bool_key, false));
}
TEST(PreferencesManager, SetAndGetInt) {
std::string int_key = "int_key";
PreferencesManager pm(kPreferencesFilePath);
EXPECT_EQ(pm.GetInteger(int_key, 1234), 1234);
pm.SetInteger(int_key, 6789);
EXPECT_EQ(pm.GetInteger(int_key, 0), 6789);
}
TEST(PreferencesManager, SetAndGetInt64) {
std::string int64_key = "int64_key";
PreferencesManager pm(kPreferencesFilePath);
EXPECT_EQ(pm.GetInt64(int64_key, 1234), 1234);
pm.SetInt64(int64_key, 56789);
EXPECT_EQ(pm.GetInt64(int64_key, 0), 56789);
}
TEST(PreferencesManager, SetAndGetString) {
std::string string_key = "string_key";
PreferencesManager pm(kPreferencesFilePath);
EXPECT_EQ(pm.GetString(string_key, "abcd"), "abcd");
pm.SetString(string_key, "this is a test string");
EXPECT_EQ(pm.GetString(string_key, ""), "this is a test string");
}
TEST(PreferencesManager, SetAndGetTime) {
std::string time_key = "time_key";
PreferencesManager pm(kPreferencesFilePath);
absl::Time time = absl::Now();
EXPECT_EQ(pm.GetTime(time_key, time), time);
pm.SetTime(time_key, time);
absl::Time ret = pm.GetTime(time_key, absl::Now());
EXPECT_EQ(absl::ToUnixNanos(ret), absl::ToUnixNanos(time));
}
TEST(PreferencesManager, MultipleSetAndGetString) {
std::string string1_key = "string1_key";
PreferencesManager pm(kPreferencesFilePath);
pm.SetString(string1_key, "this is first string");
pm.SetString(string1_key, "this is second string");
EXPECT_EQ(pm.GetString(string1_key, ""), "this is second string");
}
TEST(PreferencesManager, SetAndGetValue) {
std::string value_key = "value_key";
PreferencesManager pm(kPreferencesFilePath);
json value = {{"key1", "value1"}, {"key2", "value2"}};
EXPECT_TRUE(pm.Get(value_key, json()).empty());
pm.Set(value_key, value);
auto result = pm.Get(value_key, json());
ASSERT_FALSE(result.empty());
auto val = result["key2"];
EXPECT_EQ(val.get<std::string>(), "value2");
}
TEST(PreferencesManager, SetAndGetBooleanArray) {
std::string bool_array_key = "bool_array_key";
auto pm = PreferencesManager(kPreferencesFilePath);
auto default_result =
pm.GetBooleanArray(bool_array_key, absl::Span<const bool>({true}));
EXPECT_EQ(default_result[0], true);
pm.SetBooleanArray(bool_array_key,
absl::Span<const bool>({true, false, false, true, true}));
auto result =
pm.GetBooleanArray(bool_array_key, absl::Span<const bool>({true}));
EXPECT_EQ(result[2], false);
EXPECT_EQ(result[3], true);
}
TEST(PreferencesManager, SetAndGetIntArray) {
std::string int_array_key = "int_array_key";
auto pm = PreferencesManager(kPreferencesFilePath);
auto result = pm.GetIntegerArray(int_array_key, std::vector<int>{5, 6});
EXPECT_EQ(result[1], 6);
pm.SetIntegerArray(int_array_key, std::vector<int>{1, 7, 4, 10, 12});
result = pm.GetIntegerArray(int_array_key, std::vector<int>{11, 17, 14, 110});
EXPECT_EQ(result[3], 10);
}
TEST(PreferencesManager, SetAndGetInt64Array) {
std::string int64_array_key = "int64_array_key";
auto pm = PreferencesManager(kPreferencesFilePath);
auto result = pm.GetInt64Array(int64_array_key, std::vector<int64_t>{99});
EXPECT_EQ(result[0], 99);
pm.SetInt64Array(int64_array_key, std::vector<int64_t>{16, 7, 64, 100, 12});
result = pm.GetInt64Array(int64_array_key, std::vector<int64_t>{1, 5, 6, 12});
EXPECT_EQ(result[3], 100);
EXPECT_EQ(result[4], 12);
}
TEST(PreferencesManager, SetAndGetStringArray) {
std::string string_array_key = "string_array_key";
auto pm = PreferencesManager(kPreferencesFilePath);
auto result = pm.GetStringArray(string_array_key,
std::vector<std::string>{"value", "morning"});
EXPECT_EQ(result[1], "morning");
pm.SetStringArray(
string_array_key,
std::vector<std::string>{"one", "two", "three", "four", "five"});
result = pm.GetStringArray(string_array_key,
std::vector<std::string>{"good", "morning"});
EXPECT_EQ(result[3], "four");
}
TEST(PreferencesManager, RemoveKey) {
std::string string_key = "string_key";
auto pm = PreferencesManager(kPreferencesFilePath);
pm.SetString(string_key, "remove key");
pm.Remove(string_key);
auto result = pm.GetString(string_key, "default key");
EXPECT_EQ(result, "default key");
}
} // namespace linux
} // namespace nearby
@@ -0,0 +1,153 @@
// Copyright 2023 Google LLC
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "internal/platform/implementation/linux/preferences_repository.h"
#include <exception>
#include <filesystem> // NOLINT(build/c++17)
#include <fstream>
#include <optional>
#include "nlohmann/json.hpp"
#include "nlohmann/json_fwd.hpp"
#include "internal/platform/logging.h"
namespace nearby {
namespace linux {
namespace {
using json = ::nlohmann::json;
constexpr char kPreferencesFileName[] = "preferences.json";
constexpr char kPreferencesBackupFileName[] = "preferences_bak.json";
} // namespace
json PreferencesRepository::LoadPreferences() {
absl::MutexLock lock(&mutex_);
std::optional<json> preferences = AttemptLoad();
if (preferences.has_value()) {
// The top level root should be an object, if it's not then something went
// wrong or the file was corrupted.
if (!preferences.value().is_object()) {
NEARBY_LOGS(ERROR) << "Preferences loaded was not a valid object: "
<< preferences.value().dump(4);
return json::object();
}
return preferences.value();
}
NEARBY_LOGS(ERROR) << "Could not load preferences file, trying backup.";
// In the future we should switch to using a transaction log or another
// stable method which doesn't pose a risk of losing settings
preferences = RestoreFromBackup();
if (preferences.has_value()) {
NEARBY_LOGS(ERROR) << "Successfully recovered from backup.";
return preferences.value();
}
NEARBY_LOGS(ERROR) << "Failed to load preferences file from back up.";
return json::object();
}
bool PreferencesRepository::SavePreferences(json preferences) {
absl::MutexLock lock(&mutex_);
try {
std::filesystem::path path = path_;
if (!std::filesystem::exists(path) &&
!std::filesystem::create_directories(path)) {
NEARBY_LOGS(ERROR) << "Failed to create preferences path.";
return false;
}
std::filesystem::path full_name = path / kPreferencesFileName;
std::filesystem::path full_name_backup = path / kPreferencesBackupFileName;
// Create a backup without moving the bytes on disk
if (std::filesystem::exists(full_name)) {
NEARBY_LOGS(INFO) << "Making backup of preferences file.";
std::filesystem::rename(full_name, full_name_backup);
}
std::ofstream preferences_file(full_name.c_str());
preferences_file << preferences;
preferences_file.close();
// Make sure the file wasn't saved in a corrupted state
if (!AttemptLoad().has_value()) {
NEARBY_LOGS(ERROR) << "Preferences saved to disk in corrupted state. "
"Restoring from backup.";
if (!RestoreFromBackup().has_value()) {
NEARBY_LOGS(ERROR) << "Failed to restore preferences file.";
return false;
}
}
} catch (const std::exception& e) {
NEARBY_LOGS(ERROR) << "Failed to save preferences file: " << e.what();
return false;
}
return true;
}
std::optional<json> PreferencesRepository::AttemptLoad() {
std::filesystem::path path = path_;
std::filesystem::path full_name = path / kPreferencesFileName;
if (!std::filesystem::exists(path) || !std::filesystem::exists(full_name)) {
return std::nullopt;
}
try {
std::ifstream preferences_file(full_name.c_str());
if (!preferences_file.good()) {
return std::nullopt;
}
json preferences = json::parse(preferences_file, nullptr, false);
preferences_file.close();
if (preferences.is_discarded()) {
NEARBY_LOGS(ERROR) << "Preferences file corrupted.";
return std::nullopt;
}
return preferences;
} catch (const std::exception& e) {
NEARBY_LOGS(ERROR) << "Exception while loading preferences: " << e.what();
return std::nullopt;
}
}
std::optional<json> PreferencesRepository::RestoreFromBackup() {
std::filesystem::path path = path_;
std::filesystem::path full_name = path / kPreferencesFileName;
std::filesystem::path full_name_backup = path / kPreferencesBackupFileName;
if (!std::filesystem::exists(full_name_backup)) {
NEARBY_LOGS(WARNING)
<< "Backup requested but no backup preferences file found.";
return std::nullopt;
}
std::filesystem::rename(full_name_backup, full_name);
NEARBY_LOGS(INFO) << "Attempting load from backup preferences.";
return AttemptLoad();
}
} // namespace linux
} // namespace nearby
@@ -0,0 +1,48 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#ifndef PLATFORM_IMPLEMENTATION_LINUX_PREFERENCES_REPOSITORY_H_
#define PLATFORM_IMPLEMENTATION_LINUX_PREFERENCES_REPOSITORY_H_
#include <optional>
#include <string>
#include "absl/base/thread_annotations.h"
#include "absl/strings/string_view.h"
#include "absl/synchronization/mutex.h"
#include "nlohmann/json.hpp"
#include "nlohmann/json_fwd.hpp"
namespace nearby {
namespace linux {
class PreferencesRepository {
public:
explicit PreferencesRepository(absl::string_view path) : path_(path) {}
nlohmann::json LoadPreferences() ABSL_LOCKS_EXCLUDED(&mutex_);
bool SavePreferences(nlohmann::json preferences) ABSL_LOCKS_EXCLUDED(&mutex_);
std::optional<nlohmann::json> AttemptLoad();
std::optional<nlohmann::json> RestoreFromBackup();
private:
absl::Mutex mutex_;
const std::string path_;
};
} // namespace linux
} // namespace nearby
#endif // PLATFORM_IMPLEMENTATION_LINUX_PREFERENCES_REPOSITORY_H_
@@ -0,0 +1,161 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "internal/platform/implementation/linux/preferences_repository.h"
#include <filesystem> // NOLINT(build/c++17)
#include <fstream>
#include <optional>
#include "gtest/gtest.h"
#include "nlohmann/json.hpp"
#include "nlohmann/json_fwd.hpp"
#include "internal/platform/implementation/device_info.h"
#include "internal/platform/implementation/platform.h"
namespace nearby {
namespace linux {
namespace {
using json = ::nlohmann::json;
constexpr char kPreferencesFileName[] = "preferences.json";
constexpr char kPreferencesBackupFileName[] = "preferences_bak.json";
constexpr char kPreferencesPath[] = "Google/Nearby/Sharing";
TEST(PreferencesRepository, LoadWithBadPath) {
PreferencesRepository preferences_repository{"c:\\users\\a\\b\\c\\d\\e\\f"};
json result = preferences_repository.LoadPreferences();
EXPECT_TRUE(result.empty());
}
TEST(PreferencesRepository, RecoverFromBadPreferences) {
std::optional<std::filesystem::path> app_data_path =
api::ImplementationPlatform::CreateDeviceInfo()->GetLocalAppDataPath();
ASSERT_TRUE(app_data_path.has_value());
std::filesystem::path full_path = *app_data_path / kPreferencesPath;
std::filesystem::path full_name = full_path / kPreferencesFileName;
if (std::filesystem::exists(full_name)) {
std::filesystem::remove(full_name);
}
std::ofstream pref_file(full_name.c_str());
pref_file << "\"Bad top level object\"";
pref_file.close();
PreferencesRepository preferences_repository{full_path.string()};
EXPECT_EQ(preferences_repository.LoadPreferences(), json::object());
}
TEST(PreferencesRepository, SaveAndLoadPreferences) {
std::optional<std::filesystem::path> app_data_path =
api::ImplementationPlatform::CreateDeviceInfo()->GetLocalAppDataPath();
ASSERT_TRUE(app_data_path.has_value());
std::filesystem::path full_path = *app_data_path / kPreferencesPath;
std::filesystem::path full_name = full_path / kPreferencesFileName;
if (std::filesystem::exists(full_name)) {
std::filesystem::remove(full_name);
}
PreferencesRepository preferences_repository{full_path.string()};
json data;
data["key1"] = "value1";
data["key2"] = "value2";
EXPECT_TRUE(preferences_repository.SavePreferences(data));
json result = preferences_repository.LoadPreferences();
EXPECT_EQ(result.size(), 2);
EXPECT_EQ(result["key1"], "value1");
EXPECT_EQ(result["key2"], "value2");
std::filesystem::remove(full_name);
}
TEST(PreferencesRepository, LoadFromBackup) {
std::optional<std::filesystem::path> app_data_path =
api::ImplementationPlatform::CreateDeviceInfo()->GetLocalAppDataPath();
ASSERT_TRUE(app_data_path.has_value());
std::filesystem::path full_path = *app_data_path / kPreferencesPath;
std::filesystem::path full_name = full_path / kPreferencesFileName;
std::filesystem::path full_name_backup =
full_path / kPreferencesBackupFileName;
if (std::filesystem::exists(full_name)) {
std::filesystem::remove(full_name);
}
if (std::filesystem::exists(full_name_backup)) {
std::filesystem::remove(full_name_backup);
}
PreferencesRepository preferences_repository{full_path.string()};
json data;
data["key1"] = "value1";
data["key2"] = "value2";
std::ofstream backup_file(full_name_backup.c_str());
backup_file << data;
backup_file.close();
std::optional<json> result;
result = preferences_repository.AttemptLoad();
EXPECT_FALSE(result.has_value());
result = preferences_repository.RestoreFromBackup();
EXPECT_TRUE(result.has_value());
EXPECT_EQ(result.value()["key1"], "value1");
EXPECT_EQ(result.value()["key2"], "value2");
std::filesystem::remove(full_name);
EXPECT_FALSE(std::filesystem::exists(full_name_backup));
}
TEST(PreferencesRepository, RecoverFromCorruption) {
std::optional<std::filesystem::path> app_data_path =
api::ImplementationPlatform::CreateDeviceInfo()->GetLocalAppDataPath();
ASSERT_TRUE(app_data_path.has_value());
std::filesystem::path full_path = *app_data_path / kPreferencesPath;
std::filesystem::path full_name = full_path / kPreferencesFileName;
std::filesystem::path full_name_backup =
full_path / kPreferencesBackupFileName;
if (std::filesystem::exists(full_name)) {
std::filesystem::remove(full_name);
}
if (std::filesystem::exists(full_name_backup)) {
std::filesystem::remove(full_name_backup);
}
PreferencesRepository preferences_repository{full_path.string()};
json data;
data["key1"] = "value1";
data["key2"] = "value2";
std::ofstream preferences_file(full_name_backup.c_str());
preferences_file << data;
preferences_file.close();
std::ofstream backup_file(full_name.c_str());
backup_file << "[BAD JSON FILE]";
backup_file.close();
std::optional<json> result = preferences_repository.LoadPreferences();
EXPECT_EQ(result.value()["key1"], "value1");
EXPECT_EQ(result.value()["key2"], "value2");
std::filesystem::remove(full_name);
EXPECT_FALSE(std::filesystem::exists(full_name_backup));
}
} // namespace
} // namespace linux
} // namespace nearby