diff options
| author | Dan Engelbrecht <[email protected]> | 2024-10-23 10:31:43 +0200 |
|---|---|---|
| committer | GitHub Enterprise <[email protected]> | 2024-10-23 10:31:43 +0200 |
| commit | 530ab3394938331f224058c381a1db5d4a68e6a9 (patch) | |
| tree | 5060eb394d67b7454855aed0fa8d7d3acf5f5c98 /src | |
| parent | fix gc date (#204) (diff) | |
| download | zen-530ab3394938331f224058c381a1db5d4a68e6a9.tar.xz zen-530ab3394938331f224058c381a1db5d4a68e6a9.zip | |
workspace share security (#192)
- Improvement: Reworked workspace shares to be more secure. Workspaces and workspace shares can only be created using the `zen workspace` command, the http endpoint is disabled unless zenserver is started with the `--workspaces-allow-changes` option enabled.
- Each workspace are now configured via a `zenworkspaceconfig.json` file in the root of each workspace
- A workspace can allow shares to be created via the http interface if the workspace is created with the `--allow-share-create-from-http` option enabled
- A new http endpoint at `/ws` - issuing a `Get` operation will get you a list of workspaces
- A new http endpoint at `/ws/refresh` - issuing a `Get` will make zenserver scan for edits in workspaces and workspace shares
Diffstat (limited to 'src')
| -rw-r--r-- | src/zen/cmds/workspaces_cmd.cpp | 416 | ||||
| -rw-r--r-- | src/zen/cmds/workspaces_cmd.h | 33 | ||||
| -rw-r--r-- | src/zencore/filesystem.cpp | 30 | ||||
| -rw-r--r-- | src/zencore/include/zencore/filesystem.h | 2 | ||||
| -rw-r--r-- | src/zencore/include/zencore/testutils.h | 2 | ||||
| -rw-r--r-- | src/zenhttp/httpclient.cpp | 5 | ||||
| -rw-r--r-- | src/zenserver-test/zenserver-test.cpp | 165 | ||||
| -rw-r--r-- | src/zenserver/config.cpp | 62 | ||||
| -rw-r--r-- | src/zenserver/config.h | 3 | ||||
| -rw-r--r-- | src/zenserver/workspaces/httpworkspaces.cpp | 321 | ||||
| -rw-r--r-- | src/zenserver/workspaces/httpworkspaces.h | 25 | ||||
| -rw-r--r-- | src/zenserver/zenserver.cpp | 5 | ||||
| -rw-r--r-- | src/zenstore/include/zenstore/workspaces.h | 83 | ||||
| -rw-r--r-- | src/zenstore/workspaces.cpp | 1190 |
14 files changed, 1688 insertions, 654 deletions
diff --git a/src/zen/cmds/workspaces_cmd.cpp b/src/zen/cmds/workspaces_cmd.cpp index d83439b0a..b5d9af1be 100644 --- a/src/zen/cmds/workspaces_cmd.cpp +++ b/src/zen/cmds/workspaces_cmd.cpp @@ -11,6 +11,7 @@ #include <zenhttp/formatters.h> #include <zenhttp/httpclient.h> #include <zenhttp/httpcommon.h> +#include <zenstore/workspaces.h> #include <zenutil/chunkrequests.h> #include <zenutil/zenserverprocess.h> @@ -18,10 +19,49 @@ namespace zen { +namespace { + void ShowShare(const Workspaces::WorkspaceShareConfiguration& Share, const Oid& WorkspaceId, std::string_view Prefix) + { + ZEN_CONSOLE("{}Id: {}", Prefix, Share.Id); + ZEN_CONSOLE("{} Path: {}", Prefix, Share.SharePath); + if (!Share.Alias.empty()) + { + ZEN_CONSOLE("{} Alias: {}", Prefix, Share.Alias); + } + if (WorkspaceId != Oid::Zero) + { + ZEN_CONSOLE("{} Workspace: {}", Prefix, WorkspaceId); + } + }; + + void ShowWorkspace(const Workspaces::WorkspaceConfiguration& Workspace, std::string_view Prefix) + { + ZEN_CONSOLE("{}Id: {}", Prefix, Workspace.Id); + ZEN_CONSOLE("{} Root: {}", Prefix, Workspace.RootPath); + ZEN_CONSOLE("{} AllowHttpShares: {}", Prefix, Workspace.AllowShareCreationFromHttp); + std::string Error; + std::vector<Workspaces::WorkspaceShareConfiguration> Shares = Workspaces::ReadWorkspaceConfig(Log(), Workspace.RootPath, Error); + if (!Error.empty()) + { + ZEN_CONSOLE("{}Failed to read shares from workspace {}. Reason: '{}'", Prefix, Workspace.Id, Error); + } + else + { + ZEN_CONSOLE("{} Shares: {}", Prefix, Shares.size()); + for (const Workspaces::WorkspaceShareConfiguration& Share : Shares) + { + ShowShare(Share, Oid::Zero, fmt::format("{} ", Prefix)); + } + } + }; + +} // namespace + WorkspaceCommand::WorkspaceCommand() { m_Options.add_options()("h,help", "Print help"); m_Options.add_option("", "u", "hosturl", "Host URL", cxxopts::value(m_HostName)->default_value(""), "<hosturl>"); + m_Options.add_options()("system-dir", "Specify system root", cxxopts::value<std::filesystem::path>(m_SystemRootDir)); m_Options.add_option("", "v", "verb", "Verb for workspace - create, remove, info", cxxopts::value(m_Verb), "<verb>"); m_Options.parse_positional({"verb"}); m_Options.positional_help("verb"); @@ -29,6 +69,12 @@ WorkspaceCommand::WorkspaceCommand() m_CreateOptions.add_options()("h,help", "Print help"); m_CreateOptions.add_option("", "w", "workspace", "Workspace identity(id)", cxxopts::value(m_Id), "<workspaceid>"); m_CreateOptions.add_option("", "r", "root-path", "Root file system folder for workspace", cxxopts::value(m_Path), "<root-path>"); + m_CreateOptions.add_option("", + "", + "allow-share-create-from-http", + "Allow create and delete inside this workspace using the http API. Defaults to false", + cxxopts::value(m_AllowShareCreationFromHttp), + "<allowhttpsharecreate>"); m_CreateOptions.parse_positional({"root-path", "workspace"}); m_CreateOptions.positional_help("root-path workspace"); @@ -67,40 +113,60 @@ WorkspaceCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) m_HostName = ResolveTargetHostSpec(m_HostName); - if (m_HostName.empty()) + if (m_SystemRootDir.empty()) { - throw zen::OptionParseException("unable to resolve server specification"); + m_SystemRootDir = PickDefaultSystemRootDirectory(); + if (m_SystemRootDir.empty()) + { + throw zen::OptionParseException("unable to resolve system root directory"); + } } + std::filesystem::path StatePath = m_SystemRootDir / "workspaces"; + if (!ParseOptions(*SubOption, gsl::narrow<int>(SubCommandArguments.size()), SubCommandArguments.data())) { return 0; } - HttpClient Http(m_HostName); - if (SubOption == &m_CreateOptions) { if (m_Path.empty()) { throw zen::OptionParseException(fmt::format("path is required\n{}", m_CreateOptions.help())); } + if (m_Id.empty()) { - m_Id = Oid::Zero.ToString(); - ZEN_CONSOLE("Using generated workspace id from path '{}'", m_Path); + m_Id = Workspaces::PathToId(m_Path).ToString(); + ZEN_CONSOLE("Using generated workspace id {} from path '{}'", m_Id, m_Path); } - HttpClient::KeyValueMap Params{{"root_path", std::filesystem::absolute(m_Path).string()}}; - if (HttpClient::Response Result = Http.Put(fmt::format("/ws/{}", m_Id), Params)) + if (Oid::TryFromHexString(m_Id) == Oid::Zero) { - ZEN_CONSOLE("{}. Id: {}", Result, Result.AsText()); + throw zen::OptionParseException(fmt::format("id '{}' is invalid", m_Id)); + } + + if (Workspaces::AddWorkspace( + Log(), + StatePath, + {.Id = Oid::FromHexString(m_Id), .RootPath = m_Path, .AllowShareCreationFromHttp = m_AllowShareCreationFromHttp})) + { + if (!m_HostName.empty()) + { + HttpClient Http(m_HostName); + if (HttpClient::Response Result = Http.Get("/ws/refresh"); !Result) + { + ZEN_CONSOLE("Failed to refresh workspaces for host {}. Reason: '{}'", m_HostName, Result.ErrorMessage(""sv)); + } + } + ZEN_CONSOLE("Added/updated workspace {}", m_Id); return 0; } else { - Result.ThrowError(fmt::format("failed to create workspace {}", m_Id)); - return 1; + ZEN_CONSOLE("Workspace {} already exists", m_Id); + return 0; } } @@ -108,17 +174,39 @@ WorkspaceCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { if (m_Id.empty()) { - throw zen::OptionParseException(fmt::format("id is required", m_InfoOptions.help())); - } - if (HttpClient::Response Result = Http.Get(fmt::format("/ws/{}", m_Id))) - { - ZEN_CONSOLE("{}", Result.ToText()); + std::string Error; + static std::vector<Workspaces::WorkspaceConfiguration> Configs = Workspaces::ReadConfig(Log(), StatePath, Error); + if (!Error.empty()) + { + ZEN_CONSOLE("Failed to read workspaces state from '{}'. Reason: '{}'", StatePath, Error); + } + else + { + ZEN_CONSOLE("Workspaces: {}", Configs.size()); + for (const Workspaces::WorkspaceConfiguration& Config : Configs) + { + ShowWorkspace(Config, " "sv); + } + } return 0; } else { - Result.ThrowError(fmt::format("failed to get info for workspace {}", m_Id)); - return 1; + if (Oid::TryFromHexString(m_Id) == Oid::Zero) + { + throw zen::OptionParseException(fmt::format("id '{}' is invalid", m_Id)); + } + + Workspaces::WorkspaceConfiguration Workspace = Workspaces::FindWorkspace(Log(), StatePath, Oid::FromHexString(m_Id)); + if (Workspace.Id != Oid::Zero) + { + ShowWorkspace(Workspace, ""sv); + return 0; + } + else + { + ZEN_CONSOLE("Workspace {} not found", m_Id); + } } } @@ -128,16 +216,29 @@ WorkspaceCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { throw zen::OptionParseException(fmt::format("id is required", m_RemoveOptions.help())); } - if (HttpClient::Response Result = Http.Delete(fmt::format("/ws/{}", m_Id))) + + if (Oid::TryFromHexString(m_Id) == Oid::Zero) { - ZEN_CONSOLE("{}", Result); - return 0; + throw zen::OptionParseException(fmt::format("id '{}' is invalid", m_Id)); + } + + if (Workspaces::RemoveWorkspace(Log(), StatePath, Oid::FromHexString(m_Id))) + { + if (!m_HostName.empty()) + { + HttpClient Http(m_HostName); + if (HttpClient::Response Result = Http.Get("/ws/refresh"); !Result) + { + ZEN_CONSOLE("Failed to refresh workspaces for host {}. Reason: '{}'", m_HostName, Result.ErrorMessage(""sv)); + } + } + ZEN_CONSOLE("Removed workspace {}", m_Id); } else { - Result.ThrowError(fmt::format("failed to remove workspace {}", m_Id)); - return 1; + ZEN_CONSOLE("Workspace {} does not exist", m_Id); } + return 0; } ZEN_ASSERT(false); @@ -149,6 +250,7 @@ WorkspaceShareCommand::WorkspaceShareCommand() { m_Options.add_options()("h,help", "Print help"); m_Options.add_option("", "u", "hosturl", "Host URL", cxxopts::value(m_HostName)->default_value(""), "<hosturl>"); + m_Options.add_options()("system-dir", "Specify system root", cxxopts::value<std::filesystem::path>(m_SystemRootDir)); m_Options.add_option("", "v", "verb", "Verb for workspace - create, remove, info", cxxopts::value(m_Verb), "<verb>"); m_Options.parse_positional({"verb"}); m_Options.positional_help("verb"); @@ -260,48 +362,116 @@ WorkspaceShareCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** m_HostName = ResolveTargetHostSpec(m_HostName); - if (m_HostName.empty()) + if (m_SystemRootDir.empty()) { - throw zen::OptionParseException("unable to resolve server specification"); + m_SystemRootDir = PickDefaultSystemRootDirectory(); + if (m_SystemRootDir.empty()) + { + throw zen::OptionParseException("unable to resolve system root directory"); + } } + std::filesystem::path StatePath = m_SystemRootDir / "workspaces"; + if (!ParseOptions(*SubOption, gsl::narrow<int>(SubCommandArguments.size()), SubCommandArguments.data())) { return 0; } - HttpClient Http(m_HostName); - if (SubOption == &m_CreateOptions) { - if (!m_WorkspaceRoot.empty()) + if (m_WorkspaceRoot.empty()) { - HttpClient::KeyValueMap Params{{"root_path", std::filesystem::absolute(m_WorkspaceRoot).string()}}; - if (HttpClient::Response Result = - Http.Put(fmt::format("/ws/{}", m_WorkspaceId.empty() ? Oid::Zero.ToString() : m_WorkspaceId), Params)) + if (m_WorkspaceId.empty()) { - if (Oid::Zero == Oid::TryFromHexString(Result.AsText())) - { - throw std::runtime_error(fmt::format("failed to create workspace {} with root path '{}'. Reason: {}", - m_WorkspaceId, - m_WorkspaceRoot, - Result.AsText())); - } - m_WorkspaceId = Result.AsText(); - if (Result.StatusCode == HttpResponseCode::Created) + throw zen::OptionParseException("workspace id or root path is required"); + } + + Oid WorkspaceId = Oid::TryFromHexString(m_WorkspaceId); + if (WorkspaceId == Oid::Zero) + { + throw zen::OptionParseException(fmt::format("id '{}' is invalid", m_WorkspaceId)); + } + + Workspaces::WorkspaceConfiguration WorkspaceConfig = Workspaces::FindWorkspace(Log(), StatePath, WorkspaceId); + if (WorkspaceConfig.Id == Oid::Zero) + { + ZEN_CONSOLE("Workspace {} does not exist", m_WorkspaceId); + return 0; + } + m_WorkspaceRoot = WorkspaceConfig.RootPath; + } + else + { + if (m_WorkspaceId.empty()) + { + m_WorkspaceId = Workspaces::PathToId(m_WorkspaceRoot).ToString(); + ZEN_CONSOLE("Using generated workspace id {} from path '{}'", m_WorkspaceId, m_WorkspaceRoot); + } + else + { + if (Oid::TryFromHexString(m_WorkspaceId) == Oid::Zero) { - ZEN_CONSOLE("Created workspace {} using root path '{}'", m_WorkspaceId, m_WorkspaceRoot); + throw zen::OptionParseException(fmt::format("workspace id '{}' is invalid", m_WorkspaceId)); } - else + } + if (Workspaces::AddWorkspace(Log(), StatePath, {.Id = Oid::FromHexString(m_WorkspaceId), .RootPath = m_WorkspaceRoot})) + { + ZEN_CONSOLE("Created workspace {} using root path '{}'", m_WorkspaceId, m_WorkspaceRoot); + } + else + { + ZEN_CONSOLE("Using existing workspace {} with root path '{}'", m_WorkspaceId, m_WorkspaceRoot); + } + } + + if (m_ShareId.empty()) + { + m_ShareId = Workspaces::PathToId(m_SharePath).ToString(); + ZEN_CONSOLE("Using generated share id {}, for path '{}'", m_ShareId, m_SharePath); + } + + if (Oid::TryFromHexString(m_ShareId) == Oid::Zero) + { + throw zen::OptionParseException(fmt::format("workspace id '{}' is invalid", m_ShareId)); + } + + if (Workspaces::AddWorkspaceShare(Log(), + m_WorkspaceRoot, + {.Id = Oid::FromHexString(m_ShareId), .SharePath = m_SharePath, .Alias = m_Alias})) + { + if (!m_HostName.empty()) + { + HttpClient Http(m_HostName); + if (HttpClient::Response Result = Http.Get("/ws/refresh"); !Result) { - ZEN_CONSOLE("Using existing workspace {} with root path '{}'", m_WorkspaceId, m_WorkspaceRoot); + ZEN_CONSOLE("Failed to refresh workspaces for host {}. Reason: '{}'", m_HostName, Result.ErrorMessage(""sv)); } } - else + ZEN_CONSOLE("Created workspace share {}", m_ShareId); + return 0; + } + else + { + ZEN_CONSOLE("Workspace share {} already exists", m_ShareId); + return 0; + } + } + + if (SubOption == &m_InfoOptions) + { + if (!m_Alias.empty()) + { + Workspaces::WorkspaceConfiguration WorkspaceConfig; + Workspaces::WorkspaceShareConfiguration ShareConfig = + Workspaces::FindWorkspaceShare(Log(), StatePath, m_Alias, WorkspaceConfig); + if (ShareConfig.Id == Oid::Zero) { - Result.ThrowError(fmt::format("failed to create workspace {} with root path '{}'", m_WorkspaceId, m_WorkspaceRoot)); - return 1; + ZEN_CONSOLE("Workspace share with alias {} does not exist", m_Alias); + return 0; } + ShowShare(ShareConfig, WorkspaceConfig.Id, ""sv); + return 0; } if (m_WorkspaceId.empty()) @@ -309,32 +479,102 @@ WorkspaceShareCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** throw zen::OptionParseException("workspace id or root path is required"); } + if (Oid::TryFromHexString(m_WorkspaceId) == Oid::Zero) + { + throw zen::OptionParseException(fmt::format("workspace id '{}' is invalid", m_WorkspaceId)); + } + + Workspaces::WorkspaceConfiguration WorkspaceConfig = Workspaces::FindWorkspace(Log(), StatePath, Oid::FromHexString(m_WorkspaceId)); + if (WorkspaceConfig.Id == Oid::Zero) + { + ZEN_CONSOLE("Workspace {} does not exist", m_WorkspaceId); + return 0; + } + m_WorkspaceRoot = WorkspaceConfig.RootPath; + if (m_ShareId.empty()) { - if (m_SharePath.ends_with(std::filesystem::path::preferred_separator)) + throw zen::OptionParseException("share id is required"); + } + + if (Oid::TryFromHexString(m_ShareId) == Oid::Zero) + { + throw zen::OptionParseException(fmt::format("workspace id '{}' is invalid", m_ShareId)); + } + + Workspaces::WorkspaceShareConfiguration Share = + Workspaces::FindWorkspaceShare(Log(), m_WorkspaceRoot, Oid::FromHexString(m_ShareId)); + if (Share.Id == Oid::Zero) + { + ZEN_CONSOLE("Workspace share {} does not exist", m_ShareId); + return 0; + } + ShowShare(Share, Oid::Zero, ""sv); + return 0; + } + + if (SubOption == &m_RemoveOptions) + { + if (!m_Alias.empty()) + { + Workspaces::WorkspaceConfiguration WorkspaceConfig; + Workspaces::WorkspaceShareConfiguration ShareConfig = + Workspaces::FindWorkspaceShare(Log(), StatePath, m_Alias, WorkspaceConfig); + if (ShareConfig.Id == Oid::Zero) { - m_SharePath.pop_back(); + ZEN_CONSOLE("Workspace share with alias {} does not exist", m_Alias); + return 0; } + m_ShareId = ShareConfig.Id.ToString(); + m_WorkspaceId = WorkspaceConfig.Id.ToString(); + m_WorkspaceRoot = WorkspaceConfig.RootPath; + } + + if (m_WorkspaceId.empty()) + { + throw zen::OptionParseException("workspace id or root path is required"); + } - m_ShareId = Oid::Zero.ToString(); - ZEN_CONSOLE("Using generated share id for path '{}'", m_SharePath); + if (Oid::TryFromHexString(m_WorkspaceId) == Oid::Zero) + { + throw zen::OptionParseException(fmt::format("workspace id '{}' is invalid", m_WorkspaceId)); } - HttpClient::KeyValueMap Params{{"share_path", m_SharePath}}; - if (!m_Alias.empty()) + Workspaces::WorkspaceConfiguration WorkspaceConfig = Workspaces::FindWorkspace(Log(), StatePath, Oid::FromHexString(m_WorkspaceId)); + if (WorkspaceConfig.Id == Oid::Zero) { - Params.Entries.insert_or_assign("alias", m_Alias); + ZEN_CONSOLE("Workspace {} does not exist", m_WorkspaceId); + return 0; + } + m_WorkspaceRoot = WorkspaceConfig.RootPath; + + if (m_ShareId.empty()) + { + throw zen::OptionParseException("share id is required"); + } + + if (Oid::TryFromHexString(m_ShareId) == Oid::Zero) + { + throw zen::OptionParseException(fmt::format("workspace id '{}' is invalid", m_ShareId)); } - if (HttpClient::Response Result = Http.Put(fmt::format("/ws/{}/{}", m_WorkspaceId, m_ShareId), Params)) + if (Workspaces::RemoveWorkspaceShare(Log(), m_WorkspaceRoot, Oid::FromHexString(m_ShareId))) { - ZEN_CONSOLE("{}. Id: {}", Result, Result.AsText()); + if (!m_HostName.empty()) + { + HttpClient Http(m_HostName); + if (HttpClient::Response Result = Http.Get("/ws/refresh"); !Result) + { + ZEN_CONSOLE("Failed to refresh workspaces for host {}. Reason: '{}'", m_HostName, Result.ErrorMessage(""sv)); + } + } + ZEN_CONSOLE("Removed workspace share {}", m_ShareId); return 0; } else { - Result.ThrowError("failed to create workspace share"sv); - return 1; + ZEN_CONSOLE("Removed workspace share {} does not exist", m_ShareId); + return 0; } } @@ -358,34 +598,6 @@ WorkspaceShareCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** } }; - if (SubOption == &m_InfoOptions) - { - if (HttpClient::Response Result = Http.Get(fmt::format("/ws/{}", GetShareIdentityUrl(m_InfoOptions)))) - { - ZEN_CONSOLE("{}", Result.ToText()); - return 0; - } - else - { - Result.ThrowError(fmt::format("failed to get info for share {} in workspace {}", m_ShareId, m_WorkspaceId)); - return 1; - } - } - - if (SubOption == &m_RemoveOptions) - { - if (HttpClient::Response Result = Http.Delete(fmt::format("/ws/{}", GetShareIdentityUrl(m_RemoveOptions)))) - { - ZEN_CONSOLE("{}", Result); - return 0; - } - else - { - Result.ThrowError(fmt::format("failed to remove share {} in workspace {}", m_WorkspaceId, m_ShareId)); - return 1; - } - } - if (SubOption == &m_FilesOptions) { HttpClient::KeyValueMap Params; @@ -398,6 +610,12 @@ WorkspaceShareCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** Params.Entries.insert_or_assign("refresh", ToString(m_Refresh)); } + if (m_HostName.empty()) + { + throw zen::OptionParseException("unable to resolve server specification"); + } + + HttpClient Http(m_HostName); if (HttpClient::Response Result = Http.Get(fmt::format("/ws/{}/files", GetShareIdentityUrl(m_FilesOptions)), {}, Params)) { ZEN_CONSOLE("{}: {}", Result, Result.ToText()); @@ -426,6 +644,12 @@ WorkspaceShareCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** Params.Entries.insert_or_assign("refresh", ToString(m_Refresh)); } + if (m_HostName.empty()) + { + throw zen::OptionParseException("unable to resolve server specification"); + } + + HttpClient Http(m_HostName); if (HttpClient::Response Result = Http.Get(fmt::format("/ws/{}/entries", GetShareIdentityUrl(m_EntriesOptions)), {}, Params)) { ZEN_CONSOLE("{}: {}", Result, Result.ToText()); @@ -438,8 +662,10 @@ WorkspaceShareCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** } } - auto ChunksToOidStrings = - [&Http, WorkspaceId = m_WorkspaceId, ShareId = m_ShareId](std::span<const std::string> ChunkIds) -> std::vector<std::string> { + auto ChunksToOidStrings = [](HttpClient& Http, + std::string_view WorkspaceId, + std::string_view ShareId, + std::span<const std::string> ChunkIds) -> std::vector<std::string> { std::vector<std::string> Oids; Oids.reserve(ChunkIds.size()); std::vector<size_t> NeedsConvertIndexes; @@ -493,7 +719,13 @@ WorkspaceShareCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** throw zen::OptionParseException("chunk id is required"); } - m_ChunkId = ChunksToOidStrings(std::vector<std::string>{m_ChunkId})[0]; + if (m_HostName.empty()) + { + throw zen::OptionParseException("unable to resolve server specification"); + } + + HttpClient Http(m_HostName); + m_ChunkId = ChunksToOidStrings(Http, m_WorkspaceId, m_ShareId, std::vector<std::string>{m_ChunkId})[0]; HttpClient::KeyValueMap Params; if (m_Offset != 0) @@ -529,7 +761,13 @@ WorkspaceShareCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** throw zen::OptionParseException("share is is required"); } - m_ChunkIds = ChunksToOidStrings(m_ChunkIds); + if (m_HostName.empty()) + { + throw zen::OptionParseException("unable to resolve server specification"); + } + + HttpClient Http(m_HostName); + m_ChunkIds = ChunksToOidStrings(Http, m_WorkspaceId, m_ShareId, m_ChunkIds); std::vector<RequestChunkEntry> ChunkRequests; ChunkRequests.resize(m_ChunkIds.size()); diff --git a/src/zen/cmds/workspaces_cmd.h b/src/zen/cmds/workspaces_cmd.h index cce3d0175..de0edd061 100644 --- a/src/zen/cmds/workspaces_cmd.h +++ b/src/zen/cmds/workspaces_cmd.h @@ -4,6 +4,8 @@ #include "../zen.h" +#include <filesystem> + namespace zen { class WorkspaceCommand : public CacheStoreCommand @@ -19,15 +21,17 @@ public: virtual cxxopts::Options& Options() override { return m_Options; } private: - cxxopts::Options m_Options{Name, Description}; - std::string m_HostName; + cxxopts::Options m_Options{Name, Description}; + std::string m_HostName; + std::filesystem::path m_SystemRootDir; std::string m_Verb; // create, info, remove std::string m_Id; - cxxopts::Options m_CreateOptions{"create", "Create a workspace"}; - std::string m_Path; + cxxopts::Options m_CreateOptions{"create", "Create a workspace"}; + std::filesystem::path m_Path; + bool m_AllowShareCreationFromHttp = false; cxxopts::Options m_InfoOptions{"info", "Info about a workspace"}; @@ -49,16 +53,17 @@ public: virtual cxxopts::Options& Options() override { return m_Options; } private: - cxxopts::Options m_Options{Name, Description}; - std::string m_HostName; - std::string m_WorkspaceId; - std::string m_WorkspaceRoot; - std::string m_Verb; // create, info, remove - std::string m_ShareId; - std::string m_Alias; - - cxxopts::Options m_CreateOptions{"create", "Create a workspace share"}; - std::string m_SharePath; + cxxopts::Options m_Options{Name, Description}; + std::string m_HostName; + std::filesystem::path m_SystemRootDir; + std::string m_WorkspaceId; + std::filesystem::path m_WorkspaceRoot; + std::string m_Verb; // create, info, remove + std::string m_ShareId; + std::string m_Alias; + + cxxopts::Options m_CreateOptions{"create", "Create a workspace share"}; + std::filesystem::path m_SharePath; bool m_Refresh = false; diff --git a/src/zencore/filesystem.cpp b/src/zencore/filesystem.cpp index ac2aabbf0..93383a656 100644 --- a/src/zencore/filesystem.cpp +++ b/src/zencore/filesystem.cpp @@ -14,6 +14,9 @@ #if ZEN_PLATFORM_WINDOWS # include <zencore/windows.h> +# include <ShlObj.h> +# pragma comment(lib, "shell32.lib") +# pragma comment(lib, "ole32.lib") #endif #if ZEN_PLATFORM_WINDOWS @@ -28,6 +31,7 @@ ZEN_THIRD_PARTY_INCLUDES_END # include <fcntl.h> # include <sys/resource.h> # include <sys/stat.h> +# include <pwd.h> # include <unistd.h> #endif @@ -38,6 +42,7 @@ ZEN_THIRD_PARTY_INCLUDES_END # include <sys/resource.h> # include <sys/stat.h> # include <sys/syslimits.h> +# include <pwd.h> # include <unistd.h> #endif @@ -1702,6 +1707,31 @@ SearchPathForExecutable(std::string_view ExecutableName) #endif } +std::filesystem::path +PickDefaultSystemRootDirectory() +{ +#if ZEN_PLATFORM_WINDOWS + // Pick sensible default + PWSTR ProgramDataDir = nullptr; + HRESULT hRes = SHGetKnownFolderPath(FOLDERID_ProgramData, 0, NULL, &ProgramDataDir); + + if (SUCCEEDED(hRes)) + { + std::filesystem::path FinalPath(ProgramDataDir); + FinalPath /= L"Epic\\Zen"; + ::CoTaskMemFree(ProgramDataDir); + + return FinalPath; + } + + return L""; +#else // ZEN_PLATFORM_WINDOWS + int UserId = getuid(); + const passwd* Passwd = getpwuid(UserId); + return std::filesystem::path(Passwd->pw_dir) / ".zen"; +#endif // ZEN_PLATFORM_WINDOWS +} + ////////////////////////////////////////////////////////////////////////// // // Testing related code follows... diff --git a/src/zencore/include/zencore/filesystem.h b/src/zencore/include/zencore/filesystem.h index 897a63d8c..2cd663afb 100644 --- a/src/zencore/include/zencore/filesystem.h +++ b/src/zencore/include/zencore/filesystem.h @@ -221,6 +221,8 @@ std::filesystem::path SearchPathForExecutable(std::string_view ExecutableName); std::error_code RotateFiles(const std::filesystem::path& Filename, std::size_t MaxFiles); std::error_code RotateDirectories(const std::filesystem::path& DirectoryName, std::size_t MaxDirectories); +std::filesystem::path PickDefaultSystemRootDirectory(); + ////////////////////////////////////////////////////////////////////////// void filesystem_forcelink(); // internal diff --git a/src/zencore/include/zencore/testutils.h b/src/zencore/include/zencore/testutils.h index 215fb71a8..6a1c0184b 100644 --- a/src/zencore/include/zencore/testutils.h +++ b/src/zencore/include/zencore/testutils.h @@ -18,7 +18,7 @@ public: ScopedTemporaryDirectory(); ~ScopedTemporaryDirectory(); - std::filesystem::path& Path() { return m_RootPath; } + const std::filesystem::path& Path() const { return m_RootPath; } private: std::filesystem::path m_RootPath; diff --git a/src/zenhttp/httpclient.cpp b/src/zenhttp/httpclient.cpp index 0d12cf815..584433f79 100644 --- a/src/zenhttp/httpclient.cpp +++ b/src/zenhttp/httpclient.cpp @@ -110,7 +110,10 @@ CommonResponse(cpr::Response&& HttpResponse, IoBuffer&& Payload = {}) } else { - return ResponseWithPayload(HttpResponse, WorkResponseCode, std::move(Payload)); + return ResponseWithPayload( + HttpResponse, + WorkResponseCode, + Payload ? std::move(Payload) : IoBufferBuilder::MakeCloneFromMemory(HttpResponse.text.data(), HttpResponse.text.size())); } } diff --git a/src/zenserver-test/zenserver-test.cpp b/src/zenserver-test/zenserver-test.cpp index 15993f5c9..ca2257361 100644 --- a/src/zenserver-test/zenserver-test.cpp +++ b/src/zenserver-test/zenserver-test.cpp @@ -3259,16 +3259,22 @@ TEST_CASE("workspaces.create") std::filesystem::path TestDir = TestEnv.CreateNewTestDir(); ZenServerInstance Instance(TestEnv); Instance.SetTestDir(TestDir); - const uint16_t PortNumber = Instance.SpawnServerAndWaitUntilReady(fmt::format("--workspaces-enabled --system-dir {}", SystemRootPath)); + const uint16_t PortNumber = Instance.SpawnServerAndWaitUntilReady( + fmt::format("--workspaces-enabled --workspaces-allow-changes --system-dir {}", SystemRootPath)); CHECK(PortNumber != 0); ScopedTemporaryDirectory TempDir; - std::filesystem::path Root1Path = TempDir.Path() / "root1"; - std::filesystem::path Root2Path = TempDir.Path() / "root2"; - std::filesystem::path Share1Path = "shared_1"; - std::filesystem::path Share2Path = "shared_2"; - CreateDirectories(Share1Path); - CreateDirectories(Share2Path); + std::filesystem::path Root1Path = TempDir.Path() / "root1"; + std::filesystem::path Root2Path = TempDir.Path() / "root2"; + DeleteDirectories(Root1Path); + DeleteDirectories(Root2Path); + + std::filesystem::path Share1Path = "shared_1"; + std::filesystem::path Share2Path = "shared_2"; + CreateDirectories(Root1Path / Share1Path); + CreateDirectories(Root1Path / Share2Path); + CreateDirectories(Root2Path / Share1Path); + CreateDirectories(Root2Path / Share2Path); Oid Root1Id = Oid::Zero; Oid Root2Id = Oid::NewOid(); @@ -3319,8 +3325,11 @@ TEST_CASE("workspaces.create") Client.Put(fmt::format("/ws/{}/{}", Root2Id, Oid::Zero), HttpClient::KeyValueMap{{"share_path", Share2Path.string()}}).StatusCode == HttpResponseCode::NotFound); + CHECK(Client.Put(fmt::format("/ws/{}", Root2Id), HttpClient::KeyValueMap{{"root_path", Root1Path.string()}}).StatusCode == + HttpResponseCode::Conflict); + if (HttpClient::Response Root2Response = - Client.Put(fmt::format("/ws/{}", Root2Id), HttpClient::KeyValueMap{{"root_path", Root1Path.string()}}); + Client.Put(fmt::format("/ws/{}", Root2Id), HttpClient::KeyValueMap{{"root_path", Root2Path.string()}}); Root2Response.StatusCode == HttpResponseCode::Created) { CHECK(Root2Id == Oid::TryFromHexString(Root2Response.AsText())); @@ -3340,6 +3349,10 @@ TEST_CASE("workspaces.create") Share2Id = Oid::TryFromHexString(Share2Response.AsText()); CHECK(Share2Id != Oid::Zero); } + else + { + CHECK(false); + } CHECK( Client.Put(fmt::format("/ws/{}/{}", Root2Id, Oid::Zero), HttpClient::KeyValueMap{{"share_path", Share2Path.string()}}).StatusCode == @@ -3352,6 +3365,117 @@ TEST_CASE("workspaces.create") CHECK( Client.Put(fmt::format("/ws/{}/{}", Root2Id, Share2Id), HttpClient::KeyValueMap{{"share_path", Share1Path.string()}}).StatusCode == HttpResponseCode::Conflict); + + CHECK(Client.Put(fmt::format("/ws/{}/{}", Root2Id, Oid::NewOid()), HttpClient::KeyValueMap{{"share_path", Share2Path.string()}}) + .StatusCode == HttpResponseCode::Conflict); + + CHECK(Client.Put(fmt::format("/ws/{}/{}", Root2Id, Oid::Zero), HttpClient::KeyValueMap{{"share_path", "idonotexist"}}).StatusCode != + HttpResponseCode::OK); + + while (true) + { + std::error_code Ec; + std::filesystem::remove_all(Root2Path / Share2Path, Ec); + if (!Ec) + break; + } + + CHECK(Client.Get(fmt::format("/ws/{}/{}/files", Root2Id, Share2Id)).StatusCode == HttpResponseCode::NotFound); +} + +TEST_CASE("workspaces.restricted") +{ + using namespace std::literals; + + std::filesystem::path SystemRootPath = TestEnv.CreateNewTestDir(); + + std::filesystem::path TestDir = TestEnv.CreateNewTestDir(); + ZenServerInstance Instance(TestEnv); + Instance.SetTestDir(TestDir); + const uint16_t PortNumber = Instance.SpawnServerAndWaitUntilReady(fmt::format("--workspaces-enabled --system-dir {}", SystemRootPath)); + CHECK(PortNumber != 0); + + ScopedTemporaryDirectory TempDir; + std::filesystem::path Root1Path = TempDir.Path() / "root1"; + std::filesystem::path Root2Path = TempDir.Path() / "root2"; + DeleteDirectories(Root1Path); + DeleteDirectories(Root2Path); + + std::filesystem::path Share1Path = "shared_1"; + std::filesystem::path Share2Path = "shared_2"; + CreateDirectories(Root1Path / Share1Path); + CreateDirectories(Root1Path / Share2Path); + CreateDirectories(Root2Path / Share1Path); + CreateDirectories(Root2Path / Share2Path); + + Oid Root1Id = Oid::NewOid(); + Oid Root2Id = Oid::NewOid(); + Oid Share1Id = Oid::NewOid(); + Oid Share2Id = Oid::NewOid(); + + HttpClient Client(Instance.GetBaseUri()); + CHECK(Client.Put(fmt::format("/ws/{}", Oid::Zero), HttpClient::KeyValueMap{{"root_path", Root1Path.string()}}).StatusCode == + HttpResponseCode::Unauthorized); + + CHECK_EQ(Client.Get(fmt::format("/ws/{}", Root1Id)).StatusCode, HttpResponseCode::NotFound); + + std::string Config1; + { + CbObjectWriter Config; + Config.BeginArray("workspaces"); + Config.BeginObject(); + Config << "id"sv << Root1Id.ToString(); + Config << "root_path"sv << Root1Path.string(); + Config << "allow_share_creation_from_http"sv << false; + Config.EndObject(); + Config.EndArray(); + ExtendableStringBuilder<256> SB; + CompactBinaryToJson(Config.Save(), SB); + Config1 = SB.ToString(); + } + WriteFile(SystemRootPath / "workspaces" / "config.json", IoBuffer(IoBuffer::Wrap, Config1.data(), Config1.size())); + + CHECK(IsHttpSuccessCode(Client.Get("/ws/refresh").StatusCode)); + + CHECK_EQ(Client.Get(fmt::format("/ws/{}", Root1Id)).StatusCode, HttpResponseCode::OK); + + CHECK(Client.Get(fmt::format("/ws/{}/{}", Root1Id, Share1Id)).StatusCode == HttpResponseCode::NotFound); + CHECK( + Client.Put(fmt::format("/ws/{}/{}", Root1Id, Oid::Zero), HttpClient::KeyValueMap{{"share_path", Share1Path.string()}}).StatusCode == + HttpResponseCode::Unauthorized); + + std::string Config2; + { + CbObjectWriter Config; + Config.BeginArray("workspaces"); + Config.BeginObject(); + Config << "id"sv << Root1Id.ToString(); + Config << "root_path"sv << Root1Path.string(); + Config << "allow_share_creation_from_http"sv << false; + Config.EndObject(); + Config.BeginObject(); + Config << "id"sv << Root2Id.ToString(); + Config << "root_path"sv << Root2Path.string(); + Config << "allow_share_creation_from_http"sv << true; + Config.EndObject(); + Config.EndArray(); + ExtendableStringBuilder<256> SB; + CompactBinaryToJson(Config.Save(), SB); + Config2 = SB.ToString(); + } + WriteFile(SystemRootPath / "workspaces" / "config.json", IoBuffer(IoBuffer::Wrap, Config2.data(), Config2.size())); + + CHECK(IsHttpSuccessCode(Client.Get("/ws/refresh").StatusCode)); + + CHECK_EQ(Client.Get(fmt::format("/ws/{}", Root2Id)).StatusCode, HttpResponseCode::OK); + + CHECK(Client.Get(fmt::format("/ws/{}/{}", Root2Id, Share2Id)).StatusCode == HttpResponseCode::NotFound); + CHECK( + Client.Put(fmt::format("/ws/{}/{}", Root2Id, Share2Id), HttpClient::KeyValueMap{{"share_path", Share2Path.string()}}).StatusCode == + HttpResponseCode::Created); + CHECK(Client.Get(fmt::format("/ws/{}/{}", Root2Id, Share2Id)).StatusCode == HttpResponseCode::OK); + + CHECK(IsHttpSuccessCode(Client.Delete(fmt::format("/ws/{}/{}", Root2Id, Share2Id)).StatusCode)); } TEST_CASE("workspaces.lifetimes") @@ -3363,19 +3487,20 @@ TEST_CASE("workspaces.lifetimes") Oid WorkspaceId = Oid::NewOid(); Oid ShareId = Oid::NewOid(); + ScopedTemporaryDirectory TempDir; + std::filesystem::path RootPath = TempDir.Path(); + DeleteDirectories(RootPath); + std::filesystem::path SharePath = RootPath / "shared_folder"; + CreateDirectories(SharePath); + { std::filesystem::path TestDir = TestEnv.CreateNewTestDir(); ZenServerInstance Instance(TestEnv); Instance.SetTestDir(TestDir); - const uint16_t PortNumber = - Instance.SpawnServerAndWaitUntilReady(fmt::format("--workspaces-enabled --system-dir {}", SystemRootPath)); + const uint16_t PortNumber = Instance.SpawnServerAndWaitUntilReady( + fmt::format("--workspaces-enabled --workspaces-allow-changes --system-dir {}", SystemRootPath)); CHECK(PortNumber != 0); - ScopedTemporaryDirectory TempDir; - std::filesystem::path RootPath = TempDir.Path(); - std::filesystem::path SharePath = RootPath / "shared_folder"; - CreateDirectories(SharePath); - HttpClient Client(Instance.GetBaseUri()); CHECK(Client.Put(fmt::format("/ws/{}", WorkspaceId), HttpClient::KeyValueMap{{"root_path", RootPath.string()}}).StatusCode == HttpResponseCode::Created); @@ -3427,14 +3552,18 @@ TEST_CASE("workspaces.lifetimes") TEST_CASE("workspaces.share") { + std::filesystem::path SystemRootPath = TestEnv.CreateNewTestDir(); + ZenServerInstance Instance(TestEnv); - const uint16_t PortNumber = Instance.SpawnServerAndWaitUntilReady("--workspaces-enabled"); + const uint16_t PortNumber = Instance.SpawnServerAndWaitUntilReady( + fmt::format("--workspaces-enabled --workspaces-allow-changes --system-dir {}", SystemRootPath)); CHECK(PortNumber != 0); ScopedTemporaryDirectory TempDir; - std::filesystem::path RootPath = TempDir.Path(); - std::filesystem::path SharePath = RootPath / "shared_folder"; + std::filesystem::path RootPath = TempDir.Path(); + DeleteDirectories(RootPath); + std::filesystem::path SharePath = RootPath / "shared_folder"; GenerateFolderContent(SharePath); HttpClient Client(Instance.GetBaseUri()); diff --git a/src/zenserver/config.cpp b/src/zenserver/config.cpp index cda5aca16..2fd9bbaf3 100644 --- a/src/zenserver/config.cpp +++ b/src/zenserver/config.cpp @@ -27,61 +27,12 @@ ZEN_THIRD_PARTY_INCLUDES_END #if ZEN_PLATFORM_WINDOWS # include <conio.h> #else -# include <pwd.h> # include <unistd.h> #endif #include <unordered_map> #include <unordered_set> -#if ZEN_PLATFORM_WINDOWS - -# include <zencore/windows.h> - -// Used for getting My Documents for default data directory -# include <ShlObj.h> -# pragma comment(lib, "shell32.lib") -# pragma comment(lib, "ole32.lib") - -namespace zen { - -std::filesystem::path -PickDefaultSystemRootDirectory() -{ - // Pick sensible default - PWSTR ProgramDataDir = nullptr; - HRESULT hRes = SHGetKnownFolderPath(FOLDERID_ProgramData, 0, NULL, &ProgramDataDir); - - if (SUCCEEDED(hRes)) - { - std::filesystem::path FinalPath(ProgramDataDir); - FinalPath /= L"Epic\\Zen"; - ::CoTaskMemFree(ProgramDataDir); - - return FinalPath; - } - - return L""; -} - -} // namespace zen - -#else - -namespace zen { - -std::filesystem::path -PickDefaultSystemRootDirectory() -{ - int UserId = getuid(); - const passwd* Passwd = getpwuid(UserId); - return std::filesystem::path(Passwd->pw_dir) / ".zen"; -} - -} // namespace zen - -#endif - namespace zen { std::filesystem::path @@ -547,6 +498,9 @@ ParseConfigFile(const std::filesystem::path& Path, ////// workspaces LuaOptions.AddOption("workspaces.enabled"sv, ServerOptions.WorksSpacesConfig.Enabled, "workspaces-enabled"sv); + LuaOptions.AddOption("workspaces.allowconfigchanges"sv, + ServerOptions.WorksSpacesConfig.AllowConfigurationChanges, + "workspaces-allow-changes"sv); // These have special command line processing so we make sure we export them if they were configured on command line if (!ServerOptions.AuthConfig.OpenIdProviders.empty()) @@ -1063,13 +1017,19 @@ ParseCliOptions(int argc, char* argv[], ZenServerOptions& ServerOptions) cxxopts::value<bool>(ServerOptions.StatsConfig.Enabled)->default_value("false"), "Enable statsd reporter (localhost:8125)"); - options.add_option("stats", + options.add_option("workspaces", "", "workspaces-enabled", "", - cxxopts::value<bool>(ServerOptions.WorksSpacesConfig.Enabled)->default_value("false"), + cxxopts::value<bool>(ServerOptions.WorksSpacesConfig.Enabled)->default_value("true"), "Enable workspaces support with folder sharing"); + options.add_option("workspaces", + "", + "workspaces-allow-changes", + "", + cxxopts::value<bool>(ServerOptions.WorksSpacesConfig.AllowConfigurationChanges)->default_value("false"), + "Allow adding/modifying/deleting of workspace and shares via http endpoint"); try { cxxopts::ParseResult Result; diff --git a/src/zenserver/config.h b/src/zenserver/config.h index 58a31bbb0..3e01cac99 100644 --- a/src/zenserver/config.h +++ b/src/zenserver/config.h @@ -131,7 +131,8 @@ struct ZenProjectStoreConfig struct ZenWorkspacesConfig { - bool Enabled = false; + bool Enabled = false; + bool AllowConfigurationChanges = false; }; struct ZenServerOptions diff --git a/src/zenserver/workspaces/httpworkspaces.cpp b/src/zenserver/workspaces/httpworkspaces.cpp index 6a4e9c466..6e27b0f1e 100644 --- a/src/zenserver/workspaces/httpworkspaces.cpp +++ b/src/zenserver/workspaces/httpworkspaces.cpp @@ -29,19 +29,51 @@ namespace { return {}; } - Oid PathToChunkId(const std::filesystem::path& Path) + void WriteWorkspaceConfig(CbWriter& Writer, const Workspaces::WorkspaceConfiguration& Config) { - const std::string PathBuffer = reinterpret_cast<const char*>(Path.generic_u8string().c_str()); - BLAKE3 Hash = BLAKE3::HashMemory(PathBuffer.data(), PathBuffer.size()); - Hash.Hash[11] = 7; // FIoChunkType::ExternalFile - return Oid::FromMemory(Hash.Hash); - } + Writer << "id" << Config.Id; + Writer << "root_path" << Config.RootPath.string(); // utf8? + Writer << "allow_share_creation_from_http" << Config.AllowShareCreationFromHttp; + }; + + void WriteWorkspaceShareConfig(CbWriter& Writer, const Workspaces::WorkspaceShareConfiguration& Config) + { + Writer << "id" << Config.Id; + Writer << "share_path" << Config.SharePath.string(); // utf8? + if (!Config.Alias.empty()) + { + Writer << "alias" << Config.Alias; + } + }; - constinit AsciiSet ValidAliasCharactersSet{"abcdefghijklmnopqrstuvwxyz0123456789+-_.[]ABCDEFGHIJKLMNOPQRSTUVWXYZ"}; + void WriteWorkspaceAndSharesConfig(CbWriter& Writer, Workspaces& Workspaces, const Workspaces::WorkspaceConfiguration& WorkspaceConfig) + { + WriteWorkspaceConfig(Writer, WorkspaceConfig); + if (std::optional<std::vector<Oid>> ShareIds = Workspaces.GetWorkspaceShares(WorkspaceConfig.Id); ShareIds) + { + for (const Oid& ShareId : *ShareIds) + { + Writer.BeginArray("shares"); + { + if (std::optional<Workspaces::WorkspaceShareConfiguration> WorkspaceShareConfig = + Workspaces.GetWorkspaceShareConfiguration(WorkspaceConfig.Id, ShareId); + WorkspaceShareConfig) + { + Writer.BeginObject(); + { + WriteWorkspaceShareConfig(Writer, *WorkspaceShareConfig); + } + Writer.EndObject(); + } + } + Writer.EndArray(); + } + } + } } // namespace -HttpWorkspacesService::HttpWorkspacesService(HttpStatsService& StatsService, const FileServeConfig& Cfg, Workspaces& Workspaces) +HttpWorkspacesService::HttpWorkspacesService(HttpStatsService& StatsService, const WorkspacesServeConfig& Cfg, Workspaces& Workspaces) : m_Log(logging::Get("workspaces")) , m_StatsService(StatsService) , m_Config(Cfg) @@ -125,7 +157,7 @@ HttpWorkspacesService::Initialize() m_StatsService.RegisterHandler("ws", *this); - m_Router.AddPattern("workspace", "([[:xdigit:]]{24})"); + m_Router.AddPattern("workspace_id", "([[:xdigit:]]{24})"); m_Router.AddPattern("share_id", "([[:xdigit:]]{24})"); m_Router.AddPattern("chunk", "([[:xdigit:]]{24})"); m_Router.AddPattern("share_alias", "([[:alnum:]_.\\+\\-\\[\\]]+)"); @@ -195,7 +227,17 @@ HttpWorkspacesService::Initialize() [this](HttpRouterRequest& Req) { WorkspaceRequest(Req); }, HttpVerb::kPut | HttpVerb::kGet | HttpVerb::kDelete); - ReadState(); + m_Router.RegisterRoute( + "refresh", + [this](HttpRouterRequest& Req) { RefreshRequest(Req); }, + HttpVerb::kGet); + + m_Router.RegisterRoute( + "", + [this](HttpRouterRequest& Req) { WorkspacesRequest(Req); }, + HttpVerb::kGet); + + RefreshState(); } std::filesystem::path @@ -205,21 +247,52 @@ HttpWorkspacesService::GetStatePath() const } void -HttpWorkspacesService::ReadState() +HttpWorkspacesService::RefreshState() { if (!m_Config.SystemRootDir.empty()) { - m_Workspaces.ReadState(GetStatePath(), [](const std::filesystem::path& Path) { return PathToChunkId(Path); }); + m_Workspaces.RefreshState(GetStatePath()); } } +bool +HttpWorkspacesService::MayChangeConfiguration(const HttpServerRequest& Req) const +{ + ZEN_UNUSED(Req); + return m_Config.AllowConfigurationChanges; +} + void -HttpWorkspacesService::WriteState() +HttpWorkspacesService::RefreshRequest(HttpRouterRequest& Req) { - if (!m_Config.SystemRootDir.empty()) + HttpServerRequest& ServerRequest = Req.ServerRequest(); + RefreshState(); + return ServerRequest.WriteResponse(HttpResponseCode::OK); +} + +void +HttpWorkspacesService::WorkspacesRequest(HttpRouterRequest& Req) +{ + HttpServerRequest& ServerRequest = Req.ServerRequest(); + + std::vector<Oid> WorkspaceIds = m_Workspaces.GetWorkspaces(); + CbObjectWriter Response; + Response.BeginArray("workspaces"); + for (const Oid& WorkspaceId : WorkspaceIds) { - m_Workspaces.WriteState(GetStatePath()); + if (std::optional<Workspaces::WorkspaceConfiguration> WorkspaceConfig = m_Workspaces.GetWorkspaceConfiguration(WorkspaceId); + WorkspaceConfig) + { + Response.BeginObject(); + { + WriteWorkspaceAndSharesConfig(Response, m_Workspaces, *WorkspaceConfig); + } + Response.EndObject(); + } } + Response.EndArray(); + + return ServerRequest.WriteResponse(HttpResponseCode::OK, Response.Save()); } void @@ -397,10 +470,11 @@ HttpWorkspacesService::WorkspaceRequest(HttpRouterRequest& Req) HttpContentType::kText, "Invalid 'root_path' parameter"); } + if (Req.GetCapture(1) == Oid::Zero.ToString()) { // Synthesize Id - WorkspaceId = PathToChunkId(WorkspacePath); + WorkspaceId = Workspaces::PathToId(WorkspacePath); ZEN_INFO("Generated workspace id from path '{}': {}", WorkspacePath, WorkspaceId); } else if (WorkspaceId == Oid::Zero) @@ -410,25 +484,54 @@ HttpWorkspacesService::WorkspaceRequest(HttpRouterRequest& Req) HttpContentType::kText, fmt::format("Invalid workspace id '{}'", Req.GetCapture(1))); } - m_WorkspacesStats.WorkspaceWriteCount++; - Workspaces::WorkspaceConfiguration NewConfig = {.Id = WorkspaceId, .RootPath = WorkspacePath}; - bool OK = m_Workspaces.AddWorkspace(NewConfig); - if (OK) + + if (!MayChangeConfiguration(ServerRequest)) { - WriteState(); - return ServerRequest.WriteResponse(HttpResponseCode::Created, HttpContentType::kText, fmt::format("{}", WorkspaceId)); + return ServerRequest.WriteResponse(HttpResponseCode::Unauthorized, + HttpContentType::kText, + fmt::format("Adding workspace {} is not allowed", WorkspaceId)); } - else + bool AllowShareCreationFromHttp = false; + if (std::string_view Value = ServerRequest.GetQueryParams().GetValue("allow_share_creation_from_http"); Value == "true"sv) + { + AllowShareCreationFromHttp = true; + } + + m_WorkspacesStats.WorkspaceWriteCount++; + Workspaces::WorkspaceConfiguration OldConfig = Workspaces::FindWorkspace(Log(), GetStatePath(), WorkspaceId); + Workspaces::WorkspaceConfiguration NewConfig = {.Id = WorkspaceId, + .RootPath = WorkspacePath, + .AllowShareCreationFromHttp = AllowShareCreationFromHttp}; + if (OldConfig.Id == WorkspaceId && (OldConfig != NewConfig)) { - Workspaces::WorkspaceConfiguration Config = m_Workspaces.GetWorkspaceConfiguration(WorkspaceId); - if (Config == NewConfig) - { - return ServerRequest.WriteResponse(HttpResponseCode::OK, HttpContentType::kText, fmt::format("{}", WorkspaceId)); - } return ServerRequest.WriteResponse( HttpResponseCode::Conflict, HttpContentType::kText, - fmt::format("Workspace {} already exists with root path '{}'", WorkspaceId, Config.RootPath)); + fmt::format("Workspace {} already exists with root path '{}'", WorkspaceId, OldConfig.RootPath)); + } + else if (OldConfig.Id == Oid::Zero) + { + if (Workspaces::WorkspaceConfiguration ConfigWithSameRoot = + Workspaces::FindWorkspace(Log(), GetStatePath(), WorkspacePath); + ConfigWithSameRoot.Id != Oid::Zero) + { + return ServerRequest.WriteResponse( + HttpResponseCode::Conflict, + HttpContentType::kText, + fmt::format("Workspace {} already exists with same root path '{}'", ConfigWithSameRoot.Id, WorkspacePath)); + } + } + + bool Created = Workspaces::AddWorkspace(Log(), GetStatePath(), NewConfig); + if (Created) + { + ZEN_ASSERT(OldConfig.Id == Oid::Zero); + RefreshState(); + return ServerRequest.WriteResponse(HttpResponseCode::Created, HttpContentType::kText, fmt::format("{}", WorkspaceId)); + } + else + { + return ServerRequest.WriteResponse(HttpResponseCode::OK, HttpContentType::kText, fmt::format("{}", WorkspaceId)); } } case HttpVerb::kGet: @@ -441,31 +544,17 @@ HttpWorkspacesService::WorkspaceRequest(HttpRouterRequest& Req) fmt::format("Invalid workspace id '{}'", Req.GetCapture(1))); } m_WorkspacesStats.WorkspaceReadCount++; - Workspaces::WorkspaceInfo Info = m_Workspaces.GetWorkspaceInfo(WorkspaceId); - if (Info.Config.Id != Oid::Zero) + std::optional<Workspaces::WorkspaceConfiguration> Workspace = m_Workspaces.GetWorkspaceConfiguration(WorkspaceId); + if (Workspace) { CbObjectWriter Response; - Response << "id" << Info.Config.Id; - Response << "root_path" << Info.Config.RootPath.string(); // utf8? - Response.BeginArray("shares"); - for (const Workspaces::WorkspaceShareConfiguration& ShareConfig : Info.Shares) - { - Response.BeginObject(); - { - Response << "id" << ShareConfig.Id; - Response << "share_path" << ShareConfig.SharePath.string(); // utf8? - if (!ShareConfig.Alias.empty()) - { - Response << "alias" << ShareConfig.Alias; - } - } - Response.EndObject(); - } - Response.EndArray(); - + WriteWorkspaceAndSharesConfig(Response, m_Workspaces, *Workspace); return ServerRequest.WriteResponse(HttpResponseCode::OK, Response.Save()); } - return ServerRequest.WriteResponse(HttpResponseCode::NotFound); + else + { + return ServerRequest.WriteResponse(HttpResponseCode::NotFound); + } } case HttpVerb::kDelete: { @@ -476,11 +565,19 @@ HttpWorkspacesService::WorkspaceRequest(HttpRouterRequest& Req) HttpContentType::kText, fmt::format("Invalid workspace id '{}'", Req.GetCapture(1))); } + + if (!MayChangeConfiguration(ServerRequest)) + { + return ServerRequest.WriteResponse(HttpResponseCode::Unauthorized, + HttpContentType::kText, + fmt::format("Removing workspace {} is not allowed", WorkspaceId)); + } + m_WorkspacesStats.WorkspaceDeleteCount++; - bool Deleted = m_Workspaces.RemoveWorkspace(WorkspaceId); + bool Deleted = Workspaces::RemoveWorkspace(Log(), GetStatePath(), WorkspaceId); if (Deleted) { - WriteState(); + RefreshState(); return ServerRequest.WriteResponse(HttpResponseCode::OK); } return ServerRequest.WriteResponse(HttpResponseCode::NotFound); @@ -938,50 +1035,88 @@ HttpWorkspacesService::ShareRequest(HttpRouterRequest& Req, const Oid& Workspace if (ShareId == Oid::Zero) { // Synthesize Id - ShareId = PathToChunkId(SharePath); + ShareId = Workspaces::PathToId(SharePath); ZEN_INFO("Generated workspace id from path '{}': {}", SharePath, ShareId); } std::string Alias = HttpServerRequest::Decode(ServerRequest.GetQueryParams().GetValue("alias"sv)); - if (!AsciiSet::HasOnly(Alias, ValidAliasCharactersSet)) + if (!AsciiSet::HasOnly(Alias, Workspaces::ValidAliasCharactersSet)) { return ServerRequest.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, "Invalid 'alias' parameter"); } - m_WorkspacesStats.WorkspaceShareWriteCount++; - if (m_Workspaces.GetWorkspaceInfo(WorkspaceId).Config.Id != WorkspaceId) + Workspaces::WorkspaceConfiguration Workspace = Workspaces::FindWorkspace(Log(), GetStatePath(), WorkspaceId); + if (Workspace.Id == Oid::Zero) { return ServerRequest.WriteResponse(HttpResponseCode::NotFound, HttpContentType::kText, fmt::format("Workspace '{}' does not exist", WorkspaceId)); } + + if (!Workspace.AllowShareCreationFromHttp) + { + if (!MayChangeConfiguration(ServerRequest)) + { + return ServerRequest.WriteResponse( + HttpResponseCode::Unauthorized, + HttpContentType::kText, + fmt::format("Adding workspace share {} in workspace {} is not allowed", WorkspaceId, ShareId)); + } + } + + m_WorkspacesStats.WorkspaceShareWriteCount++; + + const Workspaces::WorkspaceShareConfiguration OldConfig = + Workspaces::FindWorkspaceShare(Log(), Workspace.RootPath, ShareId); const Workspaces::WorkspaceShareConfiguration NewConfig = {.Id = ShareId, .SharePath = SharePath, .Alias = std::string(Alias)}; - bool OK = m_Workspaces.AddWorkspaceShare(WorkspaceId, NewConfig, [](const std::filesystem::path& Path) { - return PathToChunkId(Path); - }); - if (OK) - { - WriteState(); - return ServerRequest.WriteResponse(HttpResponseCode::Created, HttpContentType::kText, fmt::format("{}", ShareId)); - } - else + + if (OldConfig.Id == ShareId && (OldConfig != NewConfig)) { - Workspaces::WorkspaceShareConfiguration Config = m_Workspaces.GetWorkspaceShareConfiguration(WorkspaceId, ShareId); - if (Config == NewConfig) - { - return ServerRequest.WriteResponse(HttpResponseCode::OK, HttpContentType::kText, fmt::format("{}", ShareId)); - } return ServerRequest.WriteResponse( HttpResponseCode::Conflict, HttpContentType::kText, fmt::format("Workspace share '{}' already exist in workspace '{}' with share path '{}' and alias '{}'", ShareId, WorkspaceId, - Config.SharePath, - Config.Alias)); + OldConfig.SharePath, + OldConfig.Alias)); + } + else if (OldConfig.Id == Oid::Zero) + { + if (Workspaces::WorkspaceShareConfiguration ConfigWithSamePath = + Workspaces::FindWorkspaceShare(Log(), Workspace.RootPath, SharePath); + ConfigWithSamePath.Id != Oid::Zero) + { + return ServerRequest.WriteResponse( + HttpResponseCode::Conflict, + HttpContentType::kText, + fmt::format("Workspace share '{}' already exist in workspace '{}' with same share path '{}' and alias '{}'", + ShareId, + WorkspaceId, + OldConfig.SharePath, + OldConfig.Alias)); + } + } + + if (!std::filesystem::is_directory(Workspace.RootPath / NewConfig.SharePath)) + { + return ServerRequest.WriteResponse(HttpResponseCode::NotFound, + HttpContentType::kText, + fmt::format("directory {} does not exist in workspace {} root '{}'", + NewConfig.SharePath, + WorkspaceId, + Workspace.RootPath)); + } + + bool Created = Workspaces::AddWorkspaceShare(Log(), Workspace.RootPath, NewConfig); + if (Created) + { + RefreshState(); + return ServerRequest.WriteResponse(HttpResponseCode::Created, HttpContentType::kText, fmt::format("{}", ShareId)); } + return ServerRequest.WriteResponse(HttpResponseCode::OK, HttpContentType::kText, fmt::format("{}", ShareId)); } case HttpVerb::kGet: { @@ -999,20 +1134,18 @@ HttpWorkspacesService::ShareRequest(HttpRouterRequest& Req, const Oid& Workspace HttpContentType::kText, fmt::format("Invalid share id '{}'", ShareId)); } + m_WorkspacesStats.WorkspaceShareReadCount++; - Workspaces::WorkspaceShareConfiguration Config = m_Workspaces.GetWorkspaceShareConfiguration(WorkspaceId, ShareId); - if (Config.Id != Oid::Zero) + std::optional<Workspaces::WorkspaceShareConfiguration> Config = + m_Workspaces.GetWorkspaceShareConfiguration(WorkspaceId, ShareId); + if (!Config) { - CbObjectWriter Response; - Response << "id" << Config.Id; - Response << "share_path" << Config.SharePath.string(); // utf8? - if (!Config.Alias.empty()) - { - Response << "alias" << Config.Alias; - } - return ServerRequest.WriteResponse(HttpResponseCode::OK, Response.Save()); + return ServerRequest.WriteResponse(HttpResponseCode::NotFound); } - return ServerRequest.WriteResponse(HttpResponseCode::NotFound); + + CbObjectWriter Response; + WriteWorkspaceShareConfig(Response, *Config); + return ServerRequest.WriteResponse(HttpResponseCode::OK, Response.Save()); } case HttpVerb::kDelete: { @@ -1030,11 +1163,29 @@ HttpWorkspacesService::ShareRequest(HttpRouterRequest& Req, const Oid& Workspace HttpContentType::kText, fmt::format("Invalid share id '{}'", ShareId)); } + + Workspaces::WorkspaceConfiguration Workspace = Workspaces::FindWorkspace(Log(), GetStatePath(), WorkspaceId); + if (Workspace.Id == Oid::Zero) + { + return ServerRequest.WriteResponse(HttpResponseCode::NotFound); + } + + if (!Workspace.AllowShareCreationFromHttp) + { + if (!MayChangeConfiguration(ServerRequest)) + { + return ServerRequest.WriteResponse( + HttpResponseCode::Unauthorized, + HttpContentType::kText, + fmt::format("Removing workspace share {} in workspace {} is not allowed", WorkspaceId, ShareId)); + } + } + m_WorkspacesStats.WorkspaceShareDeleteCount++; - bool Deleted = m_Workspaces.RemoveWorkspaceShare(WorkspaceId, ShareId); + bool Deleted = Workspaces::RemoveWorkspaceShare(Log(), Workspace.RootPath, ShareId); if (Deleted) { - WriteState(); + RefreshState(); return ServerRequest.WriteResponse(HttpResponseCode::OK); } return ServerRequest.WriteResponse(HttpResponseCode::NotFound); diff --git a/src/zenserver/workspaces/httpworkspaces.h b/src/zenserver/workspaces/httpworkspaces.h index dfa50f822..f01f58b86 100644 --- a/src/zenserver/workspaces/httpworkspaces.h +++ b/src/zenserver/workspaces/httpworkspaces.h @@ -10,15 +10,16 @@ namespace zen { class Workspaces; -struct FileServeConfig +struct WorkspacesServeConfig { std::filesystem::path SystemRootDir; + bool AllowConfigurationChanges = false; }; class HttpWorkspacesService final : public HttpService, public IHttpStatsProvider { public: - HttpWorkspacesService(HttpStatsService& StatsService, const FileServeConfig& Cfg, Workspaces& Workspaces); + HttpWorkspacesService(HttpStatsService& StatsService, const WorkspacesServeConfig& Cfg, Workspaces& Workspaces); virtual ~HttpWorkspacesService(); virtual const char* BaseUri() const override; @@ -50,9 +51,13 @@ private: void Initialize(); std::filesystem::path GetStatePath() const; - void ReadState(); - void WriteState(); + void RefreshState(); + // void WriteState(); + bool MayChangeConfiguration(const HttpServerRequest& Req) const; + + void WorkspacesRequest(HttpRouterRequest& Req); + void RefreshRequest(HttpRouterRequest& Req); void FilesRequest(HttpRouterRequest& Req); void ChunkInfoRequest(HttpRouterRequest& Req); void BatchRequest(HttpRouterRequest& Req); @@ -75,12 +80,12 @@ private: void ChunkRequest(HttpRouterRequest& Req, const Oid& WorkspaceId, const Oid& ShareId, const Oid& ChunkId); void ShareRequest(HttpRouterRequest& Req, const Oid& WorkspaceId, const Oid& InShareId); - HttpStatsService& m_StatsService; - const FileServeConfig m_Config; - HttpRequestRouter m_Router; - Workspaces& m_Workspaces; - WorkspacesStats m_WorkspacesStats; - metrics::OperationTiming m_HttpRequests; + HttpStatsService& m_StatsService; + const WorkspacesServeConfig m_Config; + HttpRequestRouter m_Router; + Workspaces& m_Workspaces; + WorkspacesStats m_WorkspacesStats; + metrics::OperationTiming m_HttpRequests; }; } // namespace zen diff --git a/src/zenserver/zenserver.cpp b/src/zenserver/zenserver.cpp index 124e9ff5f..f6d6556a0 100644 --- a/src/zenserver/zenserver.cpp +++ b/src/zenserver/zenserver.cpp @@ -239,7 +239,10 @@ ZenServer::Initialize(const ZenServerOptions& ServerOptions, ZenServerState::Zen { m_Workspaces.reset(new Workspaces()); m_HttpWorkspacesService.reset( - new HttpWorkspacesService(m_StatsService, {.SystemRootDir = ServerOptions.SystemRootDir}, *m_Workspaces)); + new HttpWorkspacesService(m_StatsService, + {.SystemRootDir = ServerOptions.SystemRootDir, + .AllowConfigurationChanges = ServerOptions.WorksSpacesConfig.AllowConfigurationChanges}, + *m_Workspaces)); } if (ServerOptions.StructuredCacheConfig.Enabled) diff --git a/src/zenstore/include/zenstore/workspaces.h b/src/zenstore/include/zenstore/workspaces.h index a3e51b20d..3e9edf9f9 100644 --- a/src/zenstore/include/zenstore/workspaces.h +++ b/src/zenstore/include/zenstore/workspaces.h @@ -23,6 +23,8 @@ class WorkspaceShare; class Workspaces { public: + static constexpr AsciiSet ValidAliasCharactersSet{"abcdefghijklmnopqrstuvwxyz0123456789+-_.[]ABCDEFGHIJKLMNOPQRSTUVWXYZ"}; + struct ChunkRequest { Oid ChunkId; @@ -41,7 +43,11 @@ public: { Oid Id; std::filesystem::path RootPath; - inline bool operator==(const WorkspaceConfiguration& Rhs) const { return Id == Rhs.Id && RootPath == Rhs.RootPath; } + bool AllowShareCreationFromHttp = false; + inline bool operator==(const WorkspaceConfiguration& Rhs) const + { + return Id == Rhs.Id && RootPath == Rhs.RootPath && AllowShareCreationFromHttp == Rhs.AllowShareCreationFromHttp; + } }; struct WorkspaceShareConfiguration @@ -55,26 +61,9 @@ public: } }; - struct WorkspaceInfo - { - WorkspaceConfiguration Config; - std::vector<WorkspaceShareConfiguration> Shares; - }; - Workspaces(); ~Workspaces(); - bool AddWorkspace(const WorkspaceConfiguration& Configuration); - WorkspaceConfiguration GetWorkspaceConfiguration(const Oid& WorkspaceId) const; - WorkspaceInfo GetWorkspaceInfo(const Oid& WorkspaceId) const; - bool RemoveWorkspace(const Oid& WorkspaceId); - - bool AddWorkspaceShare(const Oid& WorkspaceId, - const WorkspaceShareConfiguration& Configuration, - const std::function<Oid(const std::filesystem::path& Path)>& PathToIdCB); - WorkspaceShareConfiguration GetWorkspaceShareConfiguration(const Oid& WorkspaceId, const Oid& ShareId) const; - bool RemoveWorkspaceShare(const Oid& WorkspaceId, const Oid& ShareId); - std::optional<std::vector<ShareFile>> GetWorkspaceShareFiles(const Oid& WorkspaceId, const Oid& ShareId, bool ForceRefresh, @@ -87,9 +76,12 @@ public: const std::span<const ChunkRequest> ChunkRequests, WorkerThreadPool& WorkerPool); - void WriteState(const std::filesystem::path& WorkspaceStatePath); - void ReadState(const std::filesystem::path& WorkspaceStatePath, - const std::function<Oid(const std::filesystem::path& Path)>& PathToIdCB); + std::vector<Oid> GetWorkspaces() const; + std::optional<WorkspaceConfiguration> GetWorkspaceConfiguration(const Oid& WorkspaceId) const; + std::optional<std::vector<Oid>> GetWorkspaceShares(const Oid& WorkspaceId) const; + std::optional<WorkspaceShareConfiguration> GetWorkspaceShareConfiguration(const Oid& WorkspaceId, const Oid& ShareId) const; + + void RefreshState(const std::filesystem::path& WorkspaceStatePath); struct ShareAlias { @@ -99,7 +91,56 @@ public: std::optional<ShareAlias> GetShareAlias(std::string_view Alias) const; + static bool AddWorkspace(const LoggerRef& Log, + const std::filesystem::path& WorkspaceStatePath, + const WorkspaceConfiguration& Configuration); + static bool RemoveWorkspace(const LoggerRef& Log, const std::filesystem::path& WorkspaceStatePath, const Oid& WorkspaceId); + static bool AddWorkspaceShare(const LoggerRef& Log, + const std::filesystem::path& WorkspaceRoot, + const WorkspaceShareConfiguration& Configuration); + static bool RemoveWorkspaceShare(const LoggerRef& Log, const std::filesystem::path& WorkspaceRoot, const Oid& WorkspaceShareId); + static WorkspaceConfiguration FindWorkspace(const LoggerRef& Log, + const std::filesystem::path& WorkspaceStatePath, + const Oid& WorkspaceId); + static WorkspaceConfiguration FindWorkspace(const LoggerRef& InLog, + const std::filesystem::path& WorkspaceStatePath, + const std::filesystem::path& WorkspaceRoot); + + static WorkspaceShareConfiguration FindWorkspaceShare(const LoggerRef& Log, + const std::filesystem::path& WorkspaceStatePath, + std::string_view ShareAlias, + WorkspaceConfiguration& OutWorkspace); + static WorkspaceShareConfiguration FindWorkspaceShare(const LoggerRef& InLog, + const std::filesystem::path& WorkspaceStatePath, + const Oid& WorkspaceId, + const Oid& WorkspaceShareId); + static WorkspaceShareConfiguration FindWorkspaceShare(const LoggerRef& Log, + const std::filesystem::path& WorkspaceRoot, + const Oid& WorkspaceShareId); + static WorkspaceShareConfiguration FindWorkspaceShare(const LoggerRef& Log, + const std::filesystem::path& WorkspaceRoot, + const std::filesystem::path& SharePath); + static std::vector<WorkspaceConfiguration> ReadConfig(const LoggerRef& Log, + const std::filesystem::path& WorkspaceStatePath, + std::string& OutError); + static std::vector<WorkspaceShareConfiguration> ReadWorkspaceConfig(const LoggerRef& Log, + const std::filesystem::path& WorkspaceRoot, + std::string& OutError); + + static Oid PathToId(const std::filesystem::path& Path); + private: + static void WriteConfig(const LoggerRef& Log, + const std::filesystem::path& WorkspaceStatePath, + const std::vector<WorkspaceConfiguration>& WorkspaceConfigurations); + + static void WriteWorkspaceConfig(const LoggerRef& Log, + const std::filesystem::path& WorkspaceRoot, + const std::vector<WorkspaceShareConfiguration>& WorkspaceShareConfigurations); + + void RefreshWorkspaceShares(const Oid& WorkspaceId); + bool RemoveWorkspace(RwLock::ExclusiveLockScope& Lock, const Oid& WorkspaceId); + LoggerRef& Log() { return m_Log; } Ref<Workspace> FindWorkspace(const RwLock::SharedLockScope& Lock, const Oid& WorkspaceId) const; diff --git a/src/zenstore/workspaces.cpp b/src/zenstore/workspaces.cpp index 4cf423d03..d30a27e33 100644 --- a/src/zenstore/workspaces.cpp +++ b/src/zenstore/workspaces.cpp @@ -10,6 +10,10 @@ #include <zencore/workthreadpool.h> #include <zenutil/basicfile.h> +ZEN_THIRD_PARTY_INCLUDES_START +#include <tsl/robin_set.h> +ZEN_THIRD_PARTY_INCLUDES_END + #if ZEN_WITH_TESTS # include <zencore/blake3.h> # include <zencore/testing.h> @@ -20,7 +24,10 @@ namespace zen { namespace { - std::string WorkspacesToJson(std::span<Workspaces::WorkspaceInfo> Workspaces) + static constexpr std::string_view WorkspacesConfigName("config.json"); + static constexpr std::string_view WorkspaceConfigName("zenworkspaceconfig.json"); + + std::string WorkspacesToJson(std::span<const Workspaces::WorkspaceConfiguration> Workspaces) { using namespace std::literals; @@ -28,27 +35,13 @@ namespace { Writer.BeginArray("workspaces"); - for (const Workspaces::WorkspaceInfo& Workspace : Workspaces) + for (const Workspaces::WorkspaceConfiguration& Workspace : Workspaces) { Writer.BeginObject(); { - Writer.AddObjectId("id"sv, Workspace.Config.Id); - Writer.AddString("root_path"sv, reinterpret_cast<const char*>(Workspace.Config.RootPath.u8string().c_str())); - Writer.BeginArray("shares"); - for (const Workspaces::WorkspaceShareConfiguration& Share : Workspace.Shares) - { - Writer.BeginObject(); - { - Writer.AddObjectId("id"sv, Share.Id); - Writer.AddString("share_path"sv, reinterpret_cast<const char*>(Share.SharePath.u8string().c_str())); - if (!Share.Alias.empty()) - { - Writer.AddString("alias"sv, Share.Alias); - } - } - Writer.EndObject(); - } - Writer.EndArray(); + Writer.AddObjectId("id"sv, Workspace.Id); + Writer.AddString("root_path"sv, reinterpret_cast<const char*>(Workspace.RootPath.u8string().c_str())); + Writer.AddBool("allow_share_creation_from_http"sv, Workspace.AllowShareCreationFromHttp); } Writer.EndObject(); } @@ -58,9 +51,7 @@ namespace { return Json.ToString(); } - std::vector<Workspaces::WorkspaceInfo> WorkspacesFromJson(const IoBuffer& WorkspaceJson, - const std::function<Oid(const std::filesystem::path& Path)>& PathToIdCB, - std::string& OutError) + std::vector<Workspaces::WorkspaceConfiguration> WorkspacesFromJson(const IoBuffer& WorkspaceJson, std::string& OutError) { using namespace std::literals; @@ -68,37 +59,23 @@ namespace { LoadCompactBinaryFromJson(std::string_view((const char*)(WorkspaceJson.Data()), WorkspaceJson.GetSize()), OutError); if (OutError.empty()) { - std::vector<Workspaces::WorkspaceInfo> Workspaces; + std::vector<Workspaces::WorkspaceConfiguration> Workspaces; if (CbObjectView RootObject = RootField.AsObjectView(); RootObject) { for (CbFieldView WorkspaceField : RootObject["workspaces"].AsArrayView()) { - CbObjectView Workspace = WorkspaceField.AsObjectView(); - Oid WorkspaceId = Workspace["id"sv].AsObjectId(); - std::filesystem::path RootPath = Workspace["root_path"sv].AsU8String(); + CbObjectView Workspace = WorkspaceField.AsObjectView(); + Oid WorkspaceId = Workspace["id"sv].AsObjectId(); + std::filesystem::path RootPath = Workspace["root_path"sv].AsU8String(); + bool AllowShareCreationFromHttp = Workspace["allow_share_creation_from_http"sv].AsBool(); if (WorkspaceId == Oid::Zero && !RootPath.empty()) { - WorkspaceId = PathToIdCB(RootPath); + WorkspaceId = Workspaces::PathToId(RootPath); } if (WorkspaceId != Oid::Zero && !RootPath.empty()) { - std::vector<Workspaces::WorkspaceShareConfiguration> Shares; - for (CbFieldView ShareField : Workspace["shares"].AsArrayView()) - { - CbObjectView Share = ShareField.AsObjectView(); - Oid ShareId = Share["id"sv].AsObjectId(); - std::filesystem::path SharePath = Share["share_path"sv].AsU8String(); - if (ShareId == Oid::Zero && !SharePath.empty()) - { - ShareId = PathToIdCB(SharePath); - } - std::string_view Alias = Share["alias"sv].AsString(); - if (ShareId != Oid::Zero && !SharePath.empty()) - { - Shares.push_back({.Id = ShareId, .SharePath = SharePath, .Alias = std::string(Alias)}); - } - } - Workspaces.push_back({.Config = {.Id = WorkspaceId, .RootPath = RootPath}, .Shares = std::move(Shares)}); + Workspaces.push_back( + {.Id = WorkspaceId, .RootPath = RootPath, .AllowShareCreationFromHttp = AllowShareCreationFromHttp}); } } } @@ -107,7 +84,78 @@ namespace { return {}; } + std::string WorkspaceSharesToJson(std::span<const Workspaces::WorkspaceShareConfiguration> WorkspaceShares) + { + using namespace std::literals; + + CbObjectWriter Writer; + + Writer.BeginArray("shares"); + + for (const Workspaces::WorkspaceShareConfiguration& Share : WorkspaceShares) + { + Writer.BeginObject(); + { + Writer.AddObjectId("id"sv, Share.Id); + Writer.AddString("share_path"sv, reinterpret_cast<const char*>(Share.SharePath.u8string().c_str())); + if (!Share.Alias.empty()) + { + Writer.AddString("alias"sv, Share.Alias); + } + } + Writer.EndObject(); + } + Writer.EndArray(); + ExtendableStringBuilder<512> Json; + Writer.Save().ToJson(Json); + return Json.ToString(); + } + + std::vector<Workspaces::WorkspaceShareConfiguration> WorkspaceSharesFromJson(const IoBuffer& WorkspaceJson, std::string& OutError) + { + using namespace std::literals; + + CbFieldIterator RootField = + LoadCompactBinaryFromJson(std::string_view((const char*)(WorkspaceJson.Data()), WorkspaceJson.GetSize()), OutError); + if (OutError.empty()) + { + std::vector<Workspaces::WorkspaceShareConfiguration> Shares; + if (CbObjectView RootObject = RootField.AsObjectView(); RootObject) + { + for (CbFieldView ShareField : RootObject["shares"].AsArrayView()) + { + CbObjectView Share = ShareField.AsObjectView(); + Oid ShareId = Share["id"sv].AsObjectId(); + std::filesystem::path SharePath = Share["share_path"sv].AsU8String(); + if (ShareId == Oid::Zero && !SharePath.empty()) + { + ShareId = Workspaces::PathToId(SharePath); + } + std::string_view Alias = Share["alias"sv].AsString(); + if (ShareId != Oid::Zero && !SharePath.empty()) + { + Shares.push_back({.Id = ShareId, .SharePath = SharePath, .Alias = std::string(Alias)}); + } + } + } + return Shares; + } + return {}; + } + + bool IsValidSharePath(const std::filesystem::path& RootPath, const std::filesystem::path& SharePath) + { + std::filesystem::path FullPath = std::filesystem::absolute(RootPath / SharePath); + std::filesystem::path VerifySharePath = std::filesystem::relative(FullPath, RootPath); + if (VerifySharePath != SharePath || VerifySharePath.string().starts_with("..")) + { + return false; + } + return true; + } + } // namespace + ////////////////////////////////////////////////////////////////////////// class FolderStructure @@ -151,16 +199,12 @@ private: class WorkspaceShare : public RefCounted { public: - WorkspaceShare(const Workspaces::WorkspaceShareConfiguration& Config, - std::unique_ptr<FolderStructure>&& FolderStructure, - const std::function<Oid(const std::filesystem::path& Path)>& PathToId); + WorkspaceShare(const Workspaces::WorkspaceShareConfiguration& Config, std::unique_ptr<FolderStructure>&& FolderStructure); const Workspaces::WorkspaceShareConfiguration& GetConfig() const; bool IsInitialized() const { return !!m_FolderStructure; } - const std::function<Oid(const std::filesystem::path& Path)>& GetPathToIdFunction() const { return m_PathToid; } - std::filesystem::path GetAbsolutePath(const std::filesystem::path& RootPath, const Oid& ChunkId, uint64_t& OutSize) const; const FolderStructure& GetStructure() const @@ -170,9 +214,8 @@ public: } private: - const Workspaces::WorkspaceShareConfiguration m_Config; - std::function<Oid(const std::filesystem::path& Path)> m_PathToid; - std::unique_ptr<FolderStructure> m_FolderStructure; + const Workspaces::WorkspaceShareConfiguration m_Config; + std::unique_ptr<FolderStructure> m_FolderStructure; }; ////////////////////////////////////////////////////////////////////////// @@ -180,7 +223,7 @@ private: class Workspace : public RefCounted { public: - Workspace(LoggerRef& Log, const Workspaces::WorkspaceConfiguration& Config); + Workspace(const LoggerRef& Log, const Workspaces::WorkspaceConfiguration& Config); const Workspaces::WorkspaceConfiguration& GetConfig() const; std::vector<Ref<WorkspaceShare>> GetShares() const; @@ -191,7 +234,7 @@ public: private: LoggerRef Log() { return m_Log; } - LoggerRef& m_Log; + LoggerRef m_Log; const Workspaces::WorkspaceConfiguration m_Config; tsl::robin_map<Oid, Ref<WorkspaceShare>, Oid::Hasher> m_Shares; }; @@ -211,13 +254,9 @@ FolderStructure::FolderStructure(std::vector<FileEntry>&& InEntries, std::vector namespace { struct FolderScanner { - FolderScanner(LoggerRef& Log, - WorkerThreadPool& WorkerPool, - const std::filesystem::path& Path, - const std::function<Oid(const std::filesystem::path& Path)>& PathToIdCB) + FolderScanner(LoggerRef& Log, WorkerThreadPool& WorkerPool, const std::filesystem::path& Path) : m_Log(Log) , Path(Path) - , PathToIdCB(PathToIdCB) , WorkLatch(1) , WorkerPool(WorkerPool) { @@ -226,15 +265,14 @@ namespace { void Traverse(); void Traverse(const std::filesystem::path& RelativeRoot, const std::filesystem::path& Path); - LoggerRef& Log() { return m_Log; } - LoggerRef& m_Log; - const std::filesystem::path Path; - RwLock WorkLock; - const std::function<Oid(const std::filesystem::path& Path)>& PathToIdCB; - std::vector<FolderStructure::FileEntry> FoundFiles; - std::vector<Oid> FoundFileIds; - Latch WorkLatch; - WorkerThreadPool& WorkerPool; + LoggerRef& Log() { return m_Log; } + LoggerRef& m_Log; + const std::filesystem::path Path; + RwLock WorkLock; + std::vector<FolderStructure::FileEntry> FoundFiles; + std::vector<Oid> FoundFileIds; + Latch WorkLatch; + WorkerThreadPool& WorkerPool; }; struct Visitor : public FileSystemTraversal::TreeVisitor @@ -251,7 +289,7 @@ namespace { { std::filesystem::path RelativePath = RelativeRoot.empty() ? File : RelativeRoot / File; Entries.push_back(FolderStructure::FileEntry{.RelativePath = RelativePath, .Size = FileSize}); - FileIds.push_back(Data.PathToIdCB(RelativePath)); + FileIds.push_back(Workspaces::PathToId(RelativePath)); } virtual bool VisitDirectory(const std::filesystem::path& Parent, const path_view& DirectoryName) @@ -298,27 +336,21 @@ namespace { } // namespace std::unique_ptr<FolderStructure> -ScanFolder(LoggerRef InLog, - const std::filesystem::path& Path, - const std::function<Oid(const std::filesystem::path& Path)>& PathToIdCB, - WorkerThreadPool& WorkerPool) +ScanFolder(LoggerRef InLog, const std::filesystem::path& Path, WorkerThreadPool& WorkerPool) { ZEN_TRACE_CPU("workspaces::ScanFolderImpl"); auto Log = [&InLog]() { return InLog; }; - FolderScanner Data(InLog, WorkerPool, Path, PathToIdCB); + FolderScanner Data(InLog, WorkerPool, Path); Data.Traverse(); return std::make_unique<FolderStructure>(std::move(Data.FoundFiles), std::move(Data.FoundFileIds)); } //////////////////////////////////////////////////////////// -WorkspaceShare::WorkspaceShare(const Workspaces::WorkspaceShareConfiguration& Config, - std::unique_ptr<FolderStructure>&& FolderStructure, - const std::function<Oid(const std::filesystem::path& Path)>& PathToId) +WorkspaceShare::WorkspaceShare(const Workspaces::WorkspaceShareConfiguration& Config, std::unique_ptr<FolderStructure>&& FolderStructure) : m_Config(Config) -, m_PathToid(PathToId) , m_FolderStructure(std::move(FolderStructure)) { } @@ -344,7 +376,7 @@ WorkspaceShare::GetConfig() const //////////////////////////////////////////////////////////// -Workspace::Workspace(LoggerRef& Log, const Workspaces::WorkspaceConfiguration& Config) : m_Log(Log), m_Config(Config) +Workspace::Workspace(const LoggerRef& Log, const Workspaces::WorkspaceConfiguration& Config) : m_Log(Log), m_Config(Config) { } @@ -398,184 +430,112 @@ Workspaces::~Workspaces() { } -bool -Workspaces::AddWorkspace(const WorkspaceConfiguration& Configuration) -{ - Ref<Workspace> NewWorkspace(new Workspace(m_Log, Configuration)); - - RwLock::ExclusiveLockScope Lock(m_Lock); - if (m_Workspaces.contains(Configuration.Id)) - { - return false; - } - - const std::filesystem::path RootPath = Configuration.RootPath; - if (RootPath.is_relative()) - { - throw std::invalid_argument(fmt::format("workspace root path '{}' is not an absolute path", RootPath)); - } - - m_Workspaces.insert(std::make_pair(Configuration.Id, NewWorkspace)); - ZEN_INFO("Created workspace '{}' with root '{}'", Configuration.Id, Configuration.RootPath); - return true; -} - -Workspaces::WorkspaceConfiguration -Workspaces::GetWorkspaceConfiguration(const Oid& WorkspaceId) const +void +Workspaces::RefreshWorkspaceShares(const Oid& WorkspaceId) { - RwLock::SharedLockScope Lock(m_Lock); - Ref<Workspace> Workspace = FindWorkspace(Lock, WorkspaceId); - if (Workspace) - { - return Workspace->GetConfig(); - } - return {}; -} + using namespace std::literals; -Workspaces::WorkspaceInfo -Workspaces::GetWorkspaceInfo(const Oid& WorkspaceId) const -{ Ref<Workspace> Workspace; - std::vector<Ref<WorkspaceShare>> Shares; + tsl::robin_set<Oid, Oid::Hasher> DeletedShares; { RwLock::SharedLockScope Lock(m_Lock); Workspace = FindWorkspace(Lock, WorkspaceId); if (Workspace) { - Shares = Workspace->GetShares(); - } - } - if (!Workspace) - { - return {}; - } - - WorkspaceInfo Info = {.Config = Workspace->GetConfig()}; - Info.Shares.reserve(Shares.size()); - for (const Ref<WorkspaceShare>& Share : Shares) - { - Info.Shares.push_back(Share->GetConfig()); - } - return Info; -} - -bool -Workspaces::RemoveWorkspace(const Oid& WorkspaceId) -{ - RwLock::ExclusiveLockScope Lock(m_Lock); - if (auto It = m_Workspaces.find(WorkspaceId); It != m_Workspaces.end()) - { - std::vector<std::string> Aliases; - for (const auto& AliasIt : m_ShareAliases) - { - if (AliasIt.second.WorkspaceId == WorkspaceId) + for (auto Share : Workspace->GetShares()) { - Aliases.push_back(AliasIt.first); + DeletedShares.insert(Share->GetConfig().Id); } } - - for (const std::string& Alias : Aliases) - { - m_ShareAliases.erase(Alias); - } - - m_Workspaces.erase(It); - - ZEN_INFO("Removed workspace '{}' and {} aliases", WorkspaceId, Aliases.size()); - return true; } - return false; -} -bool -Workspaces::AddWorkspaceShare(const Oid& WorkspaceId, - const WorkspaceShareConfiguration& Configuration, - const std::function<Oid(const std::filesystem::path& Path)>& PathToIdCB) -{ - Ref<Workspace> Workspace; + if (Workspace) { - RwLock::SharedLockScope Lock(m_Lock); - Workspace = FindWorkspace(Lock, WorkspaceId); - if (!Workspace) - { - return false; - } - if (Workspace->GetShare(Configuration.Id)) + const std::filesystem::path& RootPath = Workspace->GetConfig().RootPath; + std::filesystem::path ConfigPath = RootPath / WorkspaceConfigName; + if (std::filesystem::exists(ConfigPath)) { - return false; - } - } - - const std::filesystem::path RootPath = Workspace->GetConfig().RootPath; - const std::filesystem::path SharePath = Configuration.SharePath; + std::string Error; + std::vector<Workspaces::WorkspaceShareConfiguration> WorkspaceShares = ReadWorkspaceConfig(m_Log, RootPath, Error); + if (!Error.empty()) + { + ZEN_WARN("Failed to read workspace state from {}. Reason: '{}'", ConfigPath, Error); + } + else + { + for (const Workspaces::WorkspaceShareConfiguration& Configuration : WorkspaceShares) + { + const std::filesystem::path& SharePath = Configuration.SharePath; - std::filesystem::path FullPath = std::filesystem::absolute(RootPath / SharePath); - std::filesystem::path VerifySharePath = std::filesystem::relative(FullPath, RootPath); - if (VerifySharePath != Configuration.SharePath || VerifySharePath.string().starts_with("..")) - { - throw std::invalid_argument(fmt::format("workspace share path '{}' is not valid for root path '{}'", SharePath, RootPath)); - } + if (std::filesystem::is_directory(RootPath / SharePath)) + { + DeletedShares.erase(Configuration.Id); - Ref<WorkspaceShare> NewShare(new WorkspaceShare(Configuration, {}, PathToIdCB)); - { - RwLock::ExclusiveLockScope _(m_Lock); - Workspace->SetShare(Configuration.Id, std::move(NewShare)); - if (!Configuration.Alias.empty()) - { - m_ShareAliases.insert_or_assign(Configuration.Alias, ShareAlias{.WorkspaceId = WorkspaceId, .ShareId = Configuration.Id}); - } - } + if (!IsValidSharePath(RootPath, SharePath)) + { + ZEN_WARN("Skipping workspace share path '{}' as it is not valid for root path '{}'", SharePath, RootPath); + } + else + { + Ref<WorkspaceShare> NewShare(new WorkspaceShare(Configuration, {})); - ZEN_INFO("Added workspace share '{}' in workspace '{}' with path '{}'", Configuration.Id, WorkspaceId, Configuration.SharePath); + RwLock::ExclusiveLockScope _(m_Lock); + if (Ref<WorkspaceShare> Share = Workspace->GetShare(Configuration.Id); Share) + { + if (Share->GetConfig() != Configuration) + { + if (!Share->GetConfig().Alias.empty()) + { + m_ShareAliases.erase(Share->GetConfig().Alias); + } + Workspace->SetShare(Configuration.Id, std::move(NewShare)); + } + } + else + { + Workspace->SetShare(Configuration.Id, std::move(NewShare)); + if (!Configuration.Alias.empty()) + { + m_ShareAliases.insert_or_assign(Configuration.Alias, + ShareAlias{.WorkspaceId = WorkspaceId, .ShareId = Configuration.Id}); + } + } + } + } + else + { + ZEN_INFO("Skipping workspace share path '{}' that does not exist in root path '{}'", SharePath, RootPath); + } + } + } + if (!DeletedShares.empty()) + { + RwLock::ExclusiveLockScope _(m_Lock); + for (const Oid& ShareId : DeletedShares) + { + Ref<WorkspaceShare> ExistingShare = Workspace->GetShare(ShareId); + if (ExistingShare) + { + std::string Alias = ExistingShare->GetConfig().Alias; + if (!Alias.empty()) + { + if (auto AliasIt = m_ShareAliases.find(Alias); AliasIt != m_ShareAliases.end()) + { + if (AliasIt->second.WorkspaceId == Workspace->GetConfig().Id && AliasIt->second.ShareId == ShareId) + { + m_ShareAliases.erase(Alias); + } + } + } - return true; -} + Workspace->SetShare(ShareId, {}); -Workspaces::WorkspaceShareConfiguration -Workspaces::GetWorkspaceShareConfiguration(const Oid& WorkspaceId, const Oid& ShareId) const -{ - RwLock::SharedLockScope Lock(m_Lock); - Ref<Workspace> Workspace = FindWorkspace(Lock, WorkspaceId); - if (Workspace) - { - Ref<WorkspaceShare> Share = Workspace->GetShare(ShareId); - if (Share) - { - return Share->GetConfig(); + ZEN_DEBUG("Removed workspace share '{}' in workspace '{}'", ShareId, WorkspaceId); + } + } + } } } - return {}; -} - -bool -Workspaces::RemoveWorkspaceShare(const Oid& WorkspaceId, const Oid& ShareId) -{ - Ref<Workspace> Workspace; - { - RwLock::SharedLockScope Lock(m_Lock); - Workspace = FindWorkspace(Lock, WorkspaceId); - if (!Workspace) - { - return false; - } - } - RwLock::ExclusiveLockScope _(m_Lock); - Ref<WorkspaceShare> ExistingShare = Workspace->GetShare(ShareId); - if (!ExistingShare) - { - return false; - } - - std::string Alias = ExistingShare->GetConfig().Alias; - if (!Alias.empty()) - { - m_ShareAliases.erase(Alias); - } - - Workspace->SetShare(ShareId, {}); - - ZEN_INFO("Removed workspace share '{}' in workspace '{}'", ShareId, WorkspaceId); - return true; } std::optional<std::vector<Workspaces::ShareFile>> @@ -682,72 +642,148 @@ Workspaces::GetWorkspaceShareChunks(const Oid& WorkspaceId, return Chunks; } -void -Workspaces::WriteState(const std::filesystem::path& WorkspaceStatePath) +std::vector<Oid> +Workspaces::GetWorkspaces() const { - using namespace std::literals; + std::vector<Oid> Workspaces; + RwLock::SharedLockScope Lock(m_Lock); + for (auto It : m_Workspaces) + { + Workspaces.push_back(It.first); + } + return Workspaces; +} - ZEN_INFO("Writing workspaces state to {}", WorkspaceStatePath); +std::optional<Workspaces::WorkspaceConfiguration> +Workspaces::GetWorkspaceConfiguration(const Oid& WorkspaceId) const +{ + Ref<Workspace> Workspace; + { + RwLock::SharedLockScope Lock(m_Lock); + Workspace = FindWorkspace(Lock, WorkspaceId); + } + if (Workspace) + { + return Workspace->GetConfig(); + } + return {}; +} - CreateDirectories(WorkspaceStatePath); +std::optional<std::vector<Oid>> +Workspaces::GetWorkspaceShares(const Oid& WorkspaceId) const +{ + RwLock::SharedLockScope Lock(m_Lock); + Ref<Workspace> Workspace = FindWorkspace(Lock, WorkspaceId); + if (Workspace) + { + std::vector<Oid> Shares; + for (auto Share : Workspace->GetShares()) + { + Shares.push_back(Share->GetConfig().Id); + } + return Shares; + } + return {}; +} - std::vector<WorkspaceInfo> Workspaces; +std::optional<Workspaces::WorkspaceShareConfiguration> +Workspaces::GetWorkspaceShareConfiguration(const Oid& WorkspaceId, const Oid& ShareId) const +{ + RwLock::SharedLockScope Lock(m_Lock); + Ref<Workspace> Workspace = FindWorkspace(Lock, WorkspaceId); + if (Workspace) { - RwLock::SharedLockScope _(m_Lock); - for (auto WorkspaceIt : m_Workspaces) + Ref<WorkspaceShare> Share = Workspace->GetShare(ShareId); + if (Share) { - std::vector<Workspaces::WorkspaceShareConfiguration> Shares; - for (auto ShareIt : WorkspaceIt.second->GetShares()) - { - Shares.push_back(ShareIt->GetConfig()); - } - Workspaces.push_back({.Config = WorkspaceIt.second->GetConfig(), .Shares = std::move(Shares)}); + return Share->GetConfig(); } } - std::string ConfigJson = WorkspacesToJson(Workspaces); - TemporaryFile::SafeWriteFile(WorkspaceStatePath / "config.json", MemoryView(ConfigJson.data(), ConfigJson.size())); + return {}; } void -Workspaces::ReadState(const std::filesystem::path& WorkspaceStatePath, - const std::function<Oid(const std::filesystem::path& Path)>& PathToIdCB) +Workspaces::RefreshState(const std::filesystem::path& WorkspaceStatePath) { using namespace std::literals; - std::string Error; - std::vector<Workspaces::WorkspaceInfo> Workspaces = - WorkspacesFromJson(IoBufferBuilder::MakeFromFile(WorkspaceStatePath / "config.json"sv), PathToIdCB, Error); + std::string Error; + std::vector<Workspaces::WorkspaceConfiguration> Workspaces = ReadConfig(Log(), WorkspaceStatePath, Error); + if (!Error.empty()) { - ZEN_WARN("Failed to read workspace state from {}. Reason: '{}'", WorkspaceStatePath / "config.json"sv, Error); + ZEN_WARN("Failed to read workspaces state from {}. Reason: '{}'", WorkspaceStatePath, Error); } else { - for (const Workspaces::WorkspaceInfo& Workspace : Workspaces) + tsl::robin_set<Oid, Oid::Hasher> DeletedWorkspaces; { + RwLock::SharedLockScope Lock(m_Lock); + for (const auto& Workspace : m_Workspaces) + { + DeletedWorkspaces.insert(Workspace.second->GetConfig().Id); + } + } + + for (const Workspaces::WorkspaceConfiguration& Configuration : Workspaces) + { + DeletedWorkspaces.erase(Configuration.Id); try { - if (AddWorkspace(Workspace.Config)) + const std::filesystem::path& RootPath = Configuration.RootPath; + if (RootPath.is_relative()) + { + throw std::invalid_argument(fmt::format("workspace root path '{}' is not an absolute path", RootPath)); + } + else { - for (const Workspaces::WorkspaceShareConfiguration& Share : Workspace.Shares) + Ref<Workspace> NewWorkspace(new Workspace(m_Log, Configuration)); { - try + RwLock::ExclusiveLockScope Lock(m_Lock); + if (auto It = m_Workspaces.find(Configuration.Id); It != m_Workspaces.end()) { - (void)AddWorkspaceShare(Workspace.Config.Id, Share, PathToIdCB); + Ref<Workspace> Workspace(It->second); + if (Workspace->GetConfig() != Configuration) + { + RemoveWorkspace(Lock, Configuration.Id); + m_Workspaces.insert(std::make_pair(Configuration.Id, std::move(NewWorkspace))); + ZEN_DEBUG("Replaced workspace '{}' with root '{}'", Configuration.Id, Configuration.RootPath); + } } - catch (const std::exception& Ex) + else { - ZEN_WARN("Failed adding workspace share '{}' for workspace '{}'. Reason: '{}'", - Workspace.Config.Id, - Share.Id, - Ex.what()); + m_Workspaces.insert(std::make_pair(Configuration.Id, std::move(NewWorkspace))); + ZEN_DEBUG("Created workspace '{}' with root '{}'", Configuration.Id, Configuration.RootPath); } } + + try + { + RefreshWorkspaceShares(Configuration.Id); + } + catch (const std::exception& Ex) + { + ZEN_WARN("Failed refreshing workspace shares for workspace '{}'. Reason: '{}'", Configuration.Id, Ex.what()); + } } } catch (const std::exception& Ex) { - ZEN_WARN("Failed adding workspace '{}'. Reason: '{}'", Workspace.Config.Id, Ex.what()); + ZEN_WARN("Failed adding workspace '{}'. Reason: '{}'", Configuration.Id, Ex.what()); + } + } + { + RwLock::ExclusiveLockScope Lock(m_Lock); + for (const Oid& WorkspaceId : DeletedWorkspaces) + { + try + { + RemoveWorkspace(Lock, WorkspaceId); + } + catch (const std::exception& Ex) + { + ZEN_WARN("Failed removing workspace '{}'. Reason: '{}'", WorkspaceId, Ex.what()); + } } } } @@ -764,6 +800,425 @@ Workspaces::GetShareAlias(std::string_view Alias) const return {}; } +std::vector<Workspaces::WorkspaceConfiguration> +Workspaces::ReadConfig(const LoggerRef& InLog, const std::filesystem::path& WorkspaceStatePath, std::string& OutError) +{ + auto Log = [&InLog]() { return InLog; }; + + using namespace std::literals; + + ZEN_DEBUG("Reading workspaces state from {}", WorkspaceStatePath); + + const std::filesystem::path ConfigPath = WorkspaceStatePath / WorkspacesConfigName; + if (std::filesystem::exists(ConfigPath)) + { + std::vector<Workspaces::WorkspaceConfiguration> Workspaces = + WorkspacesFromJson(IoBufferBuilder::MakeFromFile(ConfigPath), OutError); + if (OutError.empty()) + { + return Workspaces; + } + } + return {}; +} + +void +Workspaces::WriteConfig(const LoggerRef& InLog, + const std::filesystem::path& WorkspaceStatePath, + const std::vector<WorkspaceConfiguration>& WorkspaceConfigurations) +{ + auto Log = [&InLog]() { return InLog; }; + + using namespace std::literals; + + ZEN_DEBUG("Writing workspaces state to {}", WorkspaceStatePath); + + CreateDirectories(WorkspaceStatePath); + + std::string ConfigJson = WorkspacesToJson(WorkspaceConfigurations); + TemporaryFile::SafeWriteFile(WorkspaceStatePath / WorkspacesConfigName, MemoryView(ConfigJson.data(), ConfigJson.size())); +} + +std::vector<Workspaces::WorkspaceShareConfiguration> +Workspaces::ReadWorkspaceConfig(const LoggerRef& InLog, const std::filesystem::path& WorkspaceRoot, std::string& OutError) +{ + auto Log = [&InLog]() { return InLog; }; + + using namespace std::literals; + + ZEN_DEBUG("Reading workspace state from {}", WorkspaceRoot); + + std::filesystem::path ConfigPath = WorkspaceRoot / WorkspaceConfigName; + if (std::filesystem::exists(ConfigPath)) + { + std::vector<Workspaces::WorkspaceShareConfiguration> WorkspaceShares = + WorkspaceSharesFromJson(IoBufferBuilder::MakeFromFile(ConfigPath), OutError); + if (OutError.empty()) + { + return WorkspaceShares; + } + } + return {}; +} + +void +Workspaces::WriteWorkspaceConfig(const LoggerRef& InLog, + const std::filesystem::path& WorkspaceRoot, + const std::vector<WorkspaceShareConfiguration>& WorkspaceShareConfigurations) +{ + auto Log = [&InLog]() { return InLog; }; + + using namespace std::literals; + + ZEN_DEBUG("Writing workspace state to {}", WorkspaceRoot); + + std::string ConfigJson = WorkspaceSharesToJson(WorkspaceShareConfigurations); + TemporaryFile::SafeWriteFile(WorkspaceRoot / WorkspaceConfigName, MemoryView(ConfigJson.data(), ConfigJson.size())); +} + +bool +Workspaces::AddWorkspace(const LoggerRef& Log, const std::filesystem::path& WorkspaceStatePath, const WorkspaceConfiguration& Configuration) +{ + if (Configuration.Id == Oid::Zero) + { + throw std::invalid_argument( + fmt::format("invalid workspace id {} for workspace with root '{}'", Configuration.Id, Configuration.RootPath)); + } + if (Configuration.RootPath.is_relative()) + { + throw std::invalid_argument(fmt::format("invalid root path '{}' for workspace {}", Configuration.RootPath, Configuration.Id)); + } + std::string Error; + std::vector<WorkspaceConfiguration> WorkspaceConfigurations = ReadConfig(Log, WorkspaceStatePath, Error); + if (!Error.empty()) + { + throw std::invalid_argument( + fmt::format("failed to read workspaces configuration from '{}'. Reason: '{}'", WorkspaceStatePath, Error)); + } + + if (auto It = + std::find_if(WorkspaceConfigurations.begin(), + WorkspaceConfigurations.end(), + [RootPath = Configuration.RootPath](const WorkspaceConfiguration& Config) { return Config.RootPath == RootPath; }); + It != WorkspaceConfigurations.end()) + { + if (It->Id != Configuration.Id) + { + throw std::invalid_argument(fmt::format("root path '{}' is already used in workspace {}", Configuration.RootPath, It->Id)); + } + } + + if (auto It = std::find_if(WorkspaceConfigurations.begin(), + WorkspaceConfigurations.end(), + [Id = Configuration.Id](const WorkspaceConfiguration& Config) { return Config.Id == Id; }); + It != WorkspaceConfigurations.end()) + { + *It = Configuration; + WriteConfig(Log, WorkspaceStatePath, WorkspaceConfigurations); + return false; + } + else + { + WorkspaceConfigurations.push_back(Configuration); + WriteConfig(Log, WorkspaceStatePath, WorkspaceConfigurations); + return true; + } +} + +bool +Workspaces::RemoveWorkspace(const LoggerRef& Log, const std::filesystem::path& WorkspaceStatePath, const Oid& WorkspaceId) +{ + std::string Error; + std::vector<WorkspaceConfiguration> WorkspaceConfigurations = ReadConfig(Log, WorkspaceStatePath, Error); + if (!Error.empty()) + { + throw std::invalid_argument( + fmt::format("failed to read workspaces configuration from '{}'. Reason: '{}'", WorkspaceStatePath, Error)); + } + if (auto It = std::find_if(WorkspaceConfigurations.begin(), + WorkspaceConfigurations.end(), + [WorkspaceId](const WorkspaceConfiguration& Config) { return Config.Id == WorkspaceId; }); + It != WorkspaceConfigurations.end()) + { + WorkspaceConfigurations.erase(It); + WriteConfig(Log, WorkspaceStatePath, WorkspaceConfigurations); + return true; + } + return false; +} + +bool +Workspaces::AddWorkspaceShare(const LoggerRef& Log, + const std::filesystem::path& WorkspaceRoot, + const WorkspaceShareConfiguration& Configuration) +{ + if (Configuration.Id == Oid::Zero) + { + throw std::invalid_argument( + fmt::format("invalid workspace share id {} for workspace with root '{}'", Configuration.Id, Configuration.SharePath)); + } + if (!IsValidSharePath(WorkspaceRoot, Configuration.SharePath)) + { + throw std::invalid_argument( + fmt::format("workspace share path '{}' is not a sub-path of workspace path '{}'", Configuration.SharePath, WorkspaceRoot)); + } + if (!std::filesystem::is_directory(WorkspaceRoot / Configuration.SharePath)) + { + throw std::invalid_argument( + fmt::format("workspace share path '{}' does not exist in workspace path '{}'", Configuration.SharePath, WorkspaceRoot)); + } + if (!AsciiSet::HasOnly(Configuration.Alias, ValidAliasCharactersSet)) + { + throw std::invalid_argument( + fmt::format("invalid workspace share alias '{}' for workspace share {}", Configuration.Alias, Configuration.Id)); + } + + std::string Error; + std::vector<WorkspaceShareConfiguration> WorkspaceShareConfigurations = ReadWorkspaceConfig(Log, WorkspaceRoot, Error); + if (!Error.empty()) + { + throw std::invalid_argument(fmt::format("failed to read workspace configuration from '{}'. Reason: '{}'", WorkspaceRoot, Error)); + } + + if (auto It = std::find_if( + WorkspaceShareConfigurations.begin(), + WorkspaceShareConfigurations.end(), + [SharePath = Configuration.SharePath](const WorkspaceShareConfiguration& Config) { return Config.SharePath == SharePath; }); + It != WorkspaceShareConfigurations.end()) + { + if (It->Id != Configuration.Id) + { + throw std::invalid_argument( + fmt::format("share path '{}' is already used in workspace as share {}", Configuration.SharePath, It->Id)); + } + } + + if (auto It = std::find_if(WorkspaceShareConfigurations.begin(), + WorkspaceShareConfigurations.end(), + [Id = Configuration.Id](const WorkspaceShareConfiguration& Config) { return Config.Id == Id; }); + It != WorkspaceShareConfigurations.end()) + { + if (*It == Configuration) + { + return false; + } + *It = Configuration; + } + else + { + WorkspaceShareConfigurations.push_back(Configuration); + } + WriteWorkspaceConfig(Log, WorkspaceRoot, WorkspaceShareConfigurations); + return true; +} + +bool +Workspaces::RemoveWorkspaceShare(const LoggerRef& Log, const std::filesystem::path& WorkspaceRoot, const Oid& WorkspaceShareId) +{ + std::string Error; + std::vector<WorkspaceShareConfiguration> WorkspaceShareConfigurations = ReadWorkspaceConfig(Log, WorkspaceRoot, Error); + if (!Error.empty()) + { + throw std::invalid_argument(fmt::format("failed to read workspace configuration from '{}'. Reason: '{}'", WorkspaceRoot, Error)); + } + if (auto It = std::find_if(WorkspaceShareConfigurations.begin(), + WorkspaceShareConfigurations.end(), + [WorkspaceShareId](const WorkspaceShareConfiguration& Config) { return Config.Id == WorkspaceShareId; }); + It != WorkspaceShareConfigurations.end()) + { + WorkspaceShareConfigurations.erase(It); + WriteWorkspaceConfig(Log, WorkspaceRoot, WorkspaceShareConfigurations); + return true; + } + return false; +} + +Workspaces::WorkspaceConfiguration +Workspaces::FindWorkspace(const LoggerRef& InLog, const std::filesystem::path& WorkspaceStatePath, const Oid& WorkspaceId) +{ + auto Log = [&InLog]() { return InLog; }; + + std::string Error; + std::vector<WorkspaceConfiguration> Workspaces = ReadConfig(InLog, WorkspaceStatePath, Error); + if (!Error.empty()) + { + throw std::invalid_argument( + fmt::format("failed to read workspaces configuration from '{}'. Reason: '{}'", WorkspaceStatePath, Error)); + } + + for (const WorkspaceConfiguration& WorkspaceConfig : Workspaces) + { + if (WorkspaceConfig.Id == WorkspaceId) + { + return WorkspaceConfig; + } + } + return {}; +} + +Workspaces::WorkspaceConfiguration +Workspaces::FindWorkspace(const LoggerRef& InLog, + const std::filesystem::path& WorkspaceStatePath, + const std::filesystem::path& WorkspaceRoot) +{ + auto Log = [&InLog]() { return InLog; }; + + std::string Error; + std::vector<WorkspaceConfiguration> Workspaces = ReadConfig(InLog, WorkspaceStatePath, Error); + if (!Error.empty()) + { + throw std::invalid_argument( + fmt::format("failed to read workspaces configuration from '{}'. Reason: '{}'", WorkspaceStatePath, Error)); + } + + for (const WorkspaceConfiguration& WorkspaceConfig : Workspaces) + { + if (WorkspaceConfig.RootPath == WorkspaceRoot) + { + return WorkspaceConfig; + } + } + return {}; +} + +Workspaces::WorkspaceShareConfiguration +Workspaces::FindWorkspaceShare(const LoggerRef& InLog, + const std::filesystem::path& WorkspaceStatePath, + std::string_view ShareAlias, + WorkspaceConfiguration& OutWorkspace) +{ + auto Log = [&InLog]() { return InLog; }; + + std::string Error; + std::vector<WorkspaceConfiguration> Workspaces = ReadConfig(InLog, WorkspaceStatePath, Error); + if (!Error.empty()) + { + throw std::invalid_argument( + fmt::format("failed to read workspaces configuration from '{}'. Reason: '{}'", WorkspaceStatePath, Error)); + } + + for (const WorkspaceConfiguration& WorkspaceConfig : Workspaces) + { + std::vector<WorkspaceShareConfiguration> Shares = ReadWorkspaceConfig(InLog, WorkspaceConfig.RootPath, Error); + if (!Error.empty()) + { + ZEN_WARN("Invalid workspace config in workspace root '{}'", WorkspaceConfig.RootPath); + } + else + { + for (const WorkspaceShareConfiguration& ShareConfig : Shares) + { + if (ShareConfig.Alias == ShareAlias) + { + OutWorkspace = WorkspaceConfig; + return ShareConfig; + } + } + } + } + return {}; +} + +Workspaces::WorkspaceShareConfiguration +Workspaces::FindWorkspaceShare(const LoggerRef& InLog, + const std::filesystem::path& WorkspaceStatePath, + const Oid& WorkspaceId, + const Oid& WorkspaceShareId) +{ + WorkspaceConfiguration Workspace = FindWorkspace(InLog, WorkspaceStatePath, WorkspaceId); + if (Workspace.Id == Oid::Zero) + { + return {}; + } + return FindWorkspaceShare(InLog, Workspace.RootPath, WorkspaceShareId); +} + +Workspaces::WorkspaceShareConfiguration +Workspaces::FindWorkspaceShare(const LoggerRef& InLog, const std::filesystem::path& WorkspaceRoot, const Oid& WorkspaceShareId) +{ + auto Log = [&InLog]() { return InLog; }; + std::string Error; + std::vector<WorkspaceShareConfiguration> Shares = ReadWorkspaceConfig(InLog, WorkspaceRoot, Error); + if (!Error.empty()) + { + ZEN_WARN("Invalid workspace config in workspace root '{}'", WorkspaceRoot); + } + else + { + for (const WorkspaceShareConfiguration& ShareConfig : Shares) + { + if (ShareConfig.Id == WorkspaceShareId) + { + return ShareConfig; + } + } + } + return {}; +} + +Workspaces::WorkspaceShareConfiguration +Workspaces::FindWorkspaceShare(const LoggerRef& InLog, const std::filesystem::path& WorkspaceRoot, const std::filesystem::path& SharePath) +{ + auto Log = [&InLog]() { return InLog; }; + std::string Error; + std::vector<WorkspaceShareConfiguration> Shares = ReadWorkspaceConfig(InLog, WorkspaceRoot, Error); + if (!Error.empty()) + { + ZEN_WARN("Invalid workspace config in workspace root '{}'", WorkspaceRoot); + } + else + { + for (const WorkspaceShareConfiguration& ShareConfig : Shares) + { + if (ShareConfig.SharePath == SharePath) + { + return ShareConfig; + } + } + } + return {}; +} + +Oid +Workspaces::PathToId(const std::filesystem::path& Path) +{ + std::string PathBuffer = reinterpret_cast<const char*>(Path.generic_u8string().c_str()); + if (PathBuffer.ends_with('/')) + { + PathBuffer.pop_back(); + } + BLAKE3 Hash = BLAKE3::HashMemory(PathBuffer.data(), PathBuffer.size()); + Hash.Hash[11] = 7; // FIoChunkType::ExternalFile + return Oid::FromMemory(Hash.Hash); +} + +bool +Workspaces::RemoveWorkspace(RwLock::ExclusiveLockScope&, const Oid& WorkspaceId) +{ + if (auto It = m_Workspaces.find(WorkspaceId); It != m_Workspaces.end()) + { + std::vector<std::string> Aliases; + for (const auto& AliasIt : m_ShareAliases) + { + if (AliasIt.second.WorkspaceId == WorkspaceId) + { + Aliases.push_back(AliasIt.first); + } + } + + for (const std::string& Alias : Aliases) + { + m_ShareAliases.erase(Alias); + } + + m_Workspaces.erase(It); + + ZEN_DEBUG("Removed workspace '{}' and {} aliases", WorkspaceId, Aliases.size()); + return true; + } + return false; +} + std::pair<Ref<Workspace>, Ref<WorkspaceShare>> Workspaces::FindWorkspaceShare(const Oid& WorkspaceId, const Oid& ShareId, bool ForceRefresh, WorkerThreadPool& WorkerPool) { @@ -783,37 +1238,44 @@ Workspaces::FindWorkspaceShare(const Oid& WorkspaceId, const Oid& ShareId, bool } } - if (ForceRefresh || !Share->IsInitialized()) + const Workspaces::WorkspaceConfiguration& WorkspaceConfig = Workspace->GetConfig(); + const Workspaces::WorkspaceShareConfiguration& ShareConfig = Share->GetConfig(); + std::filesystem::path FullSharePath = WorkspaceConfig.RootPath / ShareConfig.SharePath; + if (std::filesystem::is_directory(FullSharePath)) { - Workspaces::WorkspaceShareConfiguration Config = Share->GetConfig(); - std::filesystem::path RootPath = Workspace->GetConfig().RootPath; - std::function<Oid(const std::filesystem::path& Path)> PathToIdCB = Share->GetPathToIdFunction(); - std::unique_ptr<FolderStructure> NewStructure = ScanFolder(Log(), RootPath / Config.SharePath, PathToIdCB, WorkerPool); - if (NewStructure) + if (ForceRefresh || !Share->IsInitialized()) { - Share = Ref<WorkspaceShare>(new WorkspaceShare(Config, std::move(NewStructure), std::move(PathToIdCB))); + std::unique_ptr<FolderStructure> NewStructure = ScanFolder(Log(), FullSharePath, WorkerPool); + if (NewStructure) { - RwLock::ExclusiveLockScope _(m_Lock); - Workspace->SetShare(ShareId, Ref<WorkspaceShare>(Share)); + Share = Ref<WorkspaceShare>(new WorkspaceShare(ShareConfig, std::move(NewStructure))); + { + RwLock::ExclusiveLockScope _(m_Lock); + Workspace->SetShare(ShareId, Ref<WorkspaceShare>(Share)); + } } - } - else - { - if (!Share->IsInitialized()) + else { ZEN_WARN("Failed to scan folder {} for share {} in workspace {}, treating it as an empty share", - WorkspaceId, + FullSharePath, ShareId, - RootPath / Config.SharePath); - Share = Ref<WorkspaceShare>(new WorkspaceShare(Config, std::move(NewStructure), std::move(PathToIdCB))); + WorkspaceId); + Share = Ref<WorkspaceShare>(new WorkspaceShare(ShareConfig, std::make_unique<FolderStructure>())); { RwLock::ExclusiveLockScope _(m_Lock); Workspace->SetShare(ShareId, Ref<WorkspaceShare>(Share)); } } } + return {std::move(Workspace), std::move(Share)}; + } + else + { + ZEN_WARN("Folder {} for share {} in workspace {} does not exist, removing the share", FullSharePath, ShareId, WorkspaceId); + RwLock::ExclusiveLockScope _(m_Lock); + Workspace->SetShare(ShareId, {}); + return {}; } - return {std::move(Workspace), std::move(Share)}; } Ref<Workspace> @@ -831,13 +1293,9 @@ Workspaces::FindWorkspace(const RwLock::SharedLockScope&, const Oid& WorkspaceId #if ZEN_WITH_TESTS namespace { - Oid PathToId(const std::filesystem::path& Path) - { - return Oid::FromMemory(BLAKE3::HashMemory((const void*)Path.string().data(), Path.string().size()).Hash); - } - std::vector<std::pair<std::filesystem::path, IoBuffer>> GenerateFolderContent(const std::filesystem::path& RootPath) { + CreateDirectories(RootPath); std::vector<std::pair<std::filesystem::path, IoBuffer>> Result; Result.push_back(std::make_pair(RootPath / "root_blob_1.bin", CreateRandomBlob(4122))); Result.push_back(std::make_pair(RootPath / "root_blob_2.bin", CreateRandomBlob(2122))); @@ -869,6 +1327,7 @@ namespace { std::vector<std::pair<std::filesystem::path, IoBuffer>> GenerateFolderContent2(const std::filesystem::path& RootPath) { + CreateDirectories(RootPath); std::vector<std::pair<std::filesystem::path, IoBuffer>> Result; Result.push_back(std::make_pair(RootPath / "root_blob_3.bin", CreateRandomBlob(312))); std::filesystem::path FirstFolder(RootPath / "first_folder"); @@ -898,11 +1357,7 @@ TEST_CASE("workspaces.scanfolder") std::filesystem::path RootPath = TempDir.Path(); (void)GenerateFolderContent(RootPath); - std::unique_ptr<FolderStructure> Structure = ScanFolder( - logging::Default(), - RootPath, - [](const std::filesystem::path& Path) { return PathToId(Path); }, - WorkerPool); + std::unique_ptr<FolderStructure> Structure = ScanFolder(logging::Default(), RootPath, WorkerPool); CHECK(Structure); Structure->IterateEntries([&](const Oid& Id, const FolderStructure::FileEntry& Entry) { @@ -924,25 +1379,25 @@ TEST_CASE("workspace.share.paths") WorkerThreadPool WorkerPool(std::thread::hardware_concurrency()); ScopedTemporaryDirectory TempDir; - std::filesystem::path RootPath = TempDir.Path(); + std::filesystem::path RootPath = TempDir.Path() / "workspace"; std::vector<std::pair<std::filesystem::path, IoBuffer>> Content = GenerateFolderContent(RootPath); - Workspaces WS; - CHECK(WS.AddWorkspace({PathToId(RootPath), RootPath})); - CHECK(WS.AddWorkspaceShare(PathToId(RootPath), - {PathToId("second_folder/child_in_second"), "second_folder/child_in_second"}, - [](const std::filesystem::path& Path) { return PathToId(Path); })); - CHECK_THROWS(WS.AddWorkspaceShare(PathToId(RootPath), - {PathToId("../second_folder"), "../second_folder"}, - [](const std::filesystem::path& Path) { return PathToId(Path); })); - CHECK(WS.AddWorkspaceShare(PathToId(RootPath), {PathToId("."), "."}, [](const std::filesystem::path& Path) { return PathToId(Path); })); - CHECK_THROWS( - WS.AddWorkspaceShare(PathToId(RootPath), {PathToId(".."), ".."}, [](const std::filesystem::path& Path) { return PathToId(Path); })); - CHECK_THROWS(WS.AddWorkspaceShare(PathToId(RootPath), - {PathToId("second_folder/../second_folder/../.."), "second_folder/../second_folder/../.."}, - [](const std::filesystem::path& Path) { return PathToId(Path); })); - CHECK_THROWS(WS.AddWorkspaceShare(PathToId(RootPath), {PathToId(RootPath), RootPath}, [](const std::filesystem::path& Path) { - return PathToId(Path); - })); + CHECK(Workspaces::AddWorkspace(Log(), + TempDir.Path(), + Workspaces::WorkspaceConfiguration{.Id = Workspaces::PathToId(RootPath), .RootPath = RootPath})); + CHECK(!Workspaces::AddWorkspace(Log(), + TempDir.Path(), + Workspaces::WorkspaceConfiguration{.Id = Workspaces::PathToId(RootPath), .RootPath = RootPath})); + CHECK(Workspaces::AddWorkspaceShare(Log(), + RootPath, + {Workspaces::PathToId("second_folder/child_in_second"), "second_folder/child_in_second"})); + CHECK(!Workspaces::AddWorkspaceShare(Log(), + RootPath, + {Workspaces::PathToId("second_folder/child_in_second"), "second_folder/child_in_second"})); + CHECK_THROWS(Workspaces::AddWorkspaceShare(Log(), RootPath, {Workspaces::PathToId("../second_folder"), "../second_folder"})); + CHECK(Workspaces::AddWorkspaceShare(Log(), RootPath, {Workspaces::PathToId("."), "."})); + CHECK(!Workspaces::AddWorkspaceShare(Log(), RootPath, {Workspaces::PathToId("."), "."})); + CHECK_THROWS(Workspaces::AddWorkspaceShare(Log(), RootPath, {Workspaces::PathToId(".."), ".."})); + CHECK_THROWS(Workspaces::AddWorkspaceShare(Log(), RootPath, {Workspaces::PathToId(RootPath), RootPath})); } TEST_CASE("workspace.share.basic") @@ -952,26 +1407,30 @@ TEST_CASE("workspace.share.basic") WorkerThreadPool WorkerPool(std::thread::hardware_concurrency()); ScopedTemporaryDirectory TempDir; - std::filesystem::path RootPath = TempDir.Path(); + std::filesystem::path RootPath = TempDir.Path() / "workspace"; std::vector<std::pair<std::filesystem::path, IoBuffer>> Content = GenerateFolderContent(RootPath); + Workspaces::AddWorkspace(Log(), + TempDir.Path(), + Workspaces::WorkspaceConfiguration{.Id = Workspaces::PathToId(RootPath), .RootPath = RootPath}); + Workspaces::AddWorkspaceShare(Log(), RootPath, {Workspaces::PathToId("second_folder"), "second_folder"}); + Workspaces WS; - CHECK(WS.AddWorkspace({PathToId(RootPath), RootPath})); - CHECK(WS.AddWorkspaceShare(PathToId(RootPath), {PathToId("second_folder"), "second_folder"}, [](const std::filesystem::path& Path) { - return PathToId(Path); - })); + WS.RefreshState(TempDir.Path()); + std::filesystem::path SharePath = RootPath / "second_folder"; std::vector<std::filesystem::path> Paths = {{std::filesystem::relative(Content[4].first, SharePath)}, {std::filesystem::relative(Content[6].first, SharePath)}, {std::filesystem::relative(Content[7].first, SharePath)}, {"the_file_that_is_not_there.txt"}}; - std::vector<IoBuffer> Chunks = WS.GetWorkspaceShareChunks(PathToId(RootPath), - PathToId("second_folder"), - std::vector<Workspaces::ChunkRequest>{{.ChunkId = PathToId(Paths[0])}, - {.ChunkId = PathToId(Paths[1])}, - {.ChunkId = PathToId(Paths[2])}, - {.ChunkId = PathToId(Paths[3])}}, - WorkerPool); + std::vector<IoBuffer> Chunks = + WS.GetWorkspaceShareChunks(Workspaces::PathToId(RootPath), + Workspaces::PathToId("second_folder"), + std::vector<Workspaces::ChunkRequest>{{.ChunkId = Workspaces::PathToId(Paths[0])}, + {.ChunkId = Workspaces::PathToId(Paths[1])}, + {.ChunkId = Workspaces::PathToId(Paths[2])}, + {.ChunkId = Workspaces::PathToId(Paths[3])}}, + WorkerPool); CHECK(Chunks.size() == 4); CHECK(Chunks[0].GetView().EqualBytes(Content[4].second.GetView())); CHECK(Chunks[1].GetView().EqualBytes(Content[6].second.GetView())); @@ -983,53 +1442,56 @@ TEST_CASE("workspace.share.basic") {std::filesystem::relative(Content2[3].first, SharePath)}}; std::vector<IoBuffer> Chunks2 = WS.GetWorkspaceShareChunks( - PathToId(RootPath), - PathToId("second_folder"), - std::vector<Workspaces::ChunkRequest>{{.ChunkId = PathToId(Paths2[0])}, {.ChunkId = PathToId(Paths2[1])}}, + Workspaces::PathToId(RootPath), + Workspaces::PathToId("second_folder"), + std::vector<Workspaces::ChunkRequest>{{.ChunkId = Workspaces::PathToId(Paths2[0])}, {.ChunkId = Workspaces::PathToId(Paths2[1])}}, WorkerPool); CHECK(Chunks2.size() == 2); CHECK(Chunks2[0].GetSize() == 0); CHECK(Chunks2[1].GetSize() == 0); std::optional<std::vector<Workspaces::ShareFile>> Files = - WS.GetWorkspaceShareFiles(PathToId(RootPath), PathToId("second_folder"), true, WorkerPool); + WS.GetWorkspaceShareFiles(Workspaces::PathToId(RootPath), Workspaces::PathToId("second_folder"), true, WorkerPool); CHECK(Files.has_value()); CHECK(Files.value().size() == 6); Chunks2 = WS.GetWorkspaceShareChunks( - PathToId(RootPath), - PathToId("second_folder"), - std::vector<Workspaces::ChunkRequest>{{.ChunkId = PathToId(Paths2[0])}, {.ChunkId = PathToId(Paths2[1])}}, + Workspaces::PathToId(RootPath), + Workspaces::PathToId("second_folder"), + std::vector<Workspaces::ChunkRequest>{{.ChunkId = Workspaces::PathToId(Paths2[0])}, {.ChunkId = Workspaces::PathToId(Paths2[1])}}, WorkerPool); CHECK(Chunks2.size() == 2); CHECK(Chunks2[0].GetView().EqualBytes(Content2[2].second.GetView())); CHECK(Chunks2[1].GetView().EqualBytes(Content2[3].second.GetView())); - Workspaces::ShareFile Entry = - WS.GetWorkspaceShareChunkInfo(PathToId(RootPath), PathToId("second_folder"), PathToId(Paths2[1]), WorkerPool); - CHECK(Entry.Id == PathToId(Paths2[1])); + Workspaces::ShareFile Entry = WS.GetWorkspaceShareChunkInfo(Workspaces::PathToId(RootPath), + Workspaces::PathToId("second_folder"), + Workspaces::PathToId(Paths2[1]), + WorkerPool); + CHECK(Entry.Id == Workspaces::PathToId(Paths2[1])); CHECK(!Entry.RelativePath.empty()); CHECK(Entry.Size == Content2[3].second.GetSize()); - Files = WS.GetWorkspaceShareFiles(PathToId(RootPath), PathToId("second_folder"), false, WorkerPool); + Files = WS.GetWorkspaceShareFiles(Workspaces::PathToId(RootPath), Workspaces::PathToId("second_folder"), false, WorkerPool); CHECK(Files.has_value()); CHECK(Files.value().size() == 6); - CHECK(WS.RemoveWorkspaceShare(PathToId(RootPath), PathToId("second_folder"))); - CHECK(!WS.RemoveWorkspaceShare(PathToId(RootPath), PathToId("second_folder"))); + CHECK(Workspaces::RemoveWorkspaceShare(Log(), RootPath, Workspaces::PathToId("second_folder"))); + CHECK(!Workspaces::RemoveWorkspaceShare(Log(), RootPath, Workspaces::PathToId("second_folder"))); - Files = WS.GetWorkspaceShareFiles(PathToId(RootPath), PathToId("second_folder"), false, WorkerPool); + WS.RefreshState(TempDir.Path()); + Files = WS.GetWorkspaceShareFiles(Workspaces::PathToId(RootPath), Workspaces::PathToId("second_folder"), false, WorkerPool); CHECK(!Files.has_value()); Chunks2 = WS.GetWorkspaceShareChunks( - PathToId(RootPath), - PathToId("second_folder"), - std::vector<Workspaces::ChunkRequest>{{.ChunkId = PathToId(Paths2[0])}, {.ChunkId = PathToId(Paths2[1])}}, + Workspaces::PathToId(RootPath), + Workspaces::PathToId("second_folder"), + std::vector<Workspaces::ChunkRequest>{{.ChunkId = Workspaces::PathToId(Paths2[0])}, {.ChunkId = Workspaces::PathToId(Paths2[1])}}, WorkerPool); CHECK(Chunks2.empty()); - CHECK(WS.RemoveWorkspace(PathToId(RootPath))); - CHECK(!WS.RemoveWorkspace(PathToId(RootPath))); + CHECK(Workspaces::RemoveWorkspace(Log(), TempDir.Path(), Workspaces::PathToId(RootPath))); + CHECK(!Workspaces::RemoveWorkspace(Log(), TempDir.Path(), Workspaces::PathToId(RootPath))); } TEST_CASE("workspace.share.alias") @@ -1039,14 +1501,14 @@ TEST_CASE("workspace.share.alias") WorkerThreadPool WorkerPool(std::thread::hardware_concurrency()); ScopedTemporaryDirectory TempDir; - std::filesystem::path RootPath = TempDir.Path(); + std::filesystem::path RootPath = TempDir.Path() / "workspace"; std::vector<std::pair<std::filesystem::path, IoBuffer>> Content = GenerateFolderContent(RootPath); + CHECK(Workspaces::AddWorkspace(Log(), TempDir.Path(), {Workspaces::PathToId(RootPath), RootPath})); + CHECK(Workspaces::AddWorkspaceShare(Log(), RootPath, {Workspaces::PathToId("second_folder"), "second_folder", "my_share"})); + Workspaces WS; - CHECK(WS.AddWorkspace({PathToId(RootPath), RootPath})); - CHECK(WS.AddWorkspaceShare(PathToId(RootPath), - {PathToId("second_folder"), "second_folder", "my_share"}, - [](const std::filesystem::path& Path) { return PathToId(Path); })); + WS.RefreshState(TempDir.Path()); std::optional<Workspaces::ShareAlias> Alias = WS.GetShareAlias("my_share"); CHECK(Alias.has_value()); @@ -1057,33 +1519,37 @@ TEST_CASE("workspace.share.alias") {std::filesystem::relative(Content[6].first, SharePath)}, {std::filesystem::relative(Content[7].first, SharePath)}, {"the_file_that_is_not_there.txt"}}; - std::vector<IoBuffer> Chunks = WS.GetWorkspaceShareChunks(Alias->WorkspaceId, - Alias->ShareId, - std::vector<Workspaces::ChunkRequest>{{.ChunkId = PathToId(Paths[0])}, - {.ChunkId = PathToId(Paths[1])}, - {.ChunkId = PathToId(Paths[2])}, - {.ChunkId = PathToId(Paths[3])}}, - WorkerPool); + std::vector<IoBuffer> Chunks = + WS.GetWorkspaceShareChunks(Alias->WorkspaceId, + Alias->ShareId, + std::vector<Workspaces::ChunkRequest>{{.ChunkId = Workspaces::PathToId(Paths[0])}, + {.ChunkId = Workspaces::PathToId(Paths[1])}, + {.ChunkId = Workspaces::PathToId(Paths[2])}, + {.ChunkId = Workspaces::PathToId(Paths[3])}}, + WorkerPool); CHECK(Chunks.size() == 4); CHECK(Chunks[0].GetView().EqualBytes(Content[4].second.GetView())); CHECK(Chunks[1].GetView().EqualBytes(Content[6].second.GetView())); CHECK(Chunks[2].GetView().EqualBytes(Content[7].second.GetView())); CHECK(Chunks[3].GetSize() == 0); - CHECK(WS.RemoveWorkspaceShare(Alias->WorkspaceId, Alias->ShareId)); + CHECK(Workspaces::RemoveWorkspaceShare(Log(), RootPath, Alias->ShareId)); + WS.RefreshState(TempDir.Path()); CHECK(!WS.GetShareAlias("my_share").has_value()); - CHECK(WS.AddWorkspaceShare(PathToId(RootPath), - {PathToId("second_folder"), "second_folder", "my_share"}, - [](const std::filesystem::path& Path) { return PathToId(Path); })); + CHECK(Workspaces::AddWorkspaceShare(Log(), RootPath, {Workspaces::PathToId("second_folder"), "second_folder", "my_share"})); + WS.RefreshState(TempDir.Path()); + CHECK(WS.GetShareAlias("my_share").has_value()); - CHECK(WS.RemoveWorkspace(PathToId(RootPath))); - CHECK(!WS.RemoveWorkspace(PathToId(RootPath))); + CHECK(Workspaces::RemoveWorkspace(Log(), TempDir.Path(), Workspaces::PathToId(RootPath))); + CHECK(!Workspaces::RemoveWorkspace(Log(), TempDir.Path(), Workspaces::PathToId(RootPath))); + WS.RefreshState(TempDir.Path()); CHECK(!WS.GetShareAlias("my_share").has_value()); } + #endif void |