Compare commits
1 Commits
ea912b38a3
...
v2.0.5-rc
Author | SHA1 | Date | |
---|---|---|---|
62555e6125 |
@@ -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
|
||||||
|
@@ -4,14 +4,15 @@
|
|||||||
|
|
||||||
### 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
|
||||||
* Renamed setting `ApiAuth` to `ApiPassword`
|
* Renamed setting `ApiAuth` to `ApiPassword`
|
||||||
* Require `--name,-na` option for encryption provider
|
* Require `--name,-na` option for encryption provider
|
||||||
|
|
||||||
|
@@ -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;
|
||||||
|
@@ -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"
|
||||||
|
@@ -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_
|
||||||
|
@@ -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(
|
||||||
|
@@ -314,6 +314,11 @@ provider_type_from_string(std::string_view type,
|
|||||||
|
|
||||||
[[nodiscard]] auto provider_type_to_string(provider_type type) -> std::string;
|
[[nodiscard]] auto provider_type_to_string(provider_type type) -> std::string;
|
||||||
|
|
||||||
|
void clean_json_config(provider_type prov, nlohmann::json &data);
|
||||||
|
|
||||||
|
[[nodiscard]] auto clean_json_value(std::string_view name,
|
||||||
|
std::string_view data) -> std::string;
|
||||||
|
|
||||||
#if defined(_WIN32)
|
#if defined(_WIN32)
|
||||||
struct open_file_data final {
|
struct open_file_data final {
|
||||||
PVOID directory_buffer{nullptr};
|
PVOID directory_buffer{nullptr};
|
||||||
|
@@ -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));
|
||||||
}
|
}
|
||||||
|
@@ -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);
|
};
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
close(fd);
|
auto ret{false};
|
||||||
|
if (wait_for_lock(handle) == 0) {
|
||||||
|
ret = utils::file::read_json_file(get_lock_data_file(), mount_state);
|
||||||
|
if (ret && mount_state.empty()) {
|
||||||
|
mount_state = {
|
||||||
|
{"Active", false},
|
||||||
|
{"Location", ""},
|
||||||
|
{"PID", -1},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
flock(handle, LOCK_UN);
|
||||||
|
}
|
||||||
|
|
||||||
|
close(handle);
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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,42 +123,54 @@ 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) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ret{false};
|
||||||
if (wait_for_lock(handle) == 0) {
|
if (wait_for_lock(handle) == 0) {
|
||||||
const auto mount_id =
|
|
||||||
app_config::get_provider_display_name(pt_) + unique_id_;
|
|
||||||
json mount_state;
|
json mount_state;
|
||||||
if (not utils::file::read_json_file(get_lock_data_file(), mount_state)) {
|
if (not utils::file::read_json_file(get_lock_data_file(), mount_state)) {
|
||||||
utils::error::raise_error(function_name,
|
utils::error::raise_error(function_name,
|
||||||
"failed to read mount state file|sp|" +
|
"failed to read mount state file|sp|" +
|
||||||
get_lock_file());
|
get_lock_file());
|
||||||
}
|
}
|
||||||
if ((mount_state.find(mount_id) == mount_state.end()) ||
|
if ((mount_state.find("Active") == mount_state.end()) ||
|
||||||
(mount_state[mount_id].find("Active") ==
|
(mount_state["Active"].get<bool>() != active) ||
|
||||||
mount_state[mount_id].end()) ||
|
(active &&
|
||||||
(mount_state[mount_id]["Active"].get<bool>() != active) ||
|
((mount_state.find("Location") == mount_state.end()) ||
|
||||||
(active && ((mount_state[mount_id].find("Location") ==
|
(mount_state["Location"].get<std::string>() != mount_location)))) {
|
||||||
mount_state[mount_id].end()) ||
|
if (mount_location.empty() && not active) {
|
||||||
(mount_state[mount_id]["Location"].get<std::string>() !=
|
ret = utils::file::file{get_lock_data_file()}.remove();
|
||||||
mount_location)))) {
|
} else {
|
||||||
const auto lines = utils::file::read_file_lines(get_lock_data_file());
|
ret = utils::file::write_json_file(
|
||||||
const auto txt = std::accumulate(
|
get_lock_data_file(),
|
||||||
lines.begin(), lines.end(), std::string(),
|
{
|
||||||
[](auto &&val, auto &&line) -> auto { return val + line; });
|
|
||||||
auto json_data = json::parse(txt.empty() ? "{}" : txt);
|
|
||||||
json_data[mount_id] = {
|
|
||||||
{"Active", active},
|
{"Active", active},
|
||||||
{"Location", active ? mount_location : ""},
|
{"Location", active ? mount_location : ""},
|
||||||
{"PID", active ? pid : -1},
|
{"PID", active ? pid : -1},
|
||||||
};
|
});
|
||||||
ret = utils::file::write_json_file(get_lock_data_file(), json_data);
|
}
|
||||||
} else {
|
} else {
|
||||||
ret = true;
|
ret = true;
|
||||||
}
|
}
|
||||||
@@ -169,17 +179,16 @@ auto lock_data::set_mount_state(bool active, const std::string &mount_location,
|
|||||||
}
|
}
|
||||||
|
|
||||||
close(handle);
|
close(handle);
|
||||||
}
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto lock_data::wait_for_lock(int fd, std::uint8_t retry_count) -> int {
|
auto lock_data::wait_for_lock(int handle, std::uint8_t retry_count) -> int {
|
||||||
static constexpr const std::uint32_t max_sleep = 100U;
|
static constexpr const std::uint32_t max_sleep{100U};
|
||||||
|
|
||||||
auto lock_status = EWOULDBLOCK;
|
auto lock_status{EWOULDBLOCK};
|
||||||
auto remain = static_cast<std::uint32_t>(retry_count * max_sleep);
|
auto remain{static_cast<std::uint32_t>(retry_count * max_sleep)};
|
||||||
while ((remain > 0) && (lock_status == EWOULDBLOCK)) {
|
while ((remain > 0) && (lock_status == EWOULDBLOCK)) {
|
||||||
lock_status = flock(fd, LOCK_EX | LOCK_NB);
|
lock_status = flock(handle, LOCK_EX | LOCK_NB);
|
||||||
if (lock_status == -1) {
|
if (lock_status == -1) {
|
||||||
lock_status = errno;
|
lock_status = errno;
|
||||||
if (lock_status == EWOULDBLOCK) {
|
if (lock_status == EWOULDBLOCK) {
|
||||||
@@ -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) {
|
||||||
|
@@ -21,150 +21,171 @@
|
|||||||
*/
|
*/
|
||||||
#if defined(_WIN32)
|
#if defined(_WIN32)
|
||||||
|
|
||||||
#include "platform/win32_platform.hpp"
|
#include "platform/platform.hpp"
|
||||||
|
|
||||||
#include "events/event_system.hpp"
|
#include "events/event_system.hpp"
|
||||||
#include "events/types/filesystem_item_added.hpp"
|
#include "events/types/filesystem_item_added.hpp"
|
||||||
#include "providers/i_provider.hpp"
|
#include "providers/i_provider.hpp"
|
||||||
|
#include "utils/config.hpp"
|
||||||
#include "utils/error_utils.hpp"
|
#include "utils/error_utils.hpp"
|
||||||
#include "utils/string.hpp"
|
#include "utils/string.hpp"
|
||||||
|
|
||||||
namespace repertory {
|
namespace repertory {
|
||||||
auto lock_data::get_mount_state(const provider_type & /*pt*/, json &mount_state)
|
lock_data::lock_data(provider_type prov, std::string unique_id)
|
||||||
-> bool {
|
: mutex_id_(create_lock_id(prov, unique_id)),
|
||||||
const auto ret = get_mount_state(mount_state);
|
mutex_handle_(::CreateMutex(nullptr, FALSE,
|
||||||
if (ret) {
|
create_lock_id(prov, unique_id).c_str())) {}
|
||||||
const auto mount_id =
|
|
||||||
app_config::get_provider_display_name(pt_) + unique_id_;
|
lock_data::~lock_data() { release(); }
|
||||||
mount_state = mount_state[mount_id].empty()
|
|
||||||
? json({{"Active", false}, {"Location", ""}, {"PID", -1}})
|
auto lock_data::get_current_mount_state(json &mount_state) -> bool {
|
||||||
: mount_state[mount_id];
|
REPERTORY_USES_FUNCTION_NAME();
|
||||||
|
|
||||||
|
HKEY key{};
|
||||||
|
if (::RegOpenKeyEx(HKEY_CURRENT_USER,
|
||||||
|
fmt::format(R"(SOFTWARE\{}\Mounts\{})",
|
||||||
|
REPERTORY_DATA_NAME, mutex_id_)
|
||||||
|
.c_str(),
|
||||||
|
0, KEY_ALL_ACCESS, &key) != ERROR_SUCCESS) {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string data;
|
||||||
|
DWORD data_size{};
|
||||||
|
|
||||||
|
DWORD type{REG_SZ};
|
||||||
|
::RegGetValueA(key, nullptr, nullptr, RRF_RT_REG_SZ, &type, nullptr,
|
||||||
|
&data_size);
|
||||||
|
|
||||||
|
data.resize(data_size);
|
||||||
|
auto res = ::RegGetValueA(key, nullptr, nullptr, RRF_RT_REG_SZ, &type,
|
||||||
|
data.data(), &data_size);
|
||||||
|
auto ret = res == ERROR_SUCCESS || res == ERROR_FILE_NOT_FOUND;
|
||||||
|
if (ret && data_size != 0U) {
|
||||||
|
try {
|
||||||
|
mount_state = json::parse(data);
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
utils::error::raise_error(function_name, e, "failed to read mount state");
|
||||||
|
ret = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
::RegCloseKey(key);
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto lock_data::get_mount_state(json &mount_state) -> bool {
|
auto lock_data::get_mount_state(json &mount_state) -> bool {
|
||||||
HKEY key;
|
if (not get_current_mount_state(mount_state)) {
|
||||||
auto ret = !::RegCreateKeyEx(
|
return false;
|
||||||
HKEY_CURRENT_USER,
|
|
||||||
("SOFTWARE\\" + std::string{REPERTORY_DATA_NAME} + "\\Mounts").c_str(), 0,
|
|
||||||
nullptr, 0, KEY_ALL_ACCESS, nullptr, &key, nullptr);
|
|
||||||
if (ret) {
|
|
||||||
DWORD i = 0u;
|
|
||||||
DWORD data_size = 0u;
|
|
||||||
std::string name;
|
|
||||||
name.resize(32767u);
|
|
||||||
auto name_size = static_cast<DWORD>(name.size());
|
|
||||||
while (ret &&
|
|
||||||
(::RegEnumValue(key, i, &name[0], &name_size, nullptr, nullptr,
|
|
||||||
nullptr, &data_size) == ERROR_SUCCESS)) {
|
|
||||||
std::string data;
|
|
||||||
data.resize(data_size);
|
|
||||||
name_size++;
|
|
||||||
if ((ret = !::RegEnumValue(key, i++, &name[0], &name_size, nullptr,
|
|
||||||
nullptr, reinterpret_cast<LPBYTE>(&data[0]),
|
|
||||||
&data_size))) {
|
|
||||||
mount_state[name.c_str()] = json::parse(data);
|
|
||||||
name_size = static_cast<DWORD>(name.size());
|
|
||||||
data_size = 0u;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
::RegCloseKey(key);
|
mount_state = mount_state.empty() ? json({
|
||||||
}
|
{"Active", false},
|
||||||
return ret;
|
{"Location", ""},
|
||||||
|
{"PID", -1},
|
||||||
|
})
|
||||||
|
: mount_state;
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto lock_data::grab_lock(std::uint8_t retry_count) -> lock_result {
|
auto lock_data::grab_lock(std::uint8_t retry_count) -> lock_result {
|
||||||
REPERTORY_USES_FUNCTION_NAME();
|
static constexpr const std::uint32_t max_sleep{100U};
|
||||||
|
|
||||||
auto ret = lock_result::success;
|
|
||||||
if (mutex_handle_ == INVALID_HANDLE_VALUE) {
|
if (mutex_handle_ == INVALID_HANDLE_VALUE) {
|
||||||
ret = lock_result::failure;
|
return lock_result::failure;
|
||||||
} else {
|
}
|
||||||
for (auto i = 0;
|
|
||||||
(i <= retry_count) && ((mutex_state_ = ::WaitForSingleObject(
|
for (std::uint8_t idx = 0U;
|
||||||
mutex_handle_, 100)) == WAIT_TIMEOUT);
|
(idx <= retry_count) &&
|
||||||
i++) {
|
((mutex_state_ = ::WaitForSingleObject(mutex_handle_, max_sleep)) ==
|
||||||
|
WAIT_TIMEOUT);
|
||||||
|
++idx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (mutex_state_) {
|
switch (mutex_state_) {
|
||||||
case WAIT_OBJECT_0: {
|
case WAIT_OBJECT_0:
|
||||||
ret = lock_result::success;
|
return lock_result::success;
|
||||||
auto should_reset = true;
|
|
||||||
json mount_state;
|
|
||||||
if (get_mount_state(pt_, mount_state)) {
|
|
||||||
if (mount_state["Active"].get<bool>() &&
|
|
||||||
mount_state["Location"] == "elevating") {
|
|
||||||
should_reset = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (should_reset) {
|
|
||||||
if (not set_mount_state(false, "", -1)) {
|
|
||||||
utils::error::raise_error(function_name, "failed to set mount state");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} break;
|
|
||||||
|
|
||||||
case WAIT_TIMEOUT:
|
case WAIT_TIMEOUT:
|
||||||
ret = lock_result::locked;
|
return lock_result::locked;
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
ret = lock_result::failure;
|
return lock_result::failure;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void lock_data::release() {
|
void lock_data::release() {
|
||||||
if (mutex_handle_ != INVALID_HANDLE_VALUE) {
|
if (mutex_handle_ == INVALID_HANDLE_VALUE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if ((mutex_state_ == WAIT_OBJECT_0) || (mutex_state_ == WAIT_ABANDONED)) {
|
if ((mutex_state_ == WAIT_OBJECT_0) || (mutex_state_ == WAIT_ABANDONED)) {
|
||||||
|
if (mutex_state_ == WAIT_OBJECT_0) {
|
||||||
|
[[maybe_unused]] auto success{set_mount_state(false, "", -1)};
|
||||||
|
}
|
||||||
|
|
||||||
::ReleaseMutex(mutex_handle_);
|
::ReleaseMutex(mutex_handle_);
|
||||||
}
|
}
|
||||||
|
|
||||||
::CloseHandle(mutex_handle_);
|
::CloseHandle(mutex_handle_);
|
||||||
mutex_handle_ = INVALID_HANDLE_VALUE;
|
mutex_handle_ = INVALID_HANDLE_VALUE;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto lock_data::set_mount_state(bool active, 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)
|
||||||
|
@@ -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) {}
|
||||||
@@ -39,6 +37,7 @@ server::server(app_config &config) : config_(config) {}
|
|||||||
void server::handle_get_config(const httplib::Request & /*req*/,
|
void server::handle_get_config(const httplib::Request & /*req*/,
|
||||||
httplib::Response &res) {
|
httplib::Response &res) {
|
||||||
auto data = config_.get_json();
|
auto data = config_.get_json();
|
||||||
|
clean_json_config(config_.get_provider_type(), data);
|
||||||
res.set_content(data.dump(), "application/json");
|
res.set_content(data.dump(), "application/json");
|
||||||
res.status = http_error_codes::ok;
|
res.status = http_error_codes::ok;
|
||||||
}
|
}
|
||||||
@@ -46,7 +45,10 @@ void server::handle_get_config(const httplib::Request & /*req*/,
|
|||||||
void server::handle_get_config_value_by_name(const httplib::Request &req,
|
void server::handle_get_config_value_by_name(const httplib::Request &req,
|
||||||
httplib::Response &res) {
|
httplib::Response &res) {
|
||||||
auto name = req.get_param_value("name");
|
auto name = req.get_param_value("name");
|
||||||
auto data = json({{"value", config_.get_value_by_name(name)}});
|
auto data = json({{
|
||||||
|
"value",
|
||||||
|
clean_json_value(name, config_.get_value_by_name(name)),
|
||||||
|
}});
|
||||||
res.set_content(data.dump(), "application/json");
|
res.set_content(data.dump(), "application/json");
|
||||||
res.status = http_error_codes::ok;
|
res.status = http_error_codes::ok;
|
||||||
}
|
}
|
||||||
@@ -56,7 +58,10 @@ void server::handle_set_config_value_by_name(const httplib::Request &req,
|
|||||||
auto name = req.get_param_value("name");
|
auto name = req.get_param_value("name");
|
||||||
auto value = req.get_param_value("value");
|
auto value = req.get_param_value("value");
|
||||||
|
|
||||||
json data = {{"value", config_.set_value_by_name(name, value)}};
|
json data = {{
|
||||||
|
"value",
|
||||||
|
clean_json_value(name, config_.set_value_by_name(name, value)),
|
||||||
|
}};
|
||||||
res.set_content(data.dump(), "application/json");
|
res.set_content(data.dump(), "application/json");
|
||||||
res.status = http_error_codes::ok;
|
res.status = http_error_codes::ok;
|
||||||
}
|
}
|
||||||
@@ -136,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");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -26,6 +26,51 @@
|
|||||||
#include "utils/string.hpp"
|
#include "utils/string.hpp"
|
||||||
|
|
||||||
namespace repertory {
|
namespace repertory {
|
||||||
|
void clean_json_config(provider_type prov, nlohmann::json &data) {
|
||||||
|
data[JSON_API_PASSWORD] = "";
|
||||||
|
|
||||||
|
switch (prov) {
|
||||||
|
case provider_type::encrypt:
|
||||||
|
data[JSON_ENCRYPT_CONFIG][JSON_ENCRYPTION_TOKEN] = "";
|
||||||
|
data[JSON_REMOTE_MOUNT][JSON_ENCRYPTION_TOKEN] = "";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case provider_type::remote:
|
||||||
|
data[JSON_REMOTE_CONFIG][JSON_ENCRYPTION_TOKEN] = "";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case provider_type::s3:
|
||||||
|
data[JSON_REMOTE_MOUNT][JSON_ENCRYPTION_TOKEN] = "";
|
||||||
|
data[JSON_S3_CONFIG][JSON_ENCRYPTION_TOKEN] = "";
|
||||||
|
data[JSON_S3_CONFIG][JSON_SECRET_KEY] = "";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case provider_type::sia:
|
||||||
|
data[JSON_REMOTE_MOUNT][JSON_ENCRYPTION_TOKEN] = "";
|
||||||
|
data[JSON_HOST_CONFIG][JSON_API_PASSWORD] = "";
|
||||||
|
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto clean_json_value(std::string_view name, std::string_view data)
|
||||||
|
-> std::string {
|
||||||
|
if (name ==
|
||||||
|
fmt::format("{}.{}", JSON_ENCRYPT_CONFIG, JSON_ENCRYPTION_TOKEN) ||
|
||||||
|
name == fmt::format("{}.{}", JSON_HOST_CONFIG, JSON_API_PASSWORD) ||
|
||||||
|
name == fmt::format("{}.{}", JSON_REMOTE_CONFIG, JSON_ENCRYPTION_TOKEN) ||
|
||||||
|
name == fmt::format("{}.{}", JSON_REMOTE_MOUNT, JSON_ENCRYPTION_TOKEN) ||
|
||||||
|
name == fmt::format("{}.{}", JSON_S3_CONFIG, JSON_ENCRYPTION_TOKEN) ||
|
||||||
|
name == fmt::format("{}.{}", JSON_S3_CONFIG, JSON_SECRET_KEY) ||
|
||||||
|
name == JSON_API_PASSWORD) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::string{data};
|
||||||
|
}
|
||||||
|
|
||||||
auto database_type_from_string(std::string type, database_type default_type)
|
auto database_type_from_string(std::string type, database_type default_type)
|
||||||
-> database_type {
|
-> database_type {
|
||||||
type = utils::string::to_lower(utils::string::trim(type));
|
type = utils::string::to_lower(utils::string::trim(type));
|
||||||
|
31
repertory/librepertory/src/utils/platform.cpp
Normal file
31
repertory/librepertory/src/utils/platform.cpp
Normal 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
|
@@ -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,24 +56,36 @@ 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");
|
||||||
|
{
|
||||||
|
auto lock_result = global_lock.grab_lock(100U);
|
||||||
|
if (lock_result != lock_result::success) {
|
||||||
|
std::cerr << "FATAL: Unable to get global lock" << std::endl;
|
||||||
|
return exit_code::lock_failed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
lock_data lock(prov, unique_id);
|
lock_data lock(prov, unique_id);
|
||||||
const auto res = lock.grab_lock();
|
auto lock_result = lock.grab_lock();
|
||||||
if (res == lock_result::locked) {
|
if (lock_result == lock_result::locked) {
|
||||||
ret = exit_code::mount_active;
|
|
||||||
std::cerr << app_config::get_provider_display_name(prov)
|
std::cerr << app_config::get_provider_display_name(prov)
|
||||||
<< " mount is already active" << std::endl;
|
<< " mount is already active" << std::endl;
|
||||||
} else if (res == lock_result::success) {
|
return exit_code::mount_active;
|
||||||
const auto generate_config = utils::cli::has_option(
|
}
|
||||||
args, utils::cli::options::generate_config_option);
|
|
||||||
if (generate_config) {
|
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);
|
app_config config(prov, data_directory);
|
||||||
if (prov == provider_type::remote) {
|
if (prov == provider_type::remote) {
|
||||||
auto cfg = config.get_remote_config();
|
auto remote_config = config.get_remote_config();
|
||||||
cfg.host_name_or_ip = remote_host;
|
remote_config.host_name_or_ip = remote_host;
|
||||||
cfg.api_port = remote_port;
|
remote_config.api_port = remote_port;
|
||||||
config.set_remote_config(cfg);
|
config.set_remote_config(remote_config);
|
||||||
} else if (prov == provider_type::sia &&
|
} else if (prov == provider_type::sia &&
|
||||||
config.get_sia_config().bucket.empty()) {
|
config.get_sia_config().bucket.empty()) {
|
||||||
[[maybe_unused]] auto bucket =
|
[[maybe_unused]] auto bucket =
|
||||||
@@ -84,103 +95,104 @@ mount(std::vector<const char *> args, std::string data_directory,
|
|||||||
std::cout << "Generated " << app_config::get_provider_display_name(prov)
|
std::cout << "Generated " << app_config::get_provider_display_name(prov)
|
||||||
<< " Configuration" << std::endl;
|
<< " Configuration" << std::endl;
|
||||||
std::cout << config.get_config_file_path() << std::endl;
|
std::cout << config.get_config_file_path() << std::endl;
|
||||||
ret = utils::file::file(config.get_config_file_path()).exists()
|
return utils::file::file(config.get_config_file_path()).exists()
|
||||||
? exit_code::success
|
? exit_code::success
|
||||||
: exit_code::file_creation_failed;
|
: exit_code::file_creation_failed;
|
||||||
} else {
|
}
|
||||||
|
|
||||||
#if defined(_WIN32)
|
#if defined(_WIN32)
|
||||||
if (utils::cli::has_option(args, utils::cli::options::hidden_option)) {
|
if (utils::cli::has_option(args, utils::cli::options::hidden_option)) {
|
||||||
::ShowWindow(::GetConsoleWindow(), SW_HIDE);
|
::ShowWindow(::GetConsoleWindow(), SW_HIDE);
|
||||||
}
|
}
|
||||||
#endif // defined(_WIN32)
|
#endif // defined(_WIN32)
|
||||||
auto drive_args =
|
|
||||||
utils::cli::parse_drive_options(args, prov, data_directory);
|
auto drive_args = utils::cli::parse_drive_options(args, prov, data_directory);
|
||||||
app_config config(prov, data_directory);
|
app_config config(prov, data_directory);
|
||||||
|
{
|
||||||
|
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 defined(_WIN32)
|
||||||
if (config.get_enable_mount_manager() &&
|
if (config.get_enable_mount_manager() && not utils::is_process_elevated()) {
|
||||||
not utils::is_process_elevated()) {
|
utils::com_init_wrapper wrapper;
|
||||||
utils::com_init_wrapper cw;
|
|
||||||
if (not lock.set_mount_state(true, "elevating", -1)) {
|
if (not lock.set_mount_state(true, "elevating", -1)) {
|
||||||
std::cerr << "failed to set mount state" << std::endl;
|
std::cerr << "failed to set mount state" << std::endl;
|
||||||
}
|
}
|
||||||
lock.release();
|
lock.release();
|
||||||
|
global_lock.release();
|
||||||
|
|
||||||
mount_result = utils::run_process_elevated(args);
|
mount_result = utils::run_process_elevated(args);
|
||||||
lock_data lock2(prov, unique_id);
|
lock_data prov_lock(prov, unique_id);
|
||||||
if (lock2.grab_lock() == lock_result::success) {
|
if (prov_lock.grab_lock() == lock_result::success) {
|
||||||
if (not lock2.set_mount_state(false, "", -1)) {
|
if (not prov_lock.set_mount_state(false, "", -1)) {
|
||||||
std::cerr << "failed to set mount state" << std::endl;
|
std::cerr << "failed to set mount state" << std::endl;
|
||||||
}
|
}
|
||||||
lock2.release();
|
prov_lock.release();
|
||||||
}
|
}
|
||||||
|
|
||||||
return exit_code::mount_result;
|
return exit_code::mount_result;
|
||||||
}
|
}
|
||||||
#endif // defined(_WIN32)
|
#endif // defined(_WIN32)
|
||||||
std::cout << "Initializing "
|
|
||||||
<< app_config::get_provider_display_name(prov)
|
std::cout << "Initializing " << app_config::get_provider_display_name(prov)
|
||||||
<< (unique_id.empty() ? ""
|
<< (unique_id.empty() ? ""
|
||||||
: (prov == provider_type::remote)
|
: (prov == provider_type::remote)
|
||||||
? " [" + remote_host + ':' +
|
? " [" + remote_host + ':' + std::to_string(remote_port) +
|
||||||
std::to_string(remote_port) + ']'
|
']'
|
||||||
: " [" + unique_id + ']')
|
: " [" + unique_id + ']')
|
||||||
<< " Drive" << std::endl;
|
<< " Drive" << std::endl;
|
||||||
if (prov == provider_type::remote) {
|
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 {
|
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(
|
remote_drive drive(
|
||||||
config,
|
config,
|
||||||
[&config]() -> std::unique_ptr<remote_instance> {
|
[&config]() -> std::unique_ptr<remote_instance> {
|
||||||
return std::unique_ptr<remote_instance>(
|
return std::unique_ptr<remote_instance>(new remote_client(config));
|
||||||
new remote_client(config));
|
|
||||||
},
|
},
|
||||||
lock);
|
lock);
|
||||||
if (not lock.set_mount_state(true, "", -1)) {
|
if (not lock.set_mount_state(true, "", -1)) {
|
||||||
std::cerr << "failed to set mount state" << std::endl;
|
std::cerr << "failed to set mount state" << std::endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
global_lock.release();
|
||||||
mount_result = drive.mount(drive_args);
|
mount_result = drive.mount(drive_args);
|
||||||
ret = exit_code::mount_result;
|
return exit_code::mount_result;
|
||||||
} catch (const std::exception &e) {
|
} catch (const std::exception &e) {
|
||||||
std::cerr << "FATAL: " << e.what() << std::endl;
|
std::cerr << "FATAL: " << e.what() << std::endl;
|
||||||
ret = exit_code::startup_exception;
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
std::cerr << "FATAL: Unable to get available port" << std::endl;
|
return exit_code::startup_exception;
|
||||||
ret = exit_code::startup_exception;
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
if (prov == provider_type::sia &&
|
try {
|
||||||
config.get_sia_config().bucket.empty()) {
|
if (prov == provider_type::sia && config.get_sia_config().bucket.empty()) {
|
||||||
[[maybe_unused]] auto bucket =
|
[[maybe_unused]] auto bucket =
|
||||||
config.set_value_by_name("SiaConfig.Bucket", unique_id);
|
config.set_value_by_name("SiaConfig.Bucket", unique_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
auto provider = create_provider(prov, config);
|
auto provider = create_provider(prov, config);
|
||||||
repertory_drive drive(config, lock, *provider);
|
repertory_drive drive(config, lock, *provider);
|
||||||
if (not lock.set_mount_state(true, "", -1)) {
|
if (not lock.set_mount_state(true, "", -1)) {
|
||||||
std::cerr << "failed to set mount state" << std::endl;
|
std::cerr << "failed to set mount state" << std::endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
global_lock.release();
|
||||||
mount_result = drive.mount(drive_args);
|
mount_result = drive.mount(drive_args);
|
||||||
ret = exit_code::mount_result;
|
return exit_code::mount_result;
|
||||||
} catch (const std::exception &e) {
|
} catch (const std::exception &e) {
|
||||||
std::cerr << "FATAL: " << e.what() << std::endl;
|
std::cerr << "FATAL: " << e.what() << std::endl;
|
||||||
ret = exit_code::startup_exception;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ret = exit_code::lock_failed;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret;
|
return exit_code::startup_exception;
|
||||||
}
|
}
|
||||||
} // namespace repertory::cli::actions
|
} // namespace repertory::cli::actions
|
||||||
|
|
||||||
|
@@ -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;
|
||||||
};
|
};
|
||||||
|
@@ -23,19 +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/path.hpp"
|
#include "utils/path.hpp"
|
||||||
#include "utils/string.hpp"
|
#include "utils/string.hpp"
|
||||||
|
|
||||||
|
#include <boost/process.hpp>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
[[nodiscard]] constexpr auto is_restricted(std::string_view data) -> bool {
|
[[nodiscard]] auto decrypt(std::string_view data, std::string_view password)
|
||||||
constexpr std::string_view invalid_chars = "&;|><$()`{}!*?";
|
-> std::string {
|
||||||
return data.find_first_of(invalid_chars) != std::string_view::npos;
|
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());
|
||||||
|
|
||||||
|
auto key = repertory::utils::encryption::create_hash_blake2b_256(password);
|
||||||
|
|
||||||
|
unsigned long long size{};
|
||||||
|
auto res = crypto_aead_xchacha20poly1305_ietf_decrypt(
|
||||||
|
reinterpret_cast<unsigned char *>(buffer.data()), &size, nullptr,
|
||||||
|
reinterpret_cast<const unsigned char *>(
|
||||||
|
&decoded.at(crypto_aead_xchacha20poly1305_IETF_NPUBBYTES)),
|
||||||
|
decoded.size() - crypto_aead_xchacha20poly1305_IETF_NPUBBYTES,
|
||||||
|
reinterpret_cast<const unsigned char *>(REPERTORY.data()),
|
||||||
|
REPERTORY.length(),
|
||||||
|
reinterpret_cast<const unsigned char *>(decoded.data()),
|
||||||
|
reinterpret_cast<const unsigned char *>(key.data()));
|
||||||
|
if (res != 0) {
|
||||||
|
throw repertory::utils::error::create_exception(function_name,
|
||||||
|
{"decryption failed"});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
buffer.begin(),
|
||||||
|
std::next(buffer.begin(), static_cast<std::int64_t>(size)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] auto decrypt_value(const repertory::ui::mgmt_app_config *config,
|
||||||
|
std::string_view key, std::string_view value,
|
||||||
|
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
|
||||||
|
|
||||||
@@ -50,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;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -83,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",
|
||||||
@@ -97,13 +187,14 @@ 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); });
|
||||||
|
|
||||||
|
server->Get("/api/v1/settings", [this](auto && /* req */, auto &&res) {
|
||||||
handle_get_settings(res);
|
handle_get_settings(res);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -114,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);
|
||||||
});
|
});
|
||||||
@@ -167,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;
|
||||||
@@ -174,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 {
|
||||||
@@ -195,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"));
|
||||||
@@ -206,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, {
|
||||||
@@ -218,11 +376,13 @@ void handlers::handle_get_mount(auto &&req, auto &&res) const {
|
|||||||
lines.erase(lines.begin());
|
lines.erase(lines.begin());
|
||||||
|
|
||||||
auto result = nlohmann::json::parse(utils::string::join(lines, '\n'));
|
auto result = nlohmann::json::parse(utils::string::join(lines, '\n'));
|
||||||
|
clean_json_config(prov, result);
|
||||||
|
|
||||||
res.set_content(result.dump(), "application/json");
|
res.set_content(result.dump(), "application/json");
|
||||||
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;
|
||||||
@@ -251,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"));
|
||||||
|
|
||||||
@@ -269,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"));
|
||||||
|
|
||||||
@@ -280,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>()) {
|
||||||
@@ -318,14 +454,27 @@ 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.erase(JSON_MOUNT_LOCATIONS);
|
settings.erase(JSON_MOUNT_LOCATIONS);
|
||||||
res.set_content(settings.dump(), "application/json");
|
res.set_content(settings.dump(), "application/json");
|
||||||
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)) {
|
||||||
@@ -335,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;
|
||||||
}
|
}
|
||||||
} else {
|
values[sub_key] = decrypted;
|
||||||
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"));
|
||||||
|
|
||||||
@@ -363,25 +532,32 @@ 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"));
|
||||||
|
|
||||||
@@ -393,16 +569,25 @@ 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 value = req.get_param_value("value");
|
auto value = req.get_param_value("value");
|
||||||
|
|
||||||
|
auto skip{false};
|
||||||
|
value = decrypt_value(config_, key, value, skip);
|
||||||
|
if (not skip) {
|
||||||
set_key_value(prov, name, key, value);
|
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)) {
|
||||||
config_->set_api_password(data.at(JSON_API_PASSWORD).get<std::string>());
|
auto password = decrypt(data.at(JSON_API_PASSWORD).get<std::string>(),
|
||||||
|
config_->get_api_password());
|
||||||
|
if (not password.empty()) {
|
||||||
|
config_->set_api_password(password);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.contains(JSON_API_PORT)) {
|
if (data.contains(JSON_API_PORT)) {
|
||||||
@@ -418,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:
|
||||||
@@ -457,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))];
|
||||||
@@ -467,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");
|
||||||
|
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)
|
#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
|
||||||
|
@@ -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;
|
||||||
|
@@ -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},
|
||||||
|
198
repertory/repertory_test/src/clean_json_test.cpp
Normal file
198
repertory/repertory_test/src/clean_json_test.cpp
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
/*
|
||||||
|
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 "test_common.hpp"
|
||||||
|
|
||||||
|
#include "app_config.hpp"
|
||||||
|
#include "types/repertory.hpp"
|
||||||
|
|
||||||
|
namespace repertory {
|
||||||
|
TEST(clean_json_test, can_clean_values) {
|
||||||
|
auto result = clean_json_value(JSON_API_PASSWORD, "moose");
|
||||||
|
EXPECT_TRUE(result.empty());
|
||||||
|
|
||||||
|
result = clean_json_value(
|
||||||
|
fmt::format("{}.{}", JSON_ENCRYPT_CONFIG, JSON_ENCRYPTION_TOKEN),
|
||||||
|
"moose");
|
||||||
|
EXPECT_TRUE(result.empty());
|
||||||
|
|
||||||
|
result = clean_json_value(
|
||||||
|
fmt::format("{}.{}", JSON_HOST_CONFIG, JSON_API_PASSWORD), "moose");
|
||||||
|
EXPECT_TRUE(result.empty());
|
||||||
|
|
||||||
|
result = clean_json_value(
|
||||||
|
fmt::format("{}.{}", JSON_REMOTE_CONFIG, JSON_ENCRYPTION_TOKEN), "moose");
|
||||||
|
EXPECT_TRUE(result.empty());
|
||||||
|
|
||||||
|
result = clean_json_value(
|
||||||
|
fmt::format("{}.{}", JSON_REMOTE_MOUNT, JSON_ENCRYPTION_TOKEN), "moose");
|
||||||
|
EXPECT_TRUE(result.empty());
|
||||||
|
|
||||||
|
result = clean_json_value(
|
||||||
|
fmt::format("{}.{}", JSON_S3_CONFIG, JSON_ENCRYPTION_TOKEN), "moose");
|
||||||
|
EXPECT_TRUE(result.empty());
|
||||||
|
|
||||||
|
result = clean_json_value(
|
||||||
|
fmt::format("{}.{}", JSON_S3_CONFIG, JSON_SECRET_KEY), "moose");
|
||||||
|
EXPECT_TRUE(result.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(clean_json_test, can_clean_encrypt_config) {
|
||||||
|
auto dir =
|
||||||
|
utils::path::combine(test::get_test_output_dir(), {
|
||||||
|
"clean_json_test",
|
||||||
|
"encrypt",
|
||||||
|
});
|
||||||
|
app_config cfg(provider_type::encrypt, dir);
|
||||||
|
cfg.set_value_by_name(JSON_API_PASSWORD, "moose");
|
||||||
|
cfg.set_value_by_name(
|
||||||
|
fmt::format("{}.{}", JSON_ENCRYPT_CONFIG, JSON_ENCRYPTION_TOKEN),
|
||||||
|
"moose");
|
||||||
|
cfg.set_value_by_name(
|
||||||
|
fmt::format("{}.{}", JSON_REMOTE_MOUNT, JSON_ENCRYPTION_TOKEN), "moose");
|
||||||
|
|
||||||
|
auto data = cfg.get_json();
|
||||||
|
EXPECT_FALSE(data.at(JSON_API_PASSWORD).get<std::string>().empty());
|
||||||
|
EXPECT_FALSE(data.at(JSON_ENCRYPT_CONFIG)
|
||||||
|
.at(JSON_ENCRYPTION_TOKEN)
|
||||||
|
.get<std::string>()
|
||||||
|
.empty());
|
||||||
|
EXPECT_FALSE(data.at(JSON_REMOTE_MOUNT)
|
||||||
|
.at(JSON_ENCRYPTION_TOKEN)
|
||||||
|
.get<std::string>()
|
||||||
|
.empty());
|
||||||
|
|
||||||
|
clean_json_config(cfg.get_provider_type(), data);
|
||||||
|
EXPECT_TRUE(data.at(JSON_API_PASSWORD).get<std::string>().empty());
|
||||||
|
EXPECT_TRUE(data.at(JSON_ENCRYPT_CONFIG)
|
||||||
|
.at(JSON_ENCRYPTION_TOKEN)
|
||||||
|
.get<std::string>()
|
||||||
|
.empty());
|
||||||
|
EXPECT_TRUE(data.at(JSON_REMOTE_MOUNT)
|
||||||
|
.at(JSON_ENCRYPTION_TOKEN)
|
||||||
|
.get<std::string>()
|
||||||
|
.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(clean_json_test, can_clean_remote_config) {
|
||||||
|
auto dir =
|
||||||
|
utils::path::combine(test::get_test_output_dir(), {
|
||||||
|
"clean_json_test",
|
||||||
|
"remote",
|
||||||
|
});
|
||||||
|
app_config cfg(provider_type::remote, dir);
|
||||||
|
cfg.set_value_by_name(JSON_API_PASSWORD, "moose");
|
||||||
|
cfg.set_value_by_name(
|
||||||
|
fmt::format("{}.{}", JSON_REMOTE_CONFIG, JSON_ENCRYPTION_TOKEN), "moose");
|
||||||
|
|
||||||
|
auto data = cfg.get_json();
|
||||||
|
EXPECT_FALSE(data.at(JSON_API_PASSWORD).get<std::string>().empty());
|
||||||
|
EXPECT_FALSE(data.at(JSON_REMOTE_CONFIG)
|
||||||
|
.at(JSON_ENCRYPTION_TOKEN)
|
||||||
|
.get<std::string>()
|
||||||
|
.empty());
|
||||||
|
|
||||||
|
clean_json_config(cfg.get_provider_type(), data);
|
||||||
|
EXPECT_TRUE(data.at(JSON_API_PASSWORD).get<std::string>().empty());
|
||||||
|
EXPECT_TRUE(data.at(JSON_REMOTE_CONFIG)
|
||||||
|
.at(JSON_ENCRYPTION_TOKEN)
|
||||||
|
.get<std::string>()
|
||||||
|
.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(clean_json_test, can_clean_s3_config) {
|
||||||
|
auto dir =
|
||||||
|
utils::path::combine(test::get_test_output_dir(), {
|
||||||
|
"clean_json_test",
|
||||||
|
"s3",
|
||||||
|
});
|
||||||
|
app_config cfg(provider_type::s3, dir);
|
||||||
|
cfg.set_value_by_name(JSON_API_PASSWORD, "moose");
|
||||||
|
cfg.set_value_by_name(
|
||||||
|
fmt::format("{}.{}", JSON_REMOTE_MOUNT, JSON_ENCRYPTION_TOKEN), "moose");
|
||||||
|
cfg.set_value_by_name(
|
||||||
|
fmt::format("{}.{}", JSON_S3_CONFIG, JSON_ENCRYPTION_TOKEN), "moose");
|
||||||
|
cfg.set_value_by_name(fmt::format("{}.{}", JSON_S3_CONFIG, JSON_SECRET_KEY),
|
||||||
|
"moose");
|
||||||
|
|
||||||
|
auto data = cfg.get_json();
|
||||||
|
EXPECT_FALSE(data.at(JSON_API_PASSWORD).get<std::string>().empty());
|
||||||
|
EXPECT_FALSE(data.at(JSON_REMOTE_MOUNT)
|
||||||
|
.at(JSON_ENCRYPTION_TOKEN)
|
||||||
|
.get<std::string>()
|
||||||
|
.empty());
|
||||||
|
EXPECT_FALSE(data.at(JSON_S3_CONFIG)
|
||||||
|
.at(JSON_ENCRYPTION_TOKEN)
|
||||||
|
.get<std::string>()
|
||||||
|
.empty());
|
||||||
|
EXPECT_FALSE(
|
||||||
|
data.at(JSON_S3_CONFIG).at(JSON_SECRET_KEY).get<std::string>().empty());
|
||||||
|
|
||||||
|
clean_json_config(cfg.get_provider_type(), data);
|
||||||
|
EXPECT_TRUE(data.at(JSON_API_PASSWORD).get<std::string>().empty());
|
||||||
|
EXPECT_TRUE(data.at(JSON_REMOTE_MOUNT)
|
||||||
|
.at(JSON_ENCRYPTION_TOKEN)
|
||||||
|
.get<std::string>()
|
||||||
|
.empty());
|
||||||
|
EXPECT_TRUE(data.at(JSON_S3_CONFIG)
|
||||||
|
.at(JSON_ENCRYPTION_TOKEN)
|
||||||
|
.get<std::string>()
|
||||||
|
.empty());
|
||||||
|
EXPECT_TRUE(
|
||||||
|
data.at(JSON_S3_CONFIG).at(JSON_SECRET_KEY).get<std::string>().empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(clean_json_test, can_clean_sia_config) {
|
||||||
|
auto dir =
|
||||||
|
utils::path::combine(test::get_test_output_dir(), {
|
||||||
|
"clean_json_test",
|
||||||
|
"sia",
|
||||||
|
});
|
||||||
|
app_config cfg(provider_type::sia, dir);
|
||||||
|
cfg.set_value_by_name(JSON_API_PASSWORD, "moose");
|
||||||
|
cfg.set_value_by_name(
|
||||||
|
fmt::format("{}.{}", JSON_HOST_CONFIG, JSON_API_PASSWORD), "moose");
|
||||||
|
cfg.set_value_by_name(
|
||||||
|
fmt::format("{}.{}", JSON_REMOTE_MOUNT, JSON_ENCRYPTION_TOKEN), "moose");
|
||||||
|
|
||||||
|
auto data = cfg.get_json();
|
||||||
|
EXPECT_FALSE(data.at(JSON_API_PASSWORD).get<std::string>().empty());
|
||||||
|
EXPECT_FALSE(data.at(JSON_HOST_CONFIG)
|
||||||
|
.at(JSON_API_PASSWORD)
|
||||||
|
.get<std::string>()
|
||||||
|
.empty());
|
||||||
|
EXPECT_FALSE(data.at(JSON_REMOTE_MOUNT)
|
||||||
|
.at(JSON_ENCRYPTION_TOKEN)
|
||||||
|
.get<std::string>()
|
||||||
|
.empty());
|
||||||
|
|
||||||
|
clean_json_config(cfg.get_provider_type(), data);
|
||||||
|
EXPECT_TRUE(data.at(JSON_API_PASSWORD).get<std::string>().empty());
|
||||||
|
EXPECT_TRUE(data.at(JSON_HOST_CONFIG)
|
||||||
|
.at(JSON_API_PASSWORD)
|
||||||
|
.get<std::string>()
|
||||||
|
.empty());
|
||||||
|
EXPECT_TRUE(data.at(JSON_REMOTE_MOUNT)
|
||||||
|
.at(JSON_ENCRYPTION_TOKEN)
|
||||||
|
.get<std::string>()
|
||||||
|
.empty());
|
||||||
|
}
|
||||||
|
} // namespace repertory
|
@@ -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
|
||||||
|
@@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -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) {
|
|
||||||
|
std::uint32_t check_port{first_port};
|
||||||
|
while (check_port <= 65535U) {
|
||||||
|
{
|
||||||
|
io_context ctx{};
|
||||||
|
tcp::socket socket(ctx);
|
||||||
|
socket.connect(
|
||||||
|
{
|
||||||
|
tcp::endpoint(ip::address_v4::loopback(),
|
||||||
|
static_cast<std::uint16_t>(check_port)),
|
||||||
|
},
|
||||||
|
error_code);
|
||||||
|
if (not error_code) {
|
||||||
|
++check_port;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
io_context ctx{};
|
io_context ctx{};
|
||||||
tcp::acceptor acceptor(ctx);
|
tcp::acceptor acceptor(ctx);
|
||||||
acceptor.open(tcp::v4(), error_code) ||
|
acceptor.open(tcp::v4(), error_code);
|
||||||
acceptor.bind({tcp::v4(), first_port}, error_code);
|
if (error_code) {
|
||||||
if (not error_code) {
|
++check_port;
|
||||||
break;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
++first_port;
|
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)
|
||||||
|
|
||||||
|
@@ -1,8 +1,11 @@
|
|||||||
import 'package:flutter/material.dart' show GlobalKey, NavigatorState;
|
import 'package:flutter/material.dart' show GlobalKey, NavigatorState;
|
||||||
|
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'];
|
||||||
@@ -12,3 +15,10 @@ const providerTypeList = ['Encrypt', 'Remote', 'S3', 'Sia'];
|
|||||||
const ringBufferSizeList = ['128', '256', '512', '1024', '2048'];
|
const ringBufferSizeList = ['128', '256', '512', '1024', '2048'];
|
||||||
|
|
||||||
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||||
|
|
||||||
|
Sodium? _sodium;
|
||||||
|
void setSodium(Sodium sodium) {
|
||||||
|
_sodium = sodium;
|
||||||
|
}
|
||||||
|
|
||||||
|
Sodium get sodium => _sodium!;
|
||||||
|
@@ -1,10 +1,21 @@
|
|||||||
|
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: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 [
|
||||||
@@ -96,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) {
|
||||||
@@ -223,19 +251,152 @@ bool validateSettings(
|
|||||||
return failed.isEmpty;
|
return failed.isEmpty;
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> convertAllToString(Map<String, dynamic> settings) {
|
Future<Map<String, dynamic>> convertAllToString(
|
||||||
settings.forEach((key, value) {
|
Map<String, dynamic> settings,
|
||||||
if (value is Map<String, dynamic>) {
|
SecureKey key,
|
||||||
convertAllToString(value);
|
) async {
|
||||||
return;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value is String) {
|
if (entry.key == 'ApiPassword' ||
|
||||||
return;
|
entry.key == 'EncryptionToken' ||
|
||||||
|
entry.key == 'SecretKey') {
|
||||||
|
if (entry.value.isEmpty) {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
settings[key] = value.toString();
|
settings[entry.key] = encryptValue(entry.value, key);
|
||||||
});
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.value is String) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
settings[entry.key] = entry.value.toString();
|
||||||
|
}
|
||||||
|
|
||||||
return settings;
|
return settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
return convert(settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
String encryptValue(String value, SecureKey key) {
|
||||||
|
if (value.isEmpty) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
final sodium = constants.sodium;
|
||||||
|
final crypto = sodium.crypto.aeadXChaCha20Poly1305IETF;
|
||||||
|
|
||||||
|
final nonce = sodium.secureRandom(crypto.nonceBytes).extractBytes();
|
||||||
|
final data = crypto.encrypt(
|
||||||
|
additionalData: Uint8List.fromList('repertory'.toCharArray()),
|
||||||
|
key: key,
|
||||||
|
message: Uint8List.fromList(value.toCharArray()),
|
||||||
|
nonce: nonce,
|
||||||
|
);
|
||||||
|
|
||||||
|
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),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@@ -2,16 +2,32 @@ 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';
|
||||||
|
import 'package:sodium_libs/sodium_libs.dart' show SodiumInit;
|
||||||
|
|
||||||
void main() {
|
void main() async {
|
||||||
|
try {
|
||||||
|
constants.setSodium(await SodiumInit.init());
|
||||||
|
} catch (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(),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,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') {
|
||||||
@@ -63,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(
|
||||||
|
child: EditMountScreen(
|
||||||
mount: mount,
|
mount: mount,
|
||||||
title:
|
title:
|
||||||
'${mount.provider} [${formatMountName(mount.type, mount.name)}] Settings',
|
'${mount.provider} [${formatMountName(mount.type, mount.name)}] Settings',
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -74,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;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
64
web/repertory/lib/models/auth.dart
Normal file
64
web/repertory/lib/models/auth.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
@@ -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;
|
||||||
|
@@ -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;
|
||||||
|
|
||||||
|
MountList(this._auth) {
|
||||||
|
_auth.mountList = this;
|
||||||
|
_auth.addListener(() {
|
||||||
|
if (_auth.authenticated) {
|
||||||
_fetch();
|
_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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -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,20 +36,21 @@ 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>(
|
||||||
|
builder: (context, auth, _) {
|
||||||
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.max,
|
mainAxisSize: MainAxisSize.max,
|
||||||
@@ -68,7 +69,9 @@ class _AddMountScreenState extends State<AddMountScreen> {
|
|||||||
DropdownButton<String>(
|
DropdownButton<String>(
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
value: _mountType,
|
value: _mountType,
|
||||||
onChanged: (mountType) => _handleChange(mountType ?? ''),
|
onChanged:
|
||||||
|
(mountType) =>
|
||||||
|
_handleChange(auth, mountType ?? ''),
|
||||||
items:
|
items:
|
||||||
constants.providerTypeList
|
constants.providerTypeList
|
||||||
.map<DropdownMenuItem<String>>((item) {
|
.map<DropdownMenuItem<String>>((item) {
|
||||||
@@ -104,7 +107,7 @@ class _AddMountScreenState extends State<AddMountScreen> {
|
|||||||
inputFormatters: [
|
inputFormatters: [
|
||||||
FilteringTextInputFormatter.deny(RegExp(r'\s')),
|
FilteringTextInputFormatter.deny(RegExp(r'\s')),
|
||||||
],
|
],
|
||||||
onChanged: (_) => _handleChange(_mountType),
|
onChanged: (_) => _handleChange(auth, _mountType),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -121,21 +124,26 @@ class _AddMountScreenState extends State<AddMountScreen> {
|
|||||||
isAdd: true,
|
isAdd: true,
|
||||||
mount: _mount!,
|
mount: _mount!,
|
||||||
settings: _settings[_mountType]!,
|
settings: _settings[_mountType]!,
|
||||||
showAdvanced: _showAdvanced,
|
showAdvanced: false,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (_mount != null) const SizedBox(height: constants.padding),
|
if (_mount != null) const SizedBox(height: constants.padding),
|
||||||
if (_mount != null)
|
if (_mount != null)
|
||||||
Builder(
|
Row(
|
||||||
builder: (context) {
|
children: [
|
||||||
return ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
|
label: const Text('Add'),
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final mountList = Provider.of<MountList>(context);
|
final mountList = Provider.of<MountList>(context);
|
||||||
|
|
||||||
List<String> failed = [];
|
List<String> failed = [];
|
||||||
if (!validateSettings(_settings[_mountType]!, failed)) {
|
if (!validateSettings(
|
||||||
|
_settings[_mountType]!,
|
||||||
|
failed,
|
||||||
|
)) {
|
||||||
for (var key in failed) {
|
for (var key in failed) {
|
||||||
displayErrorMessage(
|
displayErrorMessage(
|
||||||
context,
|
context,
|
||||||
@@ -145,7 +153,9 @@ class _AddMountScreenState extends State<AddMountScreen> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mountList.hasConfigName(_mountNameController.text)) {
|
if (mountList.hasConfigName(
|
||||||
|
_mountNameController.text,
|
||||||
|
)) {
|
||||||
return displayErrorMessage(
|
return displayErrorMessage(
|
||||||
context,
|
context,
|
||||||
"Configuration name '${_mountNameController.text}' already exists",
|
"Configuration name '${_mountNameController.text}' already exists",
|
||||||
@@ -164,7 +174,7 @@ class _AddMountScreenState extends State<AddMountScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await mountList.add(
|
final success = await mountList.add(
|
||||||
_mountType,
|
_mountType,
|
||||||
_mountType == 'Remote'
|
_mountType == 'Remote'
|
||||||
? '${_settings[_mountType]!['RemoteConfig']['HostNameOrIp']}_${_settings[_mountType]!['RemoteConfig']['ApiPort']}'
|
? '${_settings[_mountType]!['RemoteConfig']['HostNameOrIp']}_${_settings[_mountType]!['RemoteConfig']['ApiPort']}'
|
||||||
@@ -172,23 +182,32 @@ class _AddMountScreenState extends State<AddMountScreen> {
|
|||||||
_settings[_mountType]!,
|
_settings[_mountType]!,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!context.mounted) {
|
if (!success || !context.mounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
},
|
},
|
||||||
label: const Text('Add'),
|
),
|
||||||
icon: const Icon(Icons.add),
|
if (_mountType == 'Sia' || _mountType == 'S3') ...[
|
||||||
);
|
const SizedBox(width: constants.padding),
|
||||||
},
|
ElevatedButton.icon(
|
||||||
|
label: const Text('Test'),
|
||||||
|
icon: const Icon(Icons.check),
|
||||||
|
onPressed: () async {},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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],
|
||||||
|
116
web/repertory/lib/screens/auth_screen.dart
Normal file
116
web/repertory/lib/screens/auth_screen.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@@ -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';
|
||||||
|
|
||||||
@@ -23,12 +25,27 @@ class _EditMountScreenState extends State<EditMountScreen> {
|
|||||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||||
title: Text(widget.title),
|
title: Text(widget.title),
|
||||||
actions: [
|
actions: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
const Text("Advanced"),
|
const Text("Advanced"),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(_showAdvanced ? Icons.toggle_on : Icons.toggle_off),
|
icon: Icon(
|
||||||
onPressed: () => setState(() => _showAdvanced = !_showAdvanced),
|
_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(),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@@ -1,8 +1,10 @@
|
|||||||
import 'dart:convert' show jsonDecode;
|
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) {
|
||||||
@@ -28,6 +40,7 @@ class _EditSettingsScreenState extends State<EditSettingsScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return UISettingsWidget(
|
return UISettingsWidget(
|
||||||
|
origSettings: jsonDecode(jsonEncode(snapshot.requireData)),
|
||||||
settings: snapshot.requireData,
|
settings: snapshot.requireData,
|
||||||
showAdvanced: false,
|
showAdvanced: false,
|
||||||
);
|
);
|
||||||
@@ -40,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 {};
|
||||||
}
|
}
|
||||||
@@ -56,7 +76,6 @@ class _EditSettingsScreenState extends State<EditSettingsScreen> {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
// UISettingsWidget(settings: {}, showAdvanced: false),
|
|
||||||
@override
|
@override
|
||||||
void setState(VoidCallback fn) {
|
void setState(VoidCallback fn) {
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
|
@@ -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),
|
||||||
|
@@ -161,9 +161,13 @@ 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 StatefulBuilder(
|
||||||
|
builder: (context, setDialogState) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
@@ -200,21 +204,59 @@ void createPasswordSetting(
|
|||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
TextField(
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
controller: TextEditingController(text: updatedValue1),
|
controller: TextEditingController(
|
||||||
obscureText: true,
|
text: updatedValue1,
|
||||||
|
),
|
||||||
|
obscureText: hidePassword1,
|
||||||
obscuringCharacter: '*',
|
obscuringCharacter: '*',
|
||||||
onChanged: (value) => updatedValue1 = value,
|
onChanged: (value) => updatedValue1 = value,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed:
|
||||||
|
() => setDialogState(
|
||||||
|
() => hidePassword1 = !hidePassword1,
|
||||||
|
),
|
||||||
|
icon: Icon(
|
||||||
|
hidePassword1
|
||||||
|
? Icons.visibility
|
||||||
|
: Icons.visibility_off,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
const SizedBox(height: constants.padding),
|
const SizedBox(height: constants.padding),
|
||||||
TextField(
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
autofocus: false,
|
autofocus: false,
|
||||||
controller: TextEditingController(text: updatedValue2),
|
controller: TextEditingController(
|
||||||
obscureText: true,
|
text: updatedValue2,
|
||||||
|
),
|
||||||
|
obscureText: hidePassword2,
|
||||||
obscuringCharacter: '*',
|
obscuringCharacter: '*',
|
||||||
onChanged: (value) => updatedValue2 = value,
|
onChanged: (value) => updatedValue2 = value,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed:
|
||||||
|
() => setDialogState(
|
||||||
|
() => hidePassword2 = !hidePassword2,
|
||||||
|
),
|
||||||
|
icon: Icon(
|
||||||
|
hidePassword2
|
||||||
|
? Icons.visibility
|
||||||
|
: Icons.visibility_off,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
title: createSettingTitle(context, key, description),
|
title: createSettingTitle(context, key, description),
|
||||||
@@ -222,6 +264,8 @@ void createPasswordSetting(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -17,6 +17,7 @@ class MountWidget extends StatefulWidget {
|
|||||||
|
|
||||||
class _MountWidgetState extends State<MountWidget> {
|
class _MountWidgetState extends State<MountWidget> {
|
||||||
bool _enabled = true;
|
bool _enabled = true;
|
||||||
|
bool _editEnabled = true;
|
||||||
Timer? _timer;
|
Timer? _timer;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -61,7 +62,33 @@ class _MountWidgetState extends State<MountWidget> {
|
|||||||
mount.provider,
|
mount.provider,
|
||||||
style: TextStyle(color: textColor, fontWeight: FontWeight.bold),
|
style: TextStyle(color: textColor, fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
trailing: IconButton(
|
trailing: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (mount.mounted != null && !mount.mounted!)
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.edit),
|
||||||
|
color: subTextColor,
|
||||||
|
tooltip: 'Edit mount location',
|
||||||
|
onPressed: () async {
|
||||||
|
setState(() => _editEnabled = false);
|
||||||
|
final available = await mount.getAvailableLocations();
|
||||||
|
if (context.mounted) {
|
||||||
|
final location = await editMountLocation(
|
||||||
|
context,
|
||||||
|
available,
|
||||||
|
location: mount.path,
|
||||||
|
);
|
||||||
|
if (location != null) {
|
||||||
|
await mount.setMountLocation(location);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setState(() => _editEnabled = true);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
mount.mounted == null
|
mount.mounted == null
|
||||||
? Icons.hourglass_top
|
? Icons.hourglass_top
|
||||||
@@ -73,8 +100,16 @@ class _MountWidgetState extends State<MountWidget> {
|
|||||||
? Color.fromARGB(255, 163, 96, 76)
|
? Color.fromARGB(255, 163, 96, 76)
|
||||||
: subTextColor,
|
: subTextColor,
|
||||||
),
|
),
|
||||||
|
tooltip:
|
||||||
|
mount.mounted == null
|
||||||
|
? ''
|
||||||
|
: mount.mounted!
|
||||||
|
? 'Unmount'
|
||||||
|
: 'Mount',
|
||||||
onPressed: _createMountHandler(context, mount),
|
onPressed: _createMountHandler(context, mount),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -149,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
|
||||||
|
@@ -1,23 +1,29 @@
|
|||||||
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';
|
||||||
|
|
||||||
class UISettingsWidget extends StatefulWidget {
|
class UISettingsWidget extends StatefulWidget {
|
||||||
final bool showAdvanced;
|
final bool showAdvanced;
|
||||||
final Map<String, dynamic> settings;
|
final Map<String, dynamic> settings;
|
||||||
|
final Map<String, dynamic> origSettings;
|
||||||
const UISettingsWidget({
|
const UISettingsWidget({
|
||||||
super.key,
|
super.key,
|
||||||
|
required this.origSettings,
|
||||||
required this.settings,
|
required this.settings,
|
||||||
required this.showAdvanced,
|
required this.showAdvanced,
|
||||||
});
|
});
|
||||||
@@ -27,8 +33,6 @@ class UISettingsWidget extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _UISettingsWidgetState extends State<UISettingsWidget> {
|
class _UISettingsWidgetState extends State<UISettingsWidget> {
|
||||||
late Map<String, dynamic> _origSettings;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
List<SettingsTile> commonSettings = [];
|
List<SettingsTile> commonSettings = [];
|
||||||
@@ -100,16 +104,38 @@ class _UISettingsWidgetState extends State<UISettingsWidget> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
if (!DeepCollectionEquality().equals(widget.settings, _origSettings)) {
|
final settings = getChanged(widget.origSettings, widget.settings);
|
||||||
http
|
if (settings.isNotEmpty) {
|
||||||
.put(
|
final key =
|
||||||
|
Provider.of<Auth>(
|
||||||
|
constants.navigatorKey.currentContext!,
|
||||||
|
listen: false,
|
||||||
|
).key;
|
||||||
|
convertAllToString(settings, key)
|
||||||
|
.then((map) async {
|
||||||
|
try {
|
||||||
|
final authProvider = Provider.of<Auth>(
|
||||||
|
constants.navigatorKey.currentContext!,
|
||||||
|
listen: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
final auth = await authProvider.createAuth();
|
||||||
|
final response = await http.put(
|
||||||
Uri.parse(
|
Uri.parse(
|
||||||
Uri.encodeFull(
|
Uri.encodeFull(
|
||||||
'${getBaseUri()}/api/v1/settings?data=${jsonEncode(convertAllToString(widget.settings))}',
|
'${getBaseUri()}/api/v1/settings?auth=$auth&data=${jsonEncode(map)}',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
);
|
||||||
.then((_) {})
|
|
||||||
|
if (response.statusCode == 401) {
|
||||||
|
displayAuthError(authProvider);
|
||||||
|
authProvider.logoff();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('$e');
|
||||||
|
}
|
||||||
|
})
|
||||||
.catchError((e) {
|
.catchError((e) {
|
||||||
debugPrint('$e');
|
debugPrint('$e');
|
||||||
});
|
});
|
||||||
@@ -118,12 +144,6 @@ class _UISettingsWidgetState extends State<UISettingsWidget> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
_origSettings = jsonDecode(jsonEncode(widget.settings));
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void setState(VoidCallback fn) {
|
void setState(VoidCallback fn) {
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
|
@@ -38,6 +38,8 @@ dependencies:
|
|||||||
http: ^1.3.0
|
http: ^1.3.0
|
||||||
provider: ^6.1.2
|
provider: ^6.1.2
|
||||||
settings_ui: ^2.0.2
|
settings_ui: ^2.0.2
|
||||||
|
sodium_libs: ^3.4.4+1
|
||||||
|
convert: ^3.1.2
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<!--
|
<!--
|
||||||
If you are serving your web app in a path other than the root, change the
|
If you are serving your web app in a path other than the root, change the
|
||||||
href value below to reflect the base path you are serving from.
|
href value below to reflect the base path you are serving from.
|
||||||
@@ -27,17 +27,18 @@
|
|||||||
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
||||||
|
|
||||||
<!-- Favicon -->
|
<!-- Favicon -->
|
||||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
<link rel="icon" type="image/png" href="favicon.png">
|
||||||
|
|
||||||
<title>repertory</title>
|
<title>repertory</title>
|
||||||
<link rel="manifest" href="manifest.json">
|
<link rel="manifest" href="manifest.json">
|
||||||
</head>
|
<script type="text/javascript" src="sodium.js" async="true"></script>
|
||||||
<body>
|
</head>
|
||||||
|
<body>
|
||||||
<script>
|
<script>
|
||||||
window.flutterConfiguration = {
|
window.flutterConfiguration = {
|
||||||
canvasKitBaseUrl: "/canvaskit/"
|
canvasKitBaseUrl: "/canvaskit/"
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<script src="flutter_bootstrap.js" async></script>
|
<script src="flutter_bootstrap.js" async=""></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
1
web/repertory/web/sodium.js
Normal file
1
web/repertory/web/sodium.js
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user