// Copyright Epic Games, Inc. All Rights Reserved. #include "zenstore/workspaces.h" #include #include #include #include #include #include #include ZEN_THIRD_PARTY_INCLUDES_START #include ZEN_THIRD_PARTY_INCLUDES_END #if ZEN_WITH_TESTS # include # include # include #endif namespace zen { namespace { static constexpr std::string_view WorkspacesConfigName("config.json"); static constexpr std::string_view WorkspaceConfigName("zenworkspaceconfig.json"); std::string WorkspacesToJson(std::span Workspaces) { using namespace std::literals; CbObjectWriter Writer; Writer.BeginArray("workspaces"); for (const Workspaces::WorkspaceConfiguration& Workspace : Workspaces) { Writer.BeginObject(); { Writer.AddObjectId("id"sv, Workspace.Id); Writer.AddString("root_path"sv, reinterpret_cast(Workspace.RootPath.u8string().c_str())); Writer.AddBool("allow_share_creation_from_http"sv, Workspace.AllowShareCreationFromHttp); } Writer.EndObject(); } Writer.EndArray(); ExtendableStringBuilder<512> Json; Writer.Save().ToJson(Json); return Json.ToString(); } std::vector WorkspacesFromJson(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; 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(); bool AllowShareCreationFromHttp = Workspace["allow_share_creation_from_http"sv].AsBool(); if (WorkspaceId == Oid::Zero && !RootPath.empty()) { WorkspaceId = Workspaces::PathToId(RootPath); } if (WorkspaceId != Oid::Zero && !RootPath.empty()) { Workspaces.push_back( {.Id = WorkspaceId, .RootPath = RootPath, .AllowShareCreationFromHttp = AllowShareCreationFromHttp}); } } } return Workspaces; } return {}; } std::string WorkspaceSharesToJson(std::span 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(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 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 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 { public: struct FileEntry { std::filesystem::path RelativePath; uint64_t Size; }; FolderStructure() {} FolderStructure(std::vector&& InEntries, std::vector&& Ids); const FileEntry* FindEntry(const Oid& Id) const { if (auto It = IdLookup.find(Id); It != IdLookup.end()) { return &Entries[It->second]; } return nullptr; } size_t EntryCount() const { return Entries.size(); } void IterateEntries(std::function&& Callback) const { for (auto It = IdLookup.begin(); It != IdLookup.end(); It++) { Callback(It->first, Entries[It->second]); } } private: const std::vector Entries; tsl::robin_map IdLookup; }; ////////////////////////////////////////////////////////////////////////// class WorkspaceShare : public RefCounted { public: WorkspaceShare(const Workspaces::WorkspaceShareConfiguration& Config, std::unique_ptr&& FolderStructure); const Workspaces::WorkspaceShareConfiguration& GetConfig() const; bool IsInitialized() const { return !!m_FolderStructure; } std::filesystem::path GetAbsolutePath(const std::filesystem::path& RootPath, const Oid& ChunkId, uint64_t& OutSize) const; const FolderStructure& GetStructure() const { ZEN_ASSERT(m_FolderStructure); return *m_FolderStructure; } private: const Workspaces::WorkspaceShareConfiguration m_Config; std::unique_ptr m_FolderStructure; }; ////////////////////////////////////////////////////////////////////////// class Workspace : public RefCounted { public: Workspace(const LoggerRef& Log, const Workspaces::WorkspaceConfiguration& Config); const Workspaces::WorkspaceConfiguration& GetConfig() const; std::vector> GetShares() const; Ref GetShare(const Oid& ShareId) const; void SetShare(const Oid& ShareId, Ref&& Share); private: LoggerRef Log() { return m_Log; } LoggerRef m_Log; const Workspaces::WorkspaceConfiguration m_Config; tsl::robin_map, Oid::Hasher> m_Shares; }; ////////////////////////////////////////////////////////////////////////// FolderStructure::FolderStructure(std::vector&& InEntries, std::vector&& Ids) : Entries(std::move(InEntries)) { IdLookup.reserve(Entries.size()); for (size_t Index = 0; Index < Entries.size(); Index++) { Oid Id = Ids[Index]; IdLookup.insert(std::make_pair(Id, Index)); } } namespace { struct FolderScanner : public GetDirectoryContentVisitor { FolderScanner(LoggerRef& Log, WorkerThreadPool& WorkerPool, const std::filesystem::path& Path) : m_Log(Log) , Path(Path) , WorkerPool(WorkerPool) { } void Traverse(); virtual void AsyncVisitDirectory(const std::filesystem::path& RelativeRoot, DirectoryContent&& Content) override { std::vector FileEntries; std::vector PathIds; const size_t FileCount = Content.FileNames.size(); FileEntries.reserve(FileCount); PathIds.reserve(FileCount); auto FileNameIt = Content.FileNames.begin(); auto FileSizeIt = Content.FileSizes.begin(); while (FileNameIt != Content.FileNames.end()) { ZEN_ASSERT_SLOW(FileSizeIt != Content.FileSizes.end()); std::filesystem::path RelativePath = RelativeRoot.empty() ? *FileNameIt : RelativeRoot / *FileNameIt; PathIds.emplace_back(Workspaces::PathToId(RelativePath)); FileEntries.emplace_back(FolderStructure::FileEntry{.RelativePath = std::move(RelativePath), .Size = *FileSizeIt}); FileNameIt++; FileSizeIt++; } WorkLock.WithExclusiveLock([&]() { FoundFiles.insert(FoundFiles.end(), FileEntries.begin(), FileEntries.end()); FoundFileIds.insert(FoundFileIds.end(), PathIds.begin(), PathIds.end()); }); } LoggerRef& Log() { return m_Log; } LoggerRef& m_Log; const std::filesystem::path Path; RwLock WorkLock; std::vector FoundFiles; std::vector FoundFileIds; WorkerThreadPool& WorkerPool; }; void FolderScanner::Traverse() { Stopwatch Timer; const std::filesystem::path Root = std::filesystem::absolute(Path); Latch WorkLatch(1); GetDirectoryContent( Root, DirectoryContentFlags::IncludeFiles | DirectoryContentFlags::IncludeFileSizes | DirectoryContentFlags::Recursive, *this, WorkerPool, WorkLatch); WorkLatch.CountDown(); while (!WorkLatch.Wait(1000)) { WorkLock.WithSharedLock([&]() { ZEN_INFO("Found {} files in '{}'...", FoundFiles.size(), Root.string()); }); } ZEN_ASSERT(FoundFiles.size() == FoundFileIds.size()); ZEN_INFO("Found {} files in '{}' in {}", FoundFiles.size(), Path.string(), NiceLatencyNs(Timer.GetElapsedTimeUs() * 1000)); } } // namespace std::unique_ptr ScanFolder(LoggerRef InLog, const std::filesystem::path& Path, WorkerThreadPool& WorkerPool) { ZEN_TRACE_CPU("workspaces::ScanFolderImpl"); auto Log = [&InLog]() { return InLog; }; ZEN_UNUSED(Log); FolderScanner Data(InLog, WorkerPool, Path); Data.Traverse(); return std::make_unique(std::move(Data.FoundFiles), std::move(Data.FoundFileIds)); } //////////////////////////////////////////////////////////// WorkspaceShare::WorkspaceShare(const Workspaces::WorkspaceShareConfiguration& Config, std::unique_ptr&& FolderStructure) : m_Config(Config) , m_FolderStructure(std::move(FolderStructure)) { } std::filesystem::path WorkspaceShare::GetAbsolutePath(const std::filesystem::path& RootPath, const Oid& Id, uint64_t& OutSize) const { ZEN_ASSERT(m_FolderStructure); const FolderStructure::FileEntry* Entry = m_FolderStructure->FindEntry(Id); if (Entry == nullptr) { OutSize = 0; return {}; } OutSize = Entry->Size; return RootPath / m_Config.SharePath / Entry->RelativePath; } const Workspaces::WorkspaceShareConfiguration& WorkspaceShare::GetConfig() const { return m_Config; } //////////////////////////////////////////////////////////// Workspace::Workspace(const LoggerRef& Log, const Workspaces::WorkspaceConfiguration& Config) : m_Log(Log), m_Config(Config) { } const Workspaces::WorkspaceConfiguration& Workspace::GetConfig() const { return m_Config; } std::vector> Workspace::GetShares() const { std::vector> Shares; Shares.reserve(m_Shares.size()); for (auto It : m_Shares) { Shares.push_back(It.second); } return Shares; } Ref Workspace::GetShare(const Oid& ShareId) const { if (auto It = m_Shares.find(ShareId); It != m_Shares.end()) { return It->second; } return {}; } void Workspace::SetShare(const Oid& ShareId, Ref&& Share) { if (Share) { m_Shares.insert_or_assign(ShareId, std::move(Share)); } else { m_Shares.erase(ShareId); } } //////////////////////////////////////////////////////////// Workspaces::Workspaces() : m_Log(logging::Get("workspaces")) { } Workspaces::~Workspaces() { } void Workspaces::RefreshWorkspaceShares(const Oid& WorkspaceId) { using namespace std::literals; Ref Workspace; tsl::robin_set DeletedShares; { RwLock::SharedLockScope Lock(m_Lock); Workspace = FindWorkspace(Lock, WorkspaceId); if (Workspace) { for (auto Share : Workspace->GetShares()) { DeletedShares.insert(Share->GetConfig().Id); } } } if (Workspace) { const std::filesystem::path& RootPath = Workspace->GetConfig().RootPath; std::filesystem::path ConfigPath = RootPath / WorkspaceConfigName; if (IsFile(ConfigPath)) { std::string Error; std::vector 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; if (IsDir(RootPath / SharePath)) { DeletedShares.erase(Configuration.Id); if (!IsValidSharePath(RootPath, SharePath)) { ZEN_WARN("Skipping workspace share path '{}' as it is not valid for root path '{}'", SharePath, RootPath); } else { Ref NewShare(new WorkspaceShare(Configuration, {})); RwLock::ExclusiveLockScope _(m_Lock); if (Ref 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 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); } } } Workspace->SetShare(ShareId, {}); ZEN_DEBUG("Removed workspace share '{}' in workspace '{}'", ShareId, WorkspaceId); } } } } } } std::optional> Workspaces::GetWorkspaceShareFiles(const Oid& WorkspaceId, const Oid& ShareId, bool ForceRefresh, WorkerThreadPool& WorkerPool) { std::pair, Ref> WorkspaceAndShare = FindWorkspaceShare(WorkspaceId, ShareId, ForceRefresh, WorkerPool); if (!WorkspaceAndShare.second) { return {}; } const FolderStructure& Structure = WorkspaceAndShare.second->GetStructure(); std::vector Files; Files.reserve(Structure.EntryCount()); Structure.IterateEntries([&Files](const Oid& Id, const FolderStructure::FileEntry& Entry) { std::string GenericPath(reinterpret_cast(Entry.RelativePath.generic_u8string().c_str())); Files.push_back(ShareFile{.RelativePath = std::move(GenericPath), .Size = Entry.Size, .Id = Id}); }); return Files; } Workspaces::ShareFile Workspaces::GetWorkspaceShareChunkInfo(const Oid& WorkspaceId, const Oid& ShareId, const Oid& ChunkId, WorkerThreadPool& WorkerPool) { using namespace std::literals; std::pair, Ref> WorkspaceAndShare = FindWorkspaceShare(WorkspaceId, ShareId, false, WorkerPool); if (!WorkspaceAndShare.second) { return {}; } const FolderStructure::FileEntry* Entry = WorkspaceAndShare.second->GetStructure().FindEntry(ChunkId); if (Entry) { std::string GenericPath(reinterpret_cast(Entry->RelativePath.generic_u8string().c_str())); return Workspaces::ShareFile{.RelativePath = std::move(GenericPath), .Size = Entry->Size, .Id = ChunkId}; } return {}; } std::vector Workspaces::GetWorkspaceShareChunks(const Oid& WorkspaceId, const Oid& ShareId, const std::span ChunkRequests, WorkerThreadPool& WorkerPool) { if (ChunkRequests.size() == 0) { return {}; } std::pair, Ref> WorkspaceAndShare = FindWorkspaceShare(WorkspaceId, ShareId, false, WorkerPool); if (!WorkspaceAndShare.second) { return {}; } std::filesystem::path RootPath = WorkspaceAndShare.first->GetConfig().RootPath; auto GetOne = [this](const std::filesystem::path& RootPath, WorkspaceShare& Share, const ChunkRequest& Request) -> IoBuffer { uint64_t Size; std::filesystem::path Path = Share.GetAbsolutePath(RootPath, Request.ChunkId, /* out */ Size); if (!Path.empty()) { uint64_t RequestedOffset = Request.Offset; uint64_t RequestedSize = Request.Size; if (Request.Offset > 0 || Request.Size < uint64_t(-1)) { if (RequestedOffset > Size) { RequestedOffset = Size; } if ((RequestedOffset + RequestedSize) > Size) { RequestedSize = Size - RequestedOffset; } } return IoBufferBuilder::MakeFromFile(Path, RequestedOffset, RequestedSize); } return IoBuffer{}; }; if (ChunkRequests.size() == 1) { return std::vector({GetOne(RootPath, *WorkspaceAndShare.second, ChunkRequests[0])}); } std::vector Chunks; Chunks.resize(ChunkRequests.size()); Latch WorkLatch(1); for (size_t Index = 0; Index < ChunkRequests.size(); Index++) { WorkLatch.AddCount(1); WorkerPool.ScheduleWork( [&, Index]() { auto _ = MakeGuard([&WorkLatch]() { WorkLatch.CountDown(); }); try { Chunks[Index] = GetOne(RootPath, *WorkspaceAndShare.second, ChunkRequests[Index]); } catch (const std::exception& Ex) { ZEN_WARN("Exception while fetching chunks, chunk {}: {}", ChunkRequests[Index].ChunkId, Ex.what()); } }, WorkerThreadPool::EMode::DisableBacklog); } WorkLatch.CountDown(); WorkLatch.Wait(); return Chunks; } std::vector Workspaces::GetWorkspaces() const { std::vector Workspaces; RwLock::SharedLockScope Lock(m_Lock); for (auto It : m_Workspaces) { Workspaces.push_back(It.first); } return Workspaces; } std::optional Workspaces::GetWorkspaceConfiguration(const Oid& WorkspaceId) const { Ref Workspace; { RwLock::SharedLockScope Lock(m_Lock); Workspace = FindWorkspace(Lock, WorkspaceId); } if (Workspace) { return Workspace->GetConfig(); } return {}; } std::optional> Workspaces::GetWorkspaceShares(const Oid& WorkspaceId) const { RwLock::SharedLockScope Lock(m_Lock); Ref Workspace = FindWorkspace(Lock, WorkspaceId); if (Workspace) { std::vector Shares; for (auto Share : Workspace->GetShares()) { Shares.push_back(Share->GetConfig().Id); } return Shares; } return {}; } std::optional Workspaces::GetWorkspaceShareConfiguration(const Oid& WorkspaceId, const Oid& ShareId) const { RwLock::SharedLockScope Lock(m_Lock); Ref Workspace = FindWorkspace(Lock, WorkspaceId); if (Workspace) { Ref Share = Workspace->GetShare(ShareId); if (Share) { return Share->GetConfig(); } } return {}; } void Workspaces::RefreshState(const std::filesystem::path& WorkspaceStatePath) { using namespace std::literals; std::string Error; std::vector Workspaces = ReadConfig(Log(), WorkspaceStatePath, Error); if (!Error.empty()) { ZEN_WARN("Failed to read workspaces state from {}. Reason: '{}'", WorkspaceStatePath, Error); } else { tsl::robin_set 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 { 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 { Ref NewWorkspace(new Workspace(m_Log, Configuration)); { RwLock::ExclusiveLockScope Lock(m_Lock); if (auto It = m_Workspaces.find(Configuration.Id); It != m_Workspaces.end()) { Ref 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); } } else { 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: '{}'", 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()); } } } } } std::optional Workspaces::GetShareAlias(std::string_view Alias) const { RwLock::SharedLockScope Lock(m_Lock); if (auto It = m_ShareAliases.find(std::string(Alias)); It != m_ShareAliases.end()) { return It->second; } return {}; } std::vector 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 (IsFile(ConfigPath)) { std::vector 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& 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::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 (IsFile(ConfigPath)) { std::vector 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& 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)); } if (!IsDir(Configuration.RootPath)) { throw std::invalid_argument( fmt::format("workspace root path '{}' does not exist for workspace '{}'", Configuration.RootPath, Configuration.Id)); } std::string Error; std::vector 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 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 (!IsDir(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 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 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; }; ZEN_UNUSED(Log); std::string Error; std::vector 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; }; ZEN_UNUSED(Log); std::string Error; std::vector 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 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 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 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 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(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 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> Workspaces::FindWorkspaceShare(const Oid& WorkspaceId, const Oid& ShareId, bool ForceRefresh, WorkerThreadPool& WorkerPool) { Ref Workspace; Ref Share; { RwLock::SharedLockScope Lock(m_Lock); Workspace = FindWorkspace(Lock, WorkspaceId); if (!Workspace) { return {}; } Share = Workspace->GetShare(ShareId); if (!Share) { return {}; } } const Workspaces::WorkspaceConfiguration& WorkspaceConfig = Workspace->GetConfig(); const Workspaces::WorkspaceShareConfiguration& ShareConfig = Share->GetConfig(); std::filesystem::path FullSharePath = WorkspaceConfig.RootPath / ShareConfig.SharePath; if (IsDir(FullSharePath)) { if (ForceRefresh || !Share->IsInitialized()) { std::unique_ptr NewStructure = ScanFolder(Log(), FullSharePath, WorkerPool); if (NewStructure) { Share = Ref(new WorkspaceShare(ShareConfig, std::move(NewStructure))); { RwLock::ExclusiveLockScope _(m_Lock); Workspace->SetShare(ShareId, Ref(Share)); } } else { ZEN_WARN("Failed to scan folder {} for share {} in workspace {}, treating it as an empty share", FullSharePath, ShareId, WorkspaceId); Share = Ref(new WorkspaceShare(ShareConfig, std::make_unique())); { RwLock::ExclusiveLockScope _(m_Lock); Workspace->SetShare(ShareId, Ref(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 {}; } } Ref Workspaces::FindWorkspace(const RwLock::SharedLockScope&, const Oid& WorkspaceId) const { if (auto It = m_Workspaces.find(WorkspaceId); It != m_Workspaces.end()) { return It->second; } return {}; } ////////////////////////////////////////////////////////////////////////// #if ZEN_WITH_TESTS namespace { std::vector> GenerateFolderContent(const std::filesystem::path& RootPath) { CreateDirectories(RootPath); std::vector> 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))); std::filesystem::path EmptyFolder(RootPath / "empty_folder"); std::filesystem::path FirstFolder(RootPath / "first_folder"); CreateDirectories(FirstFolder); Result.push_back(std::make_pair(FirstFolder / "first_folder_blob1.bin", CreateRandomBlob(22))); Result.push_back(std::make_pair(FirstFolder / "first_folder_blob2.bin", CreateRandomBlob(122))); std::filesystem::path SecondFolder(RootPath / "second_folder"); CreateDirectories(SecondFolder); Result.push_back(std::make_pair(SecondFolder / "second_folder_blob1.bin", CreateRandomBlob(522))); Result.push_back(std::make_pair(SecondFolder / "second_folder_blob2.bin", CreateRandomBlob(122))); Result.push_back(std::make_pair(SecondFolder / "second_folder_blob3.bin", CreateRandomBlob(225))); std::filesystem::path SecondFolderChild(SecondFolder / "child_in_second"); CreateDirectories(SecondFolderChild); Result.push_back(std::make_pair(SecondFolderChild / "second_child_folder_blob1.bin", CreateRandomBlob(622))); for (const auto& It : Result) { WriteFile(It.first, It.second); } return Result; } std::vector> GenerateFolderContent2(const std::filesystem::path& RootPath) { CreateDirectories(RootPath); std::vector> Result; Result.push_back(std::make_pair(RootPath / "root_blob_3.bin", CreateRandomBlob(312))); std::filesystem::path FirstFolder(RootPath / "first_folder"); Result.push_back(std::make_pair(FirstFolder / "first_folder_blob3.bin", CreateRandomBlob(722))); std::filesystem::path SecondFolder(RootPath / "second_folder"); std::filesystem::path SecondFolderChild(SecondFolder / "child_in_second"); Result.push_back(std::make_pair(SecondFolderChild / "second_child_folder_blob2.bin", CreateRandomBlob(962))); Result.push_back(std::make_pair(SecondFolderChild / "second_child_folder_blob3.bin", CreateRandomBlob(561))); for (const auto& It : Result) { WriteFile(It.first, It.second); } return Result; } } // namespace TEST_CASE("workspaces.scanfolder") { using namespace std::literals; WorkerThreadPool WorkerPool(GetHardwareConcurrency()); ScopedTemporaryDirectory TempDir; std::filesystem::path RootPath = TempDir.Path(); (void)GenerateFolderContent(RootPath); std::unique_ptr Structure = ScanFolder(logging::Default(), RootPath, WorkerPool); CHECK(Structure); Structure->IterateEntries([&](const Oid& Id, const FolderStructure::FileEntry& Entry) { std::filesystem::path AbsPath = RootPath / Entry.RelativePath; CHECK(IsFile(AbsPath)); CHECK(FileSizeFromPath(AbsPath) == Entry.Size); const FolderStructure::FileEntry* FindEntry = Structure->FindEntry(Id); CHECK(FindEntry); std::filesystem::path Path = RootPath / FindEntry->RelativePath; CHECK(AbsPath == Path); CHECK(FileSizeFromPath(AbsPath) == FindEntry->Size); }); } TEST_CASE("workspace.share.paths") { using namespace std::literals; WorkerThreadPool WorkerPool(GetHardwareConcurrency()); ScopedTemporaryDirectory TempDir; std::filesystem::path RootPath = TempDir.Path() / "workspace"; std::vector> Content = GenerateFolderContent(RootPath); 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") { using namespace std::literals; WorkerThreadPool WorkerPool(GetHardwareConcurrency()); ScopedTemporaryDirectory TempDir; std::filesystem::path RootPath = TempDir.Path() / "workspace"; std::vector> 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; WS.RefreshState(TempDir.Path()); std::filesystem::path SharePath = RootPath / "second_folder"; std::vector 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 Chunks = WS.GetWorkspaceShareChunks(Workspaces::PathToId(RootPath), Workspaces::PathToId("second_folder"), std::vector{{.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); std::vector> Content2 = GenerateFolderContent2(RootPath); std::vector Paths2 = {{std::filesystem::relative(Content2[2].first, SharePath)}, {std::filesystem::relative(Content2[3].first, SharePath)}}; std::vector Chunks2 = WS.GetWorkspaceShareChunks( Workspaces::PathToId(RootPath), Workspaces::PathToId("second_folder"), std::vector{{.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> Files = WS.GetWorkspaceShareFiles(Workspaces::PathToId(RootPath), Workspaces::PathToId("second_folder"), true, WorkerPool); CHECK(Files.has_value()); CHECK(Files.value().size() == 6); Chunks2 = WS.GetWorkspaceShareChunks( Workspaces::PathToId(RootPath), Workspaces::PathToId("second_folder"), std::vector{{.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(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(Workspaces::PathToId(RootPath), Workspaces::PathToId("second_folder"), false, WorkerPool); CHECK(Files.has_value()); CHECK(Files.value().size() == 6); CHECK(Workspaces::RemoveWorkspaceShare(Log(), RootPath, Workspaces::PathToId("second_folder"))); CHECK(!Workspaces::RemoveWorkspaceShare(Log(), RootPath, Workspaces::PathToId("second_folder"))); WS.RefreshState(TempDir.Path()); Files = WS.GetWorkspaceShareFiles(Workspaces::PathToId(RootPath), Workspaces::PathToId("second_folder"), false, WorkerPool); CHECK(!Files.has_value()); Chunks2 = WS.GetWorkspaceShareChunks( Workspaces::PathToId(RootPath), Workspaces::PathToId("second_folder"), std::vector{{.ChunkId = Workspaces::PathToId(Paths2[0])}, {.ChunkId = Workspaces::PathToId(Paths2[1])}}, WorkerPool); CHECK(Chunks2.empty()); CHECK(Workspaces::RemoveWorkspace(Log(), TempDir.Path(), Workspaces::PathToId(RootPath))); CHECK(!Workspaces::RemoveWorkspace(Log(), TempDir.Path(), Workspaces::PathToId(RootPath))); } TEST_CASE("workspace.share.alias") { using namespace std::literals; WorkerThreadPool WorkerPool(GetHardwareConcurrency()); ScopedTemporaryDirectory TempDir; std::filesystem::path RootPath = TempDir.Path() / "workspace"; std::vector> 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; WS.RefreshState(TempDir.Path()); std::optional Alias = WS.GetShareAlias("my_share"); CHECK(Alias.has_value()); CHECK(!WS.GetShareAlias("my_share2").has_value()); std::filesystem::path SharePath = RootPath / "second_folder"; std::vector 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 Chunks = WS.GetWorkspaceShareChunks(Alias->WorkspaceId, Alias->ShareId, std::vector{{.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(Workspaces::RemoveWorkspaceShare(Log(), RootPath, Alias->ShareId)); WS.RefreshState(TempDir.Path()); CHECK(!WS.GetShareAlias("my_share").has_value()); 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(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 workspaces_forcelink() { } } // namespace zen