76 Commits

Author SHA1 Message Date
7d1574d042 make text selectable
All checks were successful
BlockStorage/repertory/pipeline/head This commit looks good
2025-03-01 16:34:47 -06:00
43280b2913 updates
All checks were successful
BlockStorage/repertory/pipeline/head This commit looks good
2025-03-01 16:25:41 -06:00
788aefdf86 layout changes 2025-03-01 16:22:54 -06:00
3453bd0b50 refactor 2025-03-01 15:51:55 -06:00
96df3168fc updates
All checks were successful
BlockStorage/repertory/pipeline/head This commit looks good
2025-03-01 15:50:01 -06:00
45381c3d0c updates 2025-03-01 15:48:40 -06:00
4c82b87b07 gui changes 2025-03-01 15:48:32 -06:00
c29072d84d gui changes 2025-03-01 15:47:34 -06:00
311a2688cb gui changes 2025-03-01 15:43:36 -06:00
968633a5b9 added status
All checks were successful
BlockStorage/repertory/pipeline/head This commit looks good
2025-03-01 15:18:30 -06:00
bcda2abfd2 fix
Some checks are pending
BlockStorage/repertory/pipeline/head Build queued...
2025-03-01 11:22:21 -06:00
7d5c252e89 test
All checks were successful
BlockStorage/repertory/pipeline/head This commit looks good
2025-03-01 11:09:11 -06:00
0aeb69a050 grab mount settings and status 2025-03-01 10:34:13 -06:00
7d441964e9 fixes
All checks were successful
BlockStorage/repertory/pipeline/head This commit looks good
2025-03-01 07:32:23 -06:00
1d78ee88b8 continue mgmt portal 2025-03-01 07:26:38 -06:00
d54ba8203a continue management portal 2025-03-01 07:13:08 -06:00
52f0d755ba refactor
All checks were successful
BlockStorage/repertory/pipeline/head This commit looks good
2025-02-28 23:14:37 -06:00
343c324050 fix sorting 2025-02-28 22:45:32 -06:00
e446757f7e flutter changes
Some checks are pending
BlockStorage/repertory/pipeline/head Build queued...
2025-02-28 22:35:46 -06:00
513bc09944 flutter changes 2025-02-28 20:49:14 -06:00
a79e696315 fix 2025-02-28 20:43:15 -06:00
26dbda290e flutter changes 2025-02-28 20:36:35 -06:00
d06b965dc1 updated build system
All checks were successful
BlockStorage/repertory/pipeline/head This commit looks good
2025-02-28 18:19:16 -06:00
6aee374725 updated build system
Some checks reported errors
BlockStorage/repertory/pipeline/head Something is wrong with the build of this commit
2025-02-28 17:42:26 -06:00
a7d570d5a0 updated build system
Some checks reported errors
BlockStorage/repertory/pipeline/head Something is wrong with the build of this commit
2025-02-28 17:34:16 -06:00
c93e6bedcc updated build system
Some checks reported errors
BlockStorage/repertory/pipeline/head Something is wrong with the build of this commit
2025-02-28 15:00:05 -06:00
93b4237e4f fix
Some checks failed
BlockStorage/repertory/pipeline/head There was a failure building this commit
2025-02-28 13:25:33 -06:00
4013ed692e fixes
Some checks reported errors
BlockStorage/repertory/pipeline/head Something is wrong with the build of this commit
2025-02-28 13:18:34 -06:00
99914382f4 fix
Some checks failed
BlockStorage/repertory/pipeline/head There was a failure building this commit
2025-02-28 12:58:51 -06:00
3a468d72ce fix
Some checks reported errors
BlockStorage/repertory/pipeline/head Something is wrong with the build of this commit
2025-02-28 10:45:18 -06:00
0611f0c9dd updated build system
Some checks reported errors
BlockStorage/repertory/pipeline/head Something is wrong with the build of this commit
2025-02-28 10:41:19 -06:00
5567e665cc updated build system
Some checks failed
BlockStorage/repertory/pipeline/head There was a failure building this commit
2025-02-28 10:06:19 -06:00
13ebd7b8b0 updated build system 2025-02-28 09:25:09 -06:00
c2e9960ad5 fix
Some checks reported errors
BlockStorage/repertory/pipeline/head Something is wrong with the build of this commit
2025-02-27 21:15:07 -06:00
86918595b3 refactor data directory
Some checks reported errors
BlockStorage/repertory/pipeline/head Something is wrong with the build of this commit
2025-02-27 21:09:47 -06:00
9f90494efb fixes
Some checks reported errors
BlockStorage/repertory/pipeline/head Something is wrong with the build of this commit
2025-02-27 20:44:45 -06:00
b3ee609e09 flutter items
Some checks reported errors
BlockStorage/repertory/pipeline/head Something is wrong with the build of this commit
2025-02-27 15:16:34 -06:00
7dc2ce8702 updated build system
Some checks reported errors
BlockStorage/repertory/pipeline/head Something is wrong with the build of this commit
2025-02-27 14:52:03 -06:00
ed5f1f04ad change base href 2025-02-27 14:48:41 -06:00
863e0b2165 refactor 2025-02-27 14:42:38 -06:00
122b4998d6 added signal handler 2025-02-27 14:35:37 -06:00
722357388c added signal handler 2025-02-27 14:29:21 -06:00
0e21d93bb3 refactor
Some checks reported errors
BlockStorage/repertory/pipeline/head Something is wrong with the build of this commit
2025-02-27 14:12:03 -06:00
e8118bac89 fix 2025-02-27 14:09:14 -06:00
c4af929d7a basic authentication 2025-02-27 13:47:52 -06:00
3826afd3bf setup and fixes 2025-02-27 13:43:21 -06:00
4a15e2f827 updated build system 2025-02-27 12:25:18 -06:00
48b6012f97 fixes 2025-02-27 12:20:10 -06:00
d37c6598c4 fixes 2025-02-27 12:18:50 -06:00
c95242e016 fixes and setup 2025-02-27 11:13:06 -06:00
6ed4db0737 initial setup 2025-02-27 09:53:14 -06:00
1560804df8 fixed config name
All checks were successful
BlockStorage/repertory/pipeline/head This commit looks good
2025-02-24 09:49:26 -06:00
cc3c0febc3 updated README.md
All checks were successful
BlockStorage/repertory/pipeline/head This commit looks good
2025-02-23 18:23:04 -06:00
0458f12e17 Update README.md 2025-02-23 18:21:57 -06:00
ae43cedb45 Update README.md 2025-02-23 18:21:15 -06:00
191eb1620b Update README.md
Some checks are pending
BlockStorage/repertory/pipeline/head Build queued...
2025-02-23 18:20:52 -06:00
9c648583fb Complete initial v2.0 documentation #33
All checks were successful
BlockStorage/repertory/pipeline/head This commit looks good
2025-02-23 18:19:52 -06:00
14d78d0b65 cleanup
All checks were successful
BlockStorage/repertory/pipeline/head This commit looks good
2025-02-23 16:34:12 -06:00
54efde0497 Complete initial v2.0 documentation #33
All checks were successful
BlockStorage/repertory/pipeline/head This commit looks good
2025-02-23 16:31:33 -06:00
5f593ab86d Complete initial v2.0 documentation #33 2025-02-23 16:30:23 -06:00
feb09746f5 Complete initial v2.0 documentation #33
All checks were successful
BlockStorage/repertory/pipeline/head This commit looks good
2025-02-23 11:19:16 -06:00
faad98c11e Complete initial v2.0 documentation #33
All checks were successful
BlockStorage/repertory/pipeline/head This commit looks good
2025-02-23 11:18:18 -06:00
12f04c6064 revert
All checks were successful
BlockStorage/repertory/pipeline/head This commit looks good
2025-02-23 00:32:04 -06:00
ca307c3bf2 update
All checks were successful
BlockStorage/repertory/pipeline/head This commit looks good
2025-02-23 00:29:44 -06:00
ac6f4bcade Complete initial v2.0 documentation #33
Some checks are pending
BlockStorage/repertory/pipeline/head Build queued...
2025-02-23 00:27:11 -06:00
9d48cd97e3 Complete initial v2.0 documentation #33
All checks were successful
BlockStorage/repertory/pipeline/head This commit looks good
2025-02-23 00:20:51 -06:00
6c6fb0554f updated CHANGELOG.md
All checks were successful
BlockStorage/repertory/pipeline/head This commit looks good
2025-02-23 00:01:24 -06:00
a134f01436 updated CHANGELOG.md
All checks were successful
BlockStorage/repertory/pipeline/head This commit looks good
2025-02-22 23:56:21 -06:00
44c33652fa cleanup 2025-02-22 23:55:46 -06:00
131c36415d Complete initial v2.0 documentation #33
All checks were successful
BlockStorage/repertory/pipeline/head This commit looks good
2025-02-22 23:06:38 -06:00
9456c8b1d2 Complete initial v2.0 documentation #33
All checks were successful
BlockStorage/repertory/pipeline/head This commit looks good
2025-02-22 23:00:26 -06:00
b8c62612d8 refactor 2025-02-22 22:01:37 -06:00
521874a56f Complete initial v2.0 documentation #33
All checks were successful
BlockStorage/repertory/pipeline/head This commit looks good
2025-02-22 19:59:22 -06:00
f41ad47262 updated CHANGES.md
All checks were successful
BlockStorage/repertory/pipeline/head This commit looks good
2025-02-22 11:36:20 -06:00
8bb2eeb88c updated CHANGELOG.md
Some checks failed
BlockStorage/repertory/pipeline/head There was a failure building this commit
2025-02-22 10:07:52 -06:00
b1aca46034 updated version 2025-02-22 09:58:57 -06:00
81 changed files with 1004 additions and 5152 deletions

View File

@ -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
View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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)

View File

@ -31,7 +31,6 @@ RUN apk add \
gflags \
gflags-dev \
git \
git-lfs \
icu-dev \
icu-libs \
icu-static \

View File

@ -31,7 +31,6 @@ RUN apk add \
gflags \
gflags-dev \
git \
git-lfs \
icu-dev \
icu-libs \
icu-static \

View File

@ -18,7 +18,6 @@ RUN apk add \
gcc \
gettext \
git \
git-lfs \
gmp \
gmp-dev \
gperf \

View File

@ -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);

View File

@ -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";

View File

@ -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"

View File

@ -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_

View File

@ -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(

View File

@ -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;
}

View File

@ -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"};

View File

@ -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) {

View File

@ -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) {

View File

@ -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

View File

@ -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");
}

View File

@ -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));

View File

@ -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>();

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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_

View File

@ -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;
}
}

View File

@ -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

View File

@ -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

View File

@ -128,7 +128,7 @@ std::atomic<std::uint64_t> app_config_test::idx{0U};
static void defaults_tests(const json &json_data, provider_type prov) {
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) {

View File

@ -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

View File

@ -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

View File

@ -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
)

View File

@ -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-'* ]] ||

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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)

View File

@ -1,8 +1,2 @@
autofocus
canvaskit
cupertino
cupertinoicons
fromargb
onetwothree
renterd
rocksdb
cupertinoicons

View File

@ -44,4 +44,3 @@ app.*.map.json
/android/app/profile
/android/app/release
.flutter-companion
pubspec.lock

View File

@ -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";

View File

@ -0,0 +1,6 @@
class DuplicateMountException implements Exception {
final String _name;
const DuplicateMountException({required name}) : _name = name, super();
String get name => _name;
}

View File

@ -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),
);
},
);
},
);
}

View File

@ -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(),
),
);
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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),
);
},
);
},
),
);
}
}

View File

@ -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;
}
}

View File

@ -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,

File diff suppressed because it is too large Load Diff

View File

@ -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);
}
}

View File

@ -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
View 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"

View File

@ -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:

View File

@ -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