Compare commits
76 Commits
v2.0.5-rc
...
7d1574d042
Author | SHA1 | Date | |
---|---|---|---|
7d1574d042 | |||
43280b2913 | |||
788aefdf86 | |||
3453bd0b50 | |||
96df3168fc | |||
45381c3d0c | |||
4c82b87b07 | |||
c29072d84d | |||
311a2688cb | |||
968633a5b9 | |||
bcda2abfd2 | |||
7d5c252e89 | |||
0aeb69a050 | |||
7d441964e9 | |||
1d78ee88b8 | |||
d54ba8203a | |||
52f0d755ba | |||
343c324050 | |||
e446757f7e | |||
513bc09944 | |||
a79e696315 | |||
26dbda290e | |||
d06b965dc1 | |||
6aee374725 | |||
a7d570d5a0 | |||
c93e6bedcc | |||
93b4237e4f | |||
4013ed692e | |||
99914382f4 | |||
3a468d72ce | |||
0611f0c9dd | |||
5567e665cc | |||
13ebd7b8b0 | |||
c2e9960ad5 | |||
86918595b3 | |||
9f90494efb | |||
b3ee609e09 | |||
7dc2ce8702 | |||
ed5f1f04ad | |||
863e0b2165 | |||
122b4998d6 | |||
722357388c | |||
0e21d93bb3 | |||
e8118bac89 | |||
c4af929d7a | |||
3826afd3bf | |||
4a15e2f827 | |||
48b6012f97 | |||
d37c6598c4 | |||
c95242e016 | |||
6ed4db0737 | |||
1560804df8 | |||
cc3c0febc3 | |||
0458f12e17 | |||
ae43cedb45 | |||
191eb1620b | |||
9c648583fb | |||
14d78d0b65 | |||
54efde0497 | |||
5f593ab86d | |||
feb09746f5 | |||
faad98c11e | |||
12f04c6064 | |||
ca307c3bf2 | |||
ac6f4bcade | |||
9d48cd97e3 | |||
6c6fb0554f | |||
a134f01436 | |||
44c33652fa | |||
131c36415d | |||
9456c8b1d2 | |||
b8c62612d8 | |||
521874a56f | |||
f41ad47262 | |||
8bb2eeb88c | |||
b1aca46034 |
@ -3,7 +3,6 @@ _mkgmtime
|
||||
_sh_denyno
|
||||
_sh_denyrd
|
||||
_sh_denyrw
|
||||
_spawnv
|
||||
aarch64
|
||||
advapi32
|
||||
armv8
|
||||
@ -115,7 +114,6 @@ googletest
|
||||
gpath
|
||||
gtest_version
|
||||
has_setxattr
|
||||
hkey
|
||||
httpapi
|
||||
httplib
|
||||
icudata
|
||||
@ -123,7 +121,6 @@ icui18n
|
||||
icuuc
|
||||
iostreams
|
||||
iphlpapi
|
||||
ipstream
|
||||
jthread
|
||||
libbitcoin
|
||||
libbitcoinsystem
|
||||
@ -145,7 +142,6 @@ libuuid_include_dirs
|
||||
libvlc
|
||||
linkflags
|
||||
localappdata
|
||||
lpbyte
|
||||
lptr
|
||||
lpwstr
|
||||
markdownlint
|
||||
@ -161,7 +157,6 @@ ncrypt
|
||||
nlohmann
|
||||
nlohmann_json
|
||||
nmakeprg
|
||||
nohup
|
||||
nominmax
|
||||
ntstatus
|
||||
nullptr
|
||||
@ -169,7 +164,6 @@ nuspell_version
|
||||
oleaut32
|
||||
openal_version
|
||||
openssldir
|
||||
pistream
|
||||
pkgconfig
|
||||
plarge_integer
|
||||
plex
|
||||
|
4
.gitattributes
vendored
4
.gitattributes
vendored
@ -1,4 +0,0 @@
|
||||
*.tgz filter=lfs diff=lfs merge=lfs -text
|
||||
*.tar.gz filter=lfs diff=lfs merge=lfs -text
|
||||
*.tar.xz filter=lfs diff=lfs merge=lfs -text
|
||||
*.zip filter=lfs diff=lfs merge=lfs -text
|
10
CHANGELOG.md
10
CHANGELOG.md
@ -4,17 +4,13 @@
|
||||
|
||||
### Issues
|
||||
|
||||
* \#39 Create management portal in Flutter
|
||||
* ~~\#12 [Unit Test] Complete all providers unit tests~~
|
||||
* ~~\#21 [Unit Test] Complete WinFSP unit tests~~
|
||||
* ~~\#22 [Unit Test] Complete FUSE unit tests~~
|
||||
|
||||
### Changes from v2.0.4-rc
|
||||
|
||||
* 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`
|
||||
* Require `--name,-na` option for encryption provider
|
||||
|
||||
## v2.0.4-rc
|
||||
|
||||
|
39
README.md
39
README.md
@ -8,8 +8,7 @@ on Windows.
|
||||
1. [Details and Features](#details-and-features)
|
||||
2. [Minimum Requirements](#minimum-requirements)
|
||||
1. [Supported Operating Systems](#supported-operating-systems)
|
||||
3. [GUI](#gui)
|
||||
4. [Usage](#usage)
|
||||
3. [Usage](#usage)
|
||||
1. [Important Options](#important-options)
|
||||
2. [Sia](#sia)
|
||||
* [Sia Initial Configuration](#sia-initial-configuration)
|
||||
@ -19,21 +18,21 @@ on Windows.
|
||||
* [S3 Initial Configuration](#s3-initial-configuration)
|
||||
* [S3 Mounting](#s3-mounting)
|
||||
* [S3 Configuration File](#s3-configuration-file)
|
||||
5. [Data Directories](#data-directories)
|
||||
4. [Data Directories](#data-directories)
|
||||
1. [Linux Directories](#linux-directories)
|
||||
2. [Windows Directories](#windows-directories)
|
||||
6. [Remote Mounting](#remote-mounting)
|
||||
5. [Remote Mounting](#remote-mounting)
|
||||
1. [Server Setup](#server-setup)
|
||||
* [Remote Mount Configuration File Section](#remote-mount-configuration-file-section)
|
||||
2. [Client Setup](#client-setup)
|
||||
* [Client Remote Mounting](#client-remote-mounting)
|
||||
* [Remote Mount Configuration File](#remote-mount-configuration-file)
|
||||
7. [Compiling](#compiling)
|
||||
6. [Compiling](#compiling)
|
||||
1. [Linux Compilation](#linux-compilation)
|
||||
2. [Windows Setup](#windows-compilation)
|
||||
8. [Credits](#credits)
|
||||
9. [Developer Public Key](#developer-public-key)
|
||||
10. [Consult the Wiki for additional information](https://git.fifthgrid.com/BlockStorage/repertory/wiki)
|
||||
7. [Credits](#credits)
|
||||
8. [Developer Public Key](#developer-public-key)
|
||||
9. [Consult the Wiki for additional information](https://git.fifthgrid.com/BlockStorage/repertory/wiki)
|
||||
|
||||
## Details and Features
|
||||
|
||||
@ -58,24 +57,6 @@ Only 64-bit operating systems are supported
|
||||
* Linux `amd64`
|
||||
* Windows 64-bit 10, 11
|
||||
|
||||
## GUI
|
||||
|
||||
As of `v2.0.5-rc`, mounts can be managed using the `Repertory Management Portal`.
|
||||
To launch the portal, execute the following command:
|
||||
|
||||
* `repertory -ui`
|
||||
* The default username is `repertory`
|
||||
* The default password is `repertory`
|
||||
|
||||
After first launch, `ui.json` will be created in the appropriate data directory.
|
||||
See [Data Directories](#data-directories).
|
||||
You should modify this file directly or use the portal to change the default
|
||||
username and password.
|
||||
|
||||
### Screenshot
|
||||
|
||||
<a href="https://ibb.co/fVyJqnbF"><img src="https://i.ibb.co/fVyJqnbF/repertory-portal.png" alt="repertory-portal" border="0"></a>
|
||||
|
||||
## Usage
|
||||
|
||||
### Important Options
|
||||
@ -151,7 +132,7 @@ username and password.
|
||||
|
||||
```json
|
||||
{
|
||||
"ApiPassword": "<random generated rpc password>",
|
||||
"ApiAuth": "<random generated rpc password>",
|
||||
"ApiPort": 10000,
|
||||
"ApiUser": "repertory",
|
||||
"DatabaseType": "rocksdb",
|
||||
@ -239,7 +220,7 @@ username and password.
|
||||
|
||||
```json
|
||||
{
|
||||
"ApiPassword": "<random generated rpc password>",
|
||||
"ApiAuth": "<random generated rpc password>",
|
||||
"ApiPort": 10100,
|
||||
"ApiUser": "repertory",
|
||||
"DatabaseType": "rocksdb",
|
||||
@ -379,7 +360,7 @@ for S3 providers.
|
||||
|
||||
```json
|
||||
{
|
||||
"ApiPassword": "<random generated rpc password>",
|
||||
"ApiAuth": "<random generated rpc password>",
|
||||
"ApiPort": 10010,
|
||||
"ApiUser": "repertory",
|
||||
"EnableDriveEvents": false,
|
||||
|
@ -1,6 +1,6 @@
|
||||
set(BINUTILS_HASH b53606f443ac8f01d1d5fc9c39497f2af322d99e14cea5c0b4b124d630379365)
|
||||
set(BOOST2_HASH 7bd7ddceec1a1dfdcbdb3e609b60d01739c38390a5f956385a12f3122049f0ca)
|
||||
set(BOOST_HASH f55c340aa49763b1925ccf02b2e83f35fdcf634c9d5164a2acb87540173c741d)
|
||||
set(BOOST2_HASH 7bd7ddceec1a1dfdcbdb3e609b60d01739c38390a5f956385a12f3122049f0ca)
|
||||
set(CPP_HTTPLIB_HASH c9b9e0524666e1cd088f0874c57c1ce7c0eaa8552f9f4e15c755d5201fc8c608)
|
||||
set(CURL_HASH 6edc063d1ebaf9cf3b3b46e9fef2f3cd8932694989ecd43d005d6e828426d09f)
|
||||
set(EXPAT_HASH 372b18f6527d162fa9658f1c74d22a37429b82d822f5a1e1fc7e00f6045a06a2)
|
||||
|
@ -31,7 +31,6 @@ RUN apk add \
|
||||
gflags \
|
||||
gflags-dev \
|
||||
git \
|
||||
git-lfs \
|
||||
icu-dev \
|
||||
icu-libs \
|
||||
icu-static \
|
||||
|
@ -31,7 +31,6 @@ RUN apk add \
|
||||
gflags \
|
||||
gflags-dev \
|
||||
git \
|
||||
git-lfs \
|
||||
icu-dev \
|
||||
icu-libs \
|
||||
icu-static \
|
||||
|
@ -18,7 +18,6 @@ RUN apk add \
|
||||
gcc \
|
||||
gettext \
|
||||
git \
|
||||
git-lfs \
|
||||
gmp \
|
||||
gmp-dev \
|
||||
gperf \
|
||||
|
@ -43,7 +43,8 @@ public:
|
||||
[[nodiscard]] static auto default_remote_api_port(const provider_type &prov)
|
||||
-> std::uint16_t;
|
||||
|
||||
[[nodiscard]] static auto default_rpc_port() -> std::uint16_t;
|
||||
[[nodiscard]] static auto default_rpc_port(const provider_type &prov)
|
||||
-> std::uint16_t;
|
||||
|
||||
[[nodiscard]] static auto get_provider_display_name(const provider_type &prov)
|
||||
-> std::string;
|
||||
@ -72,7 +73,7 @@ public:
|
||||
|
||||
private:
|
||||
provider_type prov_;
|
||||
atomic<std::string> api_password_;
|
||||
atomic<std::string> api_auth_;
|
||||
std::atomic<std::uint16_t> api_port_;
|
||||
atomic<std::string> api_user_;
|
||||
std::atomic<bool> config_changed_;
|
||||
@ -122,7 +123,7 @@ private:
|
||||
auto set_value(dest &dst, const source &src) -> bool;
|
||||
|
||||
public:
|
||||
[[nodiscard]] auto get_api_password() const -> std::string;
|
||||
[[nodiscard]] auto get_api_auth() const -> std::string;
|
||||
|
||||
[[nodiscard]] auto get_api_port() const -> std::uint16_t;
|
||||
|
||||
@ -200,7 +201,7 @@ public:
|
||||
|
||||
void save();
|
||||
|
||||
void set_api_password(const std::string &value);
|
||||
void set_api_auth(const std::string &value);
|
||||
|
||||
void set_api_port(std::uint16_t value);
|
||||
|
||||
|
@ -57,7 +57,7 @@ using json = nlohmann::json;
|
||||
inline constexpr const std::string_view REPERTORY = "repertory";
|
||||
inline constexpr const std::wstring_view REPERTORY_W = L"repertory";
|
||||
|
||||
inline constexpr const std::uint64_t REPERTORY_CONFIG_VERSION = 2ULL;
|
||||
inline constexpr const std::uint64_t REPERTORY_CONFIG_VERSION = 1ULL;
|
||||
inline constexpr const std::string_view REPERTORY_DATA_NAME = "repertory2";
|
||||
inline constexpr const std::string_view REPERTORY_MIN_REMOTE_VERSION = "2.0.0";
|
||||
|
||||
|
@ -22,13 +22,6 @@
|
||||
#ifndef 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)
|
||||
#include "platform/win32_platform.hpp"
|
||||
#include "utils/windows.hpp"
|
||||
|
@ -30,45 +30,38 @@ class i_provider;
|
||||
|
||||
class lock_data final {
|
||||
public:
|
||||
lock_data(provider_type prov, std::string_view unique_id);
|
||||
explicit lock_data(const provider_type &pt, std::string unique_id /*= ""*/);
|
||||
|
||||
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:
|
||||
std::string mutex_id_;
|
||||
|
||||
private:
|
||||
int handle_{};
|
||||
int lock_status_{EWOULDBLOCK};
|
||||
const provider_type pt_;
|
||||
const std::string unique_id_;
|
||||
const std::string mutex_id_;
|
||||
int lock_fd_;
|
||||
int lock_status_ = EWOULDBLOCK;
|
||||
|
||||
private:
|
||||
[[nodiscard]] static auto get_state_directory() -> std::string;
|
||||
|
||||
[[nodiscard]] auto get_lock_data_file() const -> std::string;
|
||||
[[nodiscard]] static auto get_lock_data_file() -> std::string;
|
||||
|
||||
[[nodiscard]] auto get_lock_file() const -> std::string;
|
||||
[[nodiscard]] auto get_lock_file() -> std::string;
|
||||
|
||||
private:
|
||||
[[nodiscard]] static auto wait_for_lock(int handle,
|
||||
std::uint8_t retry_count = 30U)
|
||||
-> int;
|
||||
[[nodiscard]] static auto
|
||||
wait_for_lock(int fd, std::uint8_t retry_count = 30u) -> int;
|
||||
|
||||
public:
|
||||
[[nodiscard]] auto get_mount_state(json &mount_state) -> bool;
|
||||
|
||||
[[nodiscard]] auto grab_lock(std::uint8_t retry_count = 30U) -> lock_result;
|
||||
|
||||
void release();
|
||||
[[nodiscard]] auto grab_lock(std::uint8_t retry_count = 30u) -> lock_result;
|
||||
|
||||
[[nodiscard]] auto set_mount_state(bool active,
|
||||
std::string_view mount_location, int pid)
|
||||
-> bool;
|
||||
const std::string &mount_location,
|
||||
int pid) -> bool;
|
||||
};
|
||||
|
||||
[[nodiscard]] auto create_meta_attributes(
|
||||
@ -83,5 +76,5 @@ public:
|
||||
const api_file &file) -> api_error;
|
||||
} // namespace repertory
|
||||
|
||||
#endif // !defined(_WIN32)
|
||||
#endif // _WIN32
|
||||
#endif // REPERTORY_INCLUDE_PLATFORM_UNIXPLATFORM_HPP_
|
||||
|
@ -23,6 +23,7 @@
|
||||
#define REPERTORY_INCLUDE_PLATFORM_WINPLATFORM_HPP_
|
||||
#if defined(_WIN32)
|
||||
|
||||
#include "app_config.hpp"
|
||||
#include "types/repertory.hpp"
|
||||
|
||||
namespace repertory {
|
||||
@ -30,32 +31,43 @@ class i_provider;
|
||||
|
||||
class lock_data final {
|
||||
public:
|
||||
explicit lock_data(provider_type prov, std::string unique_id);
|
||||
lock_data(const lock_data &) = delete;
|
||||
lock_data(lock_data &&) = delete;
|
||||
explicit lock_data(const provider_type &pt, std::string unique_id /*= ""*/)
|
||||
: pt_(pt),
|
||||
unique_id_(std::move(unique_id)),
|
||||
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) {}
|
||||
|
||||
auto operator=(const lock_data &) -> lock_data & = delete;
|
||||
auto operator=(lock_data &&) -> lock_data & = delete;
|
||||
~lock_data() { release(); }
|
||||
|
||||
private:
|
||||
std::string mutex_id_;
|
||||
HANDLE mutex_handle_{INVALID_HANDLE_VALUE};
|
||||
DWORD mutex_state_{WAIT_FAILED};
|
||||
|
||||
[[nodiscard]] auto get_current_mount_state(json &mount_state) -> bool;
|
||||
const provider_type pt_;
|
||||
const std::string unique_id_;
|
||||
const std::string mutex_id_;
|
||||
HANDLE mutex_handle_;
|
||||
DWORD mutex_state_ = WAIT_FAILED;
|
||||
|
||||
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 grab_lock(std::uint8_t retry_count = 30U) -> lock_result;
|
||||
[[nodiscard]] auto get_unique_id() const -> std::string { return unique_id_; }
|
||||
|
||||
[[nodiscard]] auto grab_lock(std::uint8_t retry_count = 30) -> lock_result;
|
||||
|
||||
void release();
|
||||
|
||||
[[nodiscard]] auto set_mount_state(bool active,
|
||||
std::string_view mount_location,
|
||||
std::int64_t pid) -> bool;
|
||||
const std::string &mount_location,
|
||||
const std::int64_t &pid) -> bool;
|
||||
};
|
||||
|
||||
[[nodiscard]] auto create_meta_attributes(
|
||||
|
@ -31,7 +31,7 @@ namespace repertory::rpc {
|
||||
const httplib::Request &req) -> bool {
|
||||
REPERTORY_USES_FUNCTION_NAME();
|
||||
|
||||
if (cfg.get_api_password().empty() || cfg.get_api_user().empty()) {
|
||||
if (cfg.get_api_auth().empty() || cfg.get_api_user().empty()) {
|
||||
utils::error::raise_error(function_name,
|
||||
"authorization user or password is not set");
|
||||
return false;
|
||||
@ -70,7 +70,7 @@ namespace repertory::rpc {
|
||||
auth.erase(auth.begin());
|
||||
|
||||
auto pwd = utils::string::join(auth, ':');
|
||||
if ((user != cfg.get_api_user()) || (pwd != cfg.get_api_password())) {
|
||||
if ((user != cfg.get_api_user()) || (pwd != cfg.get_api_auth())) {
|
||||
utils::error::raise_error(function_name, "authorization failed");
|
||||
return false;
|
||||
}
|
||||
|
@ -23,7 +23,7 @@
|
||||
#define REPERTORY_INCLUDE_TYPES_REPERTORY_HPP_
|
||||
|
||||
namespace repertory {
|
||||
constexpr const auto default_api_password_size{48U};
|
||||
constexpr const auto default_api_auth_size{48U};
|
||||
constexpr const auto default_download_timeout_secs{30U};
|
||||
constexpr const auto default_eviction_delay_mins{1U};
|
||||
constexpr const auto default_high_freq_interval_secs{std::uint16_t{30U}};
|
||||
@ -314,11 +314,6 @@ provider_type_from_string(std::string_view type,
|
||||
|
||||
[[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)
|
||||
struct open_file_data final {
|
||||
PVOID directory_buffer{nullptr};
|
||||
@ -467,6 +462,7 @@ using meta_provider_callback = std::function<void(directory_item &)>;
|
||||
|
||||
inline constexpr const auto JSON_ACCESS_KEY{"AccessKey"};
|
||||
inline constexpr const auto JSON_AGENT_STRING{"AgentString"};
|
||||
inline constexpr const auto JSON_API_AUTH{"ApiAuth"};
|
||||
inline constexpr const auto JSON_API_PARENT{"ApiParent"};
|
||||
inline constexpr const auto JSON_API_PASSWORD{"ApiPassword"};
|
||||
inline constexpr const auto JSON_API_PATH{"ApiPath"};
|
||||
@ -501,7 +497,6 @@ inline constexpr const auto JSON_MAX_UPLOAD_COUNT{"MaxUploadCount"};
|
||||
inline constexpr const auto JSON_MED_FREQ_INTERVAL_SECS{
|
||||
"MedFreqIntervalSeconds"};
|
||||
inline constexpr const auto JSON_META{"Meta"};
|
||||
inline constexpr const auto JSON_MOUNT_LOCATIONS{"MountLocations"};
|
||||
inline constexpr const auto JSON_ONLINE_CHECK_RETRY_SECS{
|
||||
"OnlineCheckRetrySeconds"};
|
||||
inline constexpr const auto JSON_PATH{"Path"};
|
||||
|
@ -66,8 +66,8 @@ void app_config::set_stop_requested() { stop_requested.store(true); }
|
||||
app_config::app_config(const provider_type &prov,
|
||||
std::string_view data_directory)
|
||||
: prov_(prov),
|
||||
api_password_(utils::generate_random_string(default_api_password_size)),
|
||||
api_port_(default_rpc_port()),
|
||||
api_auth_(utils::generate_random_string(default_api_auth_size)),
|
||||
api_port_(default_rpc_port(prov)),
|
||||
api_user_(std::string{REPERTORY}),
|
||||
config_changed_(false),
|
||||
download_timeout_secs_(default_download_timeout_secs),
|
||||
@ -124,7 +124,7 @@ app_config::app_config(const provider_type &prov,
|
||||
}
|
||||
|
||||
value_get_lookup_ = {
|
||||
{JSON_API_PASSWORD, [this]() { return get_api_password(); }},
|
||||
{JSON_API_AUTH, [this]() { return get_api_auth(); }},
|
||||
{JSON_API_PORT, [this]() { return std::to_string(get_api_port()); }},
|
||||
{JSON_API_USER, [this]() { return get_api_user(); }},
|
||||
{JSON_DATABASE_TYPE,
|
||||
@ -253,10 +253,10 @@ app_config::app_config(const provider_type &prov,
|
||||
|
||||
value_set_lookup_ = {
|
||||
{
|
||||
JSON_API_PASSWORD,
|
||||
JSON_API_AUTH,
|
||||
[this](const std::string &value) {
|
||||
set_api_password(value);
|
||||
return get_api_password();
|
||||
set_api_auth(value);
|
||||
return get_api_auth();
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -743,12 +743,20 @@ auto app_config::default_remote_api_port(const provider_type &prov)
|
||||
return PROVIDER_REMOTE_PORTS.at(static_cast<std::size_t>(prov));
|
||||
}
|
||||
|
||||
auto app_config::default_rpc_port() -> std::uint16_t { return 10000U; }
|
||||
|
||||
auto app_config::get_api_password() const -> std::string {
|
||||
return api_password_;
|
||||
auto app_config::default_rpc_port(const provider_type &prov) -> std::uint16_t {
|
||||
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_auth() const -> std::string { return api_auth_; }
|
||||
|
||||
auto app_config::get_api_port() const -> std::uint16_t { return api_port_; }
|
||||
|
||||
auto app_config::get_api_user() const -> std::string { return api_user_; }
|
||||
@ -808,7 +816,7 @@ auto app_config::get_host_config() const -> host_config { return host_config_; }
|
||||
|
||||
auto app_config::get_json() const -> json {
|
||||
json ret = {
|
||||
{JSON_API_PASSWORD, api_password_},
|
||||
{JSON_API_AUTH, api_auth_},
|
||||
{JSON_API_PORT, api_port_},
|
||||
{JSON_API_USER, api_user_},
|
||||
{JSON_DOWNLOAD_TIMEOUT_SECS, download_timeout_secs_},
|
||||
@ -933,18 +941,24 @@ auto app_config::get_preferred_download_type() const -> download_type {
|
||||
auto app_config::get_provider_display_name(const provider_type &prov)
|
||||
-> std::string {
|
||||
static const std::array<std::string,
|
||||
static_cast<std::size_t>(provider_type::unknown) + 1U>
|
||||
static_cast<std::size_t>(provider_type::unknown)>
|
||||
PROVIDER_DISPLAY_NAMES = {
|
||||
"Sia", "Remote", "S3", "Encrypt", "Unknown",
|
||||
"Sia",
|
||||
"Remote",
|
||||
"S3",
|
||||
"Encrypt",
|
||||
};
|
||||
return PROVIDER_DISPLAY_NAMES.at(static_cast<std::size_t>(prov));
|
||||
}
|
||||
|
||||
auto app_config::get_provider_name(const provider_type &prov) -> std::string {
|
||||
static const std::array<std::string,
|
||||
static_cast<std::size_t>(provider_type::unknown) + 1U>
|
||||
static_cast<std::size_t>(provider_type::unknown)>
|
||||
PROVIDER_NAMES = {
|
||||
"sia", "remote", "s3", "encrypt", "unknown",
|
||||
"sia",
|
||||
"remote",
|
||||
"s3",
|
||||
"encrypt",
|
||||
};
|
||||
return PROVIDER_NAMES.at(static_cast<std::size_t>(prov));
|
||||
}
|
||||
@ -1023,7 +1037,7 @@ auto app_config::load() -> bool {
|
||||
auto found{true};
|
||||
auto json_document = json::parse(json_text);
|
||||
|
||||
get_value(json_document, JSON_API_PASSWORD, api_password_, found);
|
||||
get_value(json_document, JSON_API_AUTH, api_auth_, found);
|
||||
get_value(json_document, JSON_API_PORT, api_port_, found);
|
||||
get_value(json_document, JSON_API_USER, api_user_, found);
|
||||
get_value(json_document, JSON_DATABASE_TYPE, db_type_, found);
|
||||
@ -1080,13 +1094,6 @@ auto app_config::load() -> bool {
|
||||
set_value(max_cache_size_bytes_, default_max_cache_size_bytes);
|
||||
}
|
||||
}
|
||||
|
||||
if (version_ == 2U) {
|
||||
if (json_document.contains("ApiAuth")) {
|
||||
api_password_ = json_document.at("ApiAuth").get<std::string>();
|
||||
}
|
||||
}
|
||||
|
||||
found = false;
|
||||
}
|
||||
|
||||
@ -1125,8 +1132,8 @@ void app_config::save() {
|
||||
});
|
||||
}
|
||||
|
||||
void app_config::set_api_password(const std::string &value) {
|
||||
set_value(api_password_, value);
|
||||
void app_config::set_api_auth(const std::string &value) {
|
||||
set_value(api_auth_, value);
|
||||
}
|
||||
|
||||
void app_config::set_api_port(std::uint16_t value) {
|
||||
|
@ -21,8 +21,9 @@
|
||||
*/
|
||||
#if !defined(_WIN32)
|
||||
|
||||
#include "platform/platform.hpp"
|
||||
#include "platform/unix_platform.hpp"
|
||||
|
||||
#include "app_config.hpp"
|
||||
#include "events/event_system.hpp"
|
||||
#include "events/types/filesystem_item_added.hpp"
|
||||
#include "providers/i_provider.hpp"
|
||||
@ -35,65 +36,61 @@
|
||||
#include "utils/unix.hpp"
|
||||
|
||||
namespace repertory {
|
||||
lock_data::lock_data(provider_type prov, std::string_view unique_id)
|
||||
: mutex_id_(create_lock_id(prov, unique_id)) {
|
||||
handle_ = open(get_lock_file().c_str(), O_CREAT | O_RDWR, S_IWUSR | S_IRUSR);
|
||||
lock_data::lock_data(const provider_type &pt, std::string unique_id /*= ""*/)
|
||||
: pt_(pt),
|
||||
unique_id_(std::move(unique_id)),
|
||||
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() { release(); }
|
||||
lock_data::lock_data()
|
||||
: pt_(provider_type::sia), unique_id_(""), mutex_id_(""), lock_fd_(-1) {}
|
||||
|
||||
auto lock_data::get_lock_data_file() const -> std::string {
|
||||
auto dir = get_state_directory();
|
||||
lock_data::~lock_data() {
|
||||
if (lock_fd_ != -1) {
|
||||
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()) {
|
||||
throw startup_exception("failed to create directory|sp|" + dir + "|err|" +
|
||||
std::to_string(utils::get_last_error_code()));
|
||||
}
|
||||
return utils::path::combine(
|
||||
dir, {"mountstate_" + std::to_string(getuid()) + ".json"});
|
||||
}
|
||||
|
||||
auto lock_data::get_lock_file() -> std::string {
|
||||
const auto dir = get_state_directory();
|
||||
if (not utils::file::directory(dir).create_directory()) {
|
||||
throw startup_exception("failed to create directory|sp|" + dir + "|err|" +
|
||||
std::to_string(utils::get_last_error_code()));
|
||||
}
|
||||
|
||||
return utils::path::combine(
|
||||
dir, {
|
||||
fmt::format("{}_{}.json", mutex_id_, getuid()),
|
||||
});
|
||||
}
|
||||
|
||||
auto lock_data::get_lock_file() const -> std::string {
|
||||
auto dir = get_state_directory();
|
||||
if (not utils::file::directory(dir).create_directory()) {
|
||||
throw startup_exception("failed to create directory|sp|" + dir + "|err|" +
|
||||
std::to_string(utils::get_last_error_code()));
|
||||
}
|
||||
|
||||
return utils::path::combine(
|
||||
dir, {
|
||||
fmt::format("{}_{}.lock", mutex_id_, getuid()),
|
||||
});
|
||||
return utils::path::combine(dir,
|
||||
{mutex_id_ + "_" + std::to_string(getuid())});
|
||||
}
|
||||
|
||||
auto lock_data::get_mount_state(json &mount_state) -> bool {
|
||||
auto handle = open(get_lock_data_file().c_str(), O_RDWR, S_IWUSR | S_IRUSR);
|
||||
if (handle == -1) {
|
||||
mount_state = {
|
||||
{"Active", false},
|
||||
{"Location", ""},
|
||||
{"PID", -1},
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
auto ret{false};
|
||||
if (wait_for_lock(handle) == 0) {
|
||||
ret = utils::file::read_json_file(get_lock_data_file(), mount_state);
|
||||
if (ret && mount_state.empty()) {
|
||||
mount_state = {
|
||||
{"Active", false},
|
||||
{"Location", ""},
|
||||
{"PID", -1},
|
||||
};
|
||||
auto ret = false;
|
||||
auto fd =
|
||||
open(get_lock_data_file().c_str(), O_CREAT | O_RDWR, S_IWUSR | S_IRUSR);
|
||||
if (fd != -1) {
|
||||
if (wait_for_lock(fd) == 0) {
|
||||
ret = utils::file::read_json_file(get_lock_data_file(), mount_state);
|
||||
flock(fd, LOCK_UN);
|
||||
}
|
||||
flock(handle, LOCK_UN);
|
||||
}
|
||||
|
||||
close(handle);
|
||||
close(fd);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
@ -101,20 +98,25 @@ auto lock_data::get_state_directory() -> std::string {
|
||||
#if defined(__APPLE__)
|
||||
return utils::path::absolute("~/Library/Application Support/" +
|
||||
std::string{REPERTORY_DATA_NAME} + "/state");
|
||||
#else // !defined(__APPLE__)
|
||||
#else
|
||||
return utils::path::absolute("~/.local/" + std::string{REPERTORY_DATA_NAME} +
|
||||
"/state");
|
||||
#endif // defined(__APPLE__)
|
||||
#endif
|
||||
}
|
||||
|
||||
auto lock_data::grab_lock(std::uint8_t retry_count) -> lock_result {
|
||||
if (handle_ == -1) {
|
||||
REPERTORY_USES_FUNCTION_NAME();
|
||||
|
||||
if (lock_fd_ == -1) {
|
||||
return lock_result::failure;
|
||||
}
|
||||
|
||||
lock_status_ = wait_for_lock(handle_, retry_count);
|
||||
lock_status_ = wait_for_lock(lock_fd_, retry_count);
|
||||
switch (lock_status_) {
|
||||
case 0:
|
||||
if (not set_mount_state(false, "", -1)) {
|
||||
utils::error::raise_error(function_name, "failed to set mount state");
|
||||
}
|
||||
return lock_result::success;
|
||||
case EWOULDBLOCK:
|
||||
return lock_result::locked;
|
||||
@ -123,72 +125,61 @@ auto lock_data::grab_lock(std::uint8_t retry_count) -> lock_result {
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
auto lock_data::set_mount_state(bool active, const std::string &mount_location,
|
||||
int pid) -> bool {
|
||||
REPERTORY_USES_FUNCTION_NAME();
|
||||
|
||||
auto ret = false;
|
||||
auto handle =
|
||||
open(get_lock_data_file().c_str(), O_CREAT | O_RDWR, S_IWUSR | S_IRUSR);
|
||||
if (handle == -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto ret{false};
|
||||
if (wait_for_lock(handle) == 0) {
|
||||
json mount_state;
|
||||
if (not utils::file::read_json_file(get_lock_data_file(), mount_state)) {
|
||||
utils::error::raise_error(function_name,
|
||||
"failed to read mount state file|sp|" +
|
||||
get_lock_file());
|
||||
}
|
||||
if ((mount_state.find("Active") == mount_state.end()) ||
|
||||
(mount_state["Active"].get<bool>() != active) ||
|
||||
(active &&
|
||||
((mount_state.find("Location") == mount_state.end()) ||
|
||||
(mount_state["Location"].get<std::string>() != mount_location)))) {
|
||||
if (mount_location.empty() && not active) {
|
||||
ret = utils::file::file{get_lock_data_file()}.remove();
|
||||
} else {
|
||||
ret = utils::file::write_json_file(
|
||||
get_lock_data_file(),
|
||||
{
|
||||
{"Active", active},
|
||||
{"Location", active ? mount_location : ""},
|
||||
{"PID", active ? pid : -1},
|
||||
});
|
||||
if (handle != -1) {
|
||||
if (wait_for_lock(handle) == 0) {
|
||||
const auto mount_id =
|
||||
app_config::get_provider_display_name(pt_) + unique_id_;
|
||||
json mount_state;
|
||||
if (not utils::file::read_json_file(get_lock_data_file(), mount_state)) {
|
||||
utils::error::raise_error(function_name,
|
||||
"failed to read mount state file|sp|" +
|
||||
get_lock_file());
|
||||
}
|
||||
} else {
|
||||
ret = true;
|
||||
if ((mount_state.find(mount_id) == mount_state.end()) ||
|
||||
(mount_state[mount_id].find("Active") ==
|
||||
mount_state[mount_id].end()) ||
|
||||
(mount_state[mount_id]["Active"].get<bool>() != active) ||
|
||||
(active && ((mount_state[mount_id].find("Location") ==
|
||||
mount_state[mount_id].end()) ||
|
||||
(mount_state[mount_id]["Location"].get<std::string>() !=
|
||||
mount_location)))) {
|
||||
const auto lines = utils::file::read_file_lines(get_lock_data_file());
|
||||
const auto txt = std::accumulate(
|
||||
lines.begin(), lines.end(), std::string(),
|
||||
[](auto &&val, auto &&line) -> auto { return val + line; });
|
||||
auto json_data = json::parse(txt.empty() ? "{}" : txt);
|
||||
json_data[mount_id] = {
|
||||
{"Active", active},
|
||||
{"Location", active ? mount_location : ""},
|
||||
{"PID", active ? pid : -1},
|
||||
};
|
||||
ret = utils::file::write_json_file(get_lock_data_file(), json_data);
|
||||
} else {
|
||||
ret = true;
|
||||
}
|
||||
|
||||
flock(handle, LOCK_UN);
|
||||
}
|
||||
|
||||
flock(handle, LOCK_UN);
|
||||
close(handle);
|
||||
}
|
||||
|
||||
close(handle);
|
||||
return ret;
|
||||
}
|
||||
|
||||
auto lock_data::wait_for_lock(int handle, std::uint8_t retry_count) -> int {
|
||||
static constexpr const std::uint32_t max_sleep{100U};
|
||||
auto lock_data::wait_for_lock(int fd, std::uint8_t retry_count) -> int {
|
||||
static constexpr const std::uint32_t max_sleep = 100U;
|
||||
|
||||
auto lock_status{EWOULDBLOCK};
|
||||
auto remain{static_cast<std::uint32_t>(retry_count * max_sleep)};
|
||||
auto lock_status = EWOULDBLOCK;
|
||||
auto remain = static_cast<std::uint32_t>(retry_count * max_sleep);
|
||||
while ((remain > 0) && (lock_status == EWOULDBLOCK)) {
|
||||
lock_status = flock(handle, LOCK_EX | LOCK_NB);
|
||||
lock_status = flock(fd, LOCK_EX | LOCK_NB);
|
||||
if (lock_status == -1) {
|
||||
lock_status = errno;
|
||||
if (lock_status == EWOULDBLOCK) {
|
||||
@ -237,13 +228,13 @@ auto provider_meta_handler(i_provider &provider, bool directory,
|
||||
const api_file &file) -> api_error {
|
||||
REPERTORY_USES_FUNCTION_NAME();
|
||||
|
||||
auto meta = create_meta_attributes(
|
||||
const auto meta = create_meta_attributes(
|
||||
file.accessed_date,
|
||||
directory ? FILE_ATTRIBUTE_DIRECTORY : FILE_ATTRIBUTE_ARCHIVE,
|
||||
file.changed_date, file.creation_date, directory, getgid(), file.key,
|
||||
directory ? S_IFDIR | S_IRUSR | S_IWUSR | S_IXUSR
|
||||
: 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);
|
||||
auto res = provider.set_item_meta(file.api_path, meta);
|
||||
if (res == api_error::success) {
|
||||
|
@ -21,171 +21,150 @@
|
||||
*/
|
||||
#if defined(_WIN32)
|
||||
|
||||
#include "platform/platform.hpp"
|
||||
#include "platform/win32_platform.hpp"
|
||||
|
||||
#include "events/event_system.hpp"
|
||||
#include "events/types/filesystem_item_added.hpp"
|
||||
#include "providers/i_provider.hpp"
|
||||
#include "utils/config.hpp"
|
||||
#include "utils/error_utils.hpp"
|
||||
#include "utils/string.hpp"
|
||||
|
||||
namespace repertory {
|
||||
lock_data::lock_data(provider_type prov, std::string unique_id)
|
||||
: mutex_id_(create_lock_id(prov, unique_id)),
|
||||
mutex_handle_(::CreateMutex(nullptr, FALSE,
|
||||
create_lock_id(prov, unique_id).c_str())) {}
|
||||
|
||||
lock_data::~lock_data() { release(); }
|
||||
|
||||
auto lock_data::get_current_mount_state(json &mount_state) -> bool {
|
||||
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;
|
||||
auto lock_data::get_mount_state(const provider_type & /*pt*/, json &mount_state)
|
||||
-> bool {
|
||||
const auto ret = get_mount_state(mount_state);
|
||||
if (ret) {
|
||||
const auto mount_id =
|
||||
app_config::get_provider_display_name(pt_) + unique_id_;
|
||||
mount_state = mount_state[mount_id].empty()
|
||||
? json({{"Active", false}, {"Location", ""}, {"PID", -1}})
|
||||
: mount_state[mount_id];
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
auto lock_data::get_mount_state(json &mount_state) -> bool {
|
||||
if (not get_current_mount_state(mount_state)) {
|
||||
return false;
|
||||
HKEY key;
|
||||
auto ret = !::RegCreateKeyEx(
|
||||
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},
|
||||
{"Location", ""},
|
||||
{"PID", -1},
|
||||
})
|
||||
: mount_state;
|
||||
|
||||
return true;
|
||||
return ret;
|
||||
}
|
||||
|
||||
auto lock_data::grab_lock(std::uint8_t retry_count) -> lock_result {
|
||||
static constexpr const std::uint32_t max_sleep{100U};
|
||||
REPERTORY_USES_FUNCTION_NAME();
|
||||
|
||||
auto ret = lock_result::success;
|
||||
if (mutex_handle_ == INVALID_HANDLE_VALUE) {
|
||||
return lock_result::failure;
|
||||
ret = lock_result::failure;
|
||||
} else {
|
||||
for (auto i = 0;
|
||||
(i <= retry_count) && ((mutex_state_ = ::WaitForSingleObject(
|
||||
mutex_handle_, 100)) == WAIT_TIMEOUT);
|
||||
i++) {
|
||||
}
|
||||
|
||||
switch (mutex_state_) {
|
||||
case WAIT_OBJECT_0: {
|
||||
ret = lock_result::success;
|
||||
auto should_reset = true;
|
||||
json mount_state;
|
||||
if (get_mount_state(pt_, mount_state)) {
|
||||
if (mount_state["Active"].get<bool>() &&
|
||||
mount_state["Location"] == "elevating") {
|
||||
should_reset = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (should_reset) {
|
||||
if (not set_mount_state(false, "", -1)) {
|
||||
utils::error::raise_error(function_name, "failed to set mount state");
|
||||
}
|
||||
}
|
||||
} break;
|
||||
|
||||
case WAIT_TIMEOUT:
|
||||
ret = lock_result::locked;
|
||||
break;
|
||||
|
||||
default:
|
||||
ret = lock_result::failure;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (std::uint8_t idx = 0U;
|
||||
(idx <= retry_count) &&
|
||||
((mutex_state_ = ::WaitForSingleObject(mutex_handle_, max_sleep)) ==
|
||||
WAIT_TIMEOUT);
|
||||
++idx) {
|
||||
}
|
||||
|
||||
switch (mutex_state_) {
|
||||
case WAIT_OBJECT_0:
|
||||
return lock_result::success;
|
||||
|
||||
case WAIT_TIMEOUT:
|
||||
return lock_result::locked;
|
||||
|
||||
default:
|
||||
return lock_result::failure;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
void lock_data::release() {
|
||||
if (mutex_handle_ == INVALID_HANDLE_VALUE) {
|
||||
return;
|
||||
}
|
||||
|
||||
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)};
|
||||
if (mutex_handle_ != INVALID_HANDLE_VALUE) {
|
||||
if ((mutex_state_ == WAIT_OBJECT_0) || (mutex_state_ == WAIT_ABANDONED)) {
|
||||
::ReleaseMutex(mutex_handle_);
|
||||
}
|
||||
|
||||
::ReleaseMutex(mutex_handle_);
|
||||
::CloseHandle(mutex_handle_);
|
||||
mutex_handle_ = INVALID_HANDLE_VALUE;
|
||||
}
|
||||
|
||||
::CloseHandle(mutex_handle_);
|
||||
mutex_handle_ = INVALID_HANDLE_VALUE;
|
||||
}
|
||||
|
||||
auto lock_data::set_mount_state(bool active, std::string_view mount_location,
|
||||
std::int64_t pid) -> bool {
|
||||
if (mutex_handle_ == INVALID_HANDLE_VALUE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
auto lock_data::set_mount_state(bool active, const std::string &mount_location,
|
||||
const std::int64_t &pid) -> bool {
|
||||
auto ret = false;
|
||||
if (mutex_handle_ != INVALID_HANDLE_VALUE) {
|
||||
const auto mount_id =
|
||||
app_config::get_provider_display_name(pt_) + unique_id_;
|
||||
json mount_state;
|
||||
[[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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@ -236,4 +215,4 @@ auto provider_meta_handler(i_provider &provider, bool directory,
|
||||
}
|
||||
} // namespace repertory
|
||||
|
||||
#endif // defined(_WIN32)
|
||||
#endif //_WIN32
|
||||
|
@ -29,7 +29,9 @@
|
||||
#include "events/types/service_stop_end.hpp"
|
||||
#include "events/types/unmount_requested.hpp"
|
||||
#include "rpc/common.hpp"
|
||||
#include "utils/base64.hpp"
|
||||
#include "utils/error_utils.hpp"
|
||||
#include "utils/string.hpp"
|
||||
|
||||
namespace repertory {
|
||||
server::server(app_config &config) : config_(config) {}
|
||||
@ -37,7 +39,6 @@ server::server(app_config &config) : config_(config) {}
|
||||
void server::handle_get_config(const httplib::Request & /*req*/,
|
||||
httplib::Response &res) {
|
||||
auto data = config_.get_json();
|
||||
clean_json_config(config_.get_provider_type(), data);
|
||||
res.set_content(data.dump(), "application/json");
|
||||
res.status = http_error_codes::ok;
|
||||
}
|
||||
@ -45,10 +46,7 @@ void server::handle_get_config(const httplib::Request & /*req*/,
|
||||
void server::handle_get_config_value_by_name(const httplib::Request &req,
|
||||
httplib::Response &res) {
|
||||
auto name = req.get_param_value("name");
|
||||
auto data = json({{
|
||||
"value",
|
||||
clean_json_value(name, config_.get_value_by_name(name)),
|
||||
}});
|
||||
auto data = json({{"value", config_.get_value_by_name(name)}});
|
||||
res.set_content(data.dump(), "application/json");
|
||||
res.status = http_error_codes::ok;
|
||||
}
|
||||
@ -58,10 +56,7 @@ void server::handle_set_config_value_by_name(const httplib::Request &req,
|
||||
auto name = req.get_param_value("name");
|
||||
auto value = req.get_param_value("value");
|
||||
|
||||
json data = {{
|
||||
"value",
|
||||
clean_json_value(name, config_.set_value_by_name(name, value)),
|
||||
}};
|
||||
json data = {{"value", config_.set_value_by_name(name, value)}};
|
||||
res.set_content(data.dump(), "application/json");
|
||||
res.status = http_error_codes::ok;
|
||||
}
|
||||
@ -141,21 +136,8 @@ void server::start() {
|
||||
|
||||
initialize(*server_);
|
||||
|
||||
server_thread_ = std::make_unique<std::thread>([this]() {
|
||||
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());
|
||||
});
|
||||
server_thread_ = std::make_unique<std::thread>(
|
||||
[this]() { server_->listen("127.0.0.1", config_.get_api_port()); });
|
||||
event_system::instance().raise<service_start_end>(function_name, "server");
|
||||
}
|
||||
|
||||
|
@ -26,51 +26,6 @@
|
||||
#include "utils/string.hpp"
|
||||
|
||||
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)
|
||||
-> database_type {
|
||||
type = utils::string::to_lower(utils::string::trim(type));
|
||||
|
@ -45,7 +45,7 @@ void get_api_authentication_data(std::string &user, std::string &password,
|
||||
|
||||
if (success) {
|
||||
if (user.empty() && password.empty()) {
|
||||
password = data[JSON_API_PASSWORD].get<std::string>();
|
||||
password = data[JSON_API_AUTH].get<std::string>();
|
||||
user = data[JSON_API_USER].get<std::string>();
|
||||
}
|
||||
port = data[JSON_API_PORT].get<std::uint16_t>();
|
||||
|
@ -1,31 +0,0 @@
|
||||
/*
|
||||
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
|
@ -36,9 +36,10 @@ template <typename drive> inline void help(std::vector<const char *> args) {
|
||||
std::cout << " -di,--drive_information Display mounted drive "
|
||||
"information"
|
||||
<< std::endl;
|
||||
std::cout << " -na,--name Unique configuration "
|
||||
"name [Required for Encrypt, S3 and Sia]"
|
||||
<< std::endl;
|
||||
std::cout
|
||||
<< " -na,--name Unique name for S3 or Sia "
|
||||
"instance [Required]"
|
||||
<< std::endl;
|
||||
std::cout << " -s3,--s3 Enables S3 mode"
|
||||
<< std::endl;
|
||||
std::cout
|
||||
|
@ -28,13 +28,14 @@
|
||||
#include "providers/provider.hpp"
|
||||
#include "types/repertory.hpp"
|
||||
#include "utils/cli_utils.hpp"
|
||||
#include "utils/file.hpp"
|
||||
#include "utils/com_init_wrapper.hpp"
|
||||
#include "utils/file_utils.hpp"
|
||||
#include "utils/string.hpp"
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include "drives/winfsp/remotewinfsp/remote_client.hpp"
|
||||
#include "drives/winfsp/remotewinfsp/remote_winfsp_drive.hpp"
|
||||
#include "drives/winfsp/winfsp_drive.hpp"
|
||||
#include "utils/com_init_wrapper.hpp"
|
||||
|
||||
using repertory_drive = repertory::winfsp_drive;
|
||||
using remote_client = repertory::remote_winfsp::remote_client;
|
||||
@ -56,143 +57,130 @@ namespace repertory::cli::actions {
|
||||
mount(std::vector<const char *> args, std::string data_directory,
|
||||
int &mount_result, provider_type prov, const std::string &remote_host,
|
||||
std::uint16_t remote_port, const std::string &unique_id) -> exit_code {
|
||||
lock_data global_lock(provider_type::unknown, "global");
|
||||
{
|
||||
auto lock_result = global_lock.grab_lock(100U);
|
||||
if (lock_result != lock_result::success) {
|
||||
std::cerr << "FATAL: Unable to get global lock" << std::endl;
|
||||
return exit_code::lock_failed;
|
||||
}
|
||||
}
|
||||
auto ret = exit_code::success;
|
||||
|
||||
lock_data lock(prov, unique_id);
|
||||
auto lock_result = lock.grab_lock();
|
||||
if (lock_result == lock_result::locked) {
|
||||
const auto res = lock.grab_lock();
|
||||
if (res == lock_result::locked) {
|
||||
ret = exit_code::mount_active;
|
||||
std::cerr << app_config::get_provider_display_name(prov)
|
||||
<< " mount is already active" << std::endl;
|
||||
return exit_code::mount_active;
|
||||
}
|
||||
|
||||
if (lock_result != lock_result::success) {
|
||||
std::cerr << "FATAL: Unable to get provider lock" << std::endl;
|
||||
return exit_code::lock_failed;
|
||||
}
|
||||
|
||||
if (utils::cli::has_option(args,
|
||||
utils::cli::options::generate_config_option)) {
|
||||
app_config config(prov, data_directory);
|
||||
if (prov == provider_type::remote) {
|
||||
auto remote_config = config.get_remote_config();
|
||||
remote_config.host_name_or_ip = remote_host;
|
||||
remote_config.api_port = remote_port;
|
||||
config.set_remote_config(remote_config);
|
||||
} else if (prov == provider_type::sia &&
|
||||
config.get_sia_config().bucket.empty()) {
|
||||
[[maybe_unused]] auto bucket =
|
||||
config.set_value_by_name("SiaConfig.Bucket", unique_id);
|
||||
}
|
||||
|
||||
std::cout << "Generated " << app_config::get_provider_display_name(prov)
|
||||
<< " Configuration" << std::endl;
|
||||
std::cout << config.get_config_file_path() << std::endl;
|
||||
return utils::file::file(config.get_config_file_path()).exists()
|
||||
? exit_code::success
|
||||
: exit_code::file_creation_failed;
|
||||
}
|
||||
|
||||
#if defined(_WIN32)
|
||||
if (utils::cli::has_option(args, utils::cli::options::hidden_option)) {
|
||||
::ShowWindow(::GetConsoleWindow(), SW_HIDE);
|
||||
}
|
||||
#endif // defined(_WIN32)
|
||||
|
||||
auto drive_args = utils::cli::parse_drive_options(args, prov, data_directory);
|
||||
app_config config(prov, data_directory);
|
||||
{
|
||||
std::uint16_t port{};
|
||||
if (not utils::get_next_available_port(config.get_api_port(), port)) {
|
||||
std::cerr << "FATAL: Unable to get available port" << std::endl;
|
||||
return exit_code::startup_exception;
|
||||
}
|
||||
config.set_api_port(port);
|
||||
}
|
||||
|
||||
#if defined(_WIN32)
|
||||
if (config.get_enable_mount_manager() && not utils::is_process_elevated()) {
|
||||
utils::com_init_wrapper wrapper;
|
||||
if (not lock.set_mount_state(true, "elevating", -1)) {
|
||||
std::cerr << "failed to set mount state" << std::endl;
|
||||
}
|
||||
lock.release();
|
||||
global_lock.release();
|
||||
|
||||
mount_result = utils::run_process_elevated(args);
|
||||
lock_data prov_lock(prov, unique_id);
|
||||
if (prov_lock.grab_lock() == lock_result::success) {
|
||||
if (not prov_lock.set_mount_state(false, "", -1)) {
|
||||
std::cerr << "failed to set mount state" << std::endl;
|
||||
}
|
||||
prov_lock.release();
|
||||
}
|
||||
|
||||
return exit_code::mount_result;
|
||||
}
|
||||
#endif // defined(_WIN32)
|
||||
|
||||
std::cout << "Initializing " << app_config::get_provider_display_name(prov)
|
||||
<< (unique_id.empty() ? ""
|
||||
: (prov == provider_type::remote)
|
||||
? " [" + remote_host + ':' + std::to_string(remote_port) +
|
||||
']'
|
||||
: " [" + unique_id + ']')
|
||||
<< " Drive" << std::endl;
|
||||
if (prov == provider_type::remote) {
|
||||
try {
|
||||
auto remote_cfg = config.get_remote_config();
|
||||
remote_cfg.host_name_or_ip = remote_host;
|
||||
remote_cfg.api_port = remote_port;
|
||||
config.set_remote_config(remote_cfg);
|
||||
|
||||
remote_drive drive(
|
||||
config,
|
||||
[&config]() -> std::unique_ptr<remote_instance> {
|
||||
return std::unique_ptr<remote_instance>(new remote_client(config));
|
||||
},
|
||||
lock);
|
||||
if (not lock.set_mount_state(true, "", -1)) {
|
||||
std::cerr << "failed to set mount state" << std::endl;
|
||||
} else if (res == lock_result::success) {
|
||||
const auto generate_config = utils::cli::has_option(
|
||||
args, utils::cli::options::generate_config_option);
|
||||
if (generate_config) {
|
||||
app_config config(prov, data_directory);
|
||||
if (prov == provider_type::remote) {
|
||||
auto cfg = config.get_remote_config();
|
||||
cfg.host_name_or_ip = remote_host;
|
||||
cfg.api_port = remote_port;
|
||||
config.set_remote_config(cfg);
|
||||
} else if (prov == provider_type::sia &&
|
||||
config.get_sia_config().bucket.empty()) {
|
||||
[[maybe_unused]] auto bucket =
|
||||
config.set_value_by_name("SiaConfig.Bucket", unique_id);
|
||||
}
|
||||
|
||||
global_lock.release();
|
||||
mount_result = drive.mount(drive_args);
|
||||
return exit_code::mount_result;
|
||||
} catch (const std::exception &e) {
|
||||
std::cerr << "FATAL: " << e.what() << std::endl;
|
||||
}
|
||||
std::cout << "Generated " << app_config::get_provider_display_name(prov)
|
||||
<< " Configuration" << std::endl;
|
||||
std::cout << config.get_config_file_path() << std::endl;
|
||||
ret = utils::file::file(config.get_config_file_path()).exists()
|
||||
? exit_code::success
|
||||
: exit_code::file_creation_failed;
|
||||
} else {
|
||||
#if defined(_WIN32)
|
||||
if (utils::cli::has_option(args, utils::cli::options::hidden_option)) {
|
||||
::ShowWindow(::GetConsoleWindow(), SW_HIDE);
|
||||
}
|
||||
#endif // defined(_WIN32)
|
||||
auto drive_args =
|
||||
utils::cli::parse_drive_options(args, prov, data_directory);
|
||||
app_config config(prov, data_directory);
|
||||
#if defined(_WIN32)
|
||||
if (config.get_enable_mount_manager() &&
|
||||
not utils::is_process_elevated()) {
|
||||
utils::com_init_wrapper cw;
|
||||
if (not lock.set_mount_state(true, "elevating", -1)) {
|
||||
std::cerr << "failed to set mount state" << std::endl;
|
||||
}
|
||||
lock.release();
|
||||
|
||||
return exit_code::startup_exception;
|
||||
mount_result = utils::run_process_elevated(args);
|
||||
lock_data lock2(prov, unique_id);
|
||||
if (lock2.grab_lock() == lock_result::success) {
|
||||
if (not lock2.set_mount_state(false, "", -1)) {
|
||||
std::cerr << "failed to set mount state" << std::endl;
|
||||
}
|
||||
lock2.release();
|
||||
}
|
||||
|
||||
return exit_code::mount_result;
|
||||
}
|
||||
#endif // defined(_WIN32)
|
||||
std::cout << "Initializing "
|
||||
<< app_config::get_provider_display_name(prov)
|
||||
<< (unique_id.empty() ? ""
|
||||
: (prov == provider_type::s3 || prov == provider_type::sia)
|
||||
? " [" + unique_id + ']'
|
||||
: " [" + remote_host + ':' +
|
||||
std::to_string(remote_port) + ']')
|
||||
<< " Drive" << std::endl;
|
||||
if (prov == provider_type::remote) {
|
||||
std::uint16_t port{0U};
|
||||
if (utils::get_next_available_port(config.get_api_port(), port)) {
|
||||
auto cfg = config.get_remote_config();
|
||||
cfg.host_name_or_ip = remote_host;
|
||||
cfg.api_port = remote_port;
|
||||
config.set_remote_config(cfg);
|
||||
config.set_api_port(port);
|
||||
|
||||
try {
|
||||
remote_drive drive(
|
||||
config,
|
||||
[&config]() -> std::unique_ptr<remote_instance> {
|
||||
return std::unique_ptr<remote_instance>(
|
||||
new remote_client(config));
|
||||
},
|
||||
lock);
|
||||
if (not lock.set_mount_state(true, "", -1)) {
|
||||
std::cerr << "failed to set mount state" << std::endl;
|
||||
}
|
||||
mount_result = drive.mount(drive_args);
|
||||
ret = exit_code::mount_result;
|
||||
} catch (const std::exception &e) {
|
||||
std::cerr << "FATAL: " << e.what() << std::endl;
|
||||
ret = exit_code::startup_exception;
|
||||
}
|
||||
} else {
|
||||
std::cerr << "FATAL: Unable to get available port" << std::endl;
|
||||
ret = exit_code::startup_exception;
|
||||
}
|
||||
} else {
|
||||
if (prov == provider_type::sia &&
|
||||
config.get_sia_config().bucket.empty()) {
|
||||
[[maybe_unused]] auto bucket =
|
||||
config.set_value_by_name("SiaConfig.Bucket", unique_id);
|
||||
}
|
||||
|
||||
try {
|
||||
auto provider = create_provider(prov, config);
|
||||
repertory_drive drive(config, lock, *provider);
|
||||
if (not lock.set_mount_state(true, "", -1)) {
|
||||
std::cerr << "failed to set mount state" << std::endl;
|
||||
}
|
||||
mount_result = drive.mount(drive_args);
|
||||
ret = exit_code::mount_result;
|
||||
} catch (const std::exception &e) {
|
||||
std::cerr << "FATAL: " << e.what() << std::endl;
|
||||
ret = exit_code::startup_exception;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ret = exit_code::lock_failed;
|
||||
}
|
||||
|
||||
try {
|
||||
if (prov == provider_type::sia && config.get_sia_config().bucket.empty()) {
|
||||
[[maybe_unused]] auto bucket =
|
||||
config.set_value_by_name("SiaConfig.Bucket", unique_id);
|
||||
}
|
||||
|
||||
auto provider = create_provider(prov, config);
|
||||
repertory_drive drive(config, lock, *provider);
|
||||
if (not lock.set_mount_state(true, "", -1)) {
|
||||
std::cerr << "failed to set mount state" << std::endl;
|
||||
}
|
||||
|
||||
global_lock.release();
|
||||
mount_result = drive.mount(drive_args);
|
||||
return exit_code::mount_result;
|
||||
} catch (const std::exception &e) {
|
||||
std::cerr << "FATAL: " << e.what() << std::endl;
|
||||
}
|
||||
|
||||
return exit_code::startup_exception;
|
||||
return ret;
|
||||
}
|
||||
} // namespace repertory::cli::actions
|
||||
|
||||
|
@ -24,7 +24,6 @@
|
||||
|
||||
#include "types/repertory.hpp"
|
||||
#include "ui/handlers.hpp"
|
||||
#include "ui/mgmt_app_config.hpp"
|
||||
#include "utils/cli_utils.hpp"
|
||||
#include "utils/file.hpp"
|
||||
#include "utils/string.hpp"
|
||||
@ -34,14 +33,13 @@ namespace repertory::cli::actions {
|
||||
ui(std::vector<const char *> args, const std::string & /*data_directory*/,
|
||||
const provider_type & /* prov */, const std::string & /* unique_id */,
|
||||
std::string /* user */, std::string /* password */) -> exit_code {
|
||||
|
||||
ui::mgmt_app_config config{};
|
||||
auto ui_port{default_ui_mgmt_port};
|
||||
|
||||
std::string data;
|
||||
auto res = utils::cli::parse_string_option(
|
||||
args, utils::cli::options::ui_port_option, data);
|
||||
if (res == exit_code::success && not data.empty()) {
|
||||
config.set_api_port(utils::string::to_uint16(data));
|
||||
ui_port = utils::string::to_uint16(data);
|
||||
}
|
||||
|
||||
if (not utils::file::change_to_process_directory()) {
|
||||
@ -53,7 +51,13 @@ ui(std::vector<const char *> args, const std::string & /*data_directory*/,
|
||||
return exit_code::ui_mount_failed;
|
||||
}
|
||||
|
||||
ui::mgmt_app_config config{
|
||||
.api_auth_ = "test",
|
||||
.api_port_ = ui_port,
|
||||
.api_user_ = "test",
|
||||
};
|
||||
ui::handlers handlers(&config, &server);
|
||||
|
||||
return exit_code::success;
|
||||
}
|
||||
} // namespace repertory::cli::actions
|
||||
|
@ -23,26 +23,21 @@
|
||||
#define REPERTORY_INCLUDE_UI_HANDLERS_HPP_
|
||||
|
||||
#include "events/consumers/console_consumer.hpp"
|
||||
#include "utils/common.hpp"
|
||||
|
||||
namespace repertory::ui {
|
||||
class mgmt_app_config;
|
||||
struct mgmt_app_config final {
|
||||
std::string api_auth_{"test"};
|
||||
std::uint16_t api_port_{default_ui_mgmt_port};
|
||||
std::string api_user_{"test"};
|
||||
|
||||
[[nodiscard]] auto get_api_auth() const -> std::string { return api_auth_; }
|
||||
|
||||
[[nodiscard]] auto get_api_port() const -> std::uint16_t { return api_port_; }
|
||||
|
||||
[[nodiscard]] auto get_api_user() const -> std::string { return api_user_; }
|
||||
};
|
||||
|
||||
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:
|
||||
handlers(mgmt_app_config *config, httplib::Server *server);
|
||||
|
||||
@ -57,63 +52,22 @@ public:
|
||||
|
||||
private:
|
||||
mgmt_app_config *config_;
|
||||
std::string repertory_binary_;
|
||||
httplib::Server *server_;
|
||||
|
||||
private:
|
||||
console_consumer console;
|
||||
mutable std::mutex mtx_;
|
||||
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:
|
||||
[[nodiscard]] auto data_directory_exists(provider_type prov,
|
||||
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_status(auto &&req, auto &&res) const;
|
||||
|
||||
void handle_get_mount_list(httplib::Response &res) const;
|
||||
|
||||
void handle_get_mount_location(const httplib::Request &req,
|
||||
httplib::Response &res) const;
|
||||
|
||||
void handle_get_mount_status(const httplib::Request &req,
|
||||
httplib::Response &res) const;
|
||||
|
||||
void handle_get_nonce(httplib::Response &res);
|
||||
|
||||
void handle_get_settings(httplib::Response &res) const;
|
||||
|
||||
void handle_post_add_mount(const httplib::Request &req,
|
||||
httplib::Response &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,
|
||||
std::vector<std::string> args,
|
||||
bool background = false) const
|
||||
[[nodiscard]] static auto read_process(provider_type prov,
|
||||
std::string_view name,
|
||||
std::string_view command)
|
||||
-> std::vector<std::string>;
|
||||
|
||||
void removed_expired_nonces();
|
||||
|
||||
void set_key_value(provider_type prov, std::string_view name,
|
||||
std::string_view key, std::string_view value) const;
|
||||
};
|
||||
} // namespace repertory::ui
|
||||
|
||||
|
@ -1,70 +0,0 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
#ifndef REPERTORY_INCLUDE_UI_MGMT_APP_CONFIG_HPP_
|
||||
#define REPERTORY_INCLUDE_UI_MGMT_APP_CONFIG_HPP_
|
||||
|
||||
#include "types/repertory.hpp"
|
||||
|
||||
namespace repertory::ui {
|
||||
class mgmt_app_config final {
|
||||
public:
|
||||
mgmt_app_config();
|
||||
|
||||
private:
|
||||
atomic<std::string> api_password_{"repertory"};
|
||||
std::atomic<std::uint16_t> api_port_{default_ui_mgmt_port};
|
||||
atomic<std::string> api_user_{"repertory"};
|
||||
std::unordered_map<provider_type,
|
||||
std::unordered_map<std::string, std::string>>
|
||||
locations_;
|
||||
mutable std::recursive_mutex mtx_;
|
||||
|
||||
private:
|
||||
void save() const;
|
||||
|
||||
public:
|
||||
[[nodiscard]] auto to_json() const -> nlohmann::json;
|
||||
|
||||
[[nodiscard]] auto get_api_password() const -> std::string {
|
||||
return api_password_;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto get_api_port() const -> std::uint16_t { return api_port_; }
|
||||
|
||||
[[nodiscard]] auto get_api_user() const -> std::string { return api_user_; }
|
||||
|
||||
[[nodiscard]] auto get_mount_location(provider_type prov,
|
||||
std::string_view name) const
|
||||
-> std::string;
|
||||
|
||||
void set_api_password(std::string_view api_password);
|
||||
|
||||
void set_api_port(std::uint16_t api_port);
|
||||
|
||||
void set_api_user(std::string_view api_user);
|
||||
|
||||
void set_mount_location(provider_type prov, std::string_view name,
|
||||
std::string_view location);
|
||||
};
|
||||
} // namespace repertory::ui
|
||||
|
||||
#endif // REPERTORY_INCLUDE_UI_MGMT_APP_CONFIG_HPP_
|
@ -105,8 +105,7 @@ auto main(int argc, char **argv) -> int {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if ((prov == provider_type::s3) || (prov == provider_type::sia) ||
|
||||
(prov == provider_type::encrypt)) {
|
||||
} else if ((prov == provider_type::s3) || (prov == provider_type::sia)) {
|
||||
std::string data;
|
||||
res = utils::cli::parse_string_option(
|
||||
args, utils::cli::options::name_option, data);
|
||||
@ -116,9 +115,9 @@ auto main(int argc, char **argv) -> int {
|
||||
if (prov == provider_type::sia) {
|
||||
unique_id = "default";
|
||||
} else {
|
||||
std::cerr << "Configuration name for '"
|
||||
std::cerr << "Name of "
|
||||
<< app_config::get_provider_display_name(prov)
|
||||
<< "' was not provided" << std::endl;
|
||||
<< " instance not provided" << std::endl;
|
||||
res = exit_code::invalid_syntax;
|
||||
}
|
||||
}
|
||||
|
@ -23,126 +23,28 @@
|
||||
|
||||
#include "app_config.hpp"
|
||||
#include "events/event_system.hpp"
|
||||
#include "rpc/common.hpp"
|
||||
#include "types/repertory.hpp"
|
||||
#include "ui/mgmt_app_config.hpp"
|
||||
#include "utils/collection.hpp"
|
||||
#include "utils/common.hpp"
|
||||
#include "utils/config.hpp"
|
||||
#include "utils/error_utils.hpp"
|
||||
#include "utils/file.hpp"
|
||||
#include "utils/hash.hpp"
|
||||
#include "utils/path.hpp"
|
||||
#include "utils/string.hpp"
|
||||
|
||||
#include <boost/process.hpp>
|
||||
|
||||
namespace {
|
||||
[[nodiscard]] auto decrypt(std::string_view data, std::string_view password)
|
||||
-> std::string {
|
||||
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 repertory::ui {
|
||||
handlers::handlers(mgmt_app_config *config, httplib::Server *server)
|
||||
: config_(config),
|
||||
#if defined(_WIN32)
|
||||
repertory_binary_(utils::path::combine(".", {"repertory.exe"})),
|
||||
#else // !defined(_WIN32)
|
||||
repertory_binary_(utils::path::combine(".", {"repertory"})),
|
||||
#endif // defined(_WIN32)
|
||||
server_(server) {
|
||||
: config_(config), server_(server) {
|
||||
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(
|
||||
[this](const httplib::Request &req,
|
||||
auto &&res) -> httplib::Server::HandlerResponse {
|
||||
if (req.path == "/api/v1/nonce" || req.path == "/ui" ||
|
||||
req.path.starts_with("/ui/")) {
|
||||
[this](auto &&req, auto &&res) -> httplib::Server::HandlerResponse {
|
||||
if (rpc::check_authorization(*config_, req)) {
|
||||
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.set_header(
|
||||
"WWW-Authenticate",
|
||||
R"(Basic realm="Repertory Management Portal", charset="UTF-8")");
|
||||
return httplib::Server::HandlerResponse::Handled;
|
||||
});
|
||||
|
||||
@ -166,56 +68,22 @@ handlers::handlers(mgmt_app_config *config, httplib::Server *server)
|
||||
}
|
||||
|
||||
res.set_content(data.dump(), "application/json");
|
||||
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);
|
||||
res.status = http_error_codes::internal_error;
|
||||
});
|
||||
|
||||
server->Get("/api/v1/mount",
|
||||
[this](auto &&req, auto &&res) { handle_get_mount(req, res); });
|
||||
|
||||
server->Get("/api/v1/mount_location", [this](auto &&req, auto &&res) {
|
||||
handle_get_mount_location(req, res);
|
||||
});
|
||||
|
||||
server->Get("/api/v1/mount_list", [this](auto && /* req */, auto &&res) {
|
||||
handle_get_mount_list(res);
|
||||
});
|
||||
|
||||
server->Get("/api/v1/mount_status", [this](auto &&req, auto &&res) {
|
||||
handle_get_mount_status(req, res);
|
||||
});
|
||||
server->Get("/api/v1/mount_status",
|
||||
[this](const httplib::Request &req, auto &&res) {
|
||||
handle_get_mount_status(req, res);
|
||||
});
|
||||
|
||||
server->Get("/api/v1/nonce",
|
||||
[this](auto && /* req */, auto &&res) { handle_get_nonce(res); });
|
||||
|
||||
server->Get("/api/v1/settings", [this](auto && /* req */, auto &&res) {
|
||||
handle_get_settings(res);
|
||||
});
|
||||
|
||||
server->Post("/api/v1/add_mount", [this](auto &&req, auto &&res) {
|
||||
handle_post_add_mount(req, res);
|
||||
});
|
||||
|
||||
server->Post("/api/v1/mount",
|
||||
[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) {
|
||||
handle_put_set_value_by_name(req, res);
|
||||
});
|
||||
|
||||
server->Put("/api/v1/settings", [this](auto &&req, auto &&res) {
|
||||
handle_put_settings(req, res);
|
||||
});
|
||||
event_system::instance().start();
|
||||
|
||||
static std::atomic<httplib::Server *> this_server{server_};
|
||||
static const auto quit_handler = [](int /* sig */) {
|
||||
@ -234,137 +102,18 @@ handlers::handlers(mgmt_app_config *config, httplib::Server *server)
|
||||
#endif // !defined(_WIN32)
|
||||
std::signal(SIGTERM, quit_handler);
|
||||
|
||||
#if defined(_WIN32)
|
||||
system(fmt::format(
|
||||
R"(start "Repertory Management Portal" "http://127.0.0.1:{}/ui")",
|
||||
config_->get_api_port())
|
||||
.c_str());
|
||||
#elif defined(__linux__)
|
||||
system(fmt::format(R"(xdg-open "http://127.0.0.1:{}/ui")",
|
||||
config_->get_api_port())
|
||||
.c_str());
|
||||
#else // error
|
||||
build fails here
|
||||
#endif
|
||||
|
||||
std::uint16_t port{};
|
||||
if (not utils::get_next_available_port(config_->get_api_port(), port)) {
|
||||
fmt::println("failed to detect if port is available|{}",
|
||||
config_->get_api_port());
|
||||
return;
|
||||
}
|
||||
|
||||
if (port != config_->get_api_port()) {
|
||||
fmt::println("failed to listen on port|{}|next available|{}",
|
||||
config_->get_api_port(), port);
|
||||
return;
|
||||
}
|
||||
|
||||
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());
|
||||
if (this_server != nullptr) {
|
||||
this_server = nullptr;
|
||||
server_->stop();
|
||||
}
|
||||
this_server = nullptr;
|
||||
}
|
||||
|
||||
handlers::~handlers() {
|
||||
if (nonce_thread_) {
|
||||
stop_requested = true;
|
||||
handlers::~handlers() { event_system::instance().stop(); }
|
||||
|
||||
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,
|
||||
std::string_view name) const -> bool {
|
||||
auto data_dir = utils::path::combine(app_config::get_root_data_directory(),
|
||||
{
|
||||
app_config::get_provider_name(prov),
|
||||
name,
|
||||
});
|
||||
auto ret = utils::file::directory{data_dir}.exists();
|
||||
if (ret) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
unique_mutex_lock lock(mtx_);
|
||||
mtx_lookup_.erase(
|
||||
fmt::format("{}-{}", name, app_config::get_provider_name(prov)));
|
||||
lock.unlock();
|
||||
return ret;
|
||||
}
|
||||
|
||||
void handlers::handle_put_mount_location(const httplib::Request &req,
|
||||
httplib::Response &res) const {
|
||||
void handlers::handle_get_mount(auto &&req, auto &&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();
|
||||
|
||||
auto prov = provider_type_from_string(req.get_param_value("type"));
|
||||
auto name = req.get_param_value("name");
|
||||
|
||||
if (not data_directory_exists(prov, name)) {
|
||||
res.status = http_error_codes::not_found;
|
||||
return;
|
||||
}
|
||||
|
||||
auto lines = launch_process(prov, name, {"-dc"});
|
||||
auto lines = handlers::read_process(prov, req.get_param_value("name"), "-dc");
|
||||
|
||||
if (lines.at(0U) != "0") {
|
||||
throw utils::error::create_exception(function_name, {
|
||||
@ -376,16 +125,20 @@ void handlers::handle_get_mount(const httplib::Request &req,
|
||||
lines.erase(lines.begin());
|
||||
|
||||
auto result = nlohmann::json::parse(utils::string::join(lines, '\n'));
|
||||
clean_json_config(prov, result);
|
||||
|
||||
res.set_content(result.dump(), "application/json");
|
||||
res.status = http_error_codes::ok;
|
||||
}
|
||||
|
||||
void handlers::handle_get_mount_list(httplib::Response &res) const {
|
||||
void handlers::handle_get_mount_list(auto &&res) const {
|
||||
auto data_dir = utils::file::directory{app_config::get_root_data_directory()};
|
||||
|
||||
nlohmann::json result;
|
||||
|
||||
auto encrypt_dir = data_dir.get_directory("encrypt");
|
||||
if (encrypt_dir && encrypt_dir->get_file("config.json")) {
|
||||
result["encrypt"].emplace_back("encrypt");
|
||||
}
|
||||
|
||||
const auto process_dir = [&data_dir, &result](std::string_view name) {
|
||||
auto name_dir = data_dir.get_directory(name);
|
||||
if (not name_dir) {
|
||||
@ -402,7 +155,6 @@ void handlers::handle_get_mount_list(httplib::Response &res) const {
|
||||
}
|
||||
};
|
||||
|
||||
process_dir("encrypt");
|
||||
process_dir("remote");
|
||||
process_dir("s3");
|
||||
process_dir("sia");
|
||||
@ -411,341 +163,91 @@ void handlers::handle_get_mount_list(httplib::Response &res) const {
|
||||
res.status = http_error_codes::ok;
|
||||
}
|
||||
|
||||
void handlers::handle_get_mount_location(const httplib::Request &req,
|
||||
httplib::Response &res) const {
|
||||
auto name = req.get_param_value("name");
|
||||
auto prov = provider_type_from_string(req.get_param_value("type"));
|
||||
|
||||
if (not data_directory_exists(prov, name)) {
|
||||
res.status = http_error_codes::not_found;
|
||||
return;
|
||||
}
|
||||
|
||||
res.set_content(
|
||||
nlohmann::json({
|
||||
{"Location", config_->get_mount_location(prov, name)},
|
||||
})
|
||||
.dump(),
|
||||
"application/json");
|
||||
res.status = http_error_codes::ok;
|
||||
}
|
||||
|
||||
void handlers::handle_get_mount_status(const httplib::Request &req,
|
||||
httplib::Response &res) const {
|
||||
auto name = req.get_param_value("name");
|
||||
auto prov = provider_type_from_string(req.get_param_value("type"));
|
||||
|
||||
if (not data_directory_exists(prov, name)) {
|
||||
res.status = http_error_codes::not_found;
|
||||
return;
|
||||
}
|
||||
|
||||
auto lines = launch_process(prov, name, {"-status"});
|
||||
|
||||
auto result = nlohmann::json::parse(utils::string::join(lines, '\n'));
|
||||
if (result.at("Location").get<std::string>().empty()) {
|
||||
result.at("Location") = config_->get_mount_location(prov, name);
|
||||
} else if (result.at("Active").get<bool>()) {
|
||||
config_->set_mount_location(prov, name,
|
||||
result.at("Location").get<std::string>());
|
||||
}
|
||||
|
||||
res.set_content(result.dump(), "application/json");
|
||||
res.status = http_error_codes::ok;
|
||||
}
|
||||
|
||||
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();
|
||||
settings[JSON_API_PASSWORD] = "";
|
||||
settings.erase(JSON_MOUNT_LOCATIONS);
|
||||
res.set_content(settings.dump(), "application/json");
|
||||
res.status = http_error_codes::ok;
|
||||
}
|
||||
|
||||
void handlers::handle_post_add_mount(const httplib::Request &req,
|
||||
httplib::Response &res) const {
|
||||
auto name = req.get_param_value("name");
|
||||
auto prov = provider_type_from_string(req.get_param_value("type"));
|
||||
if (data_directory_exists(prov, name)) {
|
||||
res.status = http_error_codes::ok;
|
||||
return;
|
||||
}
|
||||
|
||||
auto cfg = nlohmann::json::parse(req.get_param_value("config"));
|
||||
|
||||
std::map<std::string, std::string> values{};
|
||||
for (const auto &[key, value] : cfg.items()) {
|
||||
if (value.is_object()) {
|
||||
for (const auto &[key2, value2] : value.items()) {
|
||||
auto sub_key = fmt::format("{}.{}", key, key2);
|
||||
auto skip{false};
|
||||
auto decrypted = decrypt_value(
|
||||
config_, sub_key, value2.template get<std::string>(), skip);
|
||||
if (skip) {
|
||||
continue;
|
||||
}
|
||||
values[sub_key] = decrypted;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
void handlers::handle_post_mount(const httplib::Request &req,
|
||||
httplib::Response &res) {
|
||||
auto name = req.get_param_value("name");
|
||||
auto prov = provider_type_from_string(req.get_param_value("type"));
|
||||
|
||||
if (not data_directory_exists(prov, name)) {
|
||||
res.status = http_error_codes::not_found;
|
||||
return;
|
||||
}
|
||||
|
||||
auto location = utils::path::absolute(req.get_param_value("location"));
|
||||
auto unmount = utils::string::to_bool(req.get_param_value("unmount"));
|
||||
|
||||
if (unmount) {
|
||||
launch_process(prov, name, {"-unmount"});
|
||||
} else {
|
||||
#if defined(_WIN32)
|
||||
if (utils::file::directory{location}.exists()) {
|
||||
#else // !defined(_WIN32)
|
||||
if (not utils::file::directory{location}.exists()) {
|
||||
#endif // defined(_WIN32)
|
||||
config_->set_mount_location(prov, name, "");
|
||||
res.status = http_error_codes::internal_error;
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
void handlers::handle_put_set_value_by_name(const httplib::Request &req,
|
||||
httplib::Response &res) const {
|
||||
auto name = req.get_param_value("name");
|
||||
auto prov = provider_type_from_string(req.get_param_value("type"));
|
||||
|
||||
if (not data_directory_exists(prov, name)) {
|
||||
res.status = http_error_codes::not_found;
|
||||
return;
|
||||
}
|
||||
|
||||
auto key = req.get_param_value("key");
|
||||
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);
|
||||
}
|
||||
|
||||
res.status = http_error_codes::ok;
|
||||
}
|
||||
|
||||
void handlers::handle_put_settings(const httplib::Request &req,
|
||||
httplib::Response &res) const {
|
||||
auto data = nlohmann::json::parse(req.get_param_value("data"));
|
||||
|
||||
if (data.contains(JSON_API_PASSWORD)) {
|
||||
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)) {
|
||||
config_->set_api_port(
|
||||
utils::string::to_uint16(data.at(JSON_API_PORT).get<std::string>()));
|
||||
}
|
||||
|
||||
if (data.contains(JSON_API_USER)) {
|
||||
config_->set_api_user(data.at(JSON_API_USER).get<std::string>());
|
||||
}
|
||||
|
||||
res.status = http_error_codes::ok;
|
||||
}
|
||||
|
||||
auto handlers::launch_process(provider_type prov, std::string_view name,
|
||||
std::vector<std::string> args,
|
||||
bool background) const
|
||||
-> std::vector<std::string> {
|
||||
void handlers::handle_get_mount_status(auto &&req, auto &&res) const {
|
||||
REPERTORY_USES_FUNCTION_NAME();
|
||||
|
||||
auto type = req.get_param_value("type");
|
||||
auto prov = provider_type_from_string(type);
|
||||
auto status_name = app_config::get_provider_display_name(prov);
|
||||
auto name = req.get_param_value("name");
|
||||
|
||||
switch (prov) {
|
||||
case provider_type::encrypt:
|
||||
args.insert(args.begin(), "-en");
|
||||
args.insert(std::next(args.begin()), "-na");
|
||||
args.insert(std::next(args.begin(), 2U), std::string{name});
|
||||
break;
|
||||
|
||||
case provider_type::remote: {
|
||||
auto parts = utils::string::split(name, '_', false);
|
||||
args.insert(args.begin(), "-rm");
|
||||
args.insert(std::next(args.begin()),
|
||||
fmt::format("{}:{}", parts.at(0U), parts.at(1U)));
|
||||
status_name = fmt::format("{}{}:{}", status_name, parts[0U], parts[1U]);
|
||||
} break;
|
||||
|
||||
case provider_type::s3:
|
||||
args.insert(args.begin(), "-s3");
|
||||
args.insert(std::next(args.begin()), "-na");
|
||||
args.insert(std::next(args.begin(), 2U), std::string{name});
|
||||
break;
|
||||
|
||||
case provider_type::sia:
|
||||
args.insert(args.begin(), "-na");
|
||||
args.insert(std::next(args.begin()), std::string{name});
|
||||
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,
|
||||
});
|
||||
throw utils::error::create_exception(
|
||||
function_name, {
|
||||
fmt::format("`{}` is not supported", name),
|
||||
});
|
||||
}
|
||||
|
||||
unique_mutex_lock lock(mtx_);
|
||||
auto &inst_mtx = mtx_lookup_[fmt::format(
|
||||
"{}-{}", name, app_config::get_provider_name(prov))];
|
||||
lock.unlock();
|
||||
auto lines = handlers::read_process(prov, name, "-status");
|
||||
|
||||
recur_mutex_lock inst_lock(inst_mtx);
|
||||
if (background) {
|
||||
#if defined(_WIN32)
|
||||
std::array<char, MAX_PATH + 1U> path{};
|
||||
::GetSystemDirectoryA(path.data(), path.size());
|
||||
auto result =
|
||||
nlohmann::json::parse(utils::string::join(lines, '\n')).at(status_name);
|
||||
res.set_content(result.dump(), "application/json");
|
||||
res.status = http_error_codes::ok;
|
||||
}
|
||||
|
||||
args.insert(args.begin(), utils::path::combine(path.data(), {"cmd.exe"}));
|
||||
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)
|
||||
auto handlers::read_process(provider_type prov, std::string_view name,
|
||||
std::string_view command)
|
||||
-> std::vector<std::string> {
|
||||
REPERTORY_USES_FUNCTION_NAME();
|
||||
|
||||
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);
|
||||
std::string str_type;
|
||||
switch (prov) {
|
||||
case provider_type::encrypt:
|
||||
str_type = "-en";
|
||||
break;
|
||||
|
||||
#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);
|
||||
case provider_type::remote: {
|
||||
auto parts = utils::string::split(name, '_', false);
|
||||
str_type = fmt::format("-rm {}:{}", parts[0U], parts[1U]);
|
||||
} break;
|
||||
|
||||
execvp(exec_args.at(0U), const_cast<char *const *>(exec_args.data()));
|
||||
} else {
|
||||
signal(SIGCHLD, SIG_IGN);
|
||||
}
|
||||
#endif // defined(_WIN32)
|
||||
case provider_type::s3:
|
||||
str_type = fmt::format("-s3 -na {}", name);
|
||||
break;
|
||||
|
||||
case provider_type::sia:
|
||||
str_type = fmt::format("-na {}", name);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw utils::error::create_exception(
|
||||
function_name, {
|
||||
fmt::format("`{}` is not supported", name),
|
||||
});
|
||||
}
|
||||
|
||||
auto cmd_line = fmt::format("repertory {} {}", str_type, command);
|
||||
|
||||
auto *pipe = popen(cmd_line.c_str(), "r");
|
||||
if (pipe == nullptr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
boost::process::ipstream out;
|
||||
boost::process::child proc(repertory_binary_, boost::process::args(args),
|
||||
boost::process::std_out > out);
|
||||
|
||||
std::string data;
|
||||
std::string line;
|
||||
while (out && std::getline(out, line)) {
|
||||
data += line + "\n";
|
||||
std::array<char, 1024U> buffer{};
|
||||
while (feof(pipe) == 0) {
|
||||
while (fgets(buffer.data(), buffer.size(), pipe) != nullptr) {
|
||||
data += buffer.data();
|
||||
}
|
||||
}
|
||||
pclose(pipe);
|
||||
|
||||
return utils::string::split(utils::string::replace(data, "\r", ""), '\n',
|
||||
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,
|
||||
std::string_view key,
|
||||
std::string_view value) const {
|
||||
std::vector<std::string> args;
|
||||
args.emplace_back("-set");
|
||||
args.emplace_back(key);
|
||||
args.emplace_back(value);
|
||||
launch_process(prov, name, args, false);
|
||||
}
|
||||
} // namespace repertory::ui
|
||||
|
@ -1,202 +0,0 @@
|
||||
/*
|
||||
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 "ui/mgmt_app_config.hpp"
|
||||
|
||||
#include "app_config.hpp"
|
||||
#include "utils/error_utils.hpp"
|
||||
#include "utils/file.hpp"
|
||||
#include "utils/path.hpp"
|
||||
#include "utils/unix.hpp"
|
||||
#include "utils/windows.hpp"
|
||||
|
||||
namespace {
|
||||
[[nodiscard]] auto from_json(const nlohmann::json &json)
|
||||
-> std::unordered_map<repertory::provider_type,
|
||||
std::unordered_map<std::string, std::string>> {
|
||||
std::unordered_map<repertory::provider_type,
|
||||
std::unordered_map<std::string, std::string>>
|
||||
map_of_maps{
|
||||
{repertory::provider_type::encrypt, nlohmann::json::object()},
|
||||
{repertory::provider_type::remote, nlohmann::json::object()},
|
||||
{repertory::provider_type::s3, nlohmann::json::object()},
|
||||
{repertory::provider_type::sia, nlohmann::json::object()},
|
||||
};
|
||||
|
||||
if (json.is_null() || json.empty()) {
|
||||
return map_of_maps;
|
||||
}
|
||||
|
||||
for (auto &[prov, map] : map_of_maps) {
|
||||
auto prov_str = repertory::provider_type_to_string(prov);
|
||||
if (!json.contains(prov_str)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const auto &[key, value] : json.at(prov_str).items()) {
|
||||
map[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return map_of_maps;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto map_to_json(const auto &map_of_maps) -> nlohmann::json {
|
||||
auto json = nlohmann::json::object();
|
||||
for (const auto &[prov, map] : map_of_maps) {
|
||||
for (const auto &[key, value] : map) {
|
||||
json[repertory::provider_type_to_string(prov)][key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return json;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace repertory::ui {
|
||||
mgmt_app_config::mgmt_app_config() {
|
||||
REPERTORY_USES_FUNCTION_NAME();
|
||||
|
||||
auto config_file =
|
||||
utils::path::combine(app_config::get_root_data_directory(), {"ui.json"});
|
||||
|
||||
try {
|
||||
if (not utils::file::directory{app_config::get_root_data_directory()}
|
||||
.create_directory()) {
|
||||
throw utils::error::create_exception(
|
||||
function_name, {
|
||||
"failed to create directory",
|
||||
app_config::get_root_data_directory(),
|
||||
});
|
||||
}
|
||||
|
||||
nlohmann::json data;
|
||||
if (utils::file::read_json_file(config_file, data)) {
|
||||
api_password_ = data.at(JSON_API_PASSWORD).get<std::string>();
|
||||
api_port_ = data.at(JSON_API_PORT).get<std::uint16_t>();
|
||||
api_user_ = data.at(JSON_API_USER).get<std::string>();
|
||||
locations_ = from_json(data.at(JSON_MOUNT_LOCATIONS));
|
||||
return;
|
||||
}
|
||||
|
||||
utils::error::raise_error(
|
||||
function_name, utils::get_last_error_code(),
|
||||
fmt::format("failed to read file|{}", config_file));
|
||||
save();
|
||||
} catch (const std::exception &ex) {
|
||||
utils::error::raise_error(
|
||||
function_name, ex, fmt::format("failed to read file|{}", config_file));
|
||||
}
|
||||
}
|
||||
|
||||
auto mgmt_app_config::get_mount_location(provider_type prov,
|
||||
std::string_view name) const
|
||||
-> std::string {
|
||||
recur_mutex_lock lock(mtx_);
|
||||
if (locations_.contains(prov) &&
|
||||
locations_.at(prov).contains(std::string{name})) {
|
||||
return locations_.at(prov).at(std::string{name});
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
void mgmt_app_config::save() const {
|
||||
REPERTORY_USES_FUNCTION_NAME();
|
||||
|
||||
auto config_file =
|
||||
utils::path::combine(app_config::get_root_data_directory(), {"ui.json"});
|
||||
|
||||
try {
|
||||
if (not utils::file::directory{app_config::get_root_data_directory()}
|
||||
.create_directory()) {
|
||||
utils::error::raise_error(
|
||||
function_name, fmt::format("failed to create directory|{}",
|
||||
app_config::get_root_data_directory()));
|
||||
return;
|
||||
}
|
||||
|
||||
if (utils::file::write_json_file(config_file, to_json())) {
|
||||
return;
|
||||
}
|
||||
|
||||
utils::error::raise_error(
|
||||
function_name, utils::get_last_error_code(),
|
||||
fmt::format("failed to save file|{}", config_file));
|
||||
} catch (const std::exception &ex) {
|
||||
utils::error::raise_error(
|
||||
function_name, ex, fmt::format("failed to save file|{}", config_file));
|
||||
}
|
||||
}
|
||||
|
||||
void mgmt_app_config::set_api_password(std::string_view api_password) {
|
||||
if (api_password_ == std::string{api_password}) {
|
||||
return;
|
||||
}
|
||||
|
||||
api_password_ = std::string{api_password};
|
||||
save();
|
||||
}
|
||||
|
||||
void mgmt_app_config::set_api_port(std::uint16_t api_port) {
|
||||
if (api_port_ == api_port) {
|
||||
return;
|
||||
}
|
||||
|
||||
api_port_ = api_port;
|
||||
save();
|
||||
}
|
||||
|
||||
void mgmt_app_config::set_api_user(std::string_view api_user) {
|
||||
if (api_user_ == std::string{api_user}) {
|
||||
return;
|
||||
}
|
||||
|
||||
api_user_ = std::string{api_user};
|
||||
save();
|
||||
}
|
||||
|
||||
void mgmt_app_config::set_mount_location(provider_type prov,
|
||||
std::string_view name,
|
||||
std::string_view location) {
|
||||
if (name.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
recur_mutex_lock lock(mtx_);
|
||||
if (locations_[prov][std::string{name}] == std::string{location}) {
|
||||
return;
|
||||
}
|
||||
|
||||
locations_[prov][std::string{name}] = std::string{location};
|
||||
|
||||
save();
|
||||
}
|
||||
|
||||
auto mgmt_app_config::to_json() const -> nlohmann::json {
|
||||
nlohmann::json data;
|
||||
data[JSON_API_PASSWORD] = api_password_;
|
||||
data[JSON_API_PORT] = api_port_;
|
||||
data[JSON_API_USER] = api_user_;
|
||||
data[JSON_MOUNT_LOCATIONS] = map_to_json(locations_);
|
||||
return data;
|
||||
}
|
||||
} // namespace repertory::ui
|
@ -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) {
|
||||
json json_defaults = {
|
||||
{JSON_API_PORT, app_config::default_rpc_port()},
|
||||
{JSON_API_PORT, app_config::default_rpc_port(prov)},
|
||||
{JSON_API_USER, std::string{REPERTORY}},
|
||||
{JSON_DOWNLOAD_TIMEOUT_SECS, default_download_timeout_secs},
|
||||
{JSON_DATABASE_TYPE, database_type::rocksdb},
|
||||
@ -185,9 +185,9 @@ static void defaults_tests(const json &json_data, provider_type prov) {
|
||||
}
|
||||
|
||||
fmt::println("testing default|{}-{}", app_config::get_provider_name(prov),
|
||||
JSON_API_PASSWORD);
|
||||
ASSERT_EQ(std::size_t(default_api_password_size),
|
||||
json_data.at(JSON_API_PASSWORD).get<std::string>().size());
|
||||
JSON_API_AUTH);
|
||||
ASSERT_EQ(std::size_t(default_api_auth_size),
|
||||
json_data.at(JSON_API_AUTH).get<std::string>().size());
|
||||
for (const auto &[key, value] : json_defaults.items()) {
|
||||
fmt::println("testing default|{}-{}", app_config::get_provider_name(prov),
|
||||
key);
|
||||
@ -216,11 +216,11 @@ static void common_tests(app_config &config, provider_type prov) {
|
||||
ASSERT_EQ(config.get_provider_type(), prov);
|
||||
|
||||
std::map<std::string_view, std::function<void(app_config &)>> methods{
|
||||
{JSON_API_PASSWORD,
|
||||
{JSON_API_AUTH,
|
||||
[](app_config &cfg) {
|
||||
test_getter_setter(cfg, &app_config::get_api_password,
|
||||
&app_config::set_api_password, "", "auth",
|
||||
JSON_API_PASSWORD, "auth2");
|
||||
test_getter_setter(cfg, &app_config::get_api_auth,
|
||||
&app_config::set_api_auth, "", "auth",
|
||||
JSON_API_AUTH, "auth2");
|
||||
}},
|
||||
{JSON_API_PORT,
|
||||
[](app_config &cfg) {
|
||||
|
@ -1,198 +0,0 @@
|
||||
/*
|
||||
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,16 +62,13 @@ TEST(lock_data_test, set_and_unset_mount_state) {
|
||||
|
||||
json mount_state;
|
||||
EXPECT_TRUE(l.get_mount_state(mount_state));
|
||||
|
||||
EXPECT_STREQ(R"({"Active":true,"Location":"C:","PID":99})",
|
||||
mount_state.dump().c_str());
|
||||
|
||||
EXPECT_TRUE(l2.get_mount_state(mount_state));
|
||||
mount_state["Sia1"].dump().c_str());
|
||||
EXPECT_STREQ(R"({"Active":true,"Location":"D:","PID":97})",
|
||||
mount_state.dump().c_str());
|
||||
|
||||
EXPECT_TRUE(l3.get_mount_state(mount_state));
|
||||
mount_state["Remote1"].dump().c_str());
|
||||
EXPECT_STREQ(R"({"Active":true,"Location":"E:","PID":96})",
|
||||
mount_state.dump().c_str());
|
||||
mount_state["Remote2"].dump().c_str());
|
||||
|
||||
EXPECT_TRUE(l.set_mount_state(false, "C:", 99));
|
||||
EXPECT_TRUE(l2.set_mount_state(false, "D:", 98));
|
||||
@ -79,15 +76,11 @@ TEST(lock_data_test, set_and_unset_mount_state) {
|
||||
|
||||
EXPECT_TRUE(l.get_mount_state(mount_state));
|
||||
EXPECT_STREQ(R"({"Active":false,"Location":"","PID":-1})",
|
||||
mount_state.dump().c_str());
|
||||
|
||||
EXPECT_TRUE(l2.get_mount_state(mount_state));
|
||||
mount_state["Sia1"].dump().c_str());
|
||||
EXPECT_STREQ(R"({"Active":false,"Location":"","PID":-1})",
|
||||
mount_state.dump().c_str());
|
||||
|
||||
EXPECT_TRUE(l3.get_mount_state(mount_state));
|
||||
mount_state["Remote1"].dump().c_str());
|
||||
EXPECT_STREQ(R"({"Active":false,"Location":"","PID":-1})",
|
||||
mount_state.dump().c_str());
|
||||
mount_state["Remote2"].dump().c_str());
|
||||
}
|
||||
#else
|
||||
TEST(lock_data_test, set_and_unset_mount_state) {
|
||||
@ -98,13 +91,14 @@ TEST(lock_data_test, set_and_unset_mount_state) {
|
||||
EXPECT_TRUE(l.get_mount_state(mount_state));
|
||||
|
||||
EXPECT_STREQ(R"({"Active":true,"Location":"/mnt/1","PID":99})",
|
||||
mount_state.dump().c_str());
|
||||
mount_state["Sia1"].dump().c_str());
|
||||
|
||||
EXPECT_TRUE(l.set_mount_state(false, "/mnt/1", 99));
|
||||
|
||||
EXPECT_TRUE(l.get_mount_state(mount_state));
|
||||
|
||||
EXPECT_STREQ(R"({"Active":false,"Location":"","PID":-1})",
|
||||
mount_state.dump().c_str());
|
||||
mount_state["Sia1"].dump().c_str());
|
||||
}
|
||||
#endif
|
||||
} // namespace repertory
|
||||
|
@ -15,7 +15,6 @@ if [ "${PROJECT_IS_MINGW}" == "1" ] && [ "${PROJECT_STATIC_LINK}" == "OFF" ]; th
|
||||
/mingw64/bin/libstdc++-6.dll
|
||||
/mingw64/bin/libwinpthread-1.dll
|
||||
/mingw64/bin/libzlib1.dll
|
||||
/mingw64/bin/libzstd.dll
|
||||
/mingw64/bin/zlib1.dll
|
||||
)
|
||||
|
||||
|
@ -39,7 +39,6 @@ RELEASE=$(grep PROJECT_RELEASE_ITER= ./config.sh | sed s/PROJECT_RELEASE_ITER=//
|
||||
popd
|
||||
|
||||
if [ "${BRANCH}" == "master" ] || [ "${BRANCH}" == "alpha" ] ||
|
||||
[ "${BRANCH}" == "main" ] || [ "${BRANCH}" == "release" ] ||
|
||||
[ "${BRANCH}" == "beta" ] || [ "${BRANCH}" == "rc" ]; then
|
||||
DEST_DIR=${DEST_DIR}/${RELEASE}
|
||||
elif [[ ${BRANCH} = *'-alpha-'* ]] || [[ ${BRANCH} = *'-beta-'* ]] ||
|
||||
|
BIN
support/3rd_party/boost_1_87_0.tar.gz
vendored
BIN
support/3rd_party/boost_1_87_0.tar.gz
vendored
Binary file not shown.
BIN
support/3rd_party/cpp-httplib-0.19.0.tar.gz
vendored
BIN
support/3rd_party/cpp-httplib-0.19.0.tar.gz
vendored
Binary file not shown.
BIN
support/3rd_party/curl-8.12.1.tar.gz
vendored
BIN
support/3rd_party/curl-8.12.1.tar.gz
vendored
Binary file not shown.
BIN
support/3rd_party/googletest-1.15.2.tar.gz
vendored
BIN
support/3rd_party/googletest-1.15.2.tar.gz
vendored
Binary file not shown.
BIN
support/3rd_party/googletest-1.16.0.tar.gz
vendored
BIN
support/3rd_party/googletest-1.16.0.tar.gz
vendored
Binary file not shown.
BIN
support/3rd_party/json-3.11.3.tar.gz
vendored
BIN
support/3rd_party/json-3.11.3.tar.gz
vendored
Binary file not shown.
BIN
support/3rd_party/libsodium-1.0.20.tar.gz
vendored
BIN
support/3rd_party/libsodium-1.0.20.tar.gz
vendored
Binary file not shown.
BIN
support/3rd_party/mingw64/binutils-2.43.tar.xz
vendored
BIN
support/3rd_party/mingw64/binutils-2.43.tar.xz
vendored
Binary file not shown.
BIN
support/3rd_party/mingw64/expat-2.6.4.tar.gz
vendored
BIN
support/3rd_party/mingw64/expat-2.6.4.tar.gz
vendored
Binary file not shown.
BIN
support/3rd_party/mingw64/gcc-14.2.0.tar.gz
vendored
BIN
support/3rd_party/mingw64/gcc-14.2.0.tar.gz
vendored
Binary file not shown.
BIN
support/3rd_party/mingw64/icu-release-76-1.tar.gz
vendored
BIN
support/3rd_party/mingw64/icu-release-76-1.tar.gz
vendored
Binary file not shown.
BIN
support/3rd_party/mingw64/pkg-config-0.29.2.tar.gz
vendored
BIN
support/3rd_party/mingw64/pkg-config-0.29.2.tar.gz
vendored
Binary file not shown.
BIN
support/3rd_party/mingw64/zlib-1.3.1.tar.gz
vendored
BIN
support/3rd_party/mingw64/zlib-1.3.1.tar.gz
vendored
Binary file not shown.
BIN
support/3rd_party/openssl-3.4.1.tar.gz
vendored
BIN
support/3rd_party/openssl-3.4.1.tar.gz
vendored
Binary file not shown.
BIN
support/3rd_party/pugixml-1.15.tar.gz
vendored
BIN
support/3rd_party/pugixml-1.15.tar.gz
vendored
Binary file not shown.
BIN
support/3rd_party/rocksdb-9.10.0.tar.gz
vendored
BIN
support/3rd_party/rocksdb-9.10.0.tar.gz
vendored
Binary file not shown.
BIN
support/3rd_party/spdlog-1.15.1.tar.gz
vendored
BIN
support/3rd_party/spdlog-1.15.1.tar.gz
vendored
Binary file not shown.
BIN
support/3rd_party/sqlite-amalgamation-3490100.zip
vendored
BIN
support/3rd_party/sqlite-amalgamation-3490100.zip
vendored
Binary file not shown.
BIN
support/3rd_party/stduuid-1.2.3.tar.gz
vendored
BIN
support/3rd_party/stduuid-1.2.3.tar.gz
vendored
Binary file not shown.
@ -25,8 +25,8 @@
|
||||
#include "utils/string.hpp"
|
||||
|
||||
namespace repertory::utils {
|
||||
auto compare_version_strings(std::string version1, std::string version2)
|
||||
-> std::int32_t {
|
||||
auto compare_version_strings(std::string version1,
|
||||
std::string version2) -> std::int32_t {
|
||||
|
||||
if (utils::string::contains(version1, "-")) {
|
||||
version1 = utils::string::split(version1, '-', true)[0U];
|
||||
@ -131,46 +131,23 @@ auto get_next_available_port(std::uint16_t first_port,
|
||||
using ip::tcp;
|
||||
|
||||
boost::system::error_code error_code{};
|
||||
|
||||
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;
|
||||
}
|
||||
while (first_port != 0U) {
|
||||
io_context ctx{};
|
||||
tcp::acceptor acceptor(ctx);
|
||||
acceptor.open(tcp::v4(), error_code) ||
|
||||
acceptor.bind({tcp::v4(), first_port}, error_code);
|
||||
if (not error_code) {
|
||||
break;
|
||||
}
|
||||
|
||||
{
|
||||
io_context ctx{};
|
||||
tcp::acceptor acceptor(ctx);
|
||||
acceptor.open(tcp::v4(), error_code);
|
||||
if (error_code) {
|
||||
++check_port;
|
||||
continue;
|
||||
}
|
||||
acceptor.set_option(boost::asio::ip::tcp::acceptor::linger(true, 0));
|
||||
acceptor.bind({tcp::v4(), static_cast<std::uint16_t>(check_port)},
|
||||
error_code);
|
||||
if (error_code) {
|
||||
++check_port;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
available_port = static_cast<std::uint16_t>(check_port);
|
||||
return true;
|
||||
++first_port;
|
||||
}
|
||||
|
||||
return false;
|
||||
if (not error_code) {
|
||||
available_port = first_port;
|
||||
}
|
||||
|
||||
return not error_code;
|
||||
}
|
||||
#endif // defined(PROJECT_ENABLE_BOOST)
|
||||
|
||||
|
@ -1,8 +1,2 @@
|
||||
autofocus
|
||||
canvaskit
|
||||
cupertino
|
||||
cupertinoicons
|
||||
fromargb
|
||||
onetwothree
|
||||
renterd
|
||||
rocksdb
|
||||
cupertinoicons
|
1
web/repertory/.gitignore
vendored
1
web/repertory/.gitignore
vendored
@ -44,4 +44,3 @@ app.*.map.json
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
.flutter-companion
|
||||
pubspec.lock
|
||||
|
@ -1,24 +1 @@
|
||||
import 'package:flutter/material.dart' show GlobalKey, NavigatorState;
|
||||
import 'package:sodium_libs/sodium_libs.dart';
|
||||
|
||||
const addMountTitle = 'Add New Mount';
|
||||
const appLogonTitle = 'Repertory Portal Login';
|
||||
const appSettingsTitle = 'Portal Settings';
|
||||
const appTitle = 'Repertory Management Portal';
|
||||
const logonWidth = 300.0;
|
||||
const databaseTypeList = ['rocksdb', 'sqlite'];
|
||||
const downloadTypeList = ['default', 'direct', 'ring_buffer'];
|
||||
const eventLevelList = ['critical', 'error', 'warn', 'info', 'debug', 'trace'];
|
||||
const padding = 15.0;
|
||||
const protocolTypeList = ['http', 'https'];
|
||||
const providerTypeList = ['Encrypt', 'Remote', 'S3', 'Sia'];
|
||||
const ringBufferSizeList = ['128', '256', '512', '1024', '2048'];
|
||||
|
||||
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
Sodium? _sodium;
|
||||
void setSodium(Sodium sodium) {
|
||||
_sodium = sodium;
|
||||
}
|
||||
|
||||
Sodium get sodium => _sodium!;
|
||||
const String app_title = "Repertory Management Portal";
|
||||
|
6
web/repertory/lib/errors/duplicate_mount_exception.dart
Normal file
6
web/repertory/lib/errors/duplicate_mount_exception.dart
Normal file
@ -0,0 +1,6 @@
|
||||
class DuplicateMountException implements Exception {
|
||||
final String _name;
|
||||
const DuplicateMountException({required name}) : _name = name, super();
|
||||
|
||||
String get name => _name;
|
||||
}
|
@ -1,221 +1,10 @@
|
||||
import 'package:convert/convert.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
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);
|
||||
|
||||
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
|
||||
final Validator noRestrictedChars = (value) {
|
||||
return [
|
||||
'!',
|
||||
'"',
|
||||
'\$',
|
||||
'&',
|
||||
"'",
|
||||
'(',
|
||||
')',
|
||||
'*',
|
||||
';',
|
||||
'<',
|
||||
'>',
|
||||
'?',
|
||||
'[',
|
||||
']',
|
||||
'`',
|
||||
'{',
|
||||
'}',
|
||||
'|',
|
||||
].firstWhereOrNull((char) => value.contains(char)) ==
|
||||
null;
|
||||
};
|
||||
|
||||
// ignore: prefer_function_declarations_over_variables
|
||||
final Validator notEmptyValidator = (value) => value.isNotEmpty;
|
||||
|
||||
// ignore: prefer_function_declarations_over_variables
|
||||
final Validator portIsValid = (value) {
|
||||
int? intValue = int.tryParse(value);
|
||||
if (intValue == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (intValue > 0 && intValue < 65536);
|
||||
};
|
||||
|
||||
// ignore: prefer_function_declarations_over_variables
|
||||
final Validator trimNotEmptyValidator = (value) => value.trim().isNotEmpty;
|
||||
|
||||
createUriValidator<Validator>({host, port}) {
|
||||
return (value) =>
|
||||
Uri.tryParse('http://${host ?? value}:${port ?? value}/') != null;
|
||||
}
|
||||
|
||||
createHostNameOrIpValidators() => <Validator>[
|
||||
trimNotEmptyValidator,
|
||||
createUriValidator(port: 9000),
|
||||
];
|
||||
|
||||
Map<String, dynamic> createDefaultSettings(String mountType) {
|
||||
switch (mountType) {
|
||||
case 'Encrypt':
|
||||
return {
|
||||
'EncryptConfig': {'EncryptionToken': '', 'Path': ''},
|
||||
};
|
||||
case 'Remote':
|
||||
return {
|
||||
'RemoteConfig': {
|
||||
'ApiPort': 20000,
|
||||
'EncryptionToken': '',
|
||||
'HostNameOrIp': '',
|
||||
},
|
||||
};
|
||||
case 'S3':
|
||||
return {
|
||||
'S3Config': {
|
||||
'AccessKey': '',
|
||||
'Bucket': '',
|
||||
'Region': 'any',
|
||||
'SecretKey': '',
|
||||
'URL': '',
|
||||
'UsePathStyle': false,
|
||||
'UseRegionInURL': false,
|
||||
},
|
||||
};
|
||||
case 'Sia':
|
||||
return {
|
||||
'HostConfig': {
|
||||
'ApiPassword': '',
|
||||
'ApiPort': 9980,
|
||||
'HostNameOrIp': 'localhost',
|
||||
},
|
||||
'SiaConfig': {'Bucket': 'default'},
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
if (clear) {
|
||||
messenger.removeCurrentSnackBar();
|
||||
}
|
||||
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text(text, textAlign: TextAlign.center)),
|
||||
);
|
||||
}
|
||||
|
||||
String formatMountName(String type, String name) {
|
||||
if (type == 'remote') {
|
||||
return name.replaceAll('_', ':');
|
||||
if (type == "remote") {
|
||||
return name.replaceAll("_", ":");
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
String getBaseUri() {
|
||||
if (kDebugMode || !kIsWeb) {
|
||||
return 'http://127.0.0.1:30000';
|
||||
}
|
||||
|
||||
return Uri.base.origin;
|
||||
}
|
||||
|
||||
String? getSettingDescription(String settingPath) {
|
||||
switch (settingPath) {
|
||||
case 'ApiPassword':
|
||||
return "HTTP basic authentication password";
|
||||
case 'ApiUser':
|
||||
return "HTTP basic authentication user";
|
||||
case 'HostConfig.ApiPassword':
|
||||
return "RENTERD_API_PASSWORD";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
List<Validator> getSettingValidators(String settingPath) {
|
||||
switch (settingPath) {
|
||||
case 'ApiPassword':
|
||||
return [notEmptyValidator];
|
||||
case 'DatabaseType':
|
||||
return [(value) => constants.databaseTypeList.contains(value)];
|
||||
case 'PreferredDownloadType':
|
||||
return [(value) => constants.downloadTypeList.contains(value)];
|
||||
case 'EventLevel':
|
||||
return [(value) => constants.eventLevelList.contains(value)];
|
||||
case 'EncryptConfig.EncryptionToken':
|
||||
return [notEmptyValidator];
|
||||
case 'EncryptConfig.Path':
|
||||
return [trimNotEmptyValidator, noRestrictedChars];
|
||||
case 'HostConfig.ApiPassword':
|
||||
return [notEmptyValidator];
|
||||
case 'HostConfig.ApiPort':
|
||||
return [portIsValid];
|
||||
case 'HostConfig.HostNameOrIp':
|
||||
return createHostNameOrIpValidators();
|
||||
case 'HostConfig.Protocol':
|
||||
return [(value) => constants.protocolTypeList.contains(value)];
|
||||
case 'Path':
|
||||
return [trimNotEmptyValidator];
|
||||
case 'RemoteConfig.ApiPort':
|
||||
return [notEmptyValidator, portIsValid];
|
||||
case 'RemoteConfig.EncryptionToken':
|
||||
return [notEmptyValidator];
|
||||
case 'RemoteConfig.HostNameOrIp':
|
||||
return createHostNameOrIpValidators();
|
||||
case 'RemoteMount.ApiPort':
|
||||
return [notEmptyValidator, portIsValid];
|
||||
case 'RemoteMount.EncryptionToken':
|
||||
return [notEmptyValidator];
|
||||
case 'RemoteMount.HostNameOrIp':
|
||||
return createHostNameOrIpValidators();
|
||||
case 'RingBufferFileSize':
|
||||
return [(value) => constants.ringBufferSizeList.contains(value)];
|
||||
case 'S3Config.AccessKey':
|
||||
return [trimNotEmptyValidator];
|
||||
case 'S3Config.Bucket':
|
||||
return [trimNotEmptyValidator];
|
||||
case 'S3Config.SecretKey':
|
||||
return [trimNotEmptyValidator];
|
||||
case 'S3Config.URL':
|
||||
return [trimNotEmptyValidator, (value) => Uri.tryParse(value) != null];
|
||||
case 'SiaConfig.Bucket':
|
||||
return [trimNotEmptyValidator];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
String initialCaps(String txt) {
|
||||
if (txt.isEmpty) {
|
||||
return txt;
|
||||
@ -227,176 +16,3 @@ String initialCaps(String txt) {
|
||||
|
||||
return txt[0].toUpperCase() + txt.substring(1).toLowerCase();
|
||||
}
|
||||
|
||||
bool validateSettings(
|
||||
Map<String, dynamic> settings,
|
||||
List<String> failed, {
|
||||
String? rootKey,
|
||||
}) {
|
||||
settings.forEach((key, value) {
|
||||
final settingKey = rootKey == null ? key : '$rootKey.$key';
|
||||
if (value is Map<String, dynamic>) {
|
||||
validateSettings(value, failed, rootKey: settingKey);
|
||||
return;
|
||||
}
|
||||
|
||||
for (var validator in getSettingValidators(settingKey)) {
|
||||
if (validator(value.toString())) {
|
||||
continue;
|
||||
}
|
||||
failed.add(settingKey);
|
||||
}
|
||||
});
|
||||
|
||||
return failed.isEmpty;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> convertAllToString(
|
||||
Map<String, dynamic> settings,
|
||||
SecureKey key,
|
||||
) async {
|
||||
Future<Map<String, dynamic>> convert(Map<String, dynamic> settings) async {
|
||||
for (var entry in settings.entries) {
|
||||
if (entry.value is Map<String, dynamic>) {
|
||||
await convert(entry.value);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.key == 'ApiPassword' ||
|
||||
entry.key == 'EncryptionToken' ||
|
||||
entry.key == 'SecretKey') {
|
||||
if (entry.value.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
|
||||
settings[entry.key] = encryptValue(entry.value, key);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.value is String) {
|
||||
continue;
|
||||
}
|
||||
|
||||
settings[entry.key] = entry.value.toString();
|
||||
}
|
||||
|
||||
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),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -1,126 +1,50 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:repertory/constants.dart' as constants;
|
||||
import 'package:repertory/helpers.dart';
|
||||
import 'package:repertory/models/auth.dart';
|
||||
import 'package:repertory/models/mount.dart';
|
||||
import 'package:repertory/models/mount_list.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_settings_screen.dart';
|
||||
import 'package:repertory/screens/home_screen.dart';
|
||||
import 'package:sodium_libs/sodium_libs.dart' show SodiumInit;
|
||||
import 'package:repertory/widgets/mount_list_widget.dart';
|
||||
|
||||
void main() async {
|
||||
try {
|
||||
constants.setSodium(await SodiumInit.init());
|
||||
} catch (e) {
|
||||
debugPrint('$e');
|
||||
}
|
||||
|
||||
final auth = Auth();
|
||||
runApp(
|
||||
MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider(create: (_) => auth),
|
||||
ChangeNotifierProvider(create: (_) => MountList(auth)),
|
||||
],
|
||||
child: const MyApp(),
|
||||
),
|
||||
);
|
||||
void main() {
|
||||
runApp(const MyApp());
|
||||
}
|
||||
|
||||
class MyApp extends StatefulWidget {
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
State<MyApp> createState() => _MyAppState();
|
||||
}
|
||||
|
||||
class _MyAppState extends State<MyApp> {
|
||||
@override
|
||||
Widget build(context) {
|
||||
final snackBarTheme = SnackBarThemeData(
|
||||
width: MediaQuery.of(context).size.width * 0.50,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
);
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
navigatorKey: constants.navigatorKey,
|
||||
themeMode: ThemeMode.dark,
|
||||
darkTheme: ThemeData(
|
||||
useMaterial3: true,
|
||||
brightness: Brightness.dark,
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
brightness: Brightness.dark,
|
||||
onSurface: Colors.white70,
|
||||
seedColor: Colors.deepOrange,
|
||||
surface: Color.fromARGB(255, 32, 33, 36),
|
||||
surfaceContainerLow: Color.fromARGB(255, 41, 42, 45),
|
||||
),
|
||||
snackBarTheme: snackBarTheme,
|
||||
title: constants.app_title,
|
||||
theme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
|
||||
),
|
||||
title: constants.appTitle,
|
||||
initialRoute: '/auth',
|
||||
routes: {
|
||||
'/':
|
||||
(context) =>
|
||||
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) {
|
||||
if (settings.name != '/edit') {
|
||||
return null;
|
||||
}
|
||||
|
||||
final mount = settings.arguments as Mount;
|
||||
return MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return AuthCheck(
|
||||
child: EditMountScreen(
|
||||
mount: mount,
|
||||
title:
|
||||
'${mount.provider} [${formatMountName(mount.type, mount.name)}] Settings',
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
home: const MyHomePage(title: constants.app_title),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AuthCheck extends StatelessWidget {
|
||||
final Widget child;
|
||||
const AuthCheck({super.key, required this.child});
|
||||
class MyHomePage extends StatefulWidget {
|
||||
const MyHomePage({super.key, required this.title});
|
||||
|
||||
final String title;
|
||||
|
||||
@override
|
||||
State<MyHomePage> createState() => _MyHomePageState();
|
||||
}
|
||||
|
||||
class _MyHomePageState extends State<MyHomePage> {
|
||||
@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;
|
||||
},
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||
title: Text(widget.title),
|
||||
),
|
||||
body: ChangeNotifierProvider(
|
||||
create: (context) => MountList(),
|
||||
child: MountListWidget(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,64 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
@ -2,284 +2,54 @@ import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:repertory/helpers.dart';
|
||||
import 'package:repertory/models/auth.dart';
|
||||
import 'package:repertory/models/mount_list.dart';
|
||||
import 'package:repertory/types/mount_config.dart';
|
||||
|
||||
class Mount with ChangeNotifier {
|
||||
final Auth _auth;
|
||||
final MountConfig mountConfig;
|
||||
final MountList? _mountList;
|
||||
bool _isMounting = false;
|
||||
bool _isRefreshing = false;
|
||||
|
||||
Mount(this._auth, this.mountConfig, this._mountList, {isAdd = false}) {
|
||||
if (isAdd) {
|
||||
return;
|
||||
}
|
||||
Mount(this.mountConfig) {
|
||||
refresh();
|
||||
}
|
||||
|
||||
String? get bucket => mountConfig.bucket;
|
||||
String get id => '${type}_$name';
|
||||
bool? get mounted => mountConfig.mounted;
|
||||
String get name => mountConfig.name;
|
||||
String get path => mountConfig.path;
|
||||
String get provider => mountConfig.provider;
|
||||
IconData get state => mountConfig.state;
|
||||
String get type => mountConfig.type;
|
||||
|
||||
Future<void> _fetch() async {
|
||||
try {
|
||||
final auth = await _auth.createAuth();
|
||||
final response = await http.get(
|
||||
Uri.parse(
|
||||
Uri.encodeFull(
|
||||
'${getBaseUri()}/api/v1/mount?auth=$auth&name=$name&type=$type',
|
||||
),
|
||||
),
|
||||
);
|
||||
final response = await http.get(
|
||||
Uri.parse(
|
||||
Uri.encodeFull('${Uri.base.origin}/api/v1/mount?name=$name&type=$type'),
|
||||
),
|
||||
);
|
||||
|
||||
if (response.statusCode == 401) {
|
||||
_auth.logoff();
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.statusCode == 404) {
|
||||
_mountList?.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_isMounting) {
|
||||
return;
|
||||
}
|
||||
|
||||
mountConfig.updateSettings(jsonDecode(response.body));
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
debugPrint('$e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _fetchStatus() async {
|
||||
try {
|
||||
final auth = await _auth.createAuth();
|
||||
final response = await http.get(
|
||||
Uri.parse(
|
||||
Uri.encodeFull(
|
||||
'${getBaseUri()}/api/v1/mount_status?auth=$auth&name=$name&type=$type',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (response.statusCode == 401) {
|
||||
_auth.logoff();
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.statusCode == 404) {
|
||||
_mountList?.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_isMounting) {
|
||||
return;
|
||||
}
|
||||
|
||||
mountConfig.updateStatus(jsonDecode(response.body));
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
debugPrint('$e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setMountLocation(String location) async {
|
||||
try {
|
||||
mountConfig.path = location;
|
||||
|
||||
final auth = await _auth.createAuth();
|
||||
final response = await http.put(
|
||||
Uri.parse(
|
||||
Uri.encodeFull(
|
||||
'${getBaseUri()}/api/v1/mount_location?auth=$auth&name=$name&type=$type&location=$location',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (response.statusCode == 401) {
|
||||
_auth.logoff();
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.statusCode == 404) {
|
||||
_mountList?.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
return refresh();
|
||||
} catch (e) {
|
||||
debugPrint('$e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<String>> getAvailableLocations() async {
|
||||
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) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final location = jsonDecode(response.body)['Location'] as String;
|
||||
return location.trim().isEmpty ? null : location;
|
||||
} catch (e) {
|
||||
debugPrint('$e');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<bool> mount(bool unmount, {String? location}) async {
|
||||
try {
|
||||
_isMounting = true;
|
||||
|
||||
mountConfig.mounted = null;
|
||||
notifyListeners();
|
||||
|
||||
var count = 0;
|
||||
while (_isRefreshing && count++ < 10) {
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
}
|
||||
|
||||
final auth = await _auth.createAuth();
|
||||
final response = await http.post(
|
||||
Uri.parse(
|
||||
Uri.encodeFull(
|
||||
'${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) {
|
||||
_isMounting = false;
|
||||
_mountList?.reset();
|
||||
return true;
|
||||
}
|
||||
|
||||
final badLocation = (!unmount && response.statusCode == 500);
|
||||
if (badLocation) {
|
||||
mountConfig.path = "";
|
||||
}
|
||||
|
||||
await refresh(force: true);
|
||||
_isMounting = false;
|
||||
|
||||
return !badLocation;
|
||||
} catch (e) {
|
||||
debugPrint('$e');
|
||||
}
|
||||
|
||||
_isMounting = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<void> refresh({bool force = false}) async {
|
||||
if (!force && (_isMounting || _isRefreshing)) {
|
||||
if (response.statusCode != 200) {
|
||||
return;
|
||||
}
|
||||
|
||||
_isRefreshing = true;
|
||||
mountConfig.updateSettings(jsonDecode(response.body));
|
||||
|
||||
try {
|
||||
await _fetch();
|
||||
await _fetchStatus();
|
||||
} catch (e) {
|
||||
debugPrint('$e');
|
||||
}
|
||||
|
||||
_isRefreshing = false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> setValue(String key, String value) async {
|
||||
try {
|
||||
final auth = await _auth.createAuth();
|
||||
final response = await http.put(
|
||||
Uri.parse(
|
||||
Uri.encodeFull(
|
||||
'${getBaseUri()}/api/v1/set_value_by_name?auth=$auth&name=$name&type=$type&key=$key&value=$value',
|
||||
),
|
||||
Future<void> _fetchStatus() async {
|
||||
final response = await http.get(
|
||||
Uri.parse(
|
||||
Uri.encodeFull(
|
||||
'${Uri.base.origin}/api/v1/mount_status?name=$name&type=$type',
|
||||
),
|
||||
);
|
||||
),
|
||||
);
|
||||
|
||||
if (response.statusCode == 401) {
|
||||
_auth.logoff();
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.statusCode == 404) {
|
||||
_mountList?.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
return;
|
||||
}
|
||||
|
||||
return refresh();
|
||||
} catch (e) {
|
||||
debugPrint('$e');
|
||||
if (response.statusCode != 200) {
|
||||
return;
|
||||
}
|
||||
|
||||
mountConfig.updateStatus(jsonDecode(response.body));
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
await _fetch();
|
||||
return _fetchStatus();
|
||||
}
|
||||
}
|
||||
|
@ -1,100 +1,38 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart' show ModalRoute;
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:repertory/constants.dart' as constants;
|
||||
import 'package:repertory/helpers.dart';
|
||||
import 'package:repertory/models/auth.dart';
|
||||
import 'package:repertory/models/mount.dart';
|
||||
import 'package:repertory/errors/duplicate_mount_exception.dart';
|
||||
import 'package:repertory/types/mount_config.dart';
|
||||
|
||||
class MountList with ChangeNotifier {
|
||||
final Auth _auth;
|
||||
|
||||
MountList(this._auth) {
|
||||
_auth.mountList = this;
|
||||
_auth.addListener(() {
|
||||
if (_auth.authenticated) {
|
||||
_fetch();
|
||||
}
|
||||
});
|
||||
MountList() {
|
||||
_fetch();
|
||||
}
|
||||
|
||||
List<Mount> _mountList = [];
|
||||
List<MountConfig> _mountList = [];
|
||||
|
||||
Auth get auth => _auth;
|
||||
|
||||
UnmodifiableListView<Mount> get items =>
|
||||
UnmodifiableListView<Mount>(_mountList);
|
||||
|
||||
bool hasBucketName(String mountType, String bucket, {String? excludeName}) {
|
||||
final list = items.where(
|
||||
(item) => item.type.toLowerCase() == mountType.toLowerCase(),
|
||||
);
|
||||
|
||||
return (excludeName == null
|
||||
? list
|
||||
: list.whereNot(
|
||||
(item) =>
|
||||
item.name.toLowerCase() == excludeName.toLowerCase(),
|
||||
))
|
||||
.firstWhereOrNull((Mount item) {
|
||||
return item.bucket != null &&
|
||||
item.bucket!.toLowerCase() == bucket.toLowerCase();
|
||||
}) !=
|
||||
null;
|
||||
}
|
||||
|
||||
bool hasConfigName(String name) {
|
||||
return items.firstWhereOrNull(
|
||||
(item) => item.name.toLowerCase() == name.toLowerCase(),
|
||||
) !=
|
||||
null;
|
||||
}
|
||||
UnmodifiableListView get items => UnmodifiableListView(_mountList);
|
||||
|
||||
Future<void> _fetch() async {
|
||||
try {
|
||||
final auth = await _auth.createAuth();
|
||||
final response = await http.get(
|
||||
Uri.parse('${getBaseUri()}/api/v1/mount_list?auth=$auth'),
|
||||
);
|
||||
final response = await http.get(
|
||||
Uri.parse('${Uri.base.origin}/api/v1/mount_list'),
|
||||
);
|
||||
|
||||
if (response.statusCode == 401) {
|
||||
displayAuthError(_auth);
|
||||
_auth.logoff();
|
||||
return;
|
||||
}
|
||||
if (response.statusCode == 200) {
|
||||
List<MountConfig> nextList = [];
|
||||
|
||||
if (response.statusCode == 404) {
|
||||
reset();
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<Mount> nextList = [];
|
||||
|
||||
jsonDecode(response.body).forEach((type, value) {
|
||||
jsonDecode(response.body).forEach((key, value) {
|
||||
nextList.addAll(
|
||||
value
|
||||
.map(
|
||||
(name) =>
|
||||
Mount(_auth, MountConfig(type: type, name: name), this),
|
||||
)
|
||||
.toList(),
|
||||
value.map((name) => MountConfig.fromJson(key, name)).toList(),
|
||||
);
|
||||
});
|
||||
_sort(nextList);
|
||||
_mountList = nextList;
|
||||
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
debugPrint('$e');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@ -109,93 +47,21 @@ class MountList with ChangeNotifier {
|
||||
});
|
||||
}
|
||||
|
||||
Future<bool> add(
|
||||
String type,
|
||||
String name,
|
||||
Map<String, dynamic> settings,
|
||||
) 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.',
|
||||
);
|
||||
void add(MountConfig config) {
|
||||
var item = _mountList.firstWhereOrNull((cfg) => cfg.name == config.name);
|
||||
if (item != null) {
|
||||
throw DuplicateMountException(name: config.name);
|
||||
}
|
||||
|
||||
try {
|
||||
final auth = await _auth.createAuth();
|
||||
final map = await convertAllToString(
|
||||
jsonDecode(jsonEncode(settings)),
|
||||
_auth.key,
|
||||
);
|
||||
final response = await http.post(
|
||||
Uri.parse(
|
||||
Uri.encodeFull(
|
||||
'${getBaseUri()}/api/v1/add_mount?auth=$auth&name=$name&type=$type&config=${jsonEncode(map)}',
|
||||
),
|
||||
),
|
||||
);
|
||||
_mountList.add(config);
|
||||
_sort(_mountList);
|
||||
|
||||
switch (response.statusCode) {
|
||||
case 200:
|
||||
ret = true;
|
||||
break;
|
||||
case 401:
|
||||
displayAuthError(_auth);
|
||||
_auth.logoff();
|
||||
break;
|
||||
case 404:
|
||||
reset();
|
||||
break;
|
||||
default:
|
||||
displayError();
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('$e');
|
||||
displayError();
|
||||
}
|
||||
|
||||
if (ret) {
|
||||
await _fetch();
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
_mountList = [];
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> reset() async {
|
||||
if (constants.navigatorKey.currentContext == null ||
|
||||
ModalRoute.of(constants.navigatorKey.currentContext!)?.settings.name !=
|
||||
'/') {
|
||||
await constants.navigatorKey.currentState?.pushReplacementNamed('/');
|
||||
}
|
||||
void remove(String name) {
|
||||
_mountList.removeWhere((item) => item.name == name);
|
||||
|
||||
displayErrorMessage(
|
||||
constants.navigatorKey.currentContext!,
|
||||
'Mount removed externally. Reloading...',
|
||||
);
|
||||
|
||||
clear();
|
||||
|
||||
return _fetch();
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
@ -1,245 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:repertory/constants.dart' as constants;
|
||||
import 'package:repertory/helpers.dart';
|
||||
import 'package:repertory/models/auth.dart';
|
||||
import 'package:repertory/models/mount.dart';
|
||||
import 'package:repertory/models/mount_list.dart';
|
||||
import 'package:repertory/types/mount_config.dart';
|
||||
import 'package:repertory/widgets/mount_settings.dart';
|
||||
|
||||
class AddMountScreen extends StatefulWidget {
|
||||
final String title;
|
||||
const AddMountScreen({super.key, required this.title});
|
||||
|
||||
@override
|
||||
State<AddMountScreen> createState() => _AddMountScreenState();
|
||||
}
|
||||
|
||||
class _AddMountScreenState extends State<AddMountScreen> {
|
||||
Mount? _mount;
|
||||
final _mountNameController = TextEditingController();
|
||||
String _mountType = "";
|
||||
final Map<String, Map<String, dynamic>> _settings = {
|
||||
"": {},
|
||||
"Encrypt": createDefaultSettings("Encrypt"),
|
||||
"Remote": createDefaultSettings("Remote"),
|
||||
"S3": createDefaultSettings("S3"),
|
||||
"Sia": createDefaultSettings("Sia"),
|
||||
};
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||
title: Text(widget.title),
|
||||
actions: [
|
||||
Consumer<Auth>(
|
||||
builder: (context, auth, _) {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.logout),
|
||||
onPressed: () => auth.logoff(),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(constants.padding),
|
||||
child: Consumer<Auth>(
|
||||
builder: (context, auth, _) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Card(
|
||||
margin: EdgeInsets.all(0.0),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(constants.padding),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text('Provider Type'),
|
||||
const SizedBox(width: constants.padding),
|
||||
DropdownButton<String>(
|
||||
autofocus: true,
|
||||
value: _mountType,
|
||||
onChanged:
|
||||
(mountType) =>
|
||||
_handleChange(auth, mountType ?? ''),
|
||||
items:
|
||||
constants.providerTypeList
|
||||
.map<DropdownMenuItem<String>>((item) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: item,
|
||||
child: Text(item),
|
||||
);
|
||||
})
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_mountType.isNotEmpty && _mountType != 'Remote')
|
||||
const SizedBox(height: constants.padding),
|
||||
if (_mountType.isNotEmpty && _mountType != 'Remote')
|
||||
Card(
|
||||
margin: EdgeInsets.all(0.0),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(constants.padding),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text('Configuration Name'),
|
||||
const SizedBox(width: constants.padding),
|
||||
TextField(
|
||||
autofocus: true,
|
||||
controller: _mountNameController,
|
||||
keyboardType: TextInputType.text,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.deny(RegExp(r'\s')),
|
||||
],
|
||||
onChanged: (_) => _handleChange(auth, _mountType),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_mount != null) const SizedBox(height: constants.padding),
|
||||
if (_mount != null)
|
||||
Expanded(
|
||||
child: Card(
|
||||
margin: EdgeInsets.all(0.0),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(constants.padding),
|
||||
child: MountSettingsWidget(
|
||||
isAdd: true,
|
||||
mount: _mount!,
|
||||
settings: _settings[_mountType]!,
|
||||
showAdvanced: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_mount != null) const SizedBox(height: constants.padding),
|
||||
if (_mount != null)
|
||||
Row(
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
label: const Text('Add'),
|
||||
icon: const Icon(Icons.add),
|
||||
onPressed: () async {
|
||||
final mountList = Provider.of<MountList>(context);
|
||||
|
||||
List<String> failed = [];
|
||||
if (!validateSettings(
|
||||
_settings[_mountType]!,
|
||||
failed,
|
||||
)) {
|
||||
for (var key in failed) {
|
||||
displayErrorMessage(
|
||||
context,
|
||||
"Setting '$key' is not valid",
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (mountList.hasConfigName(
|
||||
_mountNameController.text,
|
||||
)) {
|
||||
return displayErrorMessage(
|
||||
context,
|
||||
"Configuration name '${_mountNameController.text}' already exists",
|
||||
);
|
||||
}
|
||||
|
||||
if (_mountType == "Sia" || _mountType == "S3") {
|
||||
final bucket =
|
||||
_settings[_mountType]!["${_mountType}Config"]["Bucket"]
|
||||
as String;
|
||||
if (mountList.hasBucketName(_mountType, bucket)) {
|
||||
return displayErrorMessage(
|
||||
context,
|
||||
"Bucket '$bucket' already exists",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final success = await mountList.add(
|
||||
_mountType,
|
||||
_mountType == 'Remote'
|
||||
? '${_settings[_mountType]!['RemoteConfig']['HostNameOrIp']}_${_settings[_mountType]!['RemoteConfig']['ApiPort']}'
|
||||
: _mountNameController.text,
|
||||
_settings[_mountType]!,
|
||||
);
|
||||
|
||||
if (!success || !context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
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(Auth auth, String mountType) {
|
||||
setState(() {
|
||||
final changed = _mountType != mountType;
|
||||
|
||||
_mountType = mountType;
|
||||
if (_mountType == 'Remote') {
|
||||
_mountNameController.text = 'remote';
|
||||
} else if (changed) {
|
||||
_mountNameController.text = mountType == 'Sia' ? 'default' : '';
|
||||
}
|
||||
|
||||
_mount =
|
||||
(_mountNameController.text.isEmpty)
|
||||
? null
|
||||
: Mount(
|
||||
auth,
|
||||
MountConfig(
|
||||
name: _mountNameController.text,
|
||||
settings: _settings[mountType],
|
||||
type: mountType,
|
||||
),
|
||||
null,
|
||||
isAdd: true,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void setState(VoidCallback fn) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
super.setState(fn);
|
||||
}
|
||||
}
|
@ -1,116 +0,0 @@
|
||||
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,70 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
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/widgets/mount_settings.dart';
|
||||
|
||||
class EditMountScreen extends StatefulWidget {
|
||||
final Mount mount;
|
||||
final String title;
|
||||
const EditMountScreen({super.key, required this.mount, required this.title});
|
||||
|
||||
@override
|
||||
State<EditMountScreen> createState() => _EditMountScreenState();
|
||||
}
|
||||
|
||||
class _EditMountScreenState extends State<EditMountScreen> {
|
||||
bool _showAdvanced = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||
title: Text(widget.title),
|
||||
actions: [
|
||||
Row(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Text("Advanced"),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
_showAdvanced ? Icons.toggle_on : Icons.toggle_off,
|
||||
),
|
||||
onPressed:
|
||||
() => setState(() => _showAdvanced = !_showAdvanced),
|
||||
),
|
||||
],
|
||||
),
|
||||
Consumer<Auth>(
|
||||
builder: (context, auth, _) {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.logout),
|
||||
onPressed: () => auth.logoff(),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
body: MountSettingsWidget(
|
||||
mount: widget.mount,
|
||||
settings: jsonDecode(jsonEncode(widget.mount.mountConfig.settings)),
|
||||
showAdvanced: _showAdvanced,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void setState(VoidCallback fn) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
super.setState(fn);
|
||||
}
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
import 'dart:convert' show jsonDecode, jsonEncode;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:repertory/helpers.dart';
|
||||
import 'package:repertory/models/auth.dart';
|
||||
import 'package:repertory/widgets/ui_settings.dart';
|
||||
|
||||
class EditSettingsScreen extends StatefulWidget {
|
||||
final String title;
|
||||
const EditSettingsScreen({super.key, required this.title});
|
||||
|
||||
@override
|
||||
State<EditSettingsScreen> createState() => _EditSettingsScreenState();
|
||||
}
|
||||
|
||||
class _EditSettingsScreenState extends State<EditSettingsScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||
title: Text(widget.title),
|
||||
actions: [
|
||||
Consumer<Auth>(
|
||||
builder: (context, auth, _) {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.logout),
|
||||
onPressed: () => auth.logoff(),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: FutureBuilder(
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
return UISettingsWidget(
|
||||
origSettings: jsonDecode(jsonEncode(snapshot.requireData)),
|
||||
settings: snapshot.requireData,
|
||||
showAdvanced: false,
|
||||
);
|
||||
},
|
||||
future: _grabSettings(),
|
||||
initialData: <String, dynamic>{},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _grabSettings() async {
|
||||
try {
|
||||
final authProvider = Provider.of<Auth>(context, listen: false);
|
||||
final auth = await authProvider.createAuth();
|
||||
final response = await http.get(
|
||||
Uri.parse('${getBaseUri()}/api/v1/settings?auth=$auth'),
|
||||
);
|
||||
|
||||
if (response.statusCode == 401) {
|
||||
authProvider.logoff();
|
||||
return {};
|
||||
}
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return jsonDecode(response.body);
|
||||
} catch (e) {
|
||||
debugPrint('$e');
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
@override
|
||||
void setState(VoidCallback fn) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
super.setState(fn);
|
||||
}
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:repertory/constants.dart' as constants;
|
||||
import 'package:repertory/models/auth.dart';
|
||||
import 'package:repertory/widgets/mount_list_widget.dart';
|
||||
|
||||
class HomeScreen extends StatefulWidget {
|
||||
final String title;
|
||||
const HomeScreen({super.key, required this.title});
|
||||
|
||||
@override
|
||||
State<HomeScreen> createState() => _HomeScreeState();
|
||||
}
|
||||
|
||||
class _HomeScreeState extends State<HomeScreen> {
|
||||
@override
|
||||
Widget build(context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||
leading: IconButton(
|
||||
onPressed: () => Navigator.pushNamed(context, '/settings'),
|
||||
icon: const Icon(Icons.storage),
|
||||
),
|
||||
title: Text(widget.title),
|
||||
actions: [
|
||||
Consumer<Auth>(
|
||||
builder: (context, auth, _) {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.logout),
|
||||
onPressed: () => auth.logoff(),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(constants.padding),
|
||||
child: MountListWidget(),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () => Navigator.pushNamed(context, '/add'),
|
||||
tooltip: 'Add Mount',
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void setState(VoidCallback fn) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
super.setState(fn);
|
||||
}
|
||||
}
|
@ -1,390 +0,0 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:repertory/constants.dart' as constants;
|
||||
import 'package:repertory/helpers.dart' show Validator, displayErrorMessage;
|
||||
import 'package:settings_ui/settings_ui.dart';
|
||||
|
||||
void createBooleanSetting(
|
||||
context,
|
||||
List<Widget> list,
|
||||
Map<String, dynamic> settings,
|
||||
String key,
|
||||
value,
|
||||
bool isAdvanced,
|
||||
bool showAdvanced,
|
||||
widget,
|
||||
Function setState, {
|
||||
String? description,
|
||||
IconData icon = Icons.quiz,
|
||||
}) {
|
||||
if (!isAdvanced || showAdvanced) {
|
||||
list.add(
|
||||
SettingsTile.switchTile(
|
||||
leading: Icon(icon),
|
||||
title: createSettingTitle(context, key, description),
|
||||
initialValue: (value as bool),
|
||||
onPressed: (_) => setState(() => settings[key] = !value),
|
||||
onToggle: (bool nextValue) {
|
||||
setState(() => settings[key] = nextValue);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void createIntListSetting(
|
||||
context,
|
||||
List<Widget> list,
|
||||
Map<String, dynamic> settings,
|
||||
String key,
|
||||
value,
|
||||
List<String> valueList,
|
||||
defaultValue,
|
||||
IconData icon,
|
||||
bool isAdvanced,
|
||||
bool showAdvanced,
|
||||
widget,
|
||||
Function setState, {
|
||||
String? description,
|
||||
}) {
|
||||
if (!isAdvanced || widget.showAdvanced) {
|
||||
list.add(
|
||||
SettingsTile.navigation(
|
||||
title: createSettingTitle(context, key, description),
|
||||
leading: Icon(icon),
|
||||
value: DropdownButton<String>(
|
||||
value: value.toString(),
|
||||
onChanged: (newValue) {
|
||||
setState(
|
||||
() =>
|
||||
settings[key] = int.parse(
|
||||
newValue ?? defaultValue.toString(),
|
||||
),
|
||||
);
|
||||
},
|
||||
items:
|
||||
valueList.map<DropdownMenuItem<String>>((item) {
|
||||
return DropdownMenuItem<String>(value: item, child: Text(item));
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void createIntSetting(
|
||||
context,
|
||||
List<Widget> list,
|
||||
Map<String, dynamic> settings,
|
||||
String key,
|
||||
value,
|
||||
bool isAdvanced,
|
||||
bool showAdvanced,
|
||||
widget,
|
||||
Function setState, {
|
||||
String? description,
|
||||
IconData icon = Icons.onetwothree,
|
||||
List<Validator> validators = const [],
|
||||
}) {
|
||||
if (!isAdvanced || widget.showAdvanced) {
|
||||
list.add(
|
||||
SettingsTile.navigation(
|
||||
leading: Icon(icon),
|
||||
title: createSettingTitle(context, key, description),
|
||||
value: Text(value.toString()),
|
||||
onPressed: (_) {
|
||||
String updatedValue = value.toString();
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text('Cancel'),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
TextButton(
|
||||
child: const Text('OK'),
|
||||
onPressed: () {
|
||||
final result = validators.firstWhereOrNull(
|
||||
(validator) => !validator(updatedValue),
|
||||
);
|
||||
if (result != null) {
|
||||
return displayErrorMessage(
|
||||
context,
|
||||
"Setting '$key' is not valid",
|
||||
);
|
||||
}
|
||||
setState(() => settings[key] = int.parse(updatedValue));
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
content: TextField(
|
||||
autofocus: true,
|
||||
controller: TextEditingController(text: updatedValue),
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
keyboardType: TextInputType.number,
|
||||
onChanged: (nextValue) => updatedValue = nextValue,
|
||||
),
|
||||
title: createSettingTitle(context, key, description),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void createPasswordSetting(
|
||||
context,
|
||||
List<Widget> list,
|
||||
Map<String, dynamic> settings,
|
||||
String key,
|
||||
value,
|
||||
bool isAdvanced,
|
||||
bool showAdvanced,
|
||||
widget,
|
||||
Function setState, {
|
||||
String? description,
|
||||
IconData icon = Icons.password,
|
||||
List<Validator> validators = const [],
|
||||
}) {
|
||||
if (!isAdvanced || widget.showAdvanced) {
|
||||
list.add(
|
||||
SettingsTile.navigation(
|
||||
leading: Icon(icon),
|
||||
title: createSettingTitle(context, key, description),
|
||||
value: Text('*' * (value as String).length),
|
||||
onPressed: (_) {
|
||||
String updatedValue1 = value;
|
||||
String updatedValue2 = value;
|
||||
bool hidePassword1 = true;
|
||||
bool hidePassword2 = true;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return StatefulBuilder(
|
||||
builder: (context, setDialogState) {
|
||||
return AlertDialog(
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text('Cancel'),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
TextButton(
|
||||
child: const Text('OK'),
|
||||
onPressed: () {
|
||||
if (updatedValue1 != updatedValue2) {
|
||||
return displayErrorMessage(
|
||||
context,
|
||||
"Setting '$key' does not match",
|
||||
);
|
||||
}
|
||||
|
||||
final result = validators.firstWhereOrNull(
|
||||
(validator) => !validator(updatedValue1),
|
||||
);
|
||||
if (result != null) {
|
||||
return displayErrorMessage(
|
||||
context,
|
||||
"Setting '$key' is not valid",
|
||||
);
|
||||
}
|
||||
|
||||
setState(() => settings[key] = updatedValue1);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
autofocus: true,
|
||||
controller: TextEditingController(
|
||||
text: updatedValue1,
|
||||
),
|
||||
obscureText: hidePassword1,
|
||||
obscuringCharacter: '*',
|
||||
onChanged: (value) => updatedValue1 = value,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed:
|
||||
() => setDialogState(
|
||||
() => hidePassword1 = !hidePassword1,
|
||||
),
|
||||
icon: Icon(
|
||||
hidePassword1
|
||||
? Icons.visibility
|
||||
: Icons.visibility_off,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: constants.padding),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
autofocus: false,
|
||||
controller: TextEditingController(
|
||||
text: updatedValue2,
|
||||
),
|
||||
obscureText: hidePassword2,
|
||||
obscuringCharacter: '*',
|
||||
onChanged: (value) => updatedValue2 = value,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed:
|
||||
() => setDialogState(
|
||||
() => hidePassword2 = !hidePassword2,
|
||||
),
|
||||
icon: Icon(
|
||||
hidePassword2
|
||||
? Icons.visibility
|
||||
: Icons.visibility_off,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
title: createSettingTitle(context, key, description),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget createSettingTitle(context, String key, String? description) {
|
||||
if (description == null) {
|
||||
return Text(key);
|
||||
}
|
||||
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(key, textAlign: TextAlign.start),
|
||||
Text(
|
||||
description,
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
textAlign: TextAlign.start,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void createStringListSetting(
|
||||
context,
|
||||
List<Widget> list,
|
||||
Map<String, dynamic> settings,
|
||||
String key,
|
||||
value,
|
||||
List<String> valueList,
|
||||
IconData icon,
|
||||
bool isAdvanced,
|
||||
bool showAdvanced,
|
||||
widget,
|
||||
Function setState, {
|
||||
String? description,
|
||||
}) {
|
||||
if (!isAdvanced || widget.showAdvanced) {
|
||||
list.add(
|
||||
SettingsTile.navigation(
|
||||
title: createSettingTitle(context, key, description),
|
||||
leading: Icon(icon),
|
||||
value: DropdownButton<String>(
|
||||
value: value,
|
||||
onChanged: (newValue) => setState(() => settings[key] = newValue),
|
||||
items:
|
||||
valueList.map<DropdownMenuItem<String>>((item) {
|
||||
return DropdownMenuItem<String>(value: item, child: Text(item));
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void createStringSetting(
|
||||
context,
|
||||
List<Widget> list,
|
||||
Map<String, dynamic> settings,
|
||||
String key,
|
||||
value,
|
||||
IconData icon,
|
||||
bool isAdvanced,
|
||||
bool showAdvanced,
|
||||
widget,
|
||||
Function setState, {
|
||||
String? description,
|
||||
List<Validator> validators = const [],
|
||||
}) {
|
||||
if (!isAdvanced || widget.showAdvanced) {
|
||||
list.add(
|
||||
SettingsTile.navigation(
|
||||
leading: Icon(icon),
|
||||
title: createSettingTitle(context, key, description),
|
||||
value: Text(value),
|
||||
onPressed: (_) {
|
||||
String updatedValue = value;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text('Cancel'),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
TextButton(
|
||||
child: const Text('OK'),
|
||||
onPressed: () {
|
||||
final result = validators.firstWhereOrNull(
|
||||
(validator) => !validator(updatedValue),
|
||||
);
|
||||
if (result != null) {
|
||||
return displayErrorMessage(
|
||||
context,
|
||||
"Setting '$key' is not valid",
|
||||
);
|
||||
}
|
||||
setState(() => settings[key] = updatedValue);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
content: TextField(
|
||||
autofocus: true,
|
||||
controller: TextEditingController(text: updatedValue),
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.deny(RegExp(r'\s')),
|
||||
],
|
||||
onChanged: (value) => updatedValue = value,
|
||||
),
|
||||
title: createSettingTitle(context, key, description),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,33 +1,30 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:repertory/helpers.dart' show initialCaps;
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MountConfig {
|
||||
bool? mounted;
|
||||
final String _name;
|
||||
String path = '';
|
||||
String _path = "";
|
||||
Map<String, dynamic> _settings = {};
|
||||
IconData _state = Icons.toggle_off;
|
||||
final String _type;
|
||||
MountConfig({required name, required type, Map<String, dynamic>? settings})
|
||||
: _name = name,
|
||||
_type = type {
|
||||
if (settings != null) {
|
||||
_settings = settings;
|
||||
}
|
||||
}
|
||||
MountConfig({required name, required type}) : _name = name, _type = type;
|
||||
|
||||
String? get bucket => _settings['${provider}Config']?["Bucket"] as String;
|
||||
String get name => _name;
|
||||
String get provider => initialCaps(_type);
|
||||
UnmodifiableMapView<String, dynamic> get settings =>
|
||||
UnmodifiableMapView<String, dynamic>(_settings);
|
||||
String get path => _path;
|
||||
UnmodifiableMapView get settings => UnmodifiableMapView(_settings);
|
||||
IconData get state => _state;
|
||||
String get type => _type;
|
||||
|
||||
factory MountConfig.fromJson(String type, String name) {
|
||||
return MountConfig(name: name, type: type);
|
||||
}
|
||||
|
||||
void updateSettings(Map<String, dynamic> settings) {
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
void updateStatus(Map<String, dynamic> status) {
|
||||
path = status['Location'] as String;
|
||||
mounted = status['Active'] as bool;
|
||||
_path = status["Location"] as String;
|
||||
_state = status["Active"] as bool ? Icons.toggle_on : Icons.toggle_off;
|
||||
}
|
||||
}
|
||||
|
@ -1,30 +1,26 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:repertory/constants.dart' as constants;
|
||||
import 'package:repertory/models/mount.dart';
|
||||
import 'package:repertory/models/mount_list.dart';
|
||||
import 'package:repertory/widgets/mount_widget.dart';
|
||||
|
||||
class MountListWidget extends StatelessWidget {
|
||||
class MountListWidget extends StatefulWidget {
|
||||
const MountListWidget({super.key});
|
||||
|
||||
@override
|
||||
State<MountListWidget> createState() => _MountListWidgetState();
|
||||
}
|
||||
|
||||
class _MountListWidgetState extends State<MountListWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<MountList>(
|
||||
builder: (context, MountList mountList, _) {
|
||||
builder: (context, mountList, widget) {
|
||||
return ListView.builder(
|
||||
itemBuilder: (context, idx) {
|
||||
return ChangeNotifierProvider(
|
||||
create: (context) => mountList.items[idx],
|
||||
key: ValueKey(mountList.items[idx].id),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom:
|
||||
idx == mountList.items.length - 1
|
||||
? 0.0
|
||||
: constants.padding,
|
||||
),
|
||||
child: const MountWidget(),
|
||||
),
|
||||
create: (context) => Mount(mountList.items[idx]),
|
||||
child: const MountWidget(),
|
||||
);
|
||||
},
|
||||
itemCount: mountList.items.length,
|
||||
|
0
web/repertory/lib/widgets/mount_setting.dart
Normal file
0
web/repertory/lib/widgets/mount_setting.dart
Normal file
0
web/repertory/lib/widgets/mount_setting_list.dart
Normal file
0
web/repertory/lib/widgets/mount_setting_list.dart
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,10 +1,7 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:repertory/constants.dart' as constants;
|
||||
import 'package:repertory/helpers.dart';
|
||||
import 'package:repertory/models/mount.dart';
|
||||
|
||||
@ -16,99 +13,51 @@ class MountWidget extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _MountWidgetState extends State<MountWidget> {
|
||||
bool _enabled = true;
|
||||
bool _editEnabled = true;
|
||||
Timer? _timer;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.all(0.0),
|
||||
child: Consumer<Mount>(
|
||||
builder: (context, Mount mount, _) {
|
||||
final textColor = Theme.of(context).colorScheme.onSurface;
|
||||
final subTextColor =
|
||||
Theme.of(context).brightness == Brightness.dark
|
||||
? Colors.white38
|
||||
: Colors.black87;
|
||||
builder: (context, mount, widget) {
|
||||
final textColor = Colors.blue;
|
||||
final subTextColor = Colors.black;
|
||||
|
||||
final isActive = mount.state == Icons.toggle_on;
|
||||
final nameText = SelectableText(
|
||||
formatMountName(mount.type, mount.name),
|
||||
style: TextStyle(color: subTextColor),
|
||||
);
|
||||
|
||||
return ListTile(
|
||||
isThreeLine: true,
|
||||
isThreeLine: isActive,
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.settings, color: textColor),
|
||||
onPressed:
|
||||
() => Navigator.pushNamed(context, '/edit', arguments: mount),
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
nameText,
|
||||
SelectableText(
|
||||
mount.path.isEmpty && mount.mounted == null
|
||||
? 'loading...'
|
||||
: mount.path.isEmpty
|
||||
? '<mount location not set>'
|
||||
: mount.path,
|
||||
style: TextStyle(color: subTextColor),
|
||||
),
|
||||
],
|
||||
onPressed: () {},
|
||||
),
|
||||
subtitle:
|
||||
isActive
|
||||
? Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
nameText,
|
||||
SelectableText(
|
||||
mount.path,
|
||||
style: TextStyle(color: subTextColor),
|
||||
),
|
||||
],
|
||||
)
|
||||
: nameText,
|
||||
title: SelectableText(
|
||||
mount.provider,
|
||||
initialCaps(mount.type),
|
||||
style: TextStyle(color: textColor, fontWeight: FontWeight.bold),
|
||||
),
|
||||
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(
|
||||
mount.mounted == null
|
||||
? Icons.hourglass_top
|
||||
: mount.mounted!
|
||||
? Icons.toggle_on
|
||||
: Icons.toggle_off,
|
||||
color:
|
||||
mount.mounted ?? false
|
||||
? Color.fromARGB(255, 163, 96, 76)
|
||||
: subTextColor,
|
||||
),
|
||||
tooltip:
|
||||
mount.mounted == null
|
||||
? ''
|
||||
: mount.mounted!
|
||||
? 'Unmount'
|
||||
: 'Mount',
|
||||
onPressed: _createMountHandler(context, mount),
|
||||
),
|
||||
],
|
||||
trailing: IconButton(
|
||||
icon: Icon(
|
||||
mount.state,
|
||||
color: isActive ? Colors.blue : Colors.grey,
|
||||
),
|
||||
onPressed: () {},
|
||||
),
|
||||
);
|
||||
},
|
||||
@ -116,49 +65,6 @@ class _MountWidgetState extends State<MountWidget> {
|
||||
);
|
||||
}
|
||||
|
||||
VoidCallback? _createMountHandler(context, Mount mount) {
|
||||
return _enabled && mount.mounted != null
|
||||
? () async {
|
||||
if (mount.mounted == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final mounted = mount.mounted!;
|
||||
|
||||
setState(() {
|
||||
_enabled = false;
|
||||
});
|
||||
|
||||
final location = await _getMountLocation(context, mount);
|
||||
|
||||
cleanup() {
|
||||
setState(() {
|
||||
_enabled = true;
|
||||
});
|
||||
}
|
||||
|
||||
if (!mounted && location == null) {
|
||||
displayErrorMessage(context, "Mount location is not set");
|
||||
return cleanup();
|
||||
}
|
||||
|
||||
final success = await mount.mount(mounted, location: location);
|
||||
if (success ||
|
||||
mounted ||
|
||||
constants.navigatorKey.currentContext == null ||
|
||||
!constants.navigatorKey.currentContext!.mounted) {
|
||||
return cleanup();
|
||||
}
|
||||
|
||||
displayErrorMessage(
|
||||
context,
|
||||
"Mount location is not available: $location",
|
||||
);
|
||||
cleanup();
|
||||
}
|
||||
: null;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
@ -166,41 +72,12 @@ class _MountWidgetState extends State<MountWidget> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<String?> _getMountLocation(context, Mount mount) async {
|
||||
if (mount.mounted ?? false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (mount.path.isNotEmpty) {
|
||||
return mount.path;
|
||||
}
|
||||
|
||||
String? location = await mount.getMountLocation();
|
||||
if (location != null) {
|
||||
return location;
|
||||
}
|
||||
|
||||
if (!context.mounted) {
|
||||
return location;
|
||||
}
|
||||
|
||||
return editMountLocation(context, await mount.getAvailableLocations());
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_timer = Timer.periodic(const Duration(seconds: 5), (_) {
|
||||
Provider.of<Mount>(context, listen: false).refresh();
|
||||
var provider = Provider.of<Mount>(context, listen: false);
|
||||
provider.refresh();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void setState(VoidCallback fn) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
super.setState(fn);
|
||||
}
|
||||
}
|
||||
|
@ -1,155 +0,0 @@
|
||||
import 'dart:convert' show jsonEncode;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:repertory/constants.dart' as constants;
|
||||
import 'package:repertory/helpers.dart'
|
||||
show
|
||||
convertAllToString,
|
||||
displayAuthError,
|
||||
getBaseUri,
|
||||
getChanged,
|
||||
getSettingDescription,
|
||||
getSettingValidators,
|
||||
trimNotEmptyValidator;
|
||||
import 'package:repertory/models/auth.dart';
|
||||
import 'package:repertory/settings.dart';
|
||||
import 'package:settings_ui/settings_ui.dart';
|
||||
|
||||
class UISettingsWidget extends StatefulWidget {
|
||||
final bool showAdvanced;
|
||||
final Map<String, dynamic> settings;
|
||||
final Map<String, dynamic> origSettings;
|
||||
const UISettingsWidget({
|
||||
super.key,
|
||||
required this.origSettings,
|
||||
required this.settings,
|
||||
required this.showAdvanced,
|
||||
});
|
||||
|
||||
@override
|
||||
State<UISettingsWidget> createState() => _UISettingsWidgetState();
|
||||
}
|
||||
|
||||
class _UISettingsWidgetState extends State<UISettingsWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<SettingsTile> commonSettings = [];
|
||||
|
||||
widget.settings.forEach((key, value) {
|
||||
switch (key) {
|
||||
case 'ApiPassword':
|
||||
{
|
||||
createPasswordSetting(
|
||||
context,
|
||||
commonSettings,
|
||||
widget.settings,
|
||||
key,
|
||||
value,
|
||||
false,
|
||||
widget.showAdvanced,
|
||||
widget,
|
||||
setState,
|
||||
description: getSettingDescription(key),
|
||||
validators: getSettingValidators(key),
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'ApiPort':
|
||||
{
|
||||
createIntSetting(
|
||||
context,
|
||||
commonSettings,
|
||||
widget.settings,
|
||||
key,
|
||||
value,
|
||||
false,
|
||||
widget.showAdvanced,
|
||||
widget,
|
||||
setState,
|
||||
description: getSettingDescription(key),
|
||||
validators: getSettingValidators(key),
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'ApiUser':
|
||||
{
|
||||
createStringSetting(
|
||||
context,
|
||||
commonSettings,
|
||||
widget.settings,
|
||||
key,
|
||||
value,
|
||||
Icons.person,
|
||||
false,
|
||||
widget.showAdvanced,
|
||||
widget,
|
||||
setState,
|
||||
description: getSettingDescription(key),
|
||||
validators: [...getSettingValidators(key), trimNotEmptyValidator],
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return SettingsList(
|
||||
shrinkWrap: false,
|
||||
sections: [
|
||||
SettingsSection(title: const Text('Settings'), tiles: commonSettings),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
final settings = getChanged(widget.origSettings, widget.settings);
|
||||
if (settings.isNotEmpty) {
|
||||
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.encodeFull(
|
||||
'${getBaseUri()}/api/v1/settings?auth=$auth&data=${jsonEncode(map)}',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (response.statusCode == 401) {
|
||||
displayAuthError(authProvider);
|
||||
authProvider.logoff();
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('$e');
|
||||
}
|
||||
})
|
||||
.catchError((e) {
|
||||
debugPrint('$e');
|
||||
});
|
||||
}
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void setState(VoidCallback fn) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
super.setState(fn);
|
||||
}
|
||||
}
|
261
web/repertory/pubspec.lock
Normal file
261
web/repertory/pubspec.lock
Normal file
@ -0,0 +1,261 @@
|
||||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.12.0"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: boolean_selector
|
||||
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: characters
|
||||
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: clock
|
||||
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
collection:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: collection
|
||||
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.19.1"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: cupertino_icons
|
||||
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.8"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fake_async
|
||||
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.2"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_lints
|
||||
sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.0"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
http:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: http
|
||||
sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_parser
|
||||
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.2"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.8"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.9"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_testing
|
||||
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
lints:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: lints
|
||||
sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.1.1"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.17"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.11.1"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.16.0"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: nested
|
||||
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path
|
||||
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: provider
|
||||
sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.2"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_span
|
||||
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.1"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.12.1"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_channel
|
||||
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
string_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: term_glyph
|
||||
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.2"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.4"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: typed_data
|
||||
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_math
|
||||
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "14.3.1"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web
|
||||
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
sdks:
|
||||
dart: ">=3.7.0 <4.0.0"
|
||||
flutter: ">=3.18.0-18.0.pre.54"
|
@ -37,9 +37,6 @@ dependencies:
|
||||
collection: ^1.19.1
|
||||
http: ^1.3.0
|
||||
provider: ^6.1.2
|
||||
settings_ui: ^2.0.2
|
||||
sodium_libs: ^3.4.4+1
|
||||
convert: ^3.1.2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
@ -1,44 +1,38 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<!--
|
||||
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.
|
||||
<head>
|
||||
<!--
|
||||
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.
|
||||
|
||||
The path provided below has to start and end with a slash "/" in order for
|
||||
it to work correctly.
|
||||
The path provided below has to start and end with a slash "/" in order for
|
||||
it to work correctly.
|
||||
|
||||
For more details:
|
||||
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
|
||||
For more details:
|
||||
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
|
||||
|
||||
This is a placeholder for base href that will be replaced by the value of
|
||||
the `--base-href` argument provided to `flutter build`.
|
||||
-->
|
||||
<base href="$FLUTTER_BASE_HREF">
|
||||
This is a placeholder for base href that will be replaced by the value of
|
||||
the `--base-href` argument provided to `flutter build`.
|
||||
-->
|
||||
<base href="$FLUTTER_BASE_HREF">
|
||||
|
||||
<meta charset="UTF-8">
|
||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||
<meta name="description" content="A new Flutter project.">
|
||||
<meta charset="UTF-8">
|
||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||
<meta name="description" content="A new Flutter project.">
|
||||
|
||||
<!-- iOS meta tags & icons -->
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="apple-mobile-web-app-title" content="repertory">
|
||||
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
||||
<!-- iOS meta tags & icons -->
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="apple-mobile-web-app-title" content="repertory">
|
||||
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/png" href="favicon.png">
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||
|
||||
<title>repertory</title>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<script type="text/javascript" src="sodium.js" async="true"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
window.flutterConfiguration = {
|
||||
canvasKitBaseUrl: "/canvaskit/"
|
||||
};
|
||||
</script>
|
||||
<script src="flutter_bootstrap.js" async=""></script>
|
||||
</body>
|
||||
<title>repertory</title>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
</head>
|
||||
<body>
|
||||
<script src="flutter_bootstrap.js" async></script>
|
||||
</body>
|
||||
</html>
|
||||
|
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user