diff options
| author | Dan Engelbrecht <[email protected]> | 2024-10-23 10:31:43 +0200 |
|---|---|---|
| committer | GitHub Enterprise <[email protected]> | 2024-10-23 10:31:43 +0200 |
| commit | 530ab3394938331f224058c381a1db5d4a68e6a9 (patch) | |
| tree | 5060eb394d67b7454855aed0fa8d7d3acf5f5c98 /src/zenstore/workspaces.cpp | |
| parent | fix gc date (#204) (diff) | |
| download | zen-530ab3394938331f224058c381a1db5d4a68e6a9.tar.xz zen-530ab3394938331f224058c381a1db5d4a68e6a9.zip | |
workspace share security (#192)
- Improvement: Reworked workspace shares to be more secure. Workspaces and workspace shares can only be created using the `zen workspace` command, the http endpoint is disabled unless zenserver is started with the `--workspaces-allow-changes` option enabled.
- Each workspace are now configured via a `zenworkspaceconfig.json` file in the root of each workspace
- A workspace can allow shares to be created via the http interface if the workspace is created with the `--allow-share-create-from-http` option enabled
- A new http endpoint at `/ws` - issuing a `Get` operation will get you a list of workspaces
- A new http endpoint at `/ws/refresh` - issuing a `Get` will make zenserver scan for edits in workspaces and workspace shares
Diffstat (limited to 'src/zenstore/workspaces.cpp')
| -rw-r--r-- | src/zenstore/workspaces.cpp | 1190 |
1 files changed, 828 insertions, 362 deletions
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 |