// Copyright Epic Games, Inc. All Rights Reserved. #include "Virtualization/IVirtualizationBackend.h" #include "Containers/StringView.h" #include "HAL/FileManager.h" #include "Misc/Parse.h" #include "Misc/Paths.h" #include "Virtualization/VirtualizationManager.h" namespace UE::Virtualization { /** * A basic backend based on the file system. This can be used to access/store virtualization * data either on a local disk or a network share. It is intended to be used as a caching system * to speed up operations (running a local cache or a shared cache for a site) rather than as the * proper backend solution. * * Ini file setup: * 'Name'=(Type=FileSystem, Path="XXX") * Where 'Name' is the backend name in the hierarchy and 'XXX' if the path to the directory where * you want to files to be stored. */ class FFileSystemBackend : public IVirtualizationBackend { public: FFileSystemBackend(FStringView ConfigName) : IVirtualizationBackend(EOperations::Both) { Name = TEXT("FFileSystemBackend - "); Name.Append(ConfigName); } virtual ~FFileSystemBackend() = default; private: virtual bool Initialize(const FString& ConfigEntry) override { if (!FParse::Value(*ConfigEntry, TEXT("Path="), RootDirectory)) { UE_LOG(LogVirtualization, Error, TEXT("[%s] 'Path=' not found in the config file"), *GetDebugString()); return false; } FPaths::NormalizeDirectoryName(RootDirectory); if (RootDirectory.IsEmpty()) { UE_LOG(LogVirtualization, Error, TEXT("[%s] Config file entry 'Path=' was empty"), *GetDebugString()); return false; } // TODO: Validate that the given path is usable? UE_LOG(LogVirtualization, Log, TEXT("[%s] Using path: '%s'"), *GetDebugString(), *RootDirectory); return true; } virtual bool PushData(const FPayloadId& Id, const FCompressedBuffer& Payload) override { TRACE_CPUPROFILER_EVENT_SCOPE(FFileSystemBackend::PushData); if (DoesExist(Id)) { UE_LOG(LogVirtualization, Verbose, TEXT("[%s] Already has a copy of the payload '%s'."), *GetDebugString(), *Id.ToString()); return true; } const FString FilePath = CreateFilePath(Id); { // TODO: Should we write to a temp file and then move it once it has written? TUniquePtr FileAr(IFileManager::Get().CreateFileWriter(*FilePath)); if (FileAr == nullptr) { UE_LOG(LogVirtualization, Error, TEXT("[%s] Failed to push payload '%s' to '%s'"), *GetDebugString(), *Id.ToString(), *FilePath); return false; } for (const FSharedBuffer& Buffer : Payload.GetCompressed().GetSegments()) { // Const cast because FArchive requires a non-const pointer! FileAr->Serialize(const_cast(Buffer.GetData()), static_cast(Buffer.GetSize())); } } return true; } virtual FCompressedBuffer PullData(const FPayloadId& Id) override { TRACE_CPUPROFILER_EVENT_SCOPE(FFileSystemBackend::PullData); const FString FilePath = CreateFilePath(Id); // TODO: Should we allow the error severity to be configured via ini or just not report this case at all? if (!IFileManager::Get().FileExists(*FilePath)) { UE_LOG(LogVirtualization, Verbose, TEXT("[%s] Does not contain the payload '%s'"), *GetDebugString(), *Id.ToString()); return FCompressedBuffer(); } TUniquePtr FileAr(IFileManager::Get().CreateFileReader(*FilePath)); if (FileAr == nullptr) { const uint32 SystemError = FPlatformMisc::GetLastError(); // If we have a system error we can give a more informative error message but don't output it if the error is zero as // this can lead to very confusing error messages. if (SystemError != 0) { TCHAR SystemErrorMsg[2048] = { 0 }; FPlatformMisc::GetSystemErrorMessage(SystemErrorMsg, sizeof(SystemErrorMsg), SystemError); UE_LOG(LogVirtualization, Error, TEXT("[%s] Failed to load payload '%s' file '%s' due to system error: '%s' (%d))"), *GetDebugString(), *Id.ToString(), *FilePath, SystemErrorMsg, SystemError); } else { UE_LOG(LogVirtualization, Error, TEXT("[%s] Failed to load payload '%s' from '%s' (reason unknown)"), *GetDebugString(), *Id.ToString(), *FilePath); } return FCompressedBuffer(); } return FCompressedBuffer::FromCompressed(*FileAr); } bool DoesExist(const FPayloadId& Id) { TRACE_CPUPROFILER_EVENT_SCOPE(FFileSystemBackend::DoesExist); const FString FilePath = CreateFilePath(Id); return IFileManager::Get().FileExists(*FilePath); } virtual FString GetDebugString() const override { return Name; } FString CreateFilePath(const FPayloadId& PayloadId) { return FString::Printf(TEXT("%s/%s.payload"), *RootDirectory, *PayloadId.ToString()); } private: FString Name; FString RootDirectory; }; UE_REGISTER_VIRTUALIZATION_BACKEND_FACTORY(FFileSystemBackend, FileSystem); } // namespace UE::Virtualization