Files
UnrealEngine/Engine/Source/Editor/UnrealEd/Private/PackageBackup.cpp
2025-05-18 13:04:45 +08:00

350 lines
14 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
/*=============================================================================
PackageBackup.cpp: Utility class for backing up a package.
=============================================================================*/
#include "PackageBackup.h"
#include "CoreGlobals.h"
#include "HAL/FileManager.h"
#include "Internationalization/Internationalization.h"
#include "Misc/AssertionMacros.h"
#include "Misc/CString.h"
#include "Misc/ConfigCacheIni.h"
#include "Misc/DateTime.h"
#include "Misc/FeedbackContext.h"
#include "Misc/PackageName.h"
#include "Misc/Paths.h"
#include "Misc/Timespan.h"
#include "UObject/ObjectMacros.h"
#include "UObject/Package.h"
/**
* Helper struct to hold information on backup files to prevent redundant checks
*/
struct FBackupFileInfo
{
int64 FileSize; /** Size of the file */
FString FileName; /** Name of the file */
FDateTime FileTimeStamp; /** Timestamp of the file */
};
/**
* Create a backup of the specified package. A backup is only created if the specified
* package meets specific criteria, as outlined in the comments for ShouldBackupPackage().
*
* @param InPackage Package which should be backed up
*
* @see FAutoPackageBackup::ShouldBackupPackage()
*
* @return true if the package was successfully backed up; false if it was not
*/
bool FAutoPackageBackup::BackupPackage( const UPackage& InPackage )
{
bool bPackageBackedUp = false;
FString OriginalFileName;
// Check if the package is valid for being backed up
if ( ShouldBackupPackage( InPackage, OriginalFileName ) )
{
GWarn->StatusUpdate( -1, -1, NSLOCTEXT("UnrealEd", "PackageBackup_Warning", "Backing up asset...") );
// Construct the backup file name by appending a timestamp in between the base file name and extension
FString DestinationFileName = GetBackupDirectory() / FPaths::GetBaseFilename(OriginalFileName);
DestinationFileName += TEXT("_");
DestinationFileName += FDateTime::Now().ToString(TEXT("%Y-%m-%d-%H-%M-%S"));
DestinationFileName += FPaths::GetExtension( OriginalFileName, true );
// Copy the file to the backup file name
IFileManager::Get().Copy( *DestinationFileName, *OriginalFileName );
bPackageBackedUp = true;
}
return bPackageBackedUp;
}
/**
* Create a backup of the specified packages. A backup is only created if a specified
* package meets specific criteria, as outlined in the comments for ShouldBackupPackage().
*
* @param InPackage Package which should be backed up
*
* @see FAutoPackageBackup::ShouldBackupPackage()
*
* @return true if all provided packages were successfully backed up; false if one or more
* were not
*/
bool FAutoPackageBackup::BackupPackages( const TArray<UPackage*>& InPackages )
{
bool bAllPackagesBackedUp = true;
for ( TArray<UPackage*>::TConstIterator PackageIter( InPackages ); PackageIter; ++PackageIter )
{
UPackage* CurPackage = *PackageIter;
check( CurPackage );
if ( !BackupPackage( *CurPackage ) )
{
bAllPackagesBackedUp = false;
}
}
return bAllPackagesBackedUp;
}
/**
* Helper function designed to determine if the provided package should be backed up or not.
* The function checks for many conditions, such as if the package is too large to backup,
* if the package has a particular attribute that should prevent it from being backed up (such
* as being marked for PIE-use), if cooking is in progress, etc.
*
* @param InPackage Package which should be checked to see if its valid for backing-up
* @param OutFileName File name of the package on disk if the function determines the package
* already existed
*
* @return true if the package is valid for backing-up; false otherwise
*/
bool FAutoPackageBackup::ShouldBackupPackage( const UPackage& InPackage, FString& OutFilename )
{
// Check various conditions to see if the package is a valid candidate for backing up
bool bShouldBackup =
GIsEditor // Backing up packages only makes sense in the editor
&& !IsRunningCommandlet() // Don't backup saves resulting from commandlets
&& IsPackageBackupEnabled() // Ensure that the package backup is enabled in the first place
&& (InPackage.HasAnyPackageFlags(PKG_PlayInEditor) == false) // Don't back up PIE packages
&& (InPackage.HasAnyPackageFlags(PKG_ContainsScript) == false); // Don't back up script packages
if (!bShouldBackup)
{
// Early out here to avoid the call to FileSize below which can be expensive on slower hard drives
return false;
}
if( bShouldBackup )
{
GWarn->StatusUpdate( -1, -1, NSLOCTEXT("UnrealEd", "PackageBackup_ValidityWarning", "Determining asset backup validity...") );
bShouldBackup = FPackageName::DoesPackageExist( InPackage.GetName(), &OutFilename ); // Make sure the file already exists (no sense in backing up a new package)
}
// If the package passed the initial backup checks, proceed to check more specific conditions
// that might disqualify the package from being backed up
const int64 FileSizeOfBackup = IFileManager::Get().FileSize( *OutFilename );
if ( bShouldBackup )
{
// Ensure that the size the backup would require is less than that of the maximum allowed
// space for backups
bShouldBackup = FileSizeOfBackup <= GetMaxAllowedBackupSpace();
}
// If all of the prior checks have passed, now see if the package has been backed up
// too recently to be considered for an additional backup
if ( bShouldBackup )
{
// Ensure that the autosave/backup directory exists
const FString& BackupSaveDir = GetBackupDirectory();
IFileManager::Get().MakeDirectory( *BackupSaveDir, 1 );
// Find all of the files in the backup directory
TArray<FString> FilesInBackupDir;
IFileManager::Get().FindFilesRecursive(FilesInBackupDir, *BackupSaveDir, TEXT("*.*"), true, false);
// Extract the base file name and extension from the passed-in package file name
FString ExistingBaseFileName = FPaths::GetBaseFilename(OutFilename);
FString ExistingFileNameExtension = FPaths::GetExtension(OutFilename);
bool bFoundExistingBackup = false;
int64 DirectorySize = 0;
FDateTime LastBackupTimeStamp = FDateTime::MinValue();
TArray<FBackupFileInfo> BackupFileInfoArray;
// Check every file in the backup directory for matches against the passed-in package
// (Additionally keep statistics on all backup files for potential maintenance)
for ( TArray<FString>::TConstIterator FileIter( FilesInBackupDir ); FileIter; ++FileIter )
{
const FString CurBackupFileName = FString( *FileIter );
// Create a new backup file info struct for keeping information about each backup file
const int32 FileInfoIndex = BackupFileInfoArray.AddZeroed();
FBackupFileInfo& CurBackupFileInfo = BackupFileInfoArray[ FileInfoIndex ];
// Record the backup file's name, size, and timestamp
CurBackupFileInfo.FileName = CurBackupFileName;
CurBackupFileInfo.FileSize = IFileManager::Get().FileSize( *CurBackupFileName );
// If we failed to get a timestamp or a valid size, something has happened to the file and it shouldn't be considered
CurBackupFileInfo.FileTimeStamp = IFileManager::Get().GetTimeStamp(*CurBackupFileName);
if (CurBackupFileInfo.FileTimeStamp == FDateTime::MinValue() || CurBackupFileInfo.FileSize == -1)
{
BackupFileInfoArray.RemoveAt( BackupFileInfoArray.Num() - 1 );
continue;
}
// Calculate total directory size by adding the size of this backup file
DirectorySize += CurBackupFileInfo.FileSize;
FString CurBackupBaseFileName = FPaths::GetBaseFilename(CurBackupFileName);
FString CurBackupFileNameExtension = FPaths::GetExtension(CurBackupFileName);
// The base file name of the backup file is going to include an underscore followed by a timestamp, so they must be removed for comparison's sake
CurBackupBaseFileName.LeftInline( CurBackupBaseFileName.Find( TEXT("_"), ESearchCase::CaseSensitive, ESearchDir::FromEnd ), EAllowShrinking::No);
// If the base file names and extensions match, we've found a backup
if ( CurBackupBaseFileName == ExistingBaseFileName && CurBackupFileNameExtension == ExistingFileNameExtension )
{
bFoundExistingBackup = true;
// Keep track of the most recent matching time stamp so we can check if the passed-in package
// has been backed up too recently
if ( CurBackupFileInfo.FileTimeStamp > LastBackupTimeStamp )
{
LastBackupTimeStamp = CurBackupFileInfo.FileTimeStamp;
}
}
}
// If there was an existing backup, check to see if it was created too recently to allow another backup
if ( bFoundExistingBackup )
{
// Check the difference in timestamp seconds against the backup interval; if not enough time has elapsed since
// the last backup, we don't want to make another one
if ((FDateTime::UtcNow() - LastBackupTimeStamp).GetTotalSeconds() < GetBackupInterval())
{
bShouldBackup = false;
}
}
// If every other check against the package has succeeded for backup purposes, ensure there is enough directory space
// available in the backup directory, as adding the new backup might use more space than the user allowed for backups.
// If the backup file size + the current directory size exceeds the max allowed space, deleted old backups until there
// is sufficient space. If enough space can't be freed for whatever reason, then no back-up will be created.
if ( bShouldBackup && ( FileSizeOfBackup + DirectorySize > GetMaxAllowedBackupSpace() ) )
{
bShouldBackup = PerformBackupSpaceMaintenance( BackupFileInfoArray, DirectorySize, FileSizeOfBackup );
}
}
return bShouldBackup;
}
/**
* Helper function that returns whether the user has package backups enabled or not. The value
* is determined by a configuration INI setting.
*
* @return true if package backups are enabled; false otherwise
*/
bool FAutoPackageBackup::IsPackageBackupEnabled()
{
bool bEnabled = false;
GConfig->GetBool( TEXT("FAutoPackageBackup"), TEXT("Enabled"), bEnabled, GEditorPerProjectIni );
return bEnabled;
}
/**
* Helper function that returns the maximum amount of space the user has designated to allow for
* package backups. This value is determined by a configuration INI setting.
*
* @return The maximum amount of space allowed, in bytes
*/
int32 FAutoPackageBackup::GetMaxAllowedBackupSpace()
{
int32 MaxSpaceAllowed = 0;
if ( GConfig->GetInt( TEXT("FAutoPackageBackup"), TEXT("MaxAllowedSpaceInMB"), MaxSpaceAllowed, GEditorPerProjectIni ) )
{
// Convert the user stored value from megabytes to bytes; <<= 20 is the same as *= 1024 * 1024
MaxSpaceAllowed <<= 20;
}
return MaxSpaceAllowed;
}
/**
* Helper function that returns the time in between backups of a package before another backup of
* the same package should be considered valid. This value is determined by a configuration INI setting,
* and prevents a package from being backed-up over and over again in a small time frame.
*
* @return The interval to wait before allowing another backup of the same package, in seconds
*/
int32 FAutoPackageBackup::GetBackupInterval()
{
int32 BackupInterval = 0;
if ( GConfig->GetInt( TEXT("FAutoPackageBackup"), TEXT("BackupIntervalInMinutes"), BackupInterval, GEditorPerProjectIni ) )
{
// Convert the user stored value from minutes to seconds
BackupInterval *= 60;
}
return BackupInterval;
}
/**
* Helper function that returns the directory to store package backups in.
*
* @return String containing the directory to store package backups in.
*/
FString FAutoPackageBackup::GetBackupDirectory()
{
FString Directory = FPaths::ProjectSavedDir() / TEXT("Backup");
return Directory;
}
/**
* Deletes old backed-up package files until the provided amount of space (in bytes)
* is available to use in the backup directory. Fails if the provided amount of space
* is more than the amount of space the user has allowed for backups or if enough space
* could not be made.
*
* @param InBackupFiles File info of the files in the backup directory
* @param InSpaceUsed The amount of space, in bytes, the files in the provided array take up
* @param InSpaceRequired The amount of space, in bytes, to assure is available
* for the purposes of package backups
*
* @return true if the space was successfully provided, false if not or if the requested space was
* greater than the max allowed space by the user
*/
bool FAutoPackageBackup::PerformBackupSpaceMaintenance( TArray<FBackupFileInfo>& InBackupFiles, int64 InSpaceUsed, int64 InSpaceRequired )
{
bool bSpaceFreed = false;
const int32 MaxAllowedSpace = GetMaxAllowedBackupSpace();
// We can only free up enough space if the required space is less than the maximum allowed space to begin with
if ( InSpaceRequired < MaxAllowedSpace )
{
GWarn->StatusUpdate( -1, -1, NSLOCTEXT("UnrealEd", "PackageBackup_MaintenanceWarning", "Performing maintenance on asset backup folder...") );
// Sort the backup files in order of their timestamps; we want to naively delete the oldest files first
struct FCompareFBackupFileInfo
{
FORCEINLINE bool operator()( const FBackupFileInfo& A, const FBackupFileInfo& B ) const
{
return A.FileTimeStamp < B.FileTimeStamp;
}
};
InBackupFiles.Sort( FCompareFBackupFileInfo() );
int64 CurSpaceUsed = InSpaceUsed;
TArray<FBackupFileInfo>::TConstIterator BackupFileIter( InBackupFiles );
// Iterate through the backup files until all of the files have been deleted or enough space has been freed
while ( ( InSpaceRequired + CurSpaceUsed > MaxAllowedSpace ) && BackupFileIter )
{
const FBackupFileInfo& CurBackupFileInfo = *BackupFileIter;
// Delete the file; this could potentially fail, but not because of a read-only flag, so if it fails
// it's likely because the file was removed by the user
IFileManager::Get().Delete( *CurBackupFileInfo.FileName, true, true );
CurSpaceUsed -= CurBackupFileInfo.FileSize;
++BackupFileIter;
}
if ( InSpaceRequired + CurSpaceUsed <= MaxAllowedSpace )
{
bSpaceFreed = true;
}
}
return bSpaceFreed;
}