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
36 changed files with 1904 additions and 917 deletions

View File

@@ -3,6 +3,7 @@ _mkgmtime
_sh_denyno _sh_denyno
_sh_denyrd _sh_denyrd
_sh_denyrw _sh_denyrw
_spawnv
aarch64 aarch64
advapi32 advapi32
armv8 armv8
@@ -114,6 +115,7 @@ googletest
gpath gpath
gtest_version gtest_version
has_setxattr has_setxattr
hkey
httpapi httpapi
httplib httplib
icudata icudata
@@ -121,6 +123,7 @@ icui18n
icuuc icuuc
iostreams iostreams
iphlpapi iphlpapi
ipstream
jthread jthread
libbitcoin libbitcoin
libbitcoinsystem libbitcoinsystem
@@ -142,6 +145,7 @@ libuuid_include_dirs
libvlc libvlc
linkflags linkflags
localappdata localappdata
lpbyte
lptr lptr
lpwstr lpwstr
markdownlint markdownlint
@@ -165,6 +169,7 @@ nuspell_version
oleaut32 oleaut32
openal_version openal_version
openssldir openssldir
pistream
pkgconfig pkgconfig
plarge_integer plarge_integer
plex plex

View File

@@ -4,14 +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
* 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

@@ -43,8 +43,7 @@ public:
[[nodiscard]] static auto default_remote_api_port(const provider_type &prov) [[nodiscard]] static auto default_remote_api_port(const provider_type &prov)
-> std::uint16_t; -> std::uint16_t;
[[nodiscard]] static auto default_rpc_port(const provider_type &prov) [[nodiscard]] static auto default_rpc_port() -> std::uint16_t;
-> std::uint16_t;
[[nodiscard]] static auto get_provider_display_name(const provider_type &prov) [[nodiscard]] static auto get_provider_display_name(const provider_type &prov)
-> std::string; -> std::string;

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,38 +30,45 @@ 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 [[nodiscard]] static auto wait_for_lock(int handle,
wait_for_lock(int fd, std::uint8_t retry_count = 30u) -> int; std::uint8_t retry_count = 30U)
-> 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();
[[nodiscard]] auto set_mount_state(bool active, [[nodiscard]] auto set_mount_state(bool active,
const std::string &mount_location, std::string_view mount_location, int pid)
int pid) -> bool; -> bool;
}; };
[[nodiscard]] auto create_meta_attributes( [[nodiscard]] auto create_meta_attributes(
@@ -76,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

@@ -67,7 +67,7 @@ app_config::app_config(const provider_type &prov,
std::string_view data_directory) std::string_view data_directory)
: prov_(prov), : prov_(prov),
api_password_(utils::generate_random_string(default_api_password_size)), api_password_(utils::generate_random_string(default_api_password_size)),
api_port_(default_rpc_port(prov)), api_port_(default_rpc_port()),
api_user_(std::string{REPERTORY}), api_user_(std::string{REPERTORY}),
config_changed_(false), config_changed_(false),
download_timeout_secs_(default_download_timeout_secs), download_timeout_secs_(default_download_timeout_secs),
@@ -743,17 +743,7 @@ auto app_config::default_remote_api_port(const provider_type &prov)
return PROVIDER_REMOTE_PORTS.at(static_cast<std::size_t>(prov)); return PROVIDER_REMOTE_PORTS.at(static_cast<std::size_t>(prov));
} }
auto app_config::default_rpc_port(const provider_type &prov) -> std::uint16_t { auto app_config::default_rpc_port() -> std::uint16_t { return 10000U; }
static const std::array<std::uint16_t,
static_cast<std::size_t>(provider_type::unknown)>
PROVIDER_RPC_PORTS = {
10000U,
10010U,
10100U,
10002U,
};
return PROVIDER_RPC_PORTS.at(static_cast<std::size_t>(prov));
}
auto app_config::get_api_password() const -> std::string { auto app_config::get_api_password() const -> std::string {
return api_password_; return api_password_;
@@ -943,24 +933,18 @@ auto app_config::get_preferred_download_type() const -> download_type {
auto app_config::get_provider_display_name(const provider_type &prov) auto app_config::get_provider_display_name(const provider_type &prov)
-> std::string { -> std::string {
static const std::array<std::string, static const std::array<std::string,
static_cast<std::size_t>(provider_type::unknown)> static_cast<std::size_t>(provider_type::unknown) + 1U>
PROVIDER_DISPLAY_NAMES = { PROVIDER_DISPLAY_NAMES = {
"Sia", "Sia", "Remote", "S3", "Encrypt", "Unknown",
"Remote",
"S3",
"Encrypt",
}; };
return PROVIDER_DISPLAY_NAMES.at(static_cast<std::size_t>(prov)); return PROVIDER_DISPLAY_NAMES.at(static_cast<std::size_t>(prov));
} }
auto app_config::get_provider_name(const provider_type &prov) -> std::string { auto app_config::get_provider_name(const provider_type &prov) -> std::string {
static const std::array<std::string, static const std::array<std::string,
static_cast<std::size_t>(provider_type::unknown)> static_cast<std::size_t>(provider_type::unknown) + 1U>
PROVIDER_NAMES = { PROVIDER_NAMES = {
"sia", "sia", "remote", "s3", "encrypt", "unknown",
"remote",
"s3",
"encrypt",
}; };
return PROVIDER_NAMES.at(static_cast<std::size_t>(prov)); return PROVIDER_NAMES.at(static_cast<std::size_t>(prov));
} }

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,61 +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() lock_data::~lock_data() { release(); }
: pt_(provider_type::sia), unique_id_(""), mutex_id_(""), lock_fd_(-1) {}
lock_data::~lock_data() { auto lock_data::get_lock_data_file() const -> std::string {
if (lock_fd_ != -1) { auto dir = get_state_directory();
if (lock_status_ == 0) {
unlink(get_lock_file().c_str());
flock(lock_fd_, LOCK_UN);
}
close(lock_fd_);
}
}
auto lock_data::get_lock_data_file() -> std::string {
const 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); };
}
close(fd); return true;
} }
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;
} }
@@ -98,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;
@@ -125,61 +123,72 @@ auto lock_data::grab_lock(std::uint8_t retry_count) -> lock_result {
} }
} }
auto lock_data::set_mount_state(bool active, const std::string &mount_location, void lock_data::release() {
if (handle_ == -1) {
return;
}
if (lock_status_ == 0) {
[[maybe_unused]] auto success{utils::file::file{get_lock_file()}.remove()};
flock(handle_, LOCK_UN);
}
close(handle_);
handle_ = -1;
}
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) {
if (wait_for_lock(handle) == 0) { return false;
const auto mount_id = }
app_config::get_provider_display_name(pt_) + unique_id_;
json mount_state;
if (not utils::file::read_json_file(get_lock_data_file(), mount_state)) {
utils::error::raise_error(function_name,
"failed to read mount state file|sp|" +
get_lock_file());
}
if ((mount_state.find(mount_id) == mount_state.end()) ||
(mount_state[mount_id].find("Active") ==
mount_state[mount_id].end()) ||
(mount_state[mount_id]["Active"].get<bool>() != active) ||
(active && ((mount_state[mount_id].find("Location") ==
mount_state[mount_id].end()) ||
(mount_state[mount_id]["Location"].get<std::string>() !=
mount_location)))) {
const auto lines = utils::file::read_file_lines(get_lock_data_file());
const auto txt = std::accumulate(
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},
{"Location", active ? mount_location : ""},
{"PID", active ? pid : -1},
};
ret = utils::file::write_json_file(get_lock_data_file(), json_data);
} else {
ret = true;
}
flock(handle, LOCK_UN); auto ret{false};
if (wait_for_lock(handle) == 0) {
json mount_state;
if (not utils::file::read_json_file(get_lock_data_file(), mount_state)) {
utils::error::raise_error(function_name,
"failed to read mount state file|sp|" +
get_lock_file());
}
if ((mount_state.find("Active") == mount_state.end()) ||
(mount_state["Active"].get<bool>() != active) ||
(active &&
((mount_state.find("Location") == mount_state.end()) ||
(mount_state["Location"].get<std::string>() != mount_location)))) {
if (mount_location.empty() && not active) {
ret = utils::file::file{get_lock_data_file()}.remove();
} else {
ret = utils::file::write_json_file(
get_lock_data_file(),
{
{"Active", active},
{"Location", active ? mount_location : ""},
{"PID", active ? pid : -1},
});
}
} else {
ret = true;
} }
close(handle); flock(handle, LOCK_UN);
} }
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) {
@@ -228,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);
} }
return ret;
mount_state = mount_state.empty() ? json({
{"Active", false},
{"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(
mutex_handle_, 100)) == WAIT_TIMEOUT);
i++) {
}
switch (mutex_state_) {
case WAIT_OBJECT_0: {
ret = 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:
ret = lock_result::locked;
break;
default:
ret = lock_result::failure;
break;
}
} }
return ret; for (std::uint8_t idx = 0U;
(idx <= retry_count) &&
((mutex_state_ = ::WaitForSingleObject(mutex_handle_, max_sleep)) ==
WAIT_TIMEOUT);
++idx) {
}
switch (mutex_state_) {
case WAIT_OBJECT_0:
return lock_result::success;
case WAIT_TIMEOUT:
return lock_result::locked;
default:
return lock_result::failure;
}
} }
void lock_data::release() { void lock_data::release() {
if (mutex_handle_ != INVALID_HANDLE_VALUE) { if (mutex_handle_ == INVALID_HANDLE_VALUE) {
if ((mutex_state_ == WAIT_OBJECT_0) || (mutex_state_ == WAIT_ABANDONED)) { return;
::ReleaseMutex(mutex_handle_);
}
::CloseHandle(mutex_handle_);
mutex_handle_ = INVALID_HANDLE_VALUE;
} }
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_);
}
::CloseHandle(mutex_handle_);
mutex_handle_ = INVALID_HANDLE_VALUE;
} }
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,
const std::int64_t &pid) -> bool { std::int64_t pid) -> bool {
auto ret = false; if (mutex_handle_ == INVALID_HANDLE_VALUE) {
if (mutex_handle_ != INVALID_HANDLE_VALUE) { return false;
const auto mount_id =
app_config::get_provider_display_name(pt_) + unique_id_;
json mount_state;
[[maybe_unused]] auto success = get_mount_state(mount_state);
if ((mount_state.find(mount_id) == mount_state.end()) ||
(mount_state[mount_id].find("Active") == mount_state[mount_id].end()) ||
(mount_state[mount_id]["Active"].get<bool>() != active) ||
(active && ((mount_state[mount_id].find("Location") ==
mount_state[mount_id].end()) ||
(mount_state[mount_id]["Location"].get<std::string>() !=
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;
}
} }
json mount_state;
[[maybe_unused]] auto success{get_mount_state(mount_state)};
if (not((mount_state.find("Active") == mount_state.end()) ||
(mount_state["Active"].get<bool>() != active) ||
(active &&
((mount_state.find("Location") == mount_state.end()) ||
(mount_state["Location"].get<std::string>() != mount_location))))) {
return 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) {}
@@ -143,8 +141,21 @@ void server::start() {
initialize(*server_); initialize(*server_);
server_thread_ = std::make_unique<std::thread>( server_thread_ = std::make_unique<std::thread>([this]() {
[this]() { server_->listen("127.0.0.1", config_.get_api_port()); }); server_->set_socket_options([](auto &&sock) {
#if defined(_WIN32)
int enable{1};
setsockopt(sock, SOL_SOCKET, SO_EXCLUSIVEADDRUSE,
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)
});
server_->listen("127.0.0.1", config_.get_api_port());
});
event_system::instance().raise<service_start_end>(function_name, "server"); event_system::instance().raise<service_start_end>(function_name, "server");
} }

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,14 +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_utils.hpp"
#include "utils/string.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;
@@ -57,130 +56,143 @@ 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 {
auto ret = exit_code::success; lock_data global_lock(provider_type::unknown, "global");
{
lock_data lock(prov, unique_id); auto lock_result = global_lock.grab_lock(100U);
const auto res = lock.grab_lock(); if (lock_result != lock_result::success) {
if (res == lock_result::locked) { std::cerr << "FATAL: Unable to get global lock" << std::endl;
ret = exit_code::mount_active; return exit_code::lock_failed;
std::cerr << app_config::get_provider_display_name(prov)
<< " mount is already active" << std::endl;
} else if (res == lock_result::success) {
const auto generate_config = utils::cli::has_option(
args, utils::cli::options::generate_config_option);
if (generate_config) {
app_config config(prov, data_directory);
if (prov == provider_type::remote) {
auto cfg = config.get_remote_config();
cfg.host_name_or_ip = remote_host;
cfg.api_port = remote_port;
config.set_remote_config(cfg);
} else if (prov == provider_type::sia &&
config.get_sia_config().bucket.empty()) {
[[maybe_unused]] auto bucket =
config.set_value_by_name("SiaConfig.Bucket", unique_id);
}
std::cout << "Generated " << app_config::get_provider_display_name(prov)
<< " Configuration" << std::endl;
std::cout << config.get_config_file_path() << std::endl;
ret = utils::file::file(config.get_config_file_path()).exists()
? exit_code::success
: exit_code::file_creation_failed;
} else {
#if defined(_WIN32)
if (utils::cli::has_option(args, utils::cli::options::hidden_option)) {
::ShowWindow(::GetConsoleWindow(), SW_HIDE);
}
#endif // defined(_WIN32)
auto drive_args =
utils::cli::parse_drive_options(args, prov, data_directory);
app_config config(prov, data_directory);
#if defined(_WIN32)
if (config.get_enable_mount_manager() &&
not utils::is_process_elevated()) {
utils::com_init_wrapper cw;
if (not lock.set_mount_state(true, "elevating", -1)) {
std::cerr << "failed to set mount state" << std::endl;
}
lock.release();
mount_result = utils::run_process_elevated(args);
lock_data lock2(prov, unique_id);
if (lock2.grab_lock() == lock_result::success) {
if (not lock2.set_mount_state(false, "", -1)) {
std::cerr << "failed to set mount state" << std::endl;
}
lock2.release();
}
return exit_code::mount_result;
}
#endif // defined(_WIN32)
std::cout << "Initializing "
<< app_config::get_provider_display_name(prov)
<< (unique_id.empty() ? ""
: (prov == provider_type::remote)
? " [" + remote_host + ':' +
std::to_string(remote_port) + ']'
: " [" + unique_id + ']')
<< " Drive" << std::endl;
if (prov == provider_type::remote) {
std::uint16_t port{0U};
if (utils::get_next_available_port(config.get_api_port(), port)) {
auto cfg = config.get_remote_config();
cfg.host_name_or_ip = remote_host;
cfg.api_port = remote_port;
config.set_remote_config(cfg);
config.set_api_port(port);
try {
remote_drive drive(
config,
[&config]() -> std::unique_ptr<remote_instance> {
return std::unique_ptr<remote_instance>(
new remote_client(config));
},
lock);
if (not lock.set_mount_state(true, "", -1)) {
std::cerr << "failed to set mount state" << std::endl;
}
mount_result = drive.mount(drive_args);
ret = exit_code::mount_result;
} catch (const std::exception &e) {
std::cerr << "FATAL: " << e.what() << std::endl;
ret = exit_code::startup_exception;
}
} else {
std::cerr << "FATAL: Unable to get available port" << std::endl;
ret = exit_code::startup_exception;
}
} else {
if (prov == provider_type::sia &&
config.get_sia_config().bucket.empty()) {
[[maybe_unused]] auto bucket =
config.set_value_by_name("SiaConfig.Bucket", unique_id);
}
try {
auto provider = create_provider(prov, config);
repertory_drive drive(config, lock, *provider);
if (not lock.set_mount_state(true, "", -1)) {
std::cerr << "failed to set mount state" << std::endl;
}
mount_result = drive.mount(drive_args);
ret = exit_code::mount_result;
} catch (const std::exception &e) {
std::cerr << "FATAL: " << e.what() << std::endl;
ret = exit_code::startup_exception;
}
}
} }
} else {
ret = exit_code::lock_failed;
} }
return ret; lock_data lock(prov, unique_id);
auto lock_result = lock.grab_lock();
if (lock_result == lock_result::locked) {
std::cerr << app_config::get_provider_display_name(prov)
<< " mount is already active" << std::endl;
return exit_code::mount_active;
}
if (lock_result != lock_result::success) {
std::cerr << "FATAL: Unable to get provider lock" << std::endl;
return exit_code::lock_failed;
}
if (utils::cli::has_option(args,
utils::cli::options::generate_config_option)) {
app_config config(prov, data_directory);
if (prov == provider_type::remote) {
auto remote_config = config.get_remote_config();
remote_config.host_name_or_ip = remote_host;
remote_config.api_port = remote_port;
config.set_remote_config(remote_config);
} else if (prov == provider_type::sia &&
config.get_sia_config().bucket.empty()) {
[[maybe_unused]] auto bucket =
config.set_value_by_name("SiaConfig.Bucket", unique_id);
}
std::cout << "Generated " << app_config::get_provider_display_name(prov)
<< " Configuration" << std::endl;
std::cout << config.get_config_file_path() << std::endl;
return utils::file::file(config.get_config_file_path()).exists()
? exit_code::success
: exit_code::file_creation_failed;
}
#if defined(_WIN32)
if (utils::cli::has_option(args, utils::cli::options::hidden_option)) {
::ShowWindow(::GetConsoleWindow(), SW_HIDE);
}
#endif // defined(_WIN32)
auto drive_args = utils::cli::parse_drive_options(args, prov, data_directory);
app_config config(prov, data_directory);
{
std::uint16_t port{};
if (not utils::get_next_available_port(config.get_api_port(), port)) {
std::cerr << "FATAL: Unable to get available port" << std::endl;
return exit_code::startup_exception;
}
config.set_api_port(port);
}
#if defined(_WIN32)
if (config.get_enable_mount_manager() && not utils::is_process_elevated()) {
utils::com_init_wrapper wrapper;
if (not lock.set_mount_state(true, "elevating", -1)) {
std::cerr << "failed to set mount state" << std::endl;
}
lock.release();
global_lock.release();
mount_result = utils::run_process_elevated(args);
lock_data prov_lock(prov, unique_id);
if (prov_lock.grab_lock() == lock_result::success) {
if (not prov_lock.set_mount_state(false, "", -1)) {
std::cerr << "failed to set mount state" << std::endl;
}
prov_lock.release();
}
return exit_code::mount_result;
}
#endif // defined(_WIN32)
std::cout << "Initializing " << app_config::get_provider_display_name(prov)
<< (unique_id.empty() ? ""
: (prov == provider_type::remote)
? " [" + remote_host + ':' + std::to_string(remote_port) +
']'
: " [" + unique_id + ']')
<< " Drive" << std::endl;
if (prov == provider_type::remote) {
try {
auto remote_cfg = config.get_remote_config();
remote_cfg.host_name_or_ip = remote_host;
remote_cfg.api_port = remote_port;
config.set_remote_config(remote_cfg);
remote_drive drive(
config,
[&config]() -> std::unique_ptr<remote_instance> {
return std::unique_ptr<remote_instance>(new remote_client(config));
},
lock);
if (not lock.set_mount_state(true, "", -1)) {
std::cerr << "failed to set mount state" << std::endl;
}
global_lock.release();
mount_result = drive.mount(drive_args);
return exit_code::mount_result;
} catch (const std::exception &e) {
std::cerr << "FATAL: " << e.what() << std::endl;
}
return exit_code::startup_exception;
}
try {
if (prov == provider_type::sia && config.get_sia_config().bucket.empty()) {
[[maybe_unused]] auto bucket =
config.set_value_by_name("SiaConfig.Bucket", unique_id);
}
auto provider = create_provider(prov, config);
repertory_drive drive(config, lock, *provider);
if (not lock.set_mount_state(true, "", -1)) {
std::cerr << "failed to set mount state" << std::endl;
}
global_lock.release();
mount_result = drive.mount(drive_args);
return exit_code::mount_result;
} catch (const std::exception &e) {
std::cerr << "FATAL: " << e.what() << std::endl;
}
return exit_code::startup_exception;
} }
} // namespace repertory::cli::actions } // namespace repertory::cli::actions

View File

@@ -23,12 +23,26 @@
#define REPERTORY_INCLUDE_UI_HANDLERS_HPP_ #define REPERTORY_INCLUDE_UI_HANDLERS_HPP_
#include "events/consumers/console_consumer.hpp" #include "events/consumers/console_consumer.hpp"
#include <unordered_map> #include "utils/common.hpp"
namespace repertory::ui { namespace repertory::ui {
class mgmt_app_config; class mgmt_app_config;
class handlers final { class handlers final {
private:
static constexpr const auto nonce_length{128U};
static constexpr const auto nonce_timeout{15U};
struct nonce_data final {
std::chrono::system_clock::time_point creation{
std::chrono::system_clock::now(),
};
std::string nonce{
utils::generate_random_string(nonce_length),
};
};
public: public:
handlers(mgmt_app_config *config, httplib::Server *server); handlers(mgmt_app_config *config, httplib::Server *server);
@@ -50,33 +64,54 @@ private:
console_consumer console; console_consumer console;
mutable std::mutex mtx_; mutable std::mutex mtx_;
mutable std::unordered_map<std::string, std::recursive_mutex> mtx_lookup_; mutable std::unordered_map<std::string, std::recursive_mutex> mtx_lookup_;
std::mutex nonce_mtx_;
std::unordered_map<std::string, nonce_data> nonce_lookup_;
std::condition_variable nonce_notify_;
std::unique_ptr<std::thread> nonce_thread_;
stop_type stop_requested{false};
private: 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_mount(auto &&req, auto &&res) const; static void handle_get_available_locations(httplib::Response &res);
void handle_get_mount_list(auto &&res) const; void handle_get_mount(const httplib::Request &req,
httplib::Response &res) const;
void handle_get_mount_location(auto &&req, auto &&res) const; void handle_get_mount_list(httplib::Response &res) const;
void handle_get_mount_status(auto &&req, auto &&res) const; void handle_get_mount_location(const httplib::Request &req,
httplib::Response &res) const;
void handle_get_settings(auto &&res) const; void handle_get_mount_status(const httplib::Request &req,
httplib::Response &res) const;
void handle_post_add_mount(auto &&req, auto &&res) const; void handle_get_nonce(httplib::Response &res);
void handle_post_mount(auto &&req, auto &&res) const; void handle_get_settings(httplib::Response &res) const;
void handle_put_set_value_by_name(auto &&req, auto &&res) const; void handle_post_add_mount(const httplib::Request &req,
httplib::Response &res) const;
void handle_put_settings(auto &&req, auto &&res) const; 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,
httplib::Response &res) const;
void handle_put_settings(const httplib::Request &req,
httplib::Response &res) const;
auto launch_process(provider_type prov, std::string_view name, auto launch_process(provider_type prov, std::string_view name,
std::string_view args, bool background = false) const std::vector<std::string> args,
bool background = false) const
-> std::vector<std::string>; -> std::vector<std::string>;
void removed_expired_nonces();
void set_key_value(provider_type prov, std::string_view name, void set_key_value(provider_type prov, std::string_view name,
std::string_view key, std::string_view value) const; std::string_view key, std::string_view value) const;
}; };

View File

@@ -23,42 +23,78 @@
#include "app_config.hpp" #include "app_config.hpp"
#include "events/event_system.hpp" #include "events/event_system.hpp"
#include "rpc/common.hpp"
#include "types/repertory.hpp" #include "types/repertory.hpp"
#include "ui/mgmt_app_config.hpp" #include "ui/mgmt_app_config.hpp"
#include "utils/collection.hpp"
#include "utils/common.hpp" #include "utils/common.hpp"
#include "utils/config.hpp"
#include "utils/error_utils.hpp" #include "utils/error_utils.hpp"
#include "utils/file.hpp" #include "utils/file.hpp"
#include "utils/hash.hpp" #include "utils/hash.hpp"
#include "utils/path.hpp" #include "utils/path.hpp"
#include "utils/string.hpp" #include "utils/string.hpp"
#include <boost/process.hpp>
namespace { namespace {
[[nodiscard]] auto decrypt(std::string_view data, std::string_view password) [[nodiscard]] auto decrypt(std::string_view data, std::string_view password)
-> std::string { -> std::string {
auto decoded = macaron::Base64::Decode(data); REPERTORY_USES_FUNCTION_NAME();
if (data.empty()) {
return std::string{data};
}
repertory::data_buffer decoded;
if (not repertory::utils::collection::from_hex_string(data, decoded)) {
throw repertory::utils::error::create_exception(function_name,
{"decryption failed"});
}
repertory::data_buffer buffer(decoded.size()); repertory::data_buffer buffer(decoded.size());
auto key = repertory::utils::encryption::create_hash_blake2b_256(password); auto key = repertory::utils::encryption::create_hash_blake2b_256(password);
std::uint64_t size{}; unsigned long long size{};
crypto_aead_xchacha20poly1305_ietf_decrypt( auto res = crypto_aead_xchacha20poly1305_ietf_decrypt(
reinterpret_cast<unsigned char *>(buffer.data()), &size, nullptr, reinterpret_cast<unsigned char *>(buffer.data()), &size, nullptr,
reinterpret_cast<const unsigned char *>( reinterpret_cast<const unsigned char *>(
&decoded.at(crypto_aead_xchacha20poly1305_IETF_NPUBBYTES)), &decoded.at(crypto_aead_xchacha20poly1305_IETF_NPUBBYTES)),
decoded.size() - crypto_aead_xchacha20poly1305_IETF_NPUBBYTES, decoded.size() - crypto_aead_xchacha20poly1305_IETF_NPUBBYTES,
reinterpret_cast<const unsigned char *>(REPERTORY.data()), 9U, reinterpret_cast<const unsigned char *>(REPERTORY.data()),
REPERTORY.length(),
reinterpret_cast<const unsigned char *>(decoded.data()), reinterpret_cast<const unsigned char *>(decoded.data()),
reinterpret_cast<const unsigned char *>(key.data())); reinterpret_cast<const unsigned char *>(key.data()));
if (res != 0) {
throw repertory::utils::error::create_exception(function_name,
{"decryption failed"});
}
return std::string( return {
buffer.begin(), buffer.begin(),
std::next(buffer.begin(), static_cast<std::int64_t>(size))); std::next(buffer.begin(), static_cast<std::int64_t>(size)),
};
} }
[[nodiscard]] constexpr auto is_restricted(std::string_view data) -> bool { [[nodiscard]] auto decrypt_value(const repertory::ui::mgmt_app_config *config,
constexpr std::string_view invalid_chars = "&;|><$()`{}!*?"; std::string_view key, std::string_view value,
return data.find_first_of(invalid_chars) != std::string_view::npos; bool &skip) -> std::string {
auto last_key{key};
auto parts = repertory::utils::string::split(key, '.', false);
if (parts.size() > 1U) {
last_key = parts.at(parts.size() - 1U);
}
if (last_key == repertory::JSON_API_PASSWORD ||
last_key == repertory::JSON_ENCRYPTION_TOKEN ||
last_key == repertory::JSON_SECRET_KEY) {
auto decrypted = decrypt(value, config->get_api_password());
if (decrypted.empty()) {
skip = true;
}
return decrypted;
}
return std::string{value};
} }
} // namespace } // namespace
@@ -73,16 +109,40 @@ handlers::handlers(mgmt_app_config *config, httplib::Server *server)
server_(server) { server_(server) {
REPERTORY_USES_FUNCTION_NAME(); REPERTORY_USES_FUNCTION_NAME();
server_->set_socket_options([](auto &&sock) {
#if defined(_WIN32)
int enable{1};
setsockopt(sock, SOL_SOCKET, SO_EXCLUSIVEADDRUSE,
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)
});
server_->set_pre_routing_handler( server_->set_pre_routing_handler(
[this](auto &&req, auto &&res) -> httplib::Server::HandlerResponse { [this](const httplib::Request &req,
if (rpc::check_authorization(*config_, req)) { auto &&res) -> httplib::Server::HandlerResponse {
if (req.path == "/api/v1/nonce" || req.path == "/ui" ||
req.path.starts_with("/ui/")) {
return httplib::Server::HandlerResponse::Unhandled; return httplib::Server::HandlerResponse::Unhandled;
} }
auto auth =
decrypt(req.get_param_value("auth"), config_->get_api_password());
if (utils::string::begins_with(
auth, fmt::format("{}_", config_->get_api_user()))) {
auto nonce = auth.substr(config_->get_api_user().length() + 1U);
mutex_lock lock(nonce_mtx_);
if (nonce_lookup_.contains(nonce)) {
nonce_lookup_.erase(nonce);
return httplib::Server::HandlerResponse::Unhandled;
}
}
res.status = http_error_codes::unauthorized; res.status = http_error_codes::unauthorized;
res.set_header(
"WWW-Authenticate",
R"(Basic realm="Repertory Management Portal", charset="UTF-8")");
return httplib::Server::HandlerResponse::Handled; return httplib::Server::HandlerResponse::Handled;
}); });
@@ -106,7 +166,14 @@ handlers::handlers(mgmt_app_config *config, httplib::Server *server)
} }
res.set_content(data.dump(), "application/json"); res.set_content(data.dump(), "application/json");
res.status = http_error_codes::internal_error; res.status = utils::string::ends_with(data["error"].get<std::string>(),
"|decryption failed")
? http_error_codes::unauthorized
: http_error_codes::internal_error;
});
server->Get("/api/v1/locations", [](auto && /* req */, auto &&res) {
handle_get_available_locations(res);
}); });
server->Get("/api/v1/mount", server->Get("/api/v1/mount",
@@ -120,15 +187,16 @@ handlers::handlers(mgmt_app_config *config, httplib::Server *server)
handle_get_mount_list(res); handle_get_mount_list(res);
}); });
server->Get("/api/v1/mount_status", server->Get("/api/v1/mount_status", [this](auto &&req, auto &&res) {
[this](const httplib::Request &req, auto &&res) { handle_get_mount_status(req, res);
handle_get_mount_status(req, res); });
});
server->Get("/api/v1/settings", server->Get("/api/v1/nonce",
[this](const httplib::Request & /* req */, auto &&res) { [this](auto && /* req */, auto &&res) { handle_get_nonce(res); });
handle_get_settings(res);
}); server->Get("/api/v1/settings", [this](auto && /* req */, auto &&res) {
handle_get_settings(res);
});
server->Post("/api/v1/add_mount", [this](auto &&req, auto &&res) { server->Post("/api/v1/add_mount", [this](auto &&req, auto &&res) {
handle_post_add_mount(req, res); handle_post_add_mount(req, res);
@@ -137,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);
}); });
@@ -190,6 +262,9 @@ handlers::handlers(mgmt_app_config *config, httplib::Server *server)
event_system::instance().start(); event_system::instance().start();
nonce_thread_ =
std::make_unique<std::thread>([this]() { removed_expired_nonces(); });
server_->listen("127.0.0.1", config_->get_api_port()); server_->listen("127.0.0.1", config_->get_api_port());
if (this_server != nullptr) { if (this_server != nullptr) {
this_server = nullptr; this_server = nullptr;
@@ -197,7 +272,20 @@ handlers::handlers(mgmt_app_config *config, httplib::Server *server)
} }
} }
handlers::~handlers() { event_system::instance().stop(); } handlers::~handlers() {
if (nonce_thread_) {
stop_requested = true;
unique_mutex_lock lock(nonce_mtx_);
nonce_notify_.notify_all();
lock.unlock();
nonce_thread_->join();
nonce_thread_.reset();
}
event_system::instance().stop();
}
auto handlers::data_directory_exists(provider_type prov, auto handlers::data_directory_exists(provider_type prov,
std::string_view name) const -> bool { std::string_view name) const -> bool {
@@ -218,7 +306,54 @@ auto handlers::data_directory_exists(provider_type prov,
return ret; return ret;
} }
void handlers::handle_get_mount(auto &&req, auto &&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)
constexpr const std::array<std::string_view, 26U> letters{
"A:", "B:", "C:", "D:", "E:", "F:", "G:", "H:", "I:",
"J:", "K:", "L:", "M:", "N:", "O:", "P:", "Q:", "R:",
"S:", "T:", "U:", "V:", "W:", "X:", "Y:", "Z:",
};
auto available = std::accumulate(
letters.begin(), letters.end(), std::vector<std::string_view>(),
[](auto &&vec, auto &&letter) -> std::vector<std::string_view> {
if (utils::file::directory{utils::path::combine(letter, {"\\"})}
.exists()) {
return vec;
}
vec.emplace_back(letter);
return vec;
});
res.set_content(nlohmann::json(available).dump(), "application/json");
#else // !defined(_WIN32)
res.set_content(nlohmann::json(std::vector<std::string_view>()).dump(),
"application/json");
#endif // defined(_WIN32)
res.status = http_error_codes::ok;
}
void handlers::handle_get_mount(const httplib::Request &req,
httplib::Response &res) const {
REPERTORY_USES_FUNCTION_NAME(); REPERTORY_USES_FUNCTION_NAME();
auto prov = provider_type_from_string(req.get_param_value("type")); auto prov = provider_type_from_string(req.get_param_value("type"));
@@ -229,7 +364,7 @@ void handlers::handle_get_mount(auto &&req, auto &&res) const {
return; return;
} }
auto lines = launch_process(prov, name, "-dc"); auto lines = launch_process(prov, name, {"-dc"});
if (lines.at(0U) != "0") { if (lines.at(0U) != "0") {
throw utils::error::create_exception(function_name, { throw utils::error::create_exception(function_name, {
@@ -247,7 +382,7 @@ void handlers::handle_get_mount(auto &&req, auto &&res) const {
res.status = http_error_codes::ok; res.status = http_error_codes::ok;
} }
void handlers::handle_get_mount_list(auto &&res) const { void handlers::handle_get_mount_list(httplib::Response &res) const {
auto data_dir = utils::file::directory{app_config::get_root_data_directory()}; auto data_dir = utils::file::directory{app_config::get_root_data_directory()};
nlohmann::json result; nlohmann::json result;
@@ -276,7 +411,8 @@ void handlers::handle_get_mount_list(auto &&res) const {
res.status = http_error_codes::ok; res.status = http_error_codes::ok;
} }
void handlers::handle_get_mount_location(auto &&req, auto &&res) const { void handlers::handle_get_mount_location(const httplib::Request &req,
httplib::Response &res) const {
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"));
@@ -294,9 +430,8 @@ void handlers::handle_get_mount_location(auto &&req, auto &&res) const {
res.status = http_error_codes::ok; res.status = http_error_codes::ok;
} }
void handlers::handle_get_mount_status(auto &&req, auto &&res) const { void handlers::handle_get_mount_status(const httplib::Request &req,
REPERTORY_USES_FUNCTION_NAME(); httplib::Response &res) const {
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"));
@@ -305,33 +440,9 @@ void handlers::handle_get_mount_status(auto &&req, auto &&res) const {
return; return;
} }
auto status_name = app_config::get_provider_display_name(prov); auto lines = launch_process(prov, name, {"-status"});
switch (prov) { auto result = nlohmann::json::parse(utils::string::join(lines, '\n'));
case provider_type::remote: {
auto parts = utils::string::split(name, '_', false);
status_name = fmt::format("{}{}:{}", status_name, parts[0U], parts[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");
nlohmann::json result(
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>()) {
@@ -343,7 +454,18 @@ void handlers::handle_get_mount_status(auto &&req, auto &&res) const {
res.status = http_error_codes::ok; res.status = http_error_codes::ok;
} }
void handlers::handle_get_settings(auto &&res) const { void handlers::handle_get_nonce(httplib::Response &res) {
mutex_lock lock(nonce_mtx_);
nonce_data nonce{};
nonce_lookup_[nonce.nonce] = nonce;
nlohmann::json data({{"nonce", nonce.nonce}});
res.set_content(data.dump(), "application/json");
res.status = http_error_codes::ok;
}
void handlers::handle_get_settings(httplib::Response &res) const {
auto settings = config_->to_json(); auto settings = config_->to_json();
settings[JSON_API_PASSWORD] = ""; settings[JSON_API_PASSWORD] = "";
settings.erase(JSON_MOUNT_LOCATIONS); settings.erase(JSON_MOUNT_LOCATIONS);
@@ -351,7 +473,8 @@ void handlers::handle_get_settings(auto &&res) const {
res.status = http_error_codes::ok; res.status = http_error_codes::ok;
} }
void handlers::handle_post_add_mount(auto &&req, auto &&res) const { void handlers::handle_post_add_mount(const httplib::Request &req,
httplib::Response &res) const {
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"));
if (data_directory_exists(prov, name)) { if (data_directory_exists(prov, name)) {
@@ -361,22 +484,42 @@ void handlers::handle_post_add_mount(auto &&req, auto &&res) const {
auto cfg = nlohmann::json::parse(req.get_param_value("config")); auto cfg = nlohmann::json::parse(req.get_param_value("config"));
launch_process(prov, name, "-gc"); std::map<std::string, std::string> values{};
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()) {
set_key_value(prov, name, fmt::format("{}.{}", key, key2), auto sub_key = fmt::format("{}.{}", key, key2);
value2.template get<std::string>()); auto skip{false};
auto decrypted = decrypt_value(
config_, sub_key, value2.template get<std::string>(), skip);
if (skip) {
continue;
}
values[sub_key] = decrypted;
} }
} else {
set_key_value(prov, name, key, value.template get<std::string>()); continue;
} }
auto skip{false};
auto decrypted =
decrypt_value(config_, key, value.template get<std::string>(), skip);
if (skip) {
continue;
}
values[key] = decrypted;
}
launch_process(prov, name, {"-gc"});
for (auto &[key, value] : values) {
set_key_value(prov, name, key, value);
} }
res.status = http_error_codes::ok; res.status = http_error_codes::ok;
} }
void handlers::handle_post_mount(auto &&req, auto &&res) const { void handlers::handle_post_mount(const httplib::Request &req,
httplib::Response &res) {
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"));
@@ -389,27 +532,33 @@ void handlers::handle_post_mount(auto &&req, auto &&res) const {
auto unmount = utils::string::to_bool(req.get_param_value("unmount")); auto unmount = utils::string::to_bool(req.get_param_value("unmount"));
if (unmount) { if (unmount) {
launch_process(prov, name, "-unmount"); launch_process(prov, name, {"-unmount"});
} else { } else {
#if defined(_WIN32) #if defined(_WIN32)
if (utils::file::directory{location}.exists()) { if (utils::file::directory{location}.exists()) {
#else // !defined(_WIN32) #else // !defined(_WIN32)
if (not utils::file::directory{location}.exists()) { if (not utils::file::directory{location}.exists()) {
#endif // defined(_WIN32) #endif // defined(_WIN32)
config_->set_mount_location(prov, name, "");
res.status = http_error_codes::internal_error; res.status = http_error_codes::internal_error;
return; return;
} }
launch_process(prov, name, fmt::format(R"("{}")", location), true);
config_->set_mount_location(prov, name, location); config_->set_mount_location(prov, name, location);
static std::mutex mount_mtx;
mutex_lock lock(mount_mtx);
launch_process(prov, name, {location}, true);
launch_process(prov, name, {"-status"});
} }
res.status = http_error_codes::ok; res.status = http_error_codes::ok;
} }
void handlers::handle_put_set_value_by_name(auto &&req, auto &&res) const { void handlers::handle_put_set_value_by_name(const httplib::Request &req,
httplib::Response &res) const {
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"));
if (not data_directory_exists(prov, name)) { if (not data_directory_exists(prov, name)) {
@@ -418,31 +567,27 @@ void handlers::handle_put_set_value_by_name(auto &&req, auto &&res) const {
} }
auto key = req.get_param_value("key"); auto key = req.get_param_value("key");
auto last_key{key};
auto value = req.get_param_value("value"); auto value = req.get_param_value("value");
auto parts = utils::string::split(key, '.', false); auto skip{false};
if (parts.size() > 1U) { value = decrypt_value(config_, key, value, skip);
last_key = parts.at(parts.size() - 1U); if (not skip) {
set_key_value(prov, name, key, value);
} }
if (last_key == JSON_API_PASSWORD || last_key == JSON_ENCRYPTION_TOKEN ||
last_key == JSON_SECRET_KEY) {
value = decrypt(value, config_->get_api_password());
}
set_key_value(prov, name, key, value);
res.status = http_error_codes::ok; res.status = http_error_codes::ok;
} }
void handlers::handle_put_settings(auto &&req, auto &&res) const { void handlers::handle_put_settings(const httplib::Request &req,
nlohmann::json data = nlohmann::json::parse(req.get_param_value("data")); httplib::Response &res) const {
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>(),
config_->get_api_password()); config_->get_api_password());
config_->set_api_password(password); if (not password.empty()) {
config_->set_api_password(password);
}
} }
if (data.contains(JSON_API_PORT)) { if (data.contains(JSON_API_PORT)) {
@@ -458,34 +603,34 @@ void handlers::handle_put_settings(auto &&req, auto &&res) const {
} }
auto handlers::launch_process(provider_type prov, std::string_view name, auto handlers::launch_process(provider_type prov, std::string_view name,
std::string_view args, bool background) const std::vector<std::string> args,
bool background) const
-> std::vector<std::string> { -> std::vector<std::string> {
REPERTORY_USES_FUNCTION_NAME(); REPERTORY_USES_FUNCTION_NAME();
if (is_restricted(name) || is_restricted(args)) {
throw utils::error::create_exception(function_name,
{
"invalid data detected",
});
}
std::string str_type;
switch (prov) { switch (prov) {
case provider_type::encrypt: case provider_type::encrypt:
str_type = fmt::format("-en -na {}", name); args.insert(args.begin(), "-en");
args.insert(std::next(args.begin()), "-na");
args.insert(std::next(args.begin(), 2U), std::string{name});
break; break;
case provider_type::remote: { case provider_type::remote: {
auto parts = utils::string::split(name, '_', false); auto parts = utils::string::split(name, '_', false);
str_type = fmt::format("-rm {}:{}", parts[0U], parts[1U]); args.insert(args.begin(), "-rm");
args.insert(std::next(args.begin()),
fmt::format("{}:{}", parts.at(0U), parts.at(1U)));
} break; } break;
case provider_type::s3: case provider_type::s3:
str_type = fmt::format("-s3 -na {}", name); args.insert(args.begin(), "-s3");
args.insert(std::next(args.begin()), "-na");
args.insert(std::next(args.begin(), 2U), std::string{name});
break; break;
case provider_type::sia: case provider_type::sia:
str_type = fmt::format("-na {}", name); args.insert(args.begin(), "-na");
args.insert(std::next(args.begin()), std::string{name});
break; break;
default: default:
@@ -497,8 +642,6 @@ auto handlers::launch_process(provider_type prov, std::string_view name,
}); });
} }
auto cmd_line = fmt::format(R"({} {} {})", repertory_binary_, str_type, args);
unique_mutex_lock lock(mtx_); unique_mutex_lock lock(mtx_);
auto &inst_mtx = mtx_lookup_[fmt::format( auto &inst_mtx = mtx_lookup_[fmt::format(
"{}-{}", name, app_config::get_provider_name(prov))]; "{}-{}", name, app_config::get_provider_name(prov))];
@@ -507,45 +650,102 @@ auto handlers::launch_process(provider_type prov, std::string_view name,
recur_mutex_lock inst_lock(inst_mtx); recur_mutex_lock inst_lock(inst_mtx);
if (background) { if (background) {
#if defined(_WIN32) #if defined(_WIN32)
system(fmt::format(R"(start "" /MIN {})", cmd_line).c_str()); std::array<char, MAX_PATH + 1U> path{};
#elif defined(__linux__) // defined(__linux__) ::GetSystemDirectoryA(path.data(), path.size());
system(fmt::format("nohup {} 1>/dev/null 2>&1", cmd_line).c_str());
#else // !defined(__linux__) && !defined(_WIN32) args.insert(args.begin(), utils::path::combine(path.data(), {"cmd.exe"}));
build fails here args.insert(std::next(args.begin()), "/c");
#endif // defined(_WIN32) args.insert(std::next(args.begin(), 2U), "start");
args.insert(std::next(args.begin(), 3U), "");
args.insert(std::next(args.begin(), 4U), "/MIN");
args.insert(std::next(args.begin(), 5U), repertory_binary_);
#else // !defined(_WIN32)
args.insert(args.begin(), repertory_binary_);
#endif // defined(_WIN32)
std::vector<const char *> exec_args;
exec_args.reserve(args.size() + 1U);
for (const auto &arg : args) {
exec_args.push_back(arg.c_str());
}
exec_args.push_back(nullptr);
#if defined(_WIN32)
_spawnv(_P_DETACH, exec_args.at(0U),
const_cast<char *const *>(exec_args.data()));
#else // !defined(_WIN32)
auto pid = fork();
if (pid == 0) {
setsid();
chdir("/");
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
open("/dev/null", O_RDONLY);
open("/dev/null", O_WRONLY);
open("/dev/null", O_WRONLY);
execvp(exec_args.at(0U), const_cast<char *const *>(exec_args.data()));
} else {
signal(SIGCHLD, SIG_IGN);
}
#endif // defined(_WIN32)
return {}; return {};
} }
auto *pipe = popen(cmd_line.c_str(), "r"); boost::process::ipstream out;
if (pipe == nullptr) { boost::process::child proc(repertory_binary_, boost::process::args(args),
throw utils::error::create_exception(function_name, boost::process::std_out > out);
{
"failed to execute command",
provider_type_to_string(prov),
name,
});
}
std::string data; std::string data;
std::array<char, 1024U> buffer{}; std::string line;
while (std::feof(pipe) == 0) { while (out && std::getline(out, line)) {
while (std::fgets(buffer.data(), buffer.size(), pipe) != nullptr) { data += line + "\n";
data += buffer.data();
}
} }
pclose(pipe);
return utils::string::split(utils::string::replace(data, "\r", ""), '\n', return utils::string::split(utils::string::replace(data, "\r", ""), '\n',
false); false);
} }
void handlers::removed_expired_nonces() {
unique_mutex_lock lock(nonce_mtx_);
lock.unlock();
while (not stop_requested) {
lock.lock();
auto nonces = nonce_lookup_;
lock.unlock();
for (const auto &[key, value] : nonces) {
if (std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::system_clock::now() - value.creation)
.count() >= nonce_timeout) {
lock.lock();
nonce_lookup_.erase(key);
lock.unlock();
}
}
if (stop_requested) {
break;
}
lock.lock();
if (stop_requested) {
break;
}
nonce_notify_.wait_for(lock, std::chrono::seconds(1U));
lock.unlock();
}
}
void handlers::set_key_value(provider_type prov, std::string_view name, void handlers::set_key_value(provider_type prov, std::string_view name,
std::string_view key, std::string_view key,
std::string_view value) const { std::string_view value) const {
#if defined(_WIN32) std::vector<std::string> args;
launch_process(prov, name, fmt::format(R"(-set {} "{}")", key, value)); args.emplace_back("-set");
#else // !defined(_WIN32) args.emplace_back(key);
launch_process(prov, name, fmt::format("-set {} '{}'", key, value)); args.emplace_back(value);
#endif // defined(_WIN32) launch_process(prov, name, args, false);
} }
} // namespace repertory::ui } // namespace repertory::ui

View File

@@ -181,10 +181,6 @@ void mgmt_app_config::set_mount_location(provider_type prov,
return; return;
} }
if (location.empty()) {
return;
}
recur_mutex_lock lock(mtx_); recur_mutex_lock lock(mtx_);
if (locations_[prov][std::string{name}] == std::string{location}) { if (locations_[prov][std::string{name}] == std::string{location}) {
return; return;

View File

@@ -128,7 +128,7 @@ std::atomic<std::uint64_t> app_config_test::idx{0U};
static void defaults_tests(const json &json_data, provider_type prov) { static void defaults_tests(const json &json_data, provider_type prov) {
json json_defaults = { json json_defaults = {
{JSON_API_PORT, app_config::default_rpc_port(prov)}, {JSON_API_PORT, app_config::default_rpc_port()},
{JSON_API_USER, std::string{REPERTORY}}, {JSON_API_USER, std::string{REPERTORY}},
{JSON_DOWNLOAD_TIMEOUT_SECS, default_download_timeout_secs}, {JSON_DOWNLOAD_TIMEOUT_SECS, default_download_timeout_secs},
{JSON_DATABASE_TYPE, database_type::rocksdb}, {JSON_DATABASE_TYPE, database_type::rocksdb},

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

@@ -15,6 +15,7 @@ if [ "${PROJECT_IS_MINGW}" == "1" ] && [ "${PROJECT_STATIC_LINK}" == "OFF" ]; th
/mingw64/bin/libstdc++-6.dll /mingw64/bin/libstdc++-6.dll
/mingw64/bin/libwinpthread-1.dll /mingw64/bin/libwinpthread-1.dll
/mingw64/bin/libzlib1.dll /mingw64/bin/libzlib1.dll
/mingw64/bin/libzstd.dll
/mingw64/bin/zlib1.dll /mingw64/bin/zlib1.dll
) )

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];
@@ -131,23 +131,46 @@ auto get_next_available_port(std::uint16_t first_port,
using ip::tcp; using ip::tcp;
boost::system::error_code error_code{}; boost::system::error_code error_code{};
while (first_port != 0U) {
io_context ctx{}; std::uint32_t check_port{first_port};
tcp::acceptor acceptor(ctx); while (check_port <= 65535U) {
acceptor.open(tcp::v4(), error_code) || {
acceptor.bind({tcp::v4(), first_port}, error_code); io_context ctx{};
if (not error_code) { tcp::socket socket(ctx);
break; socket.connect(
{
tcp::endpoint(ip::address_v4::loopback(),
static_cast<std::uint16_t>(check_port)),
},
error_code);
if (not error_code) {
++check_port;
continue;
}
} }
++first_port; {
io_context ctx{};
tcp::acceptor acceptor(ctx);
acceptor.open(tcp::v4(), error_code);
if (error_code) {
++check_port;
continue;
}
acceptor.set_option(boost::asio::ip::tcp::acceptor::linger(true, 0));
acceptor.bind({tcp::v4(), static_cast<std::uint16_t>(check_port)},
error_code);
if (error_code) {
++check_port;
continue;
}
}
available_port = static_cast<std::uint16_t>(check_port);
return true;
} }
if (not error_code) { return false;
available_port = first_port;
}
return not error_code;
} }
#endif // defined(PROJECT_ENABLE_BOOST) #endif // defined(PROJECT_ENABLE_BOOST)

View File

@@ -2,8 +2,10 @@ import 'package:flutter/material.dart' show GlobalKey, NavigatorState;
import 'package:sodium_libs/sodium_libs.dart'; import 'package:sodium_libs/sodium_libs.dart';
const addMountTitle = 'Add New Mount'; const addMountTitle = 'Add New Mount';
const appLogonTitle = 'Repertory Portal Login';
const appSettingsTitle = 'Portal Settings'; const appSettingsTitle = 'Portal Settings';
const appTitle = 'Repertory Management Portal'; const appTitle = 'Repertory Management Portal';
const logonWidth = 300.0;
const databaseTypeList = ['rocksdb', 'sqlite']; const databaseTypeList = ['rocksdb', 'sqlite'];
const downloadTypeList = ['default', 'direct', 'ring_buffer']; const downloadTypeList = ['default', 'direct', 'ring_buffer'];
const eventLevelList = ['critical', 'error', 'warn', 'info', 'debug', 'trace']; const eventLevelList = ['critical', 'error', 'warn', 'info', 'debug', 'trace'];
@@ -15,9 +17,8 @@ const ringBufferSizeList = ['128', '256', '512', '1024', '2048'];
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
Sodium? _sodium; Sodium? _sodium;
void setSodium(Sodium sodium) { void setSodium(Sodium sodium) {
_sodium = sodium; _sodium = sodium;
} }
Sodium? get sodium => _sodium; Sodium get sodium => _sodium!;

View File

@@ -1,13 +1,21 @@
import 'dart:convert'; import 'package:convert/convert.dart';
import 'package:collection/collection.dart'; 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);
class NullPasswordException implements Exception {
String error() => 'password cannot be null';
}
class AuthenticationFailedException implements Exception {
String error() => 'failed to authenticate user';
}
// ignore: prefer_function_declarations_over_variables // ignore: prefer_function_declarations_over_variables
final Validator noRestrictedChars = (value) { final Validator noRestrictedChars = (value) {
return [ return [
@@ -99,14 +107,31 @@ Map<String, dynamic> createDefaultSettings(String mountType) {
return {}; return {};
} }
void displayErrorMessage(context, String text) { void displayAuthError(Auth auth) {
if (!auth.authenticated || constants.navigatorKey.currentContext == null) {
return;
}
displayErrorMessage(
constants.navigatorKey.currentContext!,
"Authentication failed",
clear: true,
);
}
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) {
@@ -226,51 +251,152 @@ bool validateSettings(
return failed.isEmpty; return failed.isEmpty;
} }
Map<String, dynamic> convertAllToString(Map<String, dynamic> settings) { Future<Map<String, dynamic>> convertAllToString(
final password = 'test'; Map<String, dynamic> settings,
SecureKey key,
) async {
Future<Map<String, dynamic>> convert(Map<String, dynamic> settings) async {
for (var entry in settings.entries) {
if (entry.value is Map<String, dynamic>) {
await convert(entry.value);
continue;
}
settings.forEach((key, value) { if (entry.key == 'ApiPassword' ||
if (value is Map<String, dynamic>) { entry.key == 'EncryptionToken' ||
convertAllToString(value); entry.key == 'SecretKey') {
return; if (entry.value.isEmpty) {
continue;
}
settings[entry.key] = encryptValue(entry.value, key);
continue;
}
if (entry.value is String) {
continue;
}
settings[entry.key] = entry.value.toString();
} }
if (key == 'ApiPassword' || return settings;
key == 'EncryptionToken' || }
key == 'SecretKey') {
value = encryptValue(value, password);
} else if (value is String) {
return;
}
settings[key] = value.toString(); return convert(settings);
});
return settings;
} }
String encryptValue(String value, String password) { String encryptValue(String value, SecureKey key) {
final sodium = constants.sodium; if (value.isEmpty) {
if (sodium == null) {
return value; return value;
} }
final keyHash = sodium.crypto.genericHash( final sodium = constants.sodium;
outLen: sodium.crypto.aeadXChaCha20Poly1305IETF.keyBytes,
message: Uint8List.fromList(password.toCharArray()),
);
debugPrint("key: ${base64Encode(keyHash)}");
final crypto = sodium.crypto.aeadXChaCha20Poly1305IETF; final crypto = sodium.crypto.aeadXChaCha20Poly1305IETF;
final nonce = sodium.secureRandom(crypto.nonceBytes).extractBytes(); final nonce = sodium.secureRandom(crypto.nonceBytes).extractBytes();
debugPrint("nonce: ${base64Encode(nonce)}");
final data = crypto.encrypt( final data = crypto.encrypt(
additionalData: Uint8List.fromList('repertory'.toCharArray()), additionalData: Uint8List.fromList('repertory'.toCharArray()),
key: SecureKey.fromList(sodium, keyHash), key: key,
message: Uint8List.fromList(value.toCharArray()), message: Uint8List.fromList(value.toCharArray()),
nonce: nonce, nonce: nonce,
); );
return base64Encode(Uint8List.fromList([...nonce, ...data])); return hex.encode(nonce + data);
}
Map<String, dynamic> getChanged(
Map<String, dynamic> original,
Map<String, dynamic> updated,
) {
if (DeepCollectionEquality().equals(original, updated)) {
return {};
}
Map<String, dynamic> changed = {};
original.forEach((key, value) {
if (DeepCollectionEquality().equals(value, updated[key])) {
return;
}
if (value is Map<String, dynamic>) {
changed[key] = <String, dynamic>{};
value.forEach((subKey, subValue) {
if (DeepCollectionEquality().equals(subValue, updated[key][subKey])) {
return;
}
changed[key][subKey] = updated[key][subKey];
});
return;
}
changed[key] = updated[key];
});
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

@@ -2,9 +2,11 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:repertory/constants.dart' as constants; import 'package:repertory/constants.dart' as constants;
import 'package:repertory/helpers.dart'; import 'package:repertory/helpers.dart';
import 'package:repertory/models/auth.dart';
import 'package:repertory/models/mount.dart'; import 'package:repertory/models/mount.dart';
import 'package:repertory/models/mount_list.dart'; import 'package:repertory/models/mount_list.dart';
import 'package:repertory/screens/add_mount_screen.dart'; import 'package:repertory/screens/add_mount_screen.dart';
import 'package:repertory/screens/auth_screen.dart';
import 'package:repertory/screens/edit_mount_screen.dart'; import 'package:repertory/screens/edit_mount_screen.dart';
import 'package:repertory/screens/edit_settings_screen.dart'; import 'package:repertory/screens/edit_settings_screen.dart';
import 'package:repertory/screens/home_screen.dart'; import 'package:repertory/screens/home_screen.dart';
@@ -17,8 +19,15 @@ void main() async {
debugPrint('$e'); debugPrint('$e');
} }
final auth = Auth();
runApp( runApp(
ChangeNotifierProvider(create: (_) => MountList(), child: const MyApp()), MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => auth),
ChangeNotifierProvider(create: (_) => MountList(auth)),
],
child: const MyApp(),
),
); );
} }
@@ -53,14 +62,20 @@ class _MyAppState extends State<MyApp> {
snackBarTheme: snackBarTheme, snackBarTheme: snackBarTheme,
), ),
title: constants.appTitle, title: constants.appTitle,
initialRoute: '/', initialRoute: '/auth',
routes: { routes: {
'/': (context) => const HomeScreen(title: constants.appTitle), '/':
'/add':
(context) => const AddMountScreen(title: constants.addMountTitle),
'/settings':
(context) => (context) =>
const EditSettingsScreen(title: constants.appSettingsTitle), const AuthCheck(child: HomeScreen(title: constants.appTitle)),
'/add':
(context) => const AuthCheck(
child: AddMountScreen(title: constants.addMountTitle),
),
'/auth': (context) => const AuthScreen(title: constants.appTitle),
'/settings':
(context) => const AuthCheck(
child: EditSettingsScreen(title: constants.appSettingsTitle),
),
}, },
onGenerateRoute: (settings) { onGenerateRoute: (settings) {
if (settings.name != '/edit') { if (settings.name != '/edit') {
@@ -70,10 +85,12 @@ class _MyAppState extends State<MyApp> {
final mount = settings.arguments as Mount; final mount = settings.arguments as Mount;
return MaterialPageRoute( return MaterialPageRoute(
builder: (context) { builder: (context) {
return EditMountScreen( return AuthCheck(
mount: mount, child: EditMountScreen(
title: mount: mount,
'${mount.provider} [${formatMountName(mount.type, mount.name)}] Settings', title:
'${mount.provider} [${formatMountName(mount.type, mount.name)}] Settings',
),
); );
}, },
); );
@@ -81,3 +98,29 @@ class _MyAppState extends State<MyApp> {
); );
} }
} }
class AuthCheck extends StatelessWidget {
final Widget child;
const AuthCheck({super.key, required this.child});
@override
Widget build(BuildContext context) {
return Consumer<Auth>(
builder: (context, auth, __) {
if (!auth.authenticated) {
Future.delayed(Duration(milliseconds: 1), () {
if (constants.navigatorKey.currentContext == null) {
return;
}
Navigator.of(
constants.navigatorKey.currentContext!,
).pushNamedAndRemoveUntil('/auth', (Route<dynamic> route) => false);
});
return child;
}
return child;
},
);
}
}

View File

@@ -0,0 +1,64 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:repertory/constants.dart' as constants;
import 'package:repertory/helpers.dart';
import 'package:repertory/models/mount_list.dart';
import 'package:sodium_libs/sodium_libs.dart';
class Auth with ChangeNotifier {
bool _authenticated = false;
SecureKey _key = SecureKey.random(constants.sodium, 32);
String _user = "";
MountList? mountList;
bool get authenticated => _authenticated;
SecureKey get key => _key;
Future<void> authenticate(String user, String password) async {
final sodium = constants.sodium;
final keyHash = sodium.crypto.genericHash(
outLen: sodium.crypto.aeadXChaCha20Poly1305IETF.keyBytes,
message: Uint8List.fromList(password.toCharArray()),
);
_authenticated = true;
_key = SecureKey.fromList(sodium, keyHash);
_user = user;
notifyListeners();
}
Future<String> createAuth() async {
try {
final response = await http.get(
Uri.parse(Uri.encodeFull('${getBaseUri()}/api/v1/nonce')),
);
if (response.statusCode != 200) {
logoff();
return "";
}
final nonce = jsonDecode(response.body)["nonce"];
return encryptValue('${_user}_$nonce', key);
} catch (e) {
debugPrint('$e');
}
return "";
}
void logoff() {
_authenticated = false;
_key = SecureKey.random(constants.sodium, 32);
_user = "";
notifyListeners();
mountList?.clear();
}
}

View File

@@ -3,16 +3,18 @@ import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:repertory/helpers.dart'; import 'package:repertory/helpers.dart';
import 'package:repertory/models/auth.dart';
import 'package:repertory/models/mount_list.dart'; import 'package:repertory/models/mount_list.dart';
import 'package:repertory/types/mount_config.dart'; import 'package:repertory/types/mount_config.dart';
class Mount with ChangeNotifier { class Mount with ChangeNotifier {
final Auth _auth;
final MountConfig mountConfig; final MountConfig mountConfig;
final MountList? _mountList; final MountList? _mountList;
bool _isMounting = false; bool _isMounting = false;
bool _isRefreshing = false; bool _isRefreshing = false;
Mount(this.mountConfig, this._mountList, {isAdd = false}) { Mount(this._auth, this.mountConfig, this._mountList, {isAdd = false}) {
if (isAdd) { if (isAdd) {
return; return;
} }
@@ -29,12 +31,20 @@ class Mount with ChangeNotifier {
Future<void> _fetch() async { Future<void> _fetch() async {
try { try {
final auth = await _auth.createAuth();
final response = await http.get( final response = await http.get(
Uri.parse( Uri.parse(
Uri.encodeFull('${getBaseUri()}/api/v1/mount?name=$name&type=$type'), Uri.encodeFull(
'${getBaseUri()}/api/v1/mount?auth=$auth&name=$name&type=$type',
),
), ),
); );
if (response.statusCode == 401) {
_auth.logoff();
return;
}
if (response.statusCode == 404) { if (response.statusCode == 404) {
_mountList?.reset(); _mountList?.reset();
return; return;
@@ -57,14 +67,20 @@ class Mount with ChangeNotifier {
Future<void> _fetchStatus() async { Future<void> _fetchStatus() async {
try { try {
final auth = await _auth.createAuth();
final response = await http.get( final response = await http.get(
Uri.parse( Uri.parse(
Uri.encodeFull( Uri.encodeFull(
'${getBaseUri()}/api/v1/mount_status?name=$name&type=$type', '${getBaseUri()}/api/v1/mount_status?auth=$auth&name=$name&type=$type',
), ),
), ),
); );
if (response.statusCode == 401) {
_auth.logoff();
return;
}
if (response.statusCode == 404) { if (response.statusCode == 404) {
_mountList?.reset(); _mountList?.reset();
return; return;
@@ -85,16 +101,77 @@ class Mount with ChangeNotifier {
} }
} }
Future<String?> getMountLocation() async { Future<void> setMountLocation(String location) async {
try { try {
final response = await http.get( mountConfig.path = location;
final auth = await _auth.createAuth();
final response = await http.put(
Uri.parse( Uri.parse(
Uri.encodeFull( Uri.encodeFull(
'${getBaseUri()}/api/v1/mount_location?name=$name&type=$type', '${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 {
try {
final auth = await _auth.createAuth();
final response = await http.get(
Uri.parse(
Uri.encodeFull('${getBaseUri()}/api/v1/locations?auth=$auth'),
),
);
if (response.statusCode == 401) {
_auth.logoff();
return <String>[];
}
if (response.statusCode != 200) {
return <String>[];
}
return (jsonDecode(response.body) as List).cast<String>();
} catch (e) {
debugPrint('$e');
}
return <String>[];
}
Future<String?> getMountLocation() async {
try {
final auth = await _auth.createAuth();
final response = await http.get(
Uri.parse(
Uri.encodeFull(
'${getBaseUri()}/api/v1/mount_location?auth=$auth&name=$name&type=$type',
),
),
);
if (response.statusCode == 401) {
_auth.logoff();
return null;
}
if (response.statusCode != 200) { if (response.statusCode != 200) {
return null; return null;
} }
@@ -120,26 +197,36 @@ class Mount with ChangeNotifier {
await Future.delayed(Duration(seconds: 1)); await Future.delayed(Duration(seconds: 1));
} }
final auth = await _auth.createAuth();
final response = await http.post( final response = await http.post(
Uri.parse( Uri.parse(
Uri.encodeFull( Uri.encodeFull(
'${getBaseUri()}/api/v1/mount?unmount=$unmount&name=$name&type=$type&location=$location', '${getBaseUri()}/api/v1/mount?auth=$auth&unmount=$unmount&name=$name&type=$type&location=$location',
), ),
), ),
); );
if (response.statusCode == 401) {
displayAuthError(_auth);
_auth.logoff();
return false;
}
if (response.statusCode == 404) { if (response.statusCode == 404) {
_isMounting = false; _isMounting = false;
_mountList?.reset(); _mountList?.reset();
return true; return true;
} }
final badLocation = (!unmount && response.statusCode == 500);
if (badLocation) {
mountConfig.path = "";
}
await refresh(force: true); await refresh(force: true);
_isMounting = false; _isMounting = false;
if (!unmount && response.statusCode == 500) { return !badLocation;
return false;
}
} catch (e) { } catch (e) {
debugPrint('$e'); debugPrint('$e');
} }
@@ -167,14 +254,20 @@ class Mount with ChangeNotifier {
Future<void> setValue(String key, String value) async { Future<void> setValue(String key, String value) async {
try { try {
final auth = await _auth.createAuth();
final response = await http.put( final response = await http.put(
Uri.parse( Uri.parse(
Uri.encodeFull( Uri.encodeFull(
'${getBaseUri()}/api/v1/set_value_by_name?name=$name&type=$type&key=$key&value=$value', '${getBaseUri()}/api/v1/set_value_by_name?auth=$auth&name=$name&type=$type&key=$key&value=$value',
), ),
), ),
); );
if (response.statusCode == 401) {
_auth.logoff();
return;
}
if (response.statusCode == 404) { if (response.statusCode == 404) {
_mountList?.reset(); _mountList?.reset();
return; return;

View File

@@ -1,4 +1,5 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:math';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
@@ -6,16 +7,26 @@ import 'package:flutter/material.dart' show ModalRoute;
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:repertory/constants.dart' as constants; import 'package:repertory/constants.dart' as constants;
import 'package:repertory/helpers.dart'; import 'package:repertory/helpers.dart';
import 'package:repertory/models/auth.dart';
import 'package:repertory/models/mount.dart'; import 'package:repertory/models/mount.dart';
import 'package:repertory/types/mount_config.dart'; import 'package:repertory/types/mount_config.dart';
class MountList with ChangeNotifier { class MountList with ChangeNotifier {
MountList() { final Auth _auth;
_fetch();
MountList(this._auth) {
_auth.mountList = this;
_auth.addListener(() {
if (_auth.authenticated) {
_fetch();
}
});
} }
List<Mount> _mountList = []; List<Mount> _mountList = [];
Auth get auth => _auth;
UnmodifiableListView<Mount> get items => UnmodifiableListView<Mount> get items =>
UnmodifiableListView<Mount>(_mountList); UnmodifiableListView<Mount>(_mountList);
@@ -46,10 +57,17 @@ class MountList with ChangeNotifier {
Future<void> _fetch() async { Future<void> _fetch() async {
try { try {
final auth = await _auth.createAuth();
final response = await http.get( final response = await http.get(
Uri.parse('${getBaseUri()}/api/v1/mount_list'), Uri.parse('${getBaseUri()}/api/v1/mount_list?auth=$auth'),
); );
if (response.statusCode == 401) {
displayAuthError(_auth);
_auth.logoff();
return;
}
if (response.statusCode == 404) { if (response.statusCode == 404) {
reset(); reset();
return; return;
@@ -64,7 +82,10 @@ class MountList with ChangeNotifier {
jsonDecode(response.body).forEach((type, value) { jsonDecode(response.body).forEach((type, value) {
nextList.addAll( nextList.addAll(
value value
.map((name) => Mount(MountConfig(type: type, name: name), this)) .map(
(name) =>
Mount(_auth, MountConfig(type: type, name: name), this),
)
.toList(), .toList(),
); );
}); });
@@ -88,24 +109,77 @@ class MountList with ChangeNotifier {
}); });
} }
Future<void> add( Future<bool> add(
String type, String type,
String name, String name,
Map<String, dynamic> mountConfig, Map<String, dynamic> settings,
) async { ) async {
var ret = false;
var apiPort = settings['ApiPort'] ?? 10000;
for (var mount in _mountList) {
var port = mount.mountConfig.settings['ApiPort'] as int?;
if (port != null) {
apiPort = max(apiPort, port + 1);
}
}
settings["ApiPort"] = apiPort;
displayError() {
if (constants.navigatorKey.currentContext == null) {
return;
}
displayErrorMessage(
constants.navigatorKey.currentContext!,
'Add mount failed. Please try again.',
);
}
try { try {
await http.post( final auth = await _auth.createAuth();
final map = await convertAllToString(
jsonDecode(jsonEncode(settings)),
_auth.key,
);
final response = await http.post(
Uri.parse( Uri.parse(
Uri.encodeFull( Uri.encodeFull(
'${getBaseUri()}/api/v1/add_mount?name=$name&type=$type&config=${jsonEncode(convertAllToString(mountConfig))}', '${getBaseUri()}/api/v1/add_mount?auth=$auth&name=$name&type=$type&config=${jsonEncode(map)}',
), ),
), ),
); );
switch (response.statusCode) {
case 200:
ret = true;
break;
case 401:
displayAuthError(_auth);
_auth.logoff();
break;
case 404:
reset();
break;
default:
displayError();
break;
}
} catch (e) { } catch (e) {
debugPrint('$e'); debugPrint('$e');
displayError();
} }
return _fetch(); if (ret) {
await _fetch();
}
return ret;
}
void clear() {
_mountList = [];
notifyListeners();
} }
Future<void> reset() async { Future<void> reset() async {
@@ -120,15 +194,8 @@ class MountList with ChangeNotifier {
'Mount removed externally. Reloading...', 'Mount removed externally. Reloading...',
); );
_mountList = []; clear();
notifyListeners();
return _fetch(); return _fetch();
} }
void remove(String name) {
_mountList.removeWhere((item) => item.name == name);
notifyListeners();
}
} }

View File

@@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:repertory/constants.dart' as constants; import 'package:repertory/constants.dart' as constants;
import 'package:repertory/helpers.dart'; import 'package:repertory/helpers.dart';
import 'package:repertory/models/auth.dart';
import 'package:repertory/models/mount.dart'; import 'package:repertory/models/mount.dart';
import 'package:repertory/models/mount_list.dart'; import 'package:repertory/models/mount_list.dart';
import 'package:repertory/types/mount_config.dart'; import 'package:repertory/types/mount_config.dart';
@@ -27,7 +28,6 @@ class _AddMountScreenState extends State<AddMountScreen> {
"S3": createDefaultSettings("S3"), "S3": createDefaultSettings("S3"),
"Sia": createDefaultSettings("Sia"), "Sia": createDefaultSettings("Sia"),
}; };
bool _showAdvanced = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -36,159 +36,178 @@ class _AddMountScreenState extends State<AddMountScreen> {
backgroundColor: Theme.of(context).colorScheme.inversePrimary, backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title), title: Text(widget.title),
actions: [ actions: [
Row( Consumer<Auth>(
children: [ builder: (context, auth, _) {
const Text("Advanced"), return IconButton(
IconButton( icon: const Icon(Icons.logout),
icon: Icon(_showAdvanced ? Icons.toggle_on : Icons.toggle_off), onPressed: () => auth.logoff(),
onPressed: () => setState(() => _showAdvanced = !_showAdvanced), );
), },
],
), ),
], ],
), ),
body: Padding( body: Padding(
padding: const EdgeInsets.all(constants.padding), padding: const EdgeInsets.all(constants.padding),
child: Column( child: Consumer<Auth>(
crossAxisAlignment: CrossAxisAlignment.start, builder: (context, auth, _) {
mainAxisAlignment: MainAxisAlignment.start, return Column(
mainAxisSize: MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.start,
children: [ mainAxisAlignment: MainAxisAlignment.start,
Card( mainAxisSize: MainAxisSize.max,
margin: EdgeInsets.all(0.0), children: [
child: Padding( Card(
padding: const EdgeInsets.all(constants.padding),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Text('Provider Type'),
const SizedBox(width: constants.padding),
DropdownButton<String>(
autofocus: true,
value: _mountType,
onChanged: (mountType) => _handleChange(mountType ?? ''),
items:
constants.providerTypeList
.map<DropdownMenuItem<String>>((item) {
return DropdownMenuItem<String>(
value: item,
child: Text(item),
);
})
.toList(),
),
],
),
),
),
if (_mountType.isNotEmpty && _mountType != 'Remote')
const SizedBox(height: constants.padding),
if (_mountType.isNotEmpty && _mountType != 'Remote')
Card(
margin: EdgeInsets.all(0.0),
child: Padding(
padding: const EdgeInsets.all(constants.padding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Text('Configuration Name'),
const SizedBox(width: constants.padding),
TextField(
autofocus: true,
controller: _mountNameController,
keyboardType: TextInputType.text,
inputFormatters: [
FilteringTextInputFormatter.deny(RegExp(r'\s')),
],
onChanged: (_) => _handleChange(_mountType),
),
],
),
),
),
if (_mount != null) const SizedBox(height: constants.padding),
if (_mount != null)
Expanded(
child: Card(
margin: EdgeInsets.all(0.0), margin: EdgeInsets.all(0.0),
child: Padding( child: Padding(
padding: const EdgeInsets.all(constants.padding), padding: const EdgeInsets.all(constants.padding),
child: MountSettingsWidget( child: Row(
isAdd: true, crossAxisAlignment: CrossAxisAlignment.start,
mount: _mount!, mainAxisAlignment: MainAxisAlignment.start,
settings: _settings[_mountType]!, mainAxisSize: MainAxisSize.min,
showAdvanced: _showAdvanced, children: [
const Text('Provider Type'),
const SizedBox(width: constants.padding),
DropdownButton<String>(
autofocus: true,
value: _mountType,
onChanged:
(mountType) =>
_handleChange(auth, mountType ?? ''),
items:
constants.providerTypeList
.map<DropdownMenuItem<String>>((item) {
return DropdownMenuItem<String>(
value: item,
child: Text(item),
);
})
.toList(),
),
],
), ),
), ),
), ),
), if (_mountType.isNotEmpty && _mountType != 'Remote')
if (_mount != null) const SizedBox(height: constants.padding), const SizedBox(height: constants.padding),
if (_mount != null) if (_mountType.isNotEmpty && _mountType != 'Remote')
Builder( Card(
builder: (context) { margin: EdgeInsets.all(0.0),
return ElevatedButton.icon( child: Padding(
onPressed: () async { padding: const EdgeInsets.all(constants.padding),
final mountList = Provider.of<MountList>(context); child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Text('Configuration Name'),
const SizedBox(width: constants.padding),
TextField(
autofocus: true,
controller: _mountNameController,
keyboardType: TextInputType.text,
inputFormatters: [
FilteringTextInputFormatter.deny(RegExp(r'\s')),
],
onChanged: (_) => _handleChange(auth, _mountType),
),
],
),
),
),
if (_mount != null) const SizedBox(height: constants.padding),
if (_mount != null)
Expanded(
child: Card(
margin: EdgeInsets.all(0.0),
child: Padding(
padding: const EdgeInsets.all(constants.padding),
child: MountSettingsWidget(
isAdd: true,
mount: _mount!,
settings: _settings[_mountType]!,
showAdvanced: false,
),
),
),
),
if (_mount != null) const SizedBox(height: constants.padding),
if (_mount != null)
Row(
children: [
ElevatedButton.icon(
label: const Text('Add'),
icon: const Icon(Icons.add),
onPressed: () async {
final mountList = Provider.of<MountList>(context);
List<String> failed = []; List<String> failed = [];
if (!validateSettings(_settings[_mountType]!, failed)) { if (!validateSettings(
for (var key in failed) { _settings[_mountType]!,
displayErrorMessage( failed,
context, )) {
"Setting '$key' is not valid", for (var key in failed) {
displayErrorMessage(
context,
"Setting '$key' is not valid",
);
}
return;
}
if (mountList.hasConfigName(
_mountNameController.text,
)) {
return displayErrorMessage(
context,
"Configuration name '${_mountNameController.text}' already exists",
);
}
if (_mountType == "Sia" || _mountType == "S3") {
final bucket =
_settings[_mountType]!["${_mountType}Config"]["Bucket"]
as String;
if (mountList.hasBucketName(_mountType, bucket)) {
return displayErrorMessage(
context,
"Bucket '$bucket' already exists",
);
}
}
final success = await mountList.add(
_mountType,
_mountType == 'Remote'
? '${_settings[_mountType]!['RemoteConfig']['HostNameOrIp']}_${_settings[_mountType]!['RemoteConfig']['ApiPort']}'
: _mountNameController.text,
_settings[_mountType]!,
); );
}
return;
}
if (mountList.hasConfigName(_mountNameController.text)) { if (!success || !context.mounted) {
return displayErrorMessage( return;
context, }
"Configuration name '${_mountNameController.text}' already exists",
);
}
if (_mountType == "Sia" || _mountType == "S3") { Navigator.pop(context);
final bucket = },
_settings[_mountType]!["${_mountType}Config"]["Bucket"] ),
as String; if (_mountType == 'Sia' || _mountType == 'S3') ...[
if (mountList.hasBucketName(_mountType, bucket)) { const SizedBox(width: constants.padding),
return displayErrorMessage( ElevatedButton.icon(
context, label: const Text('Test'),
"Bucket '$bucket' already exists", icon: const Icon(Icons.check),
); onPressed: () async {},
} ),
} ],
],
await mountList.add( ),
_mountType, ],
_mountType == 'Remote' );
? '${_settings[_mountType]!['RemoteConfig']['HostNameOrIp']}_${_settings[_mountType]!['RemoteConfig']['ApiPort']}' },
: _mountNameController.text,
_settings[_mountType]!,
);
if (!context.mounted) {
return;
}
Navigator.pop(context);
},
label: const Text('Add'),
icon: const Icon(Icons.add),
);
},
),
],
), ),
), ),
); );
} }
void _handleChange(String mountType) { void _handleChange(Auth auth, String mountType) {
setState(() { setState(() {
final changed = _mountType != mountType; final changed = _mountType != mountType;
@@ -203,6 +222,7 @@ class _AddMountScreenState extends State<AddMountScreen> {
(_mountNameController.text.isEmpty) (_mountNameController.text.isEmpty)
? null ? null
: Mount( : Mount(
auth,
MountConfig( MountConfig(
name: _mountNameController.text, name: _mountNameController.text,
settings: _settings[mountType], settings: _settings[mountType],

View File

@@ -0,0 +1,116 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:repertory/constants.dart' as constants;
import 'package:repertory/models/auth.dart';
class AuthScreen extends StatefulWidget {
final String title;
const AuthScreen({super.key, required this.title});
@override
State<AuthScreen> createState() => _AuthScreenState();
}
class _AuthScreenState extends State<AuthScreen> {
bool _enabled = true;
final _passwordController = TextEditingController();
final _userController = TextEditingController();
@override
Widget build(context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: Consumer<Auth>(
builder: (context, auth, _) {
if (auth.authenticated) {
Future.delayed(Duration(milliseconds: 1), () {
if (constants.navigatorKey.currentContext == null) {
return;
}
Navigator.of(
constants.navigatorKey.currentContext!,
).pushNamedAndRemoveUntil('/', (Route<dynamic> route) => false);
});
return SizedBox.shrink();
}
createLoginHandler() {
return _enabled
? () async {
setState(() => _enabled = false);
await auth.authenticate(
_userController.text,
_passwordController.text,
);
setState(() => _enabled = true);
}
: null;
}
return Center(
child: Card(
child: Padding(
padding: const EdgeInsets.all(constants.padding),
child: SizedBox(
width: constants.logonWidth,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
constants.appLogonTitle,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: constants.padding),
TextField(
autofocus: true,
decoration: InputDecoration(labelText: 'Username'),
controller: _userController,
textInputAction: TextInputAction.next,
),
const SizedBox(height: constants.padding),
TextField(
obscureText: true,
decoration: InputDecoration(labelText: 'Password'),
controller: _passwordController,
textInputAction: TextInputAction.go,
onSubmitted: (_) {
final handler = createLoginHandler();
if (handler == null) {
return;
}
handler();
},
),
const SizedBox(height: constants.padding),
ElevatedButton(
onPressed: createLoginHandler(),
child: const Text('Login'),
),
],
),
),
),
),
);
},
),
);
}
@override
void setState(VoidCallback fn) {
if (!mounted) {
return;
}
super.setState(fn);
}
}

View File

@@ -1,6 +1,8 @@
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:repertory/models/auth.dart';
import 'package:repertory/models/mount.dart'; import 'package:repertory/models/mount.dart';
import 'package:repertory/widgets/mount_settings.dart'; import 'package:repertory/widgets/mount_settings.dart';
@@ -25,10 +27,25 @@ class _EditMountScreenState extends State<EditMountScreen> {
actions: [ actions: [
Row( Row(
children: [ children: [
const Text("Advanced"), Row(
IconButton( children: [
icon: Icon(_showAdvanced ? Icons.toggle_on : Icons.toggle_off), const Text("Advanced"),
onPressed: () => setState(() => _showAdvanced = !_showAdvanced), IconButton(
icon: Icon(
_showAdvanced ? Icons.toggle_on : Icons.toggle_off,
),
onPressed:
() => setState(() => _showAdvanced = !_showAdvanced),
),
],
),
Consumer<Auth>(
builder: (context, auth, _) {
return IconButton(
icon: const Icon(Icons.logout),
onPressed: () => auth.logoff(),
);
},
), ),
], ],
), ),

View File

@@ -2,7 +2,9 @@ import 'dart:convert' show jsonDecode, jsonEncode;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:provider/provider.dart';
import 'package:repertory/helpers.dart'; import 'package:repertory/helpers.dart';
import 'package:repertory/models/auth.dart';
import 'package:repertory/widgets/ui_settings.dart'; import 'package:repertory/widgets/ui_settings.dart';
class EditSettingsScreen extends StatefulWidget { class EditSettingsScreen extends StatefulWidget {
@@ -20,6 +22,16 @@ class _EditSettingsScreenState extends State<EditSettingsScreen> {
appBar: AppBar( appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary, backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title), title: Text(widget.title),
actions: [
Consumer<Auth>(
builder: (context, auth, _) {
return IconButton(
icon: const Icon(Icons.logout),
onPressed: () => auth.logoff(),
);
},
),
],
), ),
body: FutureBuilder( body: FutureBuilder(
builder: (context, snapshot) { builder: (context, snapshot) {
@@ -41,10 +53,17 @@ class _EditSettingsScreenState extends State<EditSettingsScreen> {
Future<Map<String, dynamic>> _grabSettings() async { Future<Map<String, dynamic>> _grabSettings() async {
try { try {
final authProvider = Provider.of<Auth>(context, listen: false);
final auth = await authProvider.createAuth();
final response = await http.get( final response = await http.get(
Uri.parse('${getBaseUri()}/api/v1/settings'), Uri.parse('${getBaseUri()}/api/v1/settings?auth=$auth'),
); );
if (response.statusCode == 401) {
authProvider.logoff();
return {};
}
if (response.statusCode != 200) { if (response.statusCode != 200) {
return {}; return {};
} }

View File

@@ -1,5 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:repertory/constants.dart' as constants; import 'package:repertory/constants.dart' as constants;
import 'package:repertory/models/auth.dart';
import 'package:repertory/widgets/mount_list_widget.dart'; import 'package:repertory/widgets/mount_list_widget.dart';
class HomeScreen extends StatefulWidget { class HomeScreen extends StatefulWidget {
@@ -21,6 +23,16 @@ class _HomeScreeState extends State<HomeScreen> {
icon: const Icon(Icons.storage), icon: const Icon(Icons.storage),
), ),
title: Text(widget.title), title: Text(widget.title),
actions: [
Consumer<Auth>(
builder: (context, auth, _) {
return IconButton(
icon: const Icon(Icons.logout),
onPressed: () => auth.logoff(),
);
},
),
],
), ),
body: Padding( body: Padding(
padding: const EdgeInsets.all(constants.padding), padding: const EdgeInsets.all(constants.padding),

View File

@@ -161,63 +161,107 @@ void createPasswordSetting(
onPressed: (_) { onPressed: (_) {
String updatedValue1 = value; String updatedValue1 = value;
String updatedValue2 = value; String updatedValue2 = value;
bool hidePassword1 = true;
bool hidePassword2 = true;
showDialog( showDialog(
context: context, context: context,
builder: (context) { builder: (context) {
return AlertDialog( return StatefulBuilder(
actions: [ builder: (context, setDialogState) {
TextButton( return AlertDialog(
child: const Text('Cancel'), actions: [
onPressed: () => Navigator.of(context).pop(), TextButton(
), child: const Text('Cancel'),
TextButton( onPressed: () => Navigator.of(context).pop(),
child: const Text('OK'), ),
onPressed: () { TextButton(
if (updatedValue1 != updatedValue2) { child: const Text('OK'),
return displayErrorMessage( onPressed: () {
context, if (updatedValue1 != updatedValue2) {
"Setting '$key' does not match", return displayErrorMessage(
); context,
} "Setting '$key' does not match",
);
}
final result = validators.firstWhereOrNull( final result = validators.firstWhereOrNull(
(validator) => !validator(updatedValue1), (validator) => !validator(updatedValue1),
); );
if (result != null) { if (result != null) {
return displayErrorMessage( return displayErrorMessage(
context, context,
"Setting '$key' is not valid", "Setting '$key' is not valid",
); );
} }
setState(() => settings[key] = updatedValue1); setState(() => settings[key] = updatedValue1);
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
), ),
], ],
content: Column( content: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
TextField( Row(
autofocus: true, children: [
controller: TextEditingController(text: updatedValue1), Expanded(
obscureText: true, child: TextField(
obscuringCharacter: '*', autofocus: true,
onChanged: (value) => updatedValue1 = value, controller: TextEditingController(
text: updatedValue1,
),
obscureText: hidePassword1,
obscuringCharacter: '*',
onChanged: (value) => updatedValue1 = value,
),
),
IconButton(
onPressed:
() => setDialogState(
() => hidePassword1 = !hidePassword1,
),
icon: Icon(
hidePassword1
? Icons.visibility
: Icons.visibility_off,
),
),
],
),
const SizedBox(height: constants.padding),
Row(
children: [
Expanded(
child: TextField(
autofocus: false,
controller: TextEditingController(
text: updatedValue2,
),
obscureText: hidePassword2,
obscuringCharacter: '*',
onChanged: (value) => updatedValue2 = value,
),
),
IconButton(
onPressed:
() => setDialogState(
() => hidePassword2 = !hidePassword2,
),
icon: Icon(
hidePassword2
? Icons.visibility
: Icons.visibility_off,
),
),
],
),
],
), ),
const SizedBox(height: constants.padding), title: createSettingTitle(context, key, description),
TextField( );
autofocus: false, },
controller: TextEditingController(text: updatedValue2),
obscureText: true,
obscuringCharacter: '*',
onChanged: (value) => updatedValue2 = value,
),
],
),
title: createSettingTitle(context, key, description),
); );
}, },
); );

View File

@@ -4,7 +4,7 @@ import 'package:repertory/helpers.dart' show initialCaps;
class MountConfig { class MountConfig {
bool? mounted; bool? mounted;
final String _name; final String _name;
String _path = ''; String path = '';
Map<String, dynamic> _settings = {}; Map<String, dynamic> _settings = {};
final String _type; final String _type;
MountConfig({required name, required type, Map<String, dynamic>? settings}) MountConfig({required name, required type, Map<String, dynamic>? settings})
@@ -17,7 +17,6 @@ class MountConfig {
String? get bucket => _settings['${provider}Config']?["Bucket"] as String; String? get bucket => _settings['${provider}Config']?["Bucket"] as String;
String get name => _name; String get name => _name;
String get path => _path;
String get provider => initialCaps(_type); String get provider => initialCaps(_type);
UnmodifiableMapView<String, dynamic> get settings => UnmodifiableMapView<String, dynamic> get settings =>
UnmodifiableMapView<String, dynamic>(_settings); UnmodifiableMapView<String, dynamic>(_settings);
@@ -28,7 +27,7 @@ class MountConfig {
} }
void updateStatus(Map<String, dynamic> status) { void updateStatus(Map<String, dynamic> status) {
_path = status['Location'] as String; path = status['Location'] as String;
mounted = status['Active'] as bool; mounted = status['Active'] as bool;
} }
} }

View File

@@ -1,9 +1,13 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:repertory/constants.dart' as constants; import 'package:repertory/constants.dart' as constants;
import 'package:repertory/helpers.dart' import 'package:repertory/helpers.dart'
show getSettingDescription, getSettingValidators; show
convertAllToString,
getChanged,
getSettingDescription,
getSettingValidators;
import 'package:repertory/models/auth.dart';
import 'package:repertory/models/mount.dart'; import 'package:repertory/models/mount.dart';
import 'package:repertory/models/mount_list.dart'; import 'package:repertory/models/mount_list.dart';
import 'package:repertory/settings.dart'; import 'package:repertory/settings.dart';
@@ -614,23 +618,28 @@ class _MountSettingsWidgetState extends State<MountSettingsWidget> {
@override @override
void dispose() { void dispose() {
if (!widget.isAdd) { if (!widget.isAdd) {
var settings = widget.mount.mountConfig.settings; final settings = getChanged(
if (!DeepCollectionEquality().equals(widget.settings, settings)) { widget.mount.mountConfig.settings,
widget.settings.forEach((key, value) { widget.settings,
if (!DeepCollectionEquality().equals(settings[key], value)) { );
if (settings.isNotEmpty) {
final mount = widget.mount;
final key =
Provider.of<Auth>(
constants.navigatorKey.currentContext!,
listen: false,
).key;
convertAllToString(settings, key).then((map) {
map.forEach((key, value) {
if (value is Map<String, dynamic>) { if (value is Map<String, dynamic>) {
value.forEach((subKey, subValue) { value.forEach((subKey, subValue) {
if (!DeepCollectionEquality().equals( mount.setValue('$key.$subKey', subValue);
settings[key][subKey],
subValue,
)) {
widget.mount.setValue('$key.$subKey', subValue.toString());
}
}); });
} else { return;
widget.mount.setValue(key, value.toString());
} }
}
mount.setValue(key, value);
});
}); });
} }
} }

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,19 +62,53 @@ 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(
icon: Icon( mainAxisAlignment: MainAxisAlignment.end,
mount.mounted == null mainAxisSize: MainAxisSize.min,
? Icons.hourglass_top crossAxisAlignment: CrossAxisAlignment.start,
: mount.mounted! children: [
? Icons.toggle_on if (mount.mounted != null && !mount.mounted!)
: Icons.toggle_off, IconButton(
color: icon: const Icon(Icons.edit),
mount.mounted ?? false color: subTextColor,
? Color.fromARGB(255, 163, 96, 76) tooltip: 'Edit mount location',
: subTextColor, onPressed: () async {
), setState(() => _editEnabled = false);
onPressed: _createMountHandler(context, mount), 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(
mount.mounted == null
? Icons.hourglass_top
: mount.mounted!
? Icons.toggle_on
: Icons.toggle_off,
color:
mount.mounted ?? false
? Color.fromARGB(255, 163, 96, 76)
: subTextColor,
),
tooltip:
mount.mounted == null
? ''
: mount.mounted!
? 'Unmount'
: 'Mount',
onPressed: _createMountHandler(context, mount),
),
],
), ),
); );
}, },
@@ -149,42 +184,7 @@ class _MountWidgetState extends State<MountWidget> {
return location; return location;
} }
String? currentLocation; return editMountLocation(context, await mount.getAvailableLocations());
return await showDialog(
context: context,
builder: (context) {
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: TextField(
autofocus: true,
controller: TextEditingController(text: currentLocation),
inputFormatters: [FilteringTextInputFormatter.deny(RegExp(r'\s'))],
onChanged: (value) => currentLocation = value,
),
title: const Text('Set Mount Location'),
);
},
);
} }
@override @override

View File

@@ -1,15 +1,19 @@
import 'dart:convert' show jsonDecode, jsonEncode; import 'dart:convert' show jsonEncode;
import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:provider/provider.dart';
import 'package:repertory/constants.dart' as constants;
import 'package:repertory/helpers.dart' import 'package:repertory/helpers.dart'
show show
convertAllToString, convertAllToString,
displayAuthError,
getBaseUri, getBaseUri,
getChanged,
getSettingDescription, getSettingDescription,
getSettingValidators, getSettingValidators,
trimNotEmptyValidator; trimNotEmptyValidator;
import 'package:repertory/models/auth.dart';
import 'package:repertory/settings.dart'; import 'package:repertory/settings.dart';
import 'package:settings_ui/settings_ui.dart'; import 'package:settings_ui/settings_ui.dart';
@@ -100,21 +104,38 @@ class _UISettingsWidgetState extends State<UISettingsWidget> {
@override @override
void dispose() { void dispose() {
debugPrint('current: ${jsonEncode(widget.settings)}'); final settings = getChanged(widget.origSettings, widget.settings);
debugPrint('orig: ${jsonEncode(widget.origSettings)}'); if (settings.isNotEmpty) {
if (!DeepCollectionEquality().equals( final key =
widget.settings, Provider.of<Auth>(
widget.origSettings, constants.navigatorKey.currentContext!,
)) { listen: false,
http ).key;
.put( convertAllToString(settings, key)
Uri.parse( .then((map) async {
Uri.encodeFull( try {
'${getBaseUri()}/api/v1/settings?data=${jsonEncode(convertAllToString(widget.settings))}', final authProvider = Provider.of<Auth>(
), constants.navigatorKey.currentContext!,
), listen: false,
) );
.then((_) {})
final auth = await authProvider.createAuth();
final response = await http.put(
Uri.parse(
Uri.encodeFull(
'${getBaseUri()}/api/v1/settings?auth=$auth&data=${jsonEncode(map)}',
),
),
);
if (response.statusCode == 401) {
displayAuthError(authProvider);
authProvider.logoff();
}
} catch (e) {
debugPrint('$e');
}
})
.catchError((e) { .catchError((e) {
debugPrint('$e'); debugPrint('$e');
}); });

View File

@@ -39,6 +39,7 @@ dependencies:
provider: ^6.1.2 provider: ^6.1.2
settings_ui: ^2.0.2 settings_ui: ^2.0.2
sodium_libs: ^3.4.4+1 sodium_libs: ^3.4.4+1
convert: ^3.1.2
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: