aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorDan Engelbrecht <[email protected]>2024-10-23 10:31:43 +0200
committerGitHub Enterprise <[email protected]>2024-10-23 10:31:43 +0200
commit530ab3394938331f224058c381a1db5d4a68e6a9 (patch)
tree5060eb394d67b7454855aed0fa8d7d3acf5f5c98 /src
parentfix gc date (#204) (diff)
downloadzen-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.cpp416
-rw-r--r--src/zen/cmds/workspaces_cmd.h33
-rw-r--r--src/zencore/filesystem.cpp30
-rw-r--r--src/zencore/include/zencore/filesystem.h2
-rw-r--r--src/zencore/include/zencore/testutils.h2
-rw-r--r--src/zenhttp/httpclient.cpp5
-rw-r--r--src/zenserver-test/zenserver-test.cpp165
-rw-r--r--src/zenserver/config.cpp62
-rw-r--r--src/zenserver/config.h3
-rw-r--r--src/zenserver/workspaces/httpworkspaces.cpp321
-rw-r--r--src/zenserver/workspaces/httpworkspaces.h25
-rw-r--r--src/zenserver/zenserver.cpp5
-rw-r--r--src/zenstore/include/zenstore/workspaces.h83
-rw-r--r--src/zenstore/workspaces.cpp1190
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