You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
o3de/Code/Framework/AzToolsFramework/AzToolsFramework/Archive/ArchiveComponent.cpp

601 lines
22 KiB
C++

/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include "ArchiveComponent.h"
#include <AzCore/Component/TickBus.h>
#include <AzCore/Component/ComponentApplicationBus.h>
#include <AzCore/Serialization/EditContext.h>
#include <AzFramework/Archive/INestedArchive.h>
#include <AzFramework/Archive/ZipDirStructures.h>
#include <AzFramework/Process/ProcessCommunicator.h>
#include <AzFramework/Process/ProcessWatcher.h>
#include <AzFramework/FileFunc/FileFunc.h>
namespace AzToolsFramework
{
[[maybe_unused]] constexpr const char s_traceName[] = "ArchiveComponent";
constexpr AZ::u32 s_compressionMethod = AZ::IO::INestedArchive::METHOD_DEFLATE;
constexpr AZ::s32 s_compressionLevel = AZ::IO::INestedArchive::LEVEL_NORMAL;
constexpr CompressionCodec::Codec s_compressionCodec = CompressionCodec::Codec::ZLIB;
namespace ArchiveUtils
{
// Read a file's contents into a provided buffer.
// Does not add a zero byte at the end of the buffer.
// returns true if read was successful, false otherwise.
bool ReadFile(const AZ::IO::Path& filePath, AZ::IO::OpenMode openMode, AZStd::vector<char>& outBuffer)
{
auto fileIO = AZ::IO::FileIOBase::GetDirectInstance();
if (!fileIO)
{
return false;
}
bool success = false;
AZ::IO::HandleType fileHandle = AZ::IO::InvalidHandle;
if (fileIO->Open(filePath.c_str(), openMode, fileHandle))
{
AZ::u64 fileSize = 0;
if (fileIO->Size(fileHandle, fileSize) && fileSize != 0)
{
outBuffer.resize_no_construct(fileSize);
AZ::u64 bytesRead = 0;
if (fileIO->Read(fileHandle, outBuffer.data(), fileSize, true, &bytesRead))
{
success = (fileSize == bytesRead);
}
}
fileIO->Close(fileHandle);
}
return success;
}
// Reads a text file that contains a list of file paths.
// Tokenize the file by lines.
// Calls the lineVisitor function for each line of the file.
void ProcessFileList(const AZ::IO::Path& filePath, AZStd::function<void(AZStd::string_view line)> lineVisitor)
{
AZStd::vector<char> fileBuffer;
if (ReadFile(filePath, AZ::IO::OpenMode::ModeText | AZ::IO::OpenMode::ModeRead, fileBuffer))
{
AZ::StringFunc::TokenizeVisitor(AZStd::string_view{ fileBuffer.data(), fileBuffer.size() }, lineVisitor, "\n");
}
}
} // namespace ArchiveUtils
void ArchiveComponent::Activate()
{
m_fileIO = AZ::IO::FileIOBase::GetDirectInstance();
if (m_fileIO == nullptr)
{
AZ_Error(s_traceName, false, "Failed to create a LocalFileIO instance!");
}
m_archive = AZ::Interface<AZ::IO::IArchive>::Get();
if (m_archive == nullptr)
{
AZ_Error(s_traceName, false, "Failed to get IArchive interface!");
}
ArchiveCommandsBus::Handler::BusConnect();
}
void ArchiveComponent::Deactivate()
{
ArchiveCommandsBus::Handler::BusDisconnect();
m_fileIO = nullptr;
m_archive = nullptr;
for (AZStd::thread& t : m_threads)
{
t.join();
}
m_threads = {};
}
void ArchiveComponent::Reflect(AZ::ReflectContext * context)
{
if (AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
{
serializeContext->Class<ArchiveComponent, AZ::Component>()
->Version(2)
->Attribute(AZ::Edit::Attributes::SystemComponentTags, AZStd::vector<AZ::Crc32>({ AZ_CRC_CE("AssetBuilder") }))
;
if (AZ::EditContext* editContext = serializeContext->GetEditContext())
{
editContext->Class<ArchiveComponent>(
"Archive", "Handles creation and extraction of zip archives.")
->ClassElement(AZ::Edit::ClassElements::EditorData, "")
->Attribute(AZ::Edit::Attributes::Category, "Editor")
->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("System"))
;
}
}
}
std::future<bool> ArchiveComponent::CreateArchive(
const AZStd::string& archivePath,
const AZStd::string& dirToArchive)
{
if (!CheckParamsForCreate(archivePath, dirToArchive))
{
std::promise<bool> p;
p.set_value(false);
return p.get_future();
}
auto FnCreateArchive = [this, archivePath, dirToArchive](std::promise<bool>&& p) -> void
{
auto archive = m_archive->OpenArchive(archivePath, {}, AZ::IO::INestedArchive::FLAGS_CREATE_NEW);
if (!archive)
{
AZ_Error(s_traceName, false, "Failed to create archive file '%s'", archivePath.c_str());
p.set_value(false);
return;
}
auto foundFiles = AzFramework::FileFunc::FindFilesInPath(dirToArchive, "*", true);
if (!foundFiles.IsSuccess())
{
AZ_Error(s_traceName, false, "Failed to find file listing under directory '%d'", dirToArchive.c_str());
p.set_value(false);
return;
}
bool success = true;
AZStd::vector<char> fileBuffer;
const AZ::IO::FixedMaxPath workingPath{ dirToArchive };
for (const auto& fileName : foundFiles.GetValue())
{
bool thisSuccess = false;
AZ::IO::FixedMaxPath relativePath = AZ::IO::FixedMaxPath{ fileName }.LexicallyRelative(workingPath);
AZ::IO::FixedMaxPath fullPath = (workingPath / relativePath);
if (ArchiveUtils::ReadFile(static_cast<AZ::IO::PathView>(fullPath), AZ::IO::OpenMode::ModeRead, fileBuffer))
{
int result = archive->UpdateFile(
relativePath.Native(), fileBuffer.data(), fileBuffer.size(), s_compressionMethod,
s_compressionLevel, s_compressionCodec);
thisSuccess = (result == AZ::IO::ZipDir::ZD_ERROR_SUCCESS);
AZ_Error(
s_traceName, thisSuccess, "Error %d encountered while adding '%s' to archive '%.*s'", result, fileName.c_str(),
AZ_STRING_ARG(archive->GetFullPath().Native()));
}
else
{
AZ_Error(
s_traceName, false, "Error encountered while reading '%s' to add to archive '%.*s'", fileName.c_str(),
AZ_STRING_ARG(archive->GetFullPath().Native()));
}
success = (success && thisSuccess);
}
archive.reset();
p.set_value(success);
};
// Async task...
std::promise<bool> p;
std::future<bool> f = p.get_future();
AZStd::thread_desc threadDesc;
threadDesc.m_name = "Archive Task (Create)";
m_threads.emplace_back(threadDesc, FnCreateArchive, AZStd::move(p));
return f;
}
std::future<bool> ArchiveComponent::ExtractArchive(
const AZStd::string& archivePath,
const AZStd::string& destinationPath)
{
if (!CheckParamsForExtract(archivePath, destinationPath))
{
std::promise<bool> p;
p.set_value(false);
return p.get_future();
}
auto FnExtractArchive = [this, archivePath, destinationPath](std::promise<bool>&& p) -> void
{
auto archive = m_archive->OpenArchive(archivePath, {}, AZ::IO::INestedArchive::FLAGS_READ_ONLY);
if (!archive)
{
AZ_Error(s_traceName, false, "Failed to open archive file '%s'", archivePath.c_str());
p.set_value(false);
return;
}
AZStd::vector<AZ::IO::Path> filesInArchive;
if (int result = archive->ListAllFiles(filesInArchive); result != AZ::IO::ZipDir::ZD_ERROR_SUCCESS)
{
AZ_Error(s_traceName, false, "Failed to get list of files in archive '%s'", archivePath.c_str());
p.set_value(false);
return;
}
AZStd::vector<AZ::u8> fileBuffer;
AZ::IO::Path destination{ destinationPath };
AZ::u64 fileSize = 0;
AZ::u64 numFilesWritten = 0;
AZ::u64 bytesWritten = 0;
AZ::IO::INestedArchive::Handle srcHandle{};
AZ::IO::HandleType dstHandle = AZ::IO::InvalidHandle;
constexpr AZ::IO::OpenMode openMode =
(AZ::IO::OpenMode::ModeCreatePath | AZ::IO::OpenMode::ModeWrite | AZ::IO::OpenMode::ModeUpdate);
for (const auto& filePath : filesInArchive)
{
srcHandle = archive->FindFile(filePath.Native());
AZ_Assert(srcHandle != nullptr, "File '%s' does not exist inside archive '%s'", filePath.c_str(), archivePath.c_str());
fileSize = (srcHandle != nullptr) ? archive->GetFileSize(srcHandle) : 0;
fileBuffer.resize_no_construct(fileSize);
if (auto result = archive->ReadFile(srcHandle, fileBuffer.data()); result != AZ::IO::ZipDir::ZD_ERROR_SUCCESS)
{
AZ_Error(
s_traceName, false, "Failed to read file '%s' in archive '%s' with error %d", filePath.c_str(), archivePath.c_str(),
result);
continue;
}
AZ::IO::Path destinationFile = destination / filePath;
if (!m_fileIO->Open(destinationFile.c_str(), openMode, dstHandle))
{
AZ_Error(s_traceName, false, "Failed to open '%s' for writing", destinationFile.c_str());
continue;
}
if (!m_fileIO->Write(dstHandle, fileBuffer.data(), fileSize, &bytesWritten))
{
AZ_Error(s_traceName, false, "Failed to write destination file '%s'", destinationFile.c_str());
}
else if (bytesWritten == fileSize)
{
++numFilesWritten;
}
m_fileIO->Close(dstHandle);
}
p.set_value(numFilesWritten == filesInArchive.size());
};
// Async task...
std::promise<bool> p;
std::future<bool> f = p.get_future();
AZStd::thread_desc threadDesc;
threadDesc.m_name = "Archive Task (Extract)";
m_threads.emplace_back(threadDesc, FnExtractArchive, AZStd::move(p));
return f;
}
std::future<bool> ArchiveComponent::ExtractFile(
const AZStd::string& archivePath,
const AZStd::string& fileInArchive,
const AZStd::string& destinationPath)
{
if (!CheckParamsForExtract(archivePath, destinationPath))
{
std::promise<bool> p;
p.set_value(false);
return p.get_future();
}
auto FnExtractFile = [this, archivePath, fileInArchive, destinationPath](std::promise<bool>&& p) -> void
{
auto archive = m_archive->OpenArchive(archivePath, {}, AZ::IO::INestedArchive::FLAGS_READ_ONLY);
if (!archive)
{
AZ_Error(s_traceName, false, "Failed to open archive file '%s'", archivePath.c_str());
p.set_value(false);
return;
}
AZ::IO::INestedArchive::Handle fileHandle = archive->FindFile(fileInArchive);
if (!fileHandle)
{
AZ_Error(s_traceName, false, "File '%s' does not exist inside archive '%s'", fileInArchive.c_str(), archivePath.c_str());
p.set_value(false);
return;
}
AZ::u64 fileSize = archive->GetFileSize(fileHandle);
AZStd::vector<AZ::u8> fileBuffer;
fileBuffer.resize_no_construct(fileSize);
if (auto result = archive->ReadFile(fileHandle, fileBuffer.data()); result != AZ::IO::ZipDir::ZD_ERROR_SUCCESS)
{
AZ_Error(
s_traceName, false, "Failed to read file '%s' in archive '%s' with error %d", fileInArchive.c_str(),
archivePath.c_str(), result);
p.set_value(false);
return;
}
AZ::IO::HandleType destFileHandle = AZ::IO::InvalidHandle;
AZ::IO::Path destinationFile{ destinationPath };
destinationFile /= fileInArchive;
AZ::IO::OpenMode openMode = (AZ::IO::OpenMode::ModeCreatePath | AZ::IO::OpenMode::ModeWrite | AZ::IO::OpenMode::ModeUpdate);
if (!m_fileIO->Open(destinationFile.c_str(), openMode, destFileHandle))
{
AZ_Error(s_traceName, false, "Failed to open destination file '%s' for writing", destinationFile.c_str());
p.set_value(false);
return;
}
AZ::u64 bytesWritten = 0;
if (!m_fileIO->Write(destFileHandle, fileBuffer.data(), fileSize, &bytesWritten))
{
AZ_Error(s_traceName, false, "Failed to write destination file '%s'", destinationFile.c_str());
}
m_fileIO->Close(destFileHandle);
p.set_value(bytesWritten == fileSize);
};
// Async task...
std::promise<bool> p;
std::future<bool> f = p.get_future();
AZStd::thread_desc threadDesc;
threadDesc.m_name = "Archive Task (Extract Single)";
m_threads.emplace_back(threadDesc, FnExtractFile, AZStd::move(p));
return f;
}
bool ArchiveComponent::ListFilesInArchive(const AZStd::string& archivePath, AZStd::vector<AZStd::string>& outFileEntries)
{
if (!m_fileIO || !m_archive)
{
return false;
}
if (!m_fileIO->Exists(archivePath.c_str()))
{
AZ_Error(s_traceName, false, "Archive '%s' does not exist!", archivePath.c_str());
return false;
}
auto archive = m_archive->OpenArchive(archivePath, {}, AZ::IO::INestedArchive::FLAGS_READ_ONLY);
if (!archive)
{
AZ_Error(s_traceName, false, "Failed to open archive file '%s'", archivePath.c_str());
return false;
}
AZStd::vector<AZ::IO::Path> fileEntries;
int result = archive->ListAllFiles(fileEntries);
outFileEntries.clear();
for (const auto& path : fileEntries)
{
outFileEntries.emplace_back(path.String());
}
return (result == AZ::IO::ZipDir::ZD_ERROR_SUCCESS);
}
std::future<bool> ArchiveComponent::AddFileToArchive(
const AZStd::string& archivePath,
const AZStd::string& workingDirectory,
const AZStd::string& fileToAdd)
{
if (!CheckParamsForAdd(workingDirectory, fileToAdd))
{
std::promise<bool> p;
p.set_value(false);
return p.get_future();
}
auto FnAddFileToArchive = [this, archivePath, workingDirectory, fileToAdd](std::promise<bool>&& p) -> void
{
auto archive = m_archive->OpenArchive(archivePath);
if (!archive)
{
AZ_Error(s_traceName, false, "Failed to open archive file '%s'", archivePath.c_str());
p.set_value(false);
return;
}
AZ::IO::Path workingPath{ workingDirectory };
AZ::IO::Path fullPath = workingPath / fileToAdd;
AZ::IO::PathView relativePath = AZ::IO::PathView{ fullPath }.LexicallyRelative(workingPath);
AZStd::vector<char> fileBuffer;
bool success = false;
if (ArchiveUtils::ReadFile(fullPath, AZ::IO::OpenMode::ModeRead, fileBuffer))
{
int result = archive->UpdateFile(
relativePath.Native(), fileBuffer.data(), fileBuffer.size(), s_compressionMethod,
s_compressionLevel, s_compressionCodec);
success = (result == AZ::IO::ZipDir::ZD_ERROR_SUCCESS);
AZ_Error(
s_traceName, success, "Error %d encountered while adding '%s' to archive '%.*s'", result, fileToAdd.c_str(),
AZ_STRING_ARG(archive->GetFullPath().Native()));
}
else
{
AZ_Error(
s_traceName, false, "Error encountered while reading '%s' to add to archive '%.*s'", fileToAdd.c_str(),
AZ_STRING_ARG(archive->GetFullPath().Native()));
}
archive.reset();
p.set_value(success);
};
// Async task...
std::promise<bool> p;
std::future<bool> f = p.get_future();
AZStd::thread_desc threadDesc;
threadDesc.m_name = "Archive Task (Add Single)";
m_threads.emplace_back(threadDesc, FnAddFileToArchive, AZStd::move(p));
return f;
}
std::future<bool> ArchiveComponent::AddFilesToArchive(
const AZStd::string& archivePath,
const AZStd::string& workingDirectory,
const AZStd::string& listFilePath)
{
if (!CheckParamsForAdd(workingDirectory, listFilePath))
{
std::promise<bool> p;
p.set_value(false);
return p.get_future();
}
auto FnAddFilesToArchive = [this, archivePath, workingDirectory, listFilePath](std::promise<bool>&& p) -> void
{
auto archive = m_archive->OpenArchive(archivePath);
if (!archive)
{
AZ_Error(s_traceName, false, "Failed to open archive file '%s'", archivePath.c_str());
p.set_value(false);
return;
}
bool success = true; // starts true and turns false when any error is encountered.
AZ::IO::Path basePath{ workingDirectory };
auto PerLineCallback = [&success, &basePath, &archive](AZStd::string_view filePathLine) -> void
{
AZStd::vector<char> fileBuffer;
AZ::IO::Path fullPath = (basePath / filePathLine);
if (ArchiveUtils::ReadFile(fullPath, AZ::IO::OpenMode::ModeRead, fileBuffer))
{
int result = archive->UpdateFile(
filePathLine, fileBuffer.data(), fileBuffer.size(), s_compressionMethod,
s_compressionLevel, s_compressionCodec);
bool thisSuccess = (result == AZ::IO::ZipDir::ZD_ERROR_SUCCESS);
success = (success && thisSuccess);
AZ_Error(
s_traceName, thisSuccess, "Error %d encountered while adding '%.*s' to archive '%.*s'", result,
AZ_STRING_ARG(filePathLine), AZ_STRING_ARG(archive->GetFullPath().Native()));
}
else
{
AZ_Error(
s_traceName, false, "Error encountered while reading '%.*s' to add to archive '%.*s'", AZ_STRING_ARG(filePathLine),
AZ_STRING_ARG(archive->GetFullPath().Native()));
}
};
ArchiveUtils::ProcessFileList(listFilePath, PerLineCallback);
archive.reset();
p.set_value(success);
};
// Async task...
std::promise<bool> p;
std::future<bool> f = p.get_future();
AZStd::thread_desc threadDesc;
threadDesc.m_name = "Archive Task (Add)";
m_threads.emplace_back(threadDesc, FnAddFilesToArchive, AZStd::move(p));
return f;
}
bool ArchiveComponent::CheckParamsForAdd(const AZStd::string& directory, const AZStd::string& file)
{
if (!m_fileIO || !m_archive)
{
return false;
}
if (!m_fileIO->IsDirectory(directory.c_str()))
{
AZ_Error(
s_traceName, false, "Working directory '%s' is not a directory or doesn't exist!", directory.c_str());
return false;
}
if (!file.empty())
{
auto filePath = AZ::IO::Path{ directory } / file;
if (!m_fileIO->Exists(filePath.c_str()) || m_fileIO->IsDirectory(filePath.c_str()))
{
AZ_Error(s_traceName, false, "File list '%s' is a directory or doesn't exist!", filePath.c_str());
return false;
}
}
return true;
}
bool ArchiveComponent::CheckParamsForExtract(const AZStd::string& archive, const AZStd::string& directory)
{
if (!m_fileIO || !m_archive)
{
return false;
}
if (!m_fileIO->Exists(archive.c_str()))
{
AZ_Error(s_traceName, false, "Archive '%s' does not exist!", archive.c_str());
return false;
}
if (!m_fileIO->Exists(directory.c_str()))
{
if (!m_fileIO->CreatePath(directory.c_str()))
{
AZ_Error(s_traceName, false, "Failed to create destination directory '%s'", directory.c_str());
return false;
}
}
return true;
}
bool ArchiveComponent::CheckParamsForCreate(const AZStd::string& archive, const AZStd::string& directory)
{
if (!m_fileIO || !m_archive)
{
return false;
}
if (m_fileIO->Exists(archive.c_str()))
{
AZ_Error(s_traceName, false, "Archive file '%s' already exists, cannot create a new archive there!");
return false;
}
if (!m_fileIO->IsDirectory(directory.c_str()))
{
AZ_Error(s_traceName, false, "Directory '%s' is not a directory or doesn't exist!", directory.c_str());
return false;
}
return true;
}
} // namespace AzToolsFramework