1 Commits

Author SHA1 Message Date
62555e6125 v2.0.5-rc (#41)
All checks were successful
BlockStorage/repertory/pipeline/head This commit looks good
Reviewed-on: #41
2025-03-26 07:02:38 -05:00
21 changed files with 580 additions and 438 deletions

View File

@ -115,6 +115,7 @@ googletest
gpath gpath
gtest_version gtest_version
has_setxattr has_setxattr
hkey
httpapi httpapi
httplib httplib
icudata icudata
@ -144,6 +145,7 @@ libuuid_include_dirs
libvlc libvlc
linkflags linkflags
localappdata localappdata
lpbyte
lptr lptr
lpwstr lpwstr
markdownlint markdownlint

View File

@ -4,15 +4,14 @@
### Issues ### Issues
* ~~\#12 [Unit Test] Complete all providers unit tests~~
* ~~\#21 [Unit Test] Complete WinFSP unit tests~~
* ~~\#22 [Unit Test] Complete FUSE unit tests~~
* \#39 Create management portal in Flutter * \#39 Create management portal in Flutter
### Changes from v2.0.4-rc ### Changes from v2.0.4-rc
* Continue documentation updates * Continue documentation updates
* Prevent overlapping `repertory` `ApiPort`'s * Fixed `-status` command erasing active mount information
* Fixed overlapping HTTP REST API port's
* Refactored/fixed instance locking
* Removed passwords and secret key values from API calls * Removed passwords and secret key values from API calls
* Renamed setting `ApiAuth` to `ApiPassword` * Renamed setting `ApiAuth` to `ApiPassword`
* Require `--name,-na` option for encryption provider * Require `--name,-na` option for encryption provider

View File

@ -22,6 +22,13 @@
#ifndef REPERTORY_INCLUDE_PLATFORM_PLATFORM_HPP_ #ifndef REPERTORY_INCLUDE_PLATFORM_PLATFORM_HPP_
#define REPERTORY_INCLUDE_PLATFORM_PLATFORM_HPP_ #define REPERTORY_INCLUDE_PLATFORM_PLATFORM_HPP_
#include "types/repertory.hpp"
namespace repertory {
[[nodiscard]] auto create_lock_id(provider_type prov,
std::string_view unique_id)->std::string;
}
#if defined(_WIN32) #if defined(_WIN32)
#include "platform/win32_platform.hpp" #include "platform/win32_platform.hpp"
#include "utils/windows.hpp" #include "utils/windows.hpp"

View File

@ -30,40 +30,44 @@ class i_provider;
class lock_data final { class lock_data final {
public: public:
explicit lock_data(const provider_type &pt, std::string unique_id /*= ""*/); lock_data(provider_type prov, std::string_view unique_id);
lock_data(); lock_data(const lock_data &) = delete;
lock_data(lock_data &&) = delete;
auto operator=(const lock_data &) -> lock_data & = delete;
auto operator=(lock_data &&) -> lock_data & = delete;
~lock_data(); ~lock_data();
private: private:
const provider_type pt_; std::string mutex_id_;
const std::string unique_id_;
const std::string mutex_id_; private:
int lock_fd_; int handle_{};
int lock_status_{EWOULDBLOCK}; int lock_status_{EWOULDBLOCK};
private: private:
[[nodiscard]] static auto get_state_directory() -> std::string; [[nodiscard]] static auto get_state_directory() -> std::string;
[[nodiscard]] static auto get_lock_data_file() -> std::string; [[nodiscard]] auto get_lock_data_file() const -> std::string;
[[nodiscard]] auto get_lock_file() -> std::string; [[nodiscard]] auto get_lock_file() const -> std::string;
private: private:
[[nodiscard]] static auto wait_for_lock(int fd, [[nodiscard]] static auto wait_for_lock(int handle,
std::uint8_t retry_count = 30u) std::uint8_t retry_count = 30U)
-> int; -> int;
public: public:
[[nodiscard]] auto get_mount_state(json &mount_state) -> bool; [[nodiscard]] auto get_mount_state(json &mount_state) -> bool;
[[nodiscard]] auto grab_lock(std::uint8_t retry_count = 30u) -> lock_result; [[nodiscard]] auto grab_lock(std::uint8_t retry_count = 30U) -> lock_result;
void release(); void release();
[[nodiscard]] auto set_mount_state(bool active, [[nodiscard]] auto set_mount_state(bool active,
const std::string &mount_location, int pid) std::string_view mount_location, int pid)
-> bool; -> bool;
}; };
@ -79,5 +83,5 @@ public:
const api_file &file) -> api_error; const api_file &file) -> api_error;
} // namespace repertory } // namespace repertory
#endif // _WIN32 #endif // !defined(_WIN32)
#endif // REPERTORY_INCLUDE_PLATFORM_UNIXPLATFORM_HPP_ #endif // REPERTORY_INCLUDE_PLATFORM_UNIXPLATFORM_HPP_

View File

@ -23,7 +23,6 @@
#define REPERTORY_INCLUDE_PLATFORM_WINPLATFORM_HPP_ #define REPERTORY_INCLUDE_PLATFORM_WINPLATFORM_HPP_
#if defined(_WIN32) #if defined(_WIN32)
#include "app_config.hpp"
#include "types/repertory.hpp" #include "types/repertory.hpp"
namespace repertory { namespace repertory {
@ -31,43 +30,32 @@ class i_provider;
class lock_data final { class lock_data final {
public: public:
explicit lock_data(const provider_type &pt, std::string unique_id /*= ""*/) explicit lock_data(provider_type prov, std::string unique_id);
: pt_(pt), lock_data(const lock_data &) = delete;
unique_id_(std::move(unique_id)), lock_data(lock_data &&) = delete;
mutex_id_("repertory_" + app_config::get_provider_name(pt) + "_" +
unique_id_),
mutex_handle_(::CreateMutex(nullptr, FALSE, &mutex_id_[0u])) {}
lock_data() ~lock_data();
: pt_(provider_type::sia),
unique_id_(""),
mutex_id_(""),
mutex_handle_(INVALID_HANDLE_VALUE) {}
~lock_data() { release(); } auto operator=(const lock_data &) -> lock_data & = delete;
auto operator=(lock_data &&) -> lock_data & = delete;
private: private:
const provider_type pt_; std::string mutex_id_;
const std::string unique_id_; HANDLE mutex_handle_{INVALID_HANDLE_VALUE};
const std::string mutex_id_; DWORD mutex_state_{WAIT_FAILED};
HANDLE mutex_handle_;
DWORD mutex_state_ = WAIT_FAILED; [[nodiscard]] auto get_current_mount_state(json &mount_state) -> bool;
public: public:
[[nodiscard]] auto get_mount_state(const provider_type &pt,
json &mount_state) -> bool;
[[nodiscard]] auto get_mount_state(json &mount_state) -> bool; [[nodiscard]] auto get_mount_state(json &mount_state) -> bool;
[[nodiscard]] auto get_unique_id() const -> std::string { return unique_id_; } [[nodiscard]] auto grab_lock(std::uint8_t retry_count = 30U) -> lock_result;
[[nodiscard]] auto grab_lock(std::uint8_t retry_count = 30) -> lock_result;
void release(); void release();
[[nodiscard]] auto set_mount_state(bool active, [[nodiscard]] auto set_mount_state(bool active,
const std::string &mount_location, std::string_view mount_location,
const std::int64_t &pid) -> bool; std::int64_t pid) -> bool;
}; };
[[nodiscard]] auto create_meta_attributes( [[nodiscard]] auto create_meta_attributes(

View File

@ -21,9 +21,8 @@
*/ */
#if !defined(_WIN32) #if !defined(_WIN32)
#include "platform/unix_platform.hpp" #include "platform/platform.hpp"
#include "app_config.hpp"
#include "events/event_system.hpp" #include "events/event_system.hpp"
#include "events/types/filesystem_item_added.hpp" #include "events/types/filesystem_item_added.hpp"
#include "providers/i_provider.hpp" #include "providers/i_provider.hpp"
@ -36,52 +35,65 @@
#include "utils/unix.hpp" #include "utils/unix.hpp"
namespace repertory { namespace repertory {
lock_data::lock_data(const provider_type &pt, std::string unique_id /*= ""*/) lock_data::lock_data(provider_type prov, std::string_view unique_id)
: pt_(pt), : mutex_id_(create_lock_id(prov, unique_id)) {
unique_id_(std::move(unique_id)), handle_ = open(get_lock_file().c_str(), O_CREAT | O_RDWR, S_IWUSR | S_IRUSR);
mutex_id_("repertory_" + app_config::get_provider_name(pt) + "_" +
unique_id_) {
lock_fd_ = open(get_lock_file().c_str(), O_CREAT | O_RDWR, S_IWUSR | S_IRUSR);
} }
lock_data::lock_data()
: pt_(provider_type::sia), unique_id_(""), mutex_id_(""), lock_fd_(-1) {}
lock_data::~lock_data() { release(); } lock_data::~lock_data() { release(); }
auto lock_data::get_lock_data_file() -> std::string { auto lock_data::get_lock_data_file() const -> std::string {
const auto dir = get_state_directory(); auto dir = get_state_directory();
if (not utils::file::directory(dir).create_directory()) { if (not utils::file::directory(dir).create_directory()) {
throw startup_exception("failed to create directory|sp|" + dir + "|err|" + throw startup_exception("failed to create directory|sp|" + dir + "|err|" +
std::to_string(utils::get_last_error_code())); std::to_string(utils::get_last_error_code()));
} }
return utils::path::combine( return utils::path::combine(
dir, {"mountstate_" + std::to_string(getuid()) + ".json"}); dir, {
fmt::format("{}_{}.json", mutex_id_, getuid()),
});
} }
auto lock_data::get_lock_file() -> std::string { auto lock_data::get_lock_file() const -> std::string {
const auto dir = get_state_directory(); auto dir = get_state_directory();
if (not utils::file::directory(dir).create_directory()) { if (not utils::file::directory(dir).create_directory()) {
throw startup_exception("failed to create directory|sp|" + dir + "|err|" + throw startup_exception("failed to create directory|sp|" + dir + "|err|" +
std::to_string(utils::get_last_error_code())); std::to_string(utils::get_last_error_code()));
} }
return utils::path::combine(dir, return utils::path::combine(
{mutex_id_ + "_" + std::to_string(getuid())}); dir, {
fmt::format("{}_{}.lock", mutex_id_, getuid()),
});
} }
auto lock_data::get_mount_state(json &mount_state) -> bool { auto lock_data::get_mount_state(json &mount_state) -> bool {
auto ret = false; auto handle = open(get_lock_data_file().c_str(), O_RDWR, S_IWUSR | S_IRUSR);
auto fd = if (handle == -1) {
open(get_lock_data_file().c_str(), O_CREAT | O_RDWR, S_IWUSR | S_IRUSR); mount_state = {
if (fd != -1) { {"Active", false},
if (wait_for_lock(fd) == 0) { {"Location", ""},
ret = utils::file::read_json_file(get_lock_data_file(), mount_state); {"PID", -1},
flock(fd, LOCK_UN); };
return true;
} }
close(fd); auto ret{false};
if (wait_for_lock(handle) == 0) {
ret = utils::file::read_json_file(get_lock_data_file(), mount_state);
if (ret && mount_state.empty()) {
mount_state = {
{"Active", false},
{"Location", ""},
{"PID", -1},
};
} }
flock(handle, LOCK_UN);
}
close(handle);
return ret; return ret;
} }
@ -89,25 +101,20 @@ auto lock_data::get_state_directory() -> std::string {
#if defined(__APPLE__) #if defined(__APPLE__)
return utils::path::absolute("~/Library/Application Support/" + return utils::path::absolute("~/Library/Application Support/" +
std::string{REPERTORY_DATA_NAME} + "/state"); std::string{REPERTORY_DATA_NAME} + "/state");
#else #else // !defined(__APPLE__)
return utils::path::absolute("~/.local/" + std::string{REPERTORY_DATA_NAME} + return utils::path::absolute("~/.local/" + std::string{REPERTORY_DATA_NAME} +
"/state"); "/state");
#endif #endif // defined(__APPLE__)
} }
auto lock_data::grab_lock(std::uint8_t retry_count) -> lock_result { auto lock_data::grab_lock(std::uint8_t retry_count) -> lock_result {
REPERTORY_USES_FUNCTION_NAME(); if (handle_ == -1) {
if (lock_fd_ == -1) {
return lock_result::failure; return lock_result::failure;
} }
lock_status_ = wait_for_lock(lock_fd_, retry_count); lock_status_ = wait_for_lock(handle_, retry_count);
switch (lock_status_) { switch (lock_status_) {
case 0: case 0:
if (not set_mount_state(false, "", -1)) {
utils::error::raise_error(function_name, "failed to set mount state");
}
return lock_result::success; return lock_result::success;
case EWOULDBLOCK: case EWOULDBLOCK:
return lock_result::locked; return lock_result::locked;
@ -117,55 +124,53 @@ auto lock_data::grab_lock(std::uint8_t retry_count) -> lock_result {
} }
void lock_data::release() { void lock_data::release() {
if (lock_fd_ == -1) { if (handle_ == -1) {
return; return;
} }
if (lock_status_ == 0) { if (lock_status_ == 0) {
unlink(get_lock_file().c_str()); [[maybe_unused]] auto success{utils::file::file{get_lock_file()}.remove()};
flock(lock_fd_, LOCK_UN); flock(handle_, LOCK_UN);
} }
close(lock_fd_); close(handle_);
lock_fd_ = -1; handle_ = -1;
} }
auto lock_data::set_mount_state(bool active, const std::string &mount_location, auto lock_data::set_mount_state(bool active, std::string_view mount_location,
int pid) -> bool { int pid) -> bool {
REPERTORY_USES_FUNCTION_NAME(); REPERTORY_USES_FUNCTION_NAME();
auto ret = false;
auto handle = auto handle =
open(get_lock_data_file().c_str(), O_CREAT | O_RDWR, S_IWUSR | S_IRUSR); open(get_lock_data_file().c_str(), O_CREAT | O_RDWR, S_IWUSR | S_IRUSR);
if (handle != -1) { if (handle == -1) {
return false;
}
auto ret{false};
if (wait_for_lock(handle) == 0) { if (wait_for_lock(handle) == 0) {
const auto mount_id =
app_config::get_provider_display_name(pt_) + unique_id_;
json mount_state; json mount_state;
if (not utils::file::read_json_file(get_lock_data_file(), mount_state)) { if (not utils::file::read_json_file(get_lock_data_file(), mount_state)) {
utils::error::raise_error(function_name, utils::error::raise_error(function_name,
"failed to read mount state file|sp|" + "failed to read mount state file|sp|" +
get_lock_file()); get_lock_file());
} }
if ((mount_state.find(mount_id) == mount_state.end()) || if ((mount_state.find("Active") == mount_state.end()) ||
(mount_state[mount_id].find("Active") == (mount_state["Active"].get<bool>() != active) ||
mount_state[mount_id].end()) || (active &&
(mount_state[mount_id]["Active"].get<bool>() != active) || ((mount_state.find("Location") == mount_state.end()) ||
(active && ((mount_state[mount_id].find("Location") == (mount_state["Location"].get<std::string>() != mount_location)))) {
mount_state[mount_id].end()) || if (mount_location.empty() && not active) {
(mount_state[mount_id]["Location"].get<std::string>() != ret = utils::file::file{get_lock_data_file()}.remove();
mount_location)))) { } else {
const auto lines = utils::file::read_file_lines(get_lock_data_file()); ret = utils::file::write_json_file(
const auto txt = std::accumulate( get_lock_data_file(),
lines.begin(), lines.end(), std::string(), {
[](auto &&val, auto &&line) -> auto { return val + line; });
auto json_data = json::parse(txt.empty() ? "{}" : txt);
json_data[mount_id] = {
{"Active", active}, {"Active", active},
{"Location", active ? mount_location : ""}, {"Location", active ? mount_location : ""},
{"PID", active ? pid : -1}, {"PID", active ? pid : -1},
}; });
ret = utils::file::write_json_file(get_lock_data_file(), json_data); }
} else { } else {
ret = true; ret = true;
} }
@ -174,17 +179,16 @@ auto lock_data::set_mount_state(bool active, const std::string &mount_location,
} }
close(handle); close(handle);
}
return ret; return ret;
} }
auto lock_data::wait_for_lock(int fd, std::uint8_t retry_count) -> int { auto lock_data::wait_for_lock(int handle, std::uint8_t retry_count) -> int {
static constexpr const std::uint32_t max_sleep = 100U; static constexpr const std::uint32_t max_sleep{100U};
auto lock_status = EWOULDBLOCK; auto lock_status{EWOULDBLOCK};
auto remain = static_cast<std::uint32_t>(retry_count * max_sleep); auto remain{static_cast<std::uint32_t>(retry_count * max_sleep)};
while ((remain > 0) && (lock_status == EWOULDBLOCK)) { while ((remain > 0) && (lock_status == EWOULDBLOCK)) {
lock_status = flock(fd, LOCK_EX | LOCK_NB); lock_status = flock(handle, LOCK_EX | LOCK_NB);
if (lock_status == -1) { if (lock_status == -1) {
lock_status = errno; lock_status = errno;
if (lock_status == EWOULDBLOCK) { if (lock_status == EWOULDBLOCK) {
@ -233,13 +237,13 @@ auto provider_meta_handler(i_provider &provider, bool directory,
const api_file &file) -> api_error { const api_file &file) -> api_error {
REPERTORY_USES_FUNCTION_NAME(); REPERTORY_USES_FUNCTION_NAME();
const auto meta = create_meta_attributes( auto meta = create_meta_attributes(
file.accessed_date, file.accessed_date,
directory ? FILE_ATTRIBUTE_DIRECTORY : FILE_ATTRIBUTE_ARCHIVE, directory ? FILE_ATTRIBUTE_DIRECTORY : FILE_ATTRIBUTE_ARCHIVE,
file.changed_date, file.creation_date, directory, getgid(), file.key, file.changed_date, file.creation_date, directory, getgid(), file.key,
directory ? S_IFDIR | S_IRUSR | S_IWUSR | S_IXUSR directory ? S_IFDIR | S_IRUSR | S_IWUSR | S_IXUSR
: S_IFREG | S_IRUSR | S_IWUSR, : S_IFREG | S_IRUSR | S_IWUSR,
file.modified_date, 0u, 0u, file.file_size, file.source_path, getuid(), file.modified_date, 0U, 0U, file.file_size, file.source_path, getuid(),
file.modified_date); file.modified_date);
auto res = provider.set_item_meta(file.api_path, meta); auto res = provider.set_item_meta(file.api_path, meta);
if (res == api_error::success) { if (res == api_error::success) {

View File

@ -21,150 +21,171 @@
*/ */
#if defined(_WIN32) #if defined(_WIN32)
#include "platform/win32_platform.hpp" #include "platform/platform.hpp"
#include "events/event_system.hpp" #include "events/event_system.hpp"
#include "events/types/filesystem_item_added.hpp" #include "events/types/filesystem_item_added.hpp"
#include "providers/i_provider.hpp" #include "providers/i_provider.hpp"
#include "utils/config.hpp"
#include "utils/error_utils.hpp" #include "utils/error_utils.hpp"
#include "utils/string.hpp" #include "utils/string.hpp"
namespace repertory { namespace repertory {
auto lock_data::get_mount_state(const provider_type & /*pt*/, json &mount_state) lock_data::lock_data(provider_type prov, std::string unique_id)
-> bool { : mutex_id_(create_lock_id(prov, unique_id)),
const auto ret = get_mount_state(mount_state); mutex_handle_(::CreateMutex(nullptr, FALSE,
if (ret) { create_lock_id(prov, unique_id).c_str())) {}
const auto mount_id =
app_config::get_provider_display_name(pt_) + unique_id_; lock_data::~lock_data() { release(); }
mount_state = mount_state[mount_id].empty()
? json({{"Active", false}, {"Location", ""}, {"PID", -1}}) auto lock_data::get_current_mount_state(json &mount_state) -> bool {
: mount_state[mount_id]; REPERTORY_USES_FUNCTION_NAME();
HKEY key{};
if (::RegOpenKeyEx(HKEY_CURRENT_USER,
fmt::format(R"(SOFTWARE\{}\Mounts\{})",
REPERTORY_DATA_NAME, mutex_id_)
.c_str(),
0, KEY_ALL_ACCESS, &key) != ERROR_SUCCESS) {
return true;
} }
std::string data;
DWORD data_size{};
DWORD type{REG_SZ};
::RegGetValueA(key, nullptr, nullptr, RRF_RT_REG_SZ, &type, nullptr,
&data_size);
data.resize(data_size);
auto res = ::RegGetValueA(key, nullptr, nullptr, RRF_RT_REG_SZ, &type,
data.data(), &data_size);
auto ret = res == ERROR_SUCCESS || res == ERROR_FILE_NOT_FOUND;
if (ret && data_size != 0U) {
try {
mount_state = json::parse(data);
} catch (const std::exception &e) {
utils::error::raise_error(function_name, e, "failed to read mount state");
ret = false;
}
}
::RegCloseKey(key);
return ret; return ret;
} }
auto lock_data::get_mount_state(json &mount_state) -> bool { auto lock_data::get_mount_state(json &mount_state) -> bool {
HKEY key; if (not get_current_mount_state(mount_state)) {
auto ret = !::RegCreateKeyEx( return false;
HKEY_CURRENT_USER,
("SOFTWARE\\" + std::string{REPERTORY_DATA_NAME} + "\\Mounts").c_str(), 0,
nullptr, 0, KEY_ALL_ACCESS, nullptr, &key, nullptr);
if (ret) {
DWORD i = 0u;
DWORD data_size = 0u;
std::string name;
name.resize(32767u);
auto name_size = static_cast<DWORD>(name.size());
while (ret &&
(::RegEnumValue(key, i, &name[0], &name_size, nullptr, nullptr,
nullptr, &data_size) == ERROR_SUCCESS)) {
std::string data;
data.resize(data_size);
name_size++;
if ((ret = !::RegEnumValue(key, i++, &name[0], &name_size, nullptr,
nullptr, reinterpret_cast<LPBYTE>(&data[0]),
&data_size))) {
mount_state[name.c_str()] = json::parse(data);
name_size = static_cast<DWORD>(name.size());
data_size = 0u;
} }
}
::RegCloseKey(key); mount_state = mount_state.empty() ? json({
} {"Active", false},
return ret; {"Location", ""},
{"PID", -1},
})
: mount_state;
return true;
} }
auto lock_data::grab_lock(std::uint8_t retry_count) -> lock_result { auto lock_data::grab_lock(std::uint8_t retry_count) -> lock_result {
REPERTORY_USES_FUNCTION_NAME(); static constexpr const std::uint32_t max_sleep{100U};
auto ret = lock_result::success;
if (mutex_handle_ == INVALID_HANDLE_VALUE) { if (mutex_handle_ == INVALID_HANDLE_VALUE) {
ret = lock_result::failure; return lock_result::failure;
} else { }
for (auto i = 0;
(i <= retry_count) && ((mutex_state_ = ::WaitForSingleObject( for (std::uint8_t idx = 0U;
mutex_handle_, 100)) == WAIT_TIMEOUT); (idx <= retry_count) &&
i++) { ((mutex_state_ = ::WaitForSingleObject(mutex_handle_, max_sleep)) ==
WAIT_TIMEOUT);
++idx) {
} }
switch (mutex_state_) { switch (mutex_state_) {
case WAIT_OBJECT_0: { case WAIT_OBJECT_0:
ret = lock_result::success; return lock_result::success;
auto should_reset = true;
json mount_state;
if (get_mount_state(pt_, mount_state)) {
if (mount_state["Active"].get<bool>() &&
mount_state["Location"] == "elevating") {
should_reset = false;
}
}
if (should_reset) {
if (not set_mount_state(false, "", -1)) {
utils::error::raise_error(function_name, "failed to set mount state");
}
}
} break;
case WAIT_TIMEOUT: case WAIT_TIMEOUT:
ret = lock_result::locked; return lock_result::locked;
break;
default: default:
ret = lock_result::failure; return lock_result::failure;
break;
} }
} }
return ret;
}
void lock_data::release() { void lock_data::release() {
if (mutex_handle_ != INVALID_HANDLE_VALUE) { if (mutex_handle_ == INVALID_HANDLE_VALUE) {
return;
}
if ((mutex_state_ == WAIT_OBJECT_0) || (mutex_state_ == WAIT_ABANDONED)) { if ((mutex_state_ == WAIT_OBJECT_0) || (mutex_state_ == WAIT_ABANDONED)) {
if (mutex_state_ == WAIT_OBJECT_0) {
[[maybe_unused]] auto success{set_mount_state(false, "", -1)};
}
::ReleaseMutex(mutex_handle_); ::ReleaseMutex(mutex_handle_);
} }
::CloseHandle(mutex_handle_); ::CloseHandle(mutex_handle_);
mutex_handle_ = INVALID_HANDLE_VALUE; mutex_handle_ = INVALID_HANDLE_VALUE;
} }
auto lock_data::set_mount_state(bool active, std::string_view mount_location,
std::int64_t pid) -> bool {
if (mutex_handle_ == INVALID_HANDLE_VALUE) {
return false;
} }
auto lock_data::set_mount_state(bool active, const std::string &mount_location,
const std::int64_t &pid) -> bool {
auto ret = false;
if (mutex_handle_ != INVALID_HANDLE_VALUE) {
const auto mount_id =
app_config::get_provider_display_name(pt_) + unique_id_;
json mount_state; json mount_state;
[[maybe_unused]] auto success = get_mount_state(mount_state); [[maybe_unused]] auto success{get_mount_state(mount_state)};
if ((mount_state.find(mount_id) == mount_state.end()) || if (not((mount_state.find("Active") == mount_state.end()) ||
(mount_state[mount_id].find("Active") == mount_state[mount_id].end()) || (mount_state["Active"].get<bool>() != active) ||
(mount_state[mount_id]["Active"].get<bool>() != active) || (active &&
(active && ((mount_state[mount_id].find("Location") == ((mount_state.find("Location") == mount_state.end()) ||
mount_state[mount_id].end()) || (mount_state["Location"].get<std::string>() != mount_location))))) {
(mount_state[mount_id]["Location"].get<std::string>() != return true;
mount_location)))) {
HKEY key;
if ((ret = !::RegCreateKeyEx(
HKEY_CURRENT_USER,
("SOFTWARE\\" + std::string{REPERTORY_DATA_NAME} + "\\Mounts")
.c_str(),
0, nullptr, 0, KEY_ALL_ACCESS, nullptr, &key, nullptr))) {
const auto str = json({{"Active", active},
{"Location", active ? mount_location : ""},
{"PID", active ? pid : -1}})
.dump(0);
ret = !::RegSetValueEx(key, &mount_id[0], 0, REG_SZ,
reinterpret_cast<const BYTE *>(&str[0]),
static_cast<DWORD>(str.size()));
::RegCloseKey(key);
}
} else {
ret = true;
}
} }
HKEY key{};
if (::RegCreateKeyExA(HKEY_CURRENT_USER,
fmt::format(R"(SOFTWARE\{}\Mounts\{})",
REPERTORY_DATA_NAME, mutex_id_)
.c_str(),
0, nullptr, 0, KEY_ALL_ACCESS, nullptr, &key,
nullptr) != ERROR_SUCCESS) {
return false;
}
auto ret{false};
if (mount_location.empty() && not active) {
::RegCloseKey(key);
if (::RegCreateKeyExA(
HKEY_CURRENT_USER,
fmt::format(R"(SOFTWARE\{}\Mounts)", REPERTORY_DATA_NAME).c_str(),
0, nullptr, 0, KEY_ALL_ACCESS, nullptr, &key,
nullptr) != ERROR_SUCCESS) {
return false;
}
ret = (::RegDeleteKeyA(key, mutex_id_.c_str()) == ERROR_SUCCESS);
} else {
auto data{
json({
{"Active", active},
{"Location", active ? mount_location : ""},
{"PID", active ? pid : -1},
})
.dump(),
};
ret = (::RegSetValueEx(key, nullptr, 0, REG_SZ,
reinterpret_cast<const BYTE *>(data.c_str()),
static_cast<DWORD>(data.size())) == ERROR_SUCCESS);
}
::RegCloseKey(key);
return ret; return ret;
} }
@ -215,4 +236,4 @@ auto provider_meta_handler(i_provider &provider, bool directory,
} }
} // namespace repertory } // namespace repertory
#endif //_WIN32 #endif // defined(_WIN32)

View File

@ -29,9 +29,7 @@
#include "events/types/service_stop_end.hpp" #include "events/types/service_stop_end.hpp"
#include "events/types/unmount_requested.hpp" #include "events/types/unmount_requested.hpp"
#include "rpc/common.hpp" #include "rpc/common.hpp"
#include "utils/base64.hpp"
#include "utils/error_utils.hpp" #include "utils/error_utils.hpp"
#include "utils/string.hpp"
namespace repertory { namespace repertory {
server::server(app_config &config) : config_(config) {} server::server(app_config &config) : config_(config) {}
@ -144,13 +142,17 @@ void server::start() {
initialize(*server_); initialize(*server_);
server_thread_ = std::make_unique<std::thread>([this]() { server_thread_ = std::make_unique<std::thread>([this]() {
#ifdef _WIN32
server_->set_socket_options([](auto &&sock) { server_->set_socket_options([](auto &&sock) {
int enable = 1; #if defined(_WIN32)
int enable{1};
setsockopt(sock, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, setsockopt(sock, SOL_SOCKET, SO_EXCLUSIVEADDRUSE,
reinterpret_cast<const char *>(&enable), sizeof(enable)); reinterpret_cast<const char *>(&enable), sizeof(enable));
#else // !defined(_WIN32)
linger opt{1, 0};
setsockopt(sock, SOL_SOCKET, SO_LINGER,
reinterpret_cast<const char *>(&opt), sizeof(opt));
#endif // defined(_WIN32)
}); });
#endif // _WIN32
server_->listen("127.0.0.1", config_.get_api_port()); server_->listen("127.0.0.1", config_.get_api_port());
}); });

View File

@ -0,0 +1,31 @@
/*
Copyright <2018-2025> <scott.e.graves@protonmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#include "platform/platform.hpp"
#include "app_config.hpp"
namespace repertory {
auto create_lock_id(provider_type prov, std::string_view unique_id)->std::string {
return fmt::format("{}_{}_{}", REPERTORY_DATA_NAME,
app_config::get_provider_name(prov), unique_id);
}
} // namespace repertory

View File

@ -28,13 +28,13 @@
#include "providers/provider.hpp" #include "providers/provider.hpp"
#include "types/repertory.hpp" #include "types/repertory.hpp"
#include "utils/cli_utils.hpp" #include "utils/cli_utils.hpp"
#include "utils/com_init_wrapper.hpp"
#include "utils/file.hpp" #include "utils/file.hpp"
#if defined(_WIN32) #if defined(_WIN32)
#include "drives/winfsp/remotewinfsp/remote_client.hpp" #include "drives/winfsp/remotewinfsp/remote_client.hpp"
#include "drives/winfsp/remotewinfsp/remote_winfsp_drive.hpp" #include "drives/winfsp/remotewinfsp/remote_winfsp_drive.hpp"
#include "drives/winfsp/winfsp_drive.hpp" #include "drives/winfsp/winfsp_drive.hpp"
#include "utils/com_init_wrapper.hpp"
using repertory_drive = repertory::winfsp_drive; using repertory_drive = repertory::winfsp_drive;
using remote_client = repertory::remote_winfsp::remote_client; using remote_client = repertory::remote_winfsp::remote_client;
@ -56,6 +56,15 @@ namespace repertory::cli::actions {
mount(std::vector<const char *> args, std::string data_directory, mount(std::vector<const char *> args, std::string data_directory,
int &mount_result, provider_type prov, const std::string &remote_host, int &mount_result, provider_type prov, const std::string &remote_host,
std::uint16_t remote_port, const std::string &unique_id) -> exit_code { std::uint16_t remote_port, const std::string &unique_id) -> exit_code {
lock_data global_lock(provider_type::unknown, "global");
{
auto lock_result = global_lock.grab_lock(100U);
if (lock_result != lock_result::success) {
std::cerr << "FATAL: Unable to get global lock" << std::endl;
return exit_code::lock_failed;
}
}
lock_data lock(prov, unique_id); lock_data lock(prov, unique_id);
auto lock_result = lock.grab_lock(); auto lock_result = lock.grab_lock();
if (lock_result == lock_result::locked) { if (lock_result == lock_result::locked) {
@ -97,13 +106,6 @@ mount(std::vector<const char *> args, std::string data_directory,
} }
#endif // defined(_WIN32) #endif // defined(_WIN32)
lock_data global_lock(provider_type::unknown, "global");
lock_result = global_lock.grab_lock(100U);
if (lock_result != lock_result::success) {
std::cerr << "FATAL: Unable to get global lock" << std::endl;
return exit_code::lock_failed;
}
auto drive_args = utils::cli::parse_drive_options(args, prov, data_directory); auto drive_args = utils::cli::parse_drive_options(args, prov, data_directory);
app_config config(prov, data_directory); app_config config(prov, data_directory);
{ {

View File

@ -74,7 +74,7 @@ private:
[[nodiscard]] auto data_directory_exists(provider_type prov, [[nodiscard]] auto data_directory_exists(provider_type prov,
std::string_view name) const -> bool; std::string_view name) const -> bool;
void handle_get_available_locations(httplib::Response &res) const; static void handle_get_available_locations(httplib::Response &res);
void handle_get_mount(const httplib::Request &req, void handle_get_mount(const httplib::Request &req,
httplib::Response &res) const; httplib::Response &res) const;
@ -96,6 +96,9 @@ private:
void handle_post_mount(const httplib::Request &req, httplib::Response &res); void handle_post_mount(const httplib::Request &req, httplib::Response &res);
void handle_put_mount_location(const httplib::Request &req,
httplib::Response &res) const;
void handle_put_set_value_by_name(const httplib::Request &req, void handle_put_set_value_by_name(const httplib::Request &req,
httplib::Response &res) const; httplib::Response &res) const;

View File

@ -109,13 +109,17 @@ handlers::handlers(mgmt_app_config *config, httplib::Server *server)
server_(server) { server_(server) {
REPERTORY_USES_FUNCTION_NAME(); REPERTORY_USES_FUNCTION_NAME();
#ifdef _WIN32
server_->set_socket_options([](auto &&sock) { server_->set_socket_options([](auto &&sock) {
int enable = 1; #if defined(_WIN32)
int enable{1};
setsockopt(sock, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, setsockopt(sock, SOL_SOCKET, SO_EXCLUSIVEADDRUSE,
reinterpret_cast<const char *>(&enable), sizeof(enable)); reinterpret_cast<const char *>(&enable), sizeof(enable));
#else // !defined(_WIN32)
linger opt{1, 0};
setsockopt(sock, SOL_SOCKET, SO_LINGER,
reinterpret_cast<const char *>(&opt), sizeof(opt));
#endif // defined(_WIN32)
}); });
#endif // _WIN32
server_->set_pre_routing_handler( server_->set_pre_routing_handler(
[this](const httplib::Request &req, [this](const httplib::Request &req,
@ -168,7 +172,7 @@ handlers::handlers(mgmt_app_config *config, httplib::Server *server)
: http_error_codes::internal_error; : http_error_codes::internal_error;
}); });
server->Get("/api/v1/locations", [this](auto && /* req */, auto &&res) { server->Get("/api/v1/locations", [](auto && /* req */, auto &&res) {
handle_get_available_locations(res); handle_get_available_locations(res);
}); });
@ -201,6 +205,10 @@ handlers::handlers(mgmt_app_config *config, httplib::Server *server)
server->Post("/api/v1/mount", server->Post("/api/v1/mount",
[this](auto &&req, auto &&res) { handle_post_mount(req, res); }); [this](auto &&req, auto &&res) { handle_post_mount(req, res); });
server->Put("/api/v1/mount_location", [this](auto &&req, auto &&res) {
handle_put_mount_location(req, res);
});
server->Put("/api/v1/set_value_by_name", [this](auto &&req, auto &&res) { server->Put("/api/v1/set_value_by_name", [this](auto &&req, auto &&res) {
handle_put_set_value_by_name(req, res); handle_put_set_value_by_name(req, res);
}); });
@ -298,7 +306,24 @@ auto handlers::data_directory_exists(provider_type prov,
return ret; return ret;
} }
void handlers::handle_get_available_locations(httplib::Response &res) const { void handlers::handle_put_mount_location(const httplib::Request &req,
httplib::Response &res) const {
REPERTORY_USES_FUNCTION_NAME();
auto prov = provider_type_from_string(req.get_param_value("type"));
auto name = req.get_param_value("name");
auto location = req.get_param_value("location");
if (not data_directory_exists(prov, name)) {
res.status = http_error_codes::not_found;
return;
}
config_->set_mount_location(prov, name, location);
res.status = http_error_codes::ok;
}
void handlers::handle_get_available_locations(httplib::Response &res) {
#if defined(_WIN32) #if defined(_WIN32)
constexpr const std::array<std::string_view, 26U> letters{ constexpr const std::array<std::string_view, 26U> letters{
"A:", "B:", "C:", "D:", "E:", "F:", "G:", "H:", "I:", "A:", "B:", "C:", "D:", "E:", "F:", "G:", "H:", "I:",
@ -407,8 +432,6 @@ void handlers::handle_get_mount_location(const httplib::Request &req,
void handlers::handle_get_mount_status(const httplib::Request &req, void handlers::handle_get_mount_status(const httplib::Request &req,
httplib::Response &res) const { httplib::Response &res) const {
REPERTORY_USES_FUNCTION_NAME();
auto name = req.get_param_value("name"); auto name = req.get_param_value("name");
auto prov = provider_type_from_string(req.get_param_value("type")); auto prov = provider_type_from_string(req.get_param_value("type"));
@ -417,34 +440,9 @@ void handlers::handle_get_mount_status(const httplib::Request &req,
return; return;
} }
auto status_name = app_config::get_provider_display_name(prov);
switch (prov) {
case provider_type::remote: {
auto parts = utils::string::split(name, '_', false);
status_name =
fmt::format("{}{}:{}", status_name, parts.at(0U), parts.at(1U));
} break;
case provider_type::encrypt:
case provider_type::sia:
case provider_type::s3:
status_name = fmt::format("{}{}", status_name, name);
break;
default:
throw utils::error::create_exception(function_name,
{
"provider is not supported",
provider_type_to_string(prov),
name,
});
}
auto lines = launch_process(prov, name, {"-status"}); auto lines = launch_process(prov, name, {"-status"});
nlohmann::json result( auto result = nlohmann::json::parse(utils::string::join(lines, '\n'));
nlohmann::json::parse(utils::string::join(lines, '\n')).at(status_name));
if (result.at("Location").get<std::string>().empty()) { if (result.at("Location").get<std::string>().empty()) {
result.at("Location") = config_->get_mount_location(prov, name); result.at("Location") = config_->get_mount_location(prov, name);
} else if (result.at("Active").get<bool>()) { } else if (result.at("Active").get<bool>()) {
@ -490,14 +488,14 @@ void handlers::handle_post_add_mount(const httplib::Request &req,
for (const auto &[key, value] : cfg.items()) { for (const auto &[key, value] : cfg.items()) {
if (value.is_object()) { if (value.is_object()) {
for (const auto &[key2, value2] : value.items()) { for (const auto &[key2, value2] : value.items()) {
auto subKey = fmt::format("{}.{}", key, key2); auto sub_key = fmt::format("{}.{}", key, key2);
auto skip{false}; auto skip{false};
auto decrypted = decrypt_value( auto decrypted = decrypt_value(
config_, subKey, value2.template get<std::string>(), skip); config_, sub_key, value2.template get<std::string>(), skip);
if (skip) { if (skip) {
continue; continue;
} }
values[subKey] = decrypted; values[sub_key] = decrypted;
} }
continue; continue;
@ -546,12 +544,12 @@ void handlers::handle_post_mount(const httplib::Request &req,
return; return;
} }
config_->set_mount_location(prov, name, location);
static std::mutex mount_mtx; static std::mutex mount_mtx;
mutex_lock lock(mount_mtx); mutex_lock lock(mount_mtx);
launch_process(prov, name, {location}, true); launch_process(prov, name, {location}, true);
config_->set_mount_location(prov, name, location);
launch_process(prov, name, {"-status"}); launch_process(prov, name, {"-status"});
} }
@ -582,7 +580,7 @@ void handlers::handle_put_set_value_by_name(const httplib::Request &req,
void handlers::handle_put_settings(const httplib::Request &req, void handlers::handle_put_settings(const httplib::Request &req,
httplib::Response &res) const { httplib::Response &res) const {
nlohmann::json data = nlohmann::json::parse(req.get_param_value("data")); auto data = nlohmann::json::parse(req.get_param_value("data"));
if (data.contains(JSON_API_PASSWORD)) { if (data.contains(JSON_API_PASSWORD)) {
auto password = decrypt(data.at(JSON_API_PASSWORD).get<std::string>(), auto password = decrypt(data.at(JSON_API_PASSWORD).get<std::string>(),
@ -661,10 +659,8 @@ auto handlers::launch_process(provider_type prov, std::string_view name,
args.insert(std::next(args.begin(), 3U), ""); args.insert(std::next(args.begin(), 3U), "");
args.insert(std::next(args.begin(), 4U), "/MIN"); args.insert(std::next(args.begin(), 4U), "/MIN");
args.insert(std::next(args.begin(), 5U), repertory_binary_); args.insert(std::next(args.begin(), 5U), repertory_binary_);
#elif defined(__linux__) // defined(__linux__) #else // !defined(_WIN32)
args.insert(args.begin(), repertory_binary_); args.insert(args.begin(), repertory_binary_);
#else // !defined(__linux__) && !defined(_WIN32)
build fails here
#endif // defined(_WIN32) #endif // defined(_WIN32)
std::vector<const char *> exec_args; std::vector<const char *> exec_args;
@ -677,16 +673,9 @@ auto handlers::launch_process(provider_type prov, std::string_view name,
#if defined(_WIN32) #if defined(_WIN32)
_spawnv(_P_DETACH, exec_args.at(0U), _spawnv(_P_DETACH, exec_args.at(0U),
const_cast<char *const *>(exec_args.data())); const_cast<char *const *>(exec_args.data()));
#elif defined(__linux__) // defined(__linux__) #else // !defined(_WIN32)
auto pid = fork(); auto pid = fork();
if (pid < 0) { if (pid == 0) {
throw utils::error::create_exception(function_name, {"mount failed"});
}
if (pid != 0) {
return {};
}
setsid(); setsid();
chdir("/"); chdir("/");
close(STDIN_FILENO); close(STDIN_FILENO);
@ -696,10 +685,10 @@ auto handlers::launch_process(provider_type prov, std::string_view name,
open("/dev/null", O_WRONLY); open("/dev/null", O_WRONLY);
open("/dev/null", O_WRONLY); open("/dev/null", O_WRONLY);
signal(SIGCHLD, SIG_IGN);
execvp(exec_args.at(0U), const_cast<char *const *>(exec_args.data())); execvp(exec_args.at(0U), const_cast<char *const *>(exec_args.data()));
#else // !defined(__linux__) && !defined(_WIN32) } else {
build fails here signal(SIGCHLD, SIG_IGN);
}
#endif // defined(_WIN32) #endif // defined(_WIN32)
return {}; return {};
} }

View File

@ -62,13 +62,16 @@ TEST(lock_data_test, set_and_unset_mount_state) {
json mount_state; json mount_state;
EXPECT_TRUE(l.get_mount_state(mount_state)); EXPECT_TRUE(l.get_mount_state(mount_state));
EXPECT_STREQ(R"({"Active":true,"Location":"C:","PID":99})", EXPECT_STREQ(R"({"Active":true,"Location":"C:","PID":99})",
mount_state["Sia1"].dump().c_str()); mount_state.dump().c_str());
EXPECT_TRUE(l2.get_mount_state(mount_state));
EXPECT_STREQ(R"({"Active":true,"Location":"D:","PID":97})", EXPECT_STREQ(R"({"Active":true,"Location":"D:","PID":97})",
mount_state["Remote1"].dump().c_str()); mount_state.dump().c_str());
EXPECT_TRUE(l3.get_mount_state(mount_state));
EXPECT_STREQ(R"({"Active":true,"Location":"E:","PID":96})", EXPECT_STREQ(R"({"Active":true,"Location":"E:","PID":96})",
mount_state["Remote2"].dump().c_str()); mount_state.dump().c_str());
EXPECT_TRUE(l.set_mount_state(false, "C:", 99)); EXPECT_TRUE(l.set_mount_state(false, "C:", 99));
EXPECT_TRUE(l2.set_mount_state(false, "D:", 98)); EXPECT_TRUE(l2.set_mount_state(false, "D:", 98));
@ -76,11 +79,15 @@ TEST(lock_data_test, set_and_unset_mount_state) {
EXPECT_TRUE(l.get_mount_state(mount_state)); EXPECT_TRUE(l.get_mount_state(mount_state));
EXPECT_STREQ(R"({"Active":false,"Location":"","PID":-1})", EXPECT_STREQ(R"({"Active":false,"Location":"","PID":-1})",
mount_state["Sia1"].dump().c_str()); mount_state.dump().c_str());
EXPECT_TRUE(l2.get_mount_state(mount_state));
EXPECT_STREQ(R"({"Active":false,"Location":"","PID":-1})", EXPECT_STREQ(R"({"Active":false,"Location":"","PID":-1})",
mount_state["Remote1"].dump().c_str()); mount_state.dump().c_str());
EXPECT_TRUE(l3.get_mount_state(mount_state));
EXPECT_STREQ(R"({"Active":false,"Location":"","PID":-1})", EXPECT_STREQ(R"({"Active":false,"Location":"","PID":-1})",
mount_state["Remote2"].dump().c_str()); mount_state.dump().c_str());
} }
#else #else
TEST(lock_data_test, set_and_unset_mount_state) { TEST(lock_data_test, set_and_unset_mount_state) {
@ -91,14 +98,13 @@ TEST(lock_data_test, set_and_unset_mount_state) {
EXPECT_TRUE(l.get_mount_state(mount_state)); EXPECT_TRUE(l.get_mount_state(mount_state));
EXPECT_STREQ(R"({"Active":true,"Location":"/mnt/1","PID":99})", EXPECT_STREQ(R"({"Active":true,"Location":"/mnt/1","PID":99})",
mount_state["Sia1"].dump().c_str()); mount_state.dump().c_str());
EXPECT_TRUE(l.set_mount_state(false, "/mnt/1", 99)); EXPECT_TRUE(l.set_mount_state(false, "/mnt/1", 99));
EXPECT_TRUE(l.get_mount_state(mount_state)); EXPECT_TRUE(l.get_mount_state(mount_state));
EXPECT_STREQ(R"({"Active":false,"Location":"","PID":-1})", EXPECT_STREQ(R"({"Active":false,"Location":"","PID":-1})",
mount_state["Sia1"].dump().c_str()); mount_state.dump().c_str());
} }
#endif #endif
} // namespace repertory } // namespace repertory

View File

@ -25,8 +25,8 @@
#include "utils/string.hpp" #include "utils/string.hpp"
namespace repertory::utils { namespace repertory::utils {
auto compare_version_strings(std::string version1, auto compare_version_strings(std::string version1, std::string version2)
std::string version2) -> std::int32_t { -> std::int32_t {
if (utils::string::contains(version1, "-")) { if (utils::string::contains(version1, "-")) {
version1 = utils::string::split(version1, '-', true)[0U]; version1 = utils::string::split(version1, '-', true)[0U];
@ -157,7 +157,7 @@ auto get_next_available_port(std::uint16_t first_port,
++check_port; ++check_port;
continue; continue;
} }
acceptor.set_option(boost::asio::ip::tcp::acceptor::linger(true, 0));
acceptor.bind({tcp::v4(), static_cast<std::uint16_t>(check_port)}, acceptor.bind({tcp::v4(), static_cast<std::uint16_t>(check_port)},
error_code); error_code);
if (error_code) { if (error_code) {

View File

@ -3,7 +3,8 @@ import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:repertory/constants.dart' as constants; import 'package:repertory/constants.dart' as constants;
import 'package:sodium_libs/sodium_libs.dart'; import 'package:repertory/models/auth.dart';
import 'package:sodium_libs/sodium_libs.dart' show SecureKey, StringX;
typedef Validator = bool Function(String); typedef Validator = bool Function(String);
@ -106,25 +107,31 @@ Map<String, dynamic> createDefaultSettings(String mountType) {
return {}; return {};
} }
void displayAuthError() { void displayAuthError(Auth auth) {
if (constants.navigatorKey.currentContext == null) { if (!auth.authenticated || constants.navigatorKey.currentContext == null) {
return; return;
} }
displayErrorMessage( displayErrorMessage(
constants.navigatorKey.currentContext!, constants.navigatorKey.currentContext!,
"Authentication failed", "Authentication failed",
clear: true,
); );
} }
void displayErrorMessage(context, String text) { void displayErrorMessage(context, String text, {bool clear = false}) {
if (!context.mounted) { if (!context.mounted) {
return; return;
} }
ScaffoldMessenger.of( final messenger = ScaffoldMessenger.of(context);
context, if (clear) {
).showSnackBar(SnackBar(content: Text(text, textAlign: TextAlign.center))); messenger.removeCurrentSnackBar();
}
messenger.showSnackBar(
SnackBar(content: Text(text, textAlign: TextAlign.center)),
);
} }
String formatMountName(String type, String name) { String formatMountName(String type, String name) {
@ -329,3 +336,67 @@ Map<String, dynamic> getChanged(
return changed; return changed;
} }
Future<String?> editMountLocation(
context,
List<String> available, {
bool allowEmpty = false,
String? location,
}) async {
String? currentLocation = location;
final controller = TextEditingController(text: currentLocation);
return await showDialog(
context: context,
builder: (context) {
return StatefulBuilder(
builder: (context, setState) {
return AlertDialog(
actions: [
TextButton(
child: const Text('Cancel'),
onPressed: () => Navigator.of(context).pop(null),
),
TextButton(
child: const Text('OK'),
onPressed: () {
final result = getSettingValidators('Path').firstWhereOrNull(
(validator) => !validator(currentLocation ?? ''),
);
if (result != null) {
return displayErrorMessage(
context,
"Mount location is not valid",
);
}
Navigator.of(context).pop(currentLocation);
},
),
],
content:
available.isEmpty
? TextField(
autofocus: true,
controller: controller,
onChanged:
(value) => setState(() => currentLocation = value),
)
: DropdownButton<String>(
hint: const Text("Select drive"),
value: currentLocation,
onChanged:
(value) => setState(() => currentLocation = value),
items:
available.map<DropdownMenuItem<String>>((item) {
return DropdownMenuItem<String>(
value: item,
child: Text(item),
);
}).toList(),
),
title: const Text('Mount Location', textAlign: TextAlign.center),
);
},
);
},
);
}

View File

@ -54,9 +54,11 @@ class Auth with ChangeNotifier {
void logoff() { void logoff() {
_authenticated = false; _authenticated = false;
_key = SecureKey.random(constants.sodium, 32);
_user = ""; _user = "";
mountList?.clear();
notifyListeners(); notifyListeners();
mountList?.clear();
} }
} }

View File

@ -101,6 +101,35 @@ class Mount with ChangeNotifier {
} }
} }
Future<void> setMountLocation(String location) async {
try {
mountConfig.path = location;
final auth = await _auth.createAuth();
final response = await http.put(
Uri.parse(
Uri.encodeFull(
'${getBaseUri()}/api/v1/mount_location?auth=$auth&name=$name&type=$type&location=$location',
),
),
);
if (response.statusCode == 401) {
_auth.logoff();
return;
}
if (response.statusCode == 404) {
_mountList?.reset();
return;
}
return refresh();
} catch (e) {
debugPrint('$e');
}
}
Future<List<String>> getAvailableLocations() async { Future<List<String>> getAvailableLocations() async {
try { try {
final auth = await _auth.createAuth(); final auth = await _auth.createAuth();
@ -178,7 +207,7 @@ class Mount with ChangeNotifier {
); );
if (response.statusCode == 401) { if (response.statusCode == 401) {
displayAuthError(); displayAuthError(_auth);
_auth.logoff(); _auth.logoff();
return false; return false;
} }

View File

@ -63,7 +63,7 @@ class MountList with ChangeNotifier {
); );
if (response.statusCode == 401) { if (response.statusCode == 401) {
displayAuthError(); displayAuthError(_auth);
_auth.logoff(); _auth.logoff();
return; return;
} }
@ -155,7 +155,7 @@ class MountList with ChangeNotifier {
ret = true; ret = true;
break; break;
case 401: case 401:
displayAuthError(); displayAuthError(_auth);
_auth.logoff(); _auth.logoff();
break; break;
case 404: case 404:

View File

@ -38,6 +38,19 @@ class _AuthScreenState extends State<AuthScreen> {
return SizedBox.shrink(); return SizedBox.shrink();
} }
createLoginHandler() {
return _enabled
? () async {
setState(() => _enabled = false);
await auth.authenticate(
_userController.text,
_passwordController.text,
);
setState(() => _enabled = true);
}
: null;
}
return Center( return Center(
child: Card( child: Card(
child: Padding( child: Padding(
@ -59,26 +72,26 @@ class _AuthScreenState extends State<AuthScreen> {
autofocus: true, autofocus: true,
decoration: InputDecoration(labelText: 'Username'), decoration: InputDecoration(labelText: 'Username'),
controller: _userController, controller: _userController,
textInputAction: TextInputAction.next,
), ),
const SizedBox(height: constants.padding), const SizedBox(height: constants.padding),
TextField( TextField(
obscureText: true, obscureText: true,
decoration: InputDecoration(labelText: 'Password'), decoration: InputDecoration(labelText: 'Password'),
controller: _passwordController, controller: _passwordController,
textInputAction: TextInputAction.go,
onSubmitted: (_) {
final handler = createLoginHandler();
if (handler == null) {
return;
}
handler();
},
), ),
const SizedBox(height: constants.padding), const SizedBox(height: constants.padding),
ElevatedButton( ElevatedButton(
onPressed: onPressed: createLoginHandler(),
_enabled
? () async {
setState(() => _enabled = false);
await auth.authenticate(
_userController.text,
_passwordController.text,
);
setState(() => _enabled = true);
}
: null,
child: const Text('Login'), child: const Text('Login'),
), ),
], ],

View File

@ -17,6 +17,7 @@ class MountWidget extends StatefulWidget {
class _MountWidgetState extends State<MountWidget> { class _MountWidgetState extends State<MountWidget> {
bool _enabled = true; bool _enabled = true;
bool _editEnabled = true;
Timer? _timer; Timer? _timer;
@override @override
@ -61,7 +62,33 @@ class _MountWidgetState extends State<MountWidget> {
mount.provider, mount.provider,
style: TextStyle(color: textColor, fontWeight: FontWeight.bold), style: TextStyle(color: textColor, fontWeight: FontWeight.bold),
), ),
trailing: IconButton( trailing: Row(
mainAxisAlignment: MainAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (mount.mounted != null && !mount.mounted!)
IconButton(
icon: const Icon(Icons.edit),
color: subTextColor,
tooltip: 'Edit mount location',
onPressed: () async {
setState(() => _editEnabled = false);
final available = await mount.getAvailableLocations();
if (context.mounted) {
final location = await editMountLocation(
context,
available,
location: mount.path,
);
if (location != null) {
await mount.setMountLocation(location);
}
}
setState(() => _editEnabled = true);
},
),
IconButton(
icon: Icon( icon: Icon(
mount.mounted == null mount.mounted == null
? Icons.hourglass_top ? Icons.hourglass_top
@ -73,8 +100,16 @@ class _MountWidgetState extends State<MountWidget> {
? Color.fromARGB(255, 163, 96, 76) ? Color.fromARGB(255, 163, 96, 76)
: subTextColor, : subTextColor,
), ),
tooltip:
mount.mounted == null
? ''
: mount.mounted!
? 'Unmount'
: 'Mount',
onPressed: _createMountHandler(context, mount), onPressed: _createMountHandler(context, mount),
), ),
],
),
); );
}, },
), ),
@ -149,70 +184,7 @@ class _MountWidgetState extends State<MountWidget> {
return location; return location;
} }
final available = await mount.getAvailableLocations(); return editMountLocation(context, await mount.getAvailableLocations());
String? currentLocation;
return await showDialog(
context: context,
builder: (context) {
return StatefulBuilder(
builder: (context, setState) {
return AlertDialog(
actions: [
TextButton(
child: const Text('Cancel'),
onPressed: () => Navigator.of(context).pop(null),
),
TextButton(
child: const Text('OK'),
onPressed: () {
final result = getSettingValidators(
'Path',
).firstWhereOrNull(
(validator) => !validator(currentLocation ?? ''),
);
if (result != null) {
return displayErrorMessage(
context,
"Mount location is not valid",
);
}
Navigator.of(context).pop(currentLocation);
},
),
],
content:
available.isEmpty
? TextField(
autofocus: true,
controller: TextEditingController(
text: currentLocation,
),
inputFormatters: [
FilteringTextInputFormatter.deny(RegExp(r'\s')),
],
onChanged:
(value) => setState(() => currentLocation = value),
)
: DropdownButton<String>(
hint: const Text("Select drive"),
value: currentLocation,
onChanged:
(value) => setState(() => currentLocation = value),
items:
available.map<DropdownMenuItem<String>>((item) {
return DropdownMenuItem<String>(
value: item,
child: Text(item),
);
}).toList(),
),
title: const Text('Set Mount Location'),
);
},
);
},
);
} }
@override @override

View File

@ -106,7 +106,6 @@ class _UISettingsWidgetState extends State<UISettingsWidget> {
void dispose() { void dispose() {
final settings = getChanged(widget.origSettings, widget.settings); final settings = getChanged(widget.origSettings, widget.settings);
if (settings.isNotEmpty) { if (settings.isNotEmpty) {
debugPrint("start");
final key = final key =
Provider.of<Auth>( Provider.of<Auth>(
constants.navigatorKey.currentContext!, constants.navigatorKey.currentContext!,
@ -114,15 +113,13 @@ class _UISettingsWidgetState extends State<UISettingsWidget> {
).key; ).key;
convertAllToString(settings, key) convertAllToString(settings, key)
.then((map) async { .then((map) async {
debugPrint("map");
try { try {
final authProvider = Provider.of<Auth>( final authProvider = Provider.of<Auth>(
constants.navigatorKey.currentContext!, constants.navigatorKey.currentContext!,
listen: false, listen: false,
); );
final auth = await authProvider.createAuth();
debugPrint("auth"); final auth = await authProvider.createAuth();
final response = await http.put( final response = await http.put(
Uri.parse( Uri.parse(
Uri.encodeFull( Uri.encodeFull(
@ -132,7 +129,7 @@ class _UISettingsWidgetState extends State<UISettingsWidget> {
); );
if (response.statusCode == 401) { if (response.statusCode == 401) {
displayAuthError(); displayAuthError(authProvider);
authProvider.logoff(); authProvider.logoff();
} }
} catch (e) { } catch (e) {