Files
UnrealEngine/Engine/Plugins/Runtime/ChunkDownloader/Source/Private/Download.cpp
2025-05-18 13:04:45 +08:00

238 lines
6.8 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "Download.h"
#include "ChunkDownloaderLog.h"
#include "HAL/FileManager.h"
#include "HAL/PlatformFile.h"
#include "Interfaces/IHttpResponse.h"
#define LOCTEXT_NAMESPACE "ChunkDownloader"
FDownload::FDownload(const TSharedRef<FChunkDownloader>& DownloaderIn, const TSharedRef<FChunkDownloader::FPakFile>& PakFileIn)
: Downloader(DownloaderIn)
, PakFile(PakFileIn)
, TargetFile(Downloader->CacheFolder / PakFileIn->Entry.FileName)
{
// couple of sanity checks for our flags
check(!PakFile->bIsCached);
check(!PakFile->bIsEmbedded);
check(!PakFile->bIsMounted);
}
FDownload::~FDownload()
{
}
void FDownload::Start()
{
check(!bHasCompleted);
// check to make sure we have enough space for this download
if (!HasDeviceSpaceRequired())
{
TWeakPtr<FDownload> WeakThisPtr = AsShared();
FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateLambda([WeakThisPtr](float Unused) {
TSharedPtr<FDownload> SharedThis = WeakThisPtr.Pin();
if (SharedThis.IsValid())
{
SharedThis->OnCompleted(false, LOCTEXT("NotEnoughSpace", "Not enough space on device."));
}
return false;
}), 1.0);
return;
}
// try to download from the CDN
StartDownload(0);
}
void FDownload::Cancel(bool bResult)
{
check(!bHasCompleted);
UE_LOG(LogChunkDownloader, Warning, TEXT("Canceling download of '%s'. result=%s"), *PakFile->Entry.FileName, bResult ? TEXT("true") : TEXT("false"));
// cancel the platform specific file download
if (!bIsCancelled)
{
bIsCancelled = true;
CancelCallback();
}
// fire the completion results
OnCompleted(bResult, FText::Format(LOCTEXT("DownloadCanceled", "Download of '%s' was canceled."), FText::FromString(PakFile->Entry.FileName)));
}
void FDownload::UpdateFileSize()
{
IFileManager& FileManager = IFileManager::Get();
int64 FileSizeOnDisk = FileManager.FileSize(*TargetFile);
PakFile->SizeOnDisk = (FileSizeOnDisk > 0) ? (uint64)FileSizeOnDisk : 0;
}
bool FDownload::ValidateFile() const
{
if (PakFile->SizeOnDisk != PakFile->Entry.FileSize)
{
UE_LOG(LogChunkDownloader, Error, TEXT("Size mismatch. Expected %llu, got %llu"), PakFile->Entry.FileSize, PakFile->SizeOnDisk);
return false;
}
if (PakFile->Entry.FileVersion.StartsWith(TEXT("SHA1:")))
{
// check the sha1 hash
if (!FChunkDownloader::CheckFileSha1Hash(TargetFile, PakFile->Entry.FileVersion))
{
UE_LOG(LogChunkDownloader, Error, TEXT("Checksum mismatch. Expected %s"), *PakFile->Entry.FileVersion);
return false;
}
}
return true;
}
bool FDownload::HasDeviceSpaceRequired() const
{
uint64 TotalDiskSpace = 0;
uint64 TotalDiskFreeSpace = 0;
if (FPlatformMisc::GetDiskTotalAndFreeSpace(Downloader->CacheFolder, TotalDiskSpace, TotalDiskFreeSpace))
{
uint64 BytesNeeded = PakFile->Entry.FileSize - PakFile->SizeOnDisk;
if (TotalDiskFreeSpace < BytesNeeded)
{
// not enough space
UE_LOG(LogChunkDownloader, Warning, TEXT("Unable to download '%s'. Needed %llu bytes had %llu bytes free (of %llu bytes)"),
*PakFile->Entry.FileName, PakFile->Entry.FileSize, TotalDiskFreeSpace, TotalDiskSpace);
return false;
}
}
return true;
}
void FDownload::StartDownload(int TryNumber)
{
// only handle completion once
check(!bHasCompleted);
BeginTime = FDateTime::UtcNow();
OnDownloadProgress(0);
// download the next url
check(Downloader->BuildBaseUrls.Num() > 0);
FString Url = Downloader->BuildBaseUrls[TryNumber % Downloader->BuildBaseUrls.Num()] / PakFile->Entry.RelativeUrl;
UE_LOG(LogChunkDownloader, Log, TEXT("Downloading %s from %s"), *PakFile->Entry.FileName, *Url);
TWeakPtr<FDownload> WeakThisPtr = AsShared();
CancelCallback = PlatformStreamDownload(Url, TargetFile, [WeakThisPtr](int32 BytesReceived) {
TSharedPtr<FDownload> SharedThis = WeakThisPtr.Pin();
if (SharedThis.IsValid() && !SharedThis->bHasCompleted)
{
SharedThis->OnDownloadProgress(BytesReceived);
}
}, [WeakThisPtr, TryNumber, Url](int32 HttpStatus) {
TSharedPtr<FDownload> SharedThis = WeakThisPtr.Pin();
if (SharedThis.IsValid() && !SharedThis->bHasCompleted)
{
SharedThis->OnDownloadComplete(Url, TryNumber, HttpStatus);
}
});
}
void FDownload::OnDownloadProgress(int32 BytesReceived)
{
Downloader->LoadingModeStats.BytesDownloaded -= LastBytesReceived;
LastBytesReceived = BytesReceived;
Downloader->LoadingModeStats.BytesDownloaded += LastBytesReceived;
}
void FDownload::OnDownloadComplete(const FString& Url, int TryNumber, int32 HttpStatus)
{
// only handle completion once
check(!bHasCompleted);
// update file size on disk
UpdateFileSize();
// report analytics
if (Downloader->OnDownloadAnalytics)
{
Downloader->OnDownloadAnalytics(PakFile->Entry.FileName, Url, PakFile->SizeOnDisk, FDateTime::UtcNow() - BeginTime, HttpStatus);
}
// handle success
if (EHttpResponseCodes::IsOk(HttpStatus))
{
// make sure the file is complete
if (ValidateFile())
{
PakFile->bIsCached = true;
OnCompleted(true, FText());
return;
}
// if we fail validation, delete the file and start over
UE_LOG(LogChunkDownloader, Error, TEXT("%s from %s failed validation"), *TargetFile, *Url);
IPlatformFile::GetPlatformPhysical().DeleteFile(*TargetFile);
}
// check again to make sure we have enough space for this download
if (!HasDeviceSpaceRequired())
{
OnCompleted(false, LOCTEXT("NotEnoughSpace", "Not enough space on device."));
return;
}
// compute delay before re-starting download
float SecondsToDelay = (TryNumber + 1) * 5.0f;
if (SecondsToDelay > 60)
{
SecondsToDelay = 60;
}
// set a ticker to delay
UE_LOG(LogChunkDownloader, Log, TEXT("Will re-attempt to download %s in %f seconds"), *PakFile->Entry.FileName, SecondsToDelay);
TWeakPtr<FDownload> WeakThisPtr = AsShared();
FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateLambda([WeakThisPtr, TryNumber](float Unused) {
TSharedPtr<FDownload> SharedThis = WeakThisPtr.Pin();
if (SharedThis.IsValid() && !SharedThis->bHasCompleted)
{
SharedThis->StartDownload(TryNumber + 1);
}
return false;
}), SecondsToDelay);
}
void FDownload::OnCompleted(bool bSuccess, const FText& ErrorText)
{
// make sure we don't complete more than once
check(!bHasCompleted);
bHasCompleted = true;
// increment files downloaded
OnDownloadProgress(bSuccess ? PakFile->SizeOnDisk : 0);
++Downloader->LoadingModeStats.FilesDownloaded;
if (!bSuccess && !ErrorText.IsEmpty())
{
Downloader->LoadingModeStats.LastError = ErrorText;
}
// queue up callbacks
for (const auto& Callback : PakFile->PostDownloadCallbacks)
{
Downloader->ExecuteNextTick(Callback, bSuccess);
}
PakFile->PostDownloadCallbacks.Empty();
// remove from download requests
if (ensure(Downloader->DownloadRequests.RemoveSingle(PakFile) > 0))
{
Downloader->IssueDownloads();
}
// unhook from pak file (this may delete us)
if (PakFile->Download.Get() == this)
{
PakFile->Download.Reset();
}
}
#undef LOCTEXT_NAMESPACE