#include "WindowsFileUtilityPrivatePCH.h" #include "WFUFolderWatchInterface.h" #include "WFUFileListInterface.h" #include "WindowsFileUtilityFunctionLibrary.h" //static TMAP definition TMap> UWindowsFileUtilityFunctionLibrary::Watchers = TMap>(); int TotalWatchers = 0; UWindowsFileUtilityFunctionLibrary::UWindowsFileUtilityFunctionLibrary(const class FObjectInitializer& PCIP) : Super(PCIP) { } #if PLATFORM_WINDOWS #include "AllowWindowsPlatformTypes.h" #include #include #pragma comment(lib, "Shlwapi.lib") bool UWindowsFileUtilityFunctionLibrary::DoesFileExist(const FString& FullPath) { return 0 != PathFileExistsW(*FullPath); } bool UWindowsFileUtilityFunctionLibrary::MoveFileTo(const FString& From, const FString& To) { //Using windows api return 0 != MoveFileW(*From, *To); } bool UWindowsFileUtilityFunctionLibrary::CreateDirectoryAt(const FString& FullPath) { //Using windows api return 0 != CreateDirectoryW(*FullPath, NULL); } bool UWindowsFileUtilityFunctionLibrary::DeleteFileAt(const FString& FullPath) { //Using windows api return 0 != DeleteFileW(*FullPath); } bool UWindowsFileUtilityFunctionLibrary::DeleteEmptyFolder(const FString& FullPath) { //Using windows api return 0 != RemoveDirectoryW(*FullPath); } bool IsSubPathOf(const FString& path, const FString& basePath) { return path.Contains(basePath); } //Dangerous function not recommended to be exposed to blueprint bool UWindowsFileUtilityFunctionLibrary::DeleteFolderRecursively(const FString& FullPath) { //Only allow user to delete folders sub-class to game folder if (!IsSubPathOf(FullPath, FPaths::ProjectDir())) { return false; } int len = _tcslen(*FullPath); TCHAR *pszFrom = new TCHAR[len + 2]; wcscpy_s(pszFrom, len + 2, *FullPath); pszFrom[len] = 0; pszFrom[len + 1] = 0; SHFILEOPSTRUCT fileop; fileop.hwnd = NULL; // no status display fileop.wFunc = FO_DELETE; // delete operation fileop.pFrom = pszFrom; // source file name as double null terminated string fileop.pTo = NULL; // no destination needed fileop.fFlags = FOF_NOCONFIRMATION | FOF_SILENT; // do not prompt the user fileop.fAnyOperationsAborted = FALSE; fileop.lpszProgressTitle = NULL; fileop.hNameMappings = NULL; int ret = SHFileOperation(&fileop); delete[] pszFrom; return (ret == 0); } void UWindowsFileUtilityFunctionLibrary::WatchFolder(const FString& FullPath, UObject* WatcherDelegate) { //Do we have an entry for this path? if (!Watchers.Contains(FullPath)) { //Make an entry TArray FreshList; Watchers.Add(FullPath, FreshList); Watchers[FullPath] = FreshList; } else { //if we do do we already watch from this object? TArray& PathWatchers = Watchers[FullPath]; for (auto Watcher : PathWatchers) { if (Watcher.Delegate == WatcherDelegate) { //Already accounted for UE_LOG(LogTemp, Warning, TEXT("UWindowsFileUtilityFunctionLibrary::WatchFolder Duplicate watcher ignored!")); return; } } } //Add to watchers FWatcher FreshWatcher; FreshWatcher.Delegate = WatcherDelegate; FreshWatcher.Path = FullPath; const FWatcher* WatcherPtr = &FreshWatcher; //fork this off to another process WFULambdaRunnable* Runnable = WFULambdaRunnable::RunLambdaOnBackGroundThread([FullPath, WatcherDelegate, WatcherPtr]() { UWindowsFileUtilityFunctionLibrary::WatchFolderOnBgThread(FullPath, WatcherPtr); }); FreshWatcher.Runnable = Runnable; TArray& PathWatchers = Watchers[FullPath]; PathWatchers.Add(FreshWatcher); } void UWindowsFileUtilityFunctionLibrary::StopWatchingFolder(const FString& FullPath, UObject* WatcherDelegate) { //Do we have an entry? if (!Watchers.Contains(FullPath)) { return; } //We have an entry for this path, remove our watcher TArray PathWatchers = Watchers[FullPath]; for (int i = 0; i < PathWatchers.Num();i++) { FWatcher& PathWatcher = PathWatchers[i]; if (PathWatcher.Delegate == WatcherDelegate) { //Stop the runnable PathWatcher.ShouldRun = false; PathWatcher.Runnable->Stop(); //Remove the watcher and we're done PathWatchers.RemoveAt(i); break; } } } void UWindowsFileUtilityFunctionLibrary::ListContentsOfFolder(const FString& FullPath, UObject* Delegate) { //Longer than max path? throw error if (FullPath.Len() > MAX_PATH) { UE_LOG(LogTemp, Warning, TEXT("UWindowsFileUtilityFunctionLibrary::ListContentsOfFolder Error, path too long, listing aborted.")); return; } WFULambdaRunnable* Runnable = WFULambdaRunnable::RunLambdaOnBackGroundThread([&FullPath, Delegate]() { WIN32_FIND_DATA ffd; LARGE_INTEGER filesize; HANDLE hFind = INVALID_HANDLE_VALUE; DWORD dwError = 0; FString SearchPath = FullPath + TEXT("\\*"); hFind = FindFirstFile(*SearchPath, &ffd); if (INVALID_HANDLE_VALUE == hFind) { UE_LOG(LogTemp, Warning, TEXT("UWindowsFileUtilityFunctionLibrary::ListContentsOfFolder Error, invalid handle, listing aborted.")); return; } //Arrays to hold full information on Done TArray FileNames; TArray FolderNames; //List loop, callback on game thread do { FString Name = FString(ffd.cFileName); FString ItemPath = FullPath + TEXT("\\") + Name; //UE_LOG(LogTemp, Log, TEXT("Name: <%s>"), *Name); if (Name.Equals(FString(TEXT("."))) || Name.Equals(FString(TEXT(".."))) ) { //ignore these first } //Folder else if (ffd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { FolderNames.Add(Name); WFULambdaRunnable::RunShortLambdaOnGameThread([Delegate, ItemPath, Name] { ((IWFUFileListInterface*)Delegate)->Execute_OnListDirectoryFound((UObject*)Delegate, Name, ItemPath); }); } //File else { FileNames.Add(Name); filesize.LowPart = ffd.nFileSizeLow; filesize.HighPart = ffd.nFileSizeHigh; int32 TruncatedFileSize = filesize.QuadPart; WFULambdaRunnable::RunShortLambdaOnGameThread([Delegate, ItemPath, Name, TruncatedFileSize] { ((IWFUFileListInterface*)Delegate)->Execute_OnListFileFound((UObject*)Delegate, Name, TruncatedFileSize, ItemPath); }); } } while (FindNextFile(hFind, &ffd) != 0); dwError = GetLastError(); if (dwError != ERROR_NO_MORE_FILES) { UE_LOG(LogTemp, Warning, TEXT("UWindowsFileUtilityFunctionLibrary::ListContentsOfFolder Error while listing.")); return; } FindClose(hFind); //Done callback with full list of names found WFULambdaRunnable::RunShortLambdaOnGameThread([Delegate, FullPath, FileNames, FolderNames] { ((IWFUFileListInterface*)Delegate)->Execute_OnListDone((UObject*)Delegate, FullPath, FileNames, FolderNames); }); }); } void UWindowsFileUtilityFunctionLibrary::ListContentsOfFolderToCallback(const FString& FullPath, TFunction&, const TArray&)> OnListCompleteCallback) { UWFUFileListLambdaDelegate* LambdaDelegate = NewObject(); LambdaDelegate->SetOnDoneCallback(OnListCompleteCallback); ListContentsOfFolder(FullPath, LambdaDelegate); } void UWindowsFileUtilityFunctionLibrary::WatchFolderOnBgThread(const FString& FullPath, const FWatcher* WatcherPtr) { //mostly from https://msdn.microsoft.com/en-us/library/windows/desktop/aa365261(v=vs.85).aspx //call the delegate when the folder changes //TODO: find out which file changed DWORD dwWaitStatus; HANDLE dwChangeHandles[2]; TCHAR lpDrive[4]; TCHAR lpFile[_MAX_FNAME]; TCHAR lpExt[_MAX_EXT]; //finding out about the notification FILE_NOTIFY_INFORMATION strFileNotifyInfo[1024]; DWORD dwBytesReturned = 0; _tsplitpath_s(*FullPath, lpDrive, 4, NULL, 0, lpFile, _MAX_FNAME, lpExt, _MAX_EXT); lpDrive[2] = (TCHAR)'\\'; lpDrive[3] = (TCHAR)'\0'; // Watch the directory for file creation and deletion. dwChangeHandles[0] = FindFirstChangeNotification( *FullPath, // directory to watch TRUE, // watch the subtree FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_CREATION | FILE_NOTIFY_CHANGE_ATTRIBUTES | FILE_NOTIFY_CHANGE_SIZE// watch for generic file changes ); // watch last write or file size change if (dwChangeHandles[0] == INVALID_HANDLE_VALUE) { UE_LOG(LogTemp, Warning, TEXT("\n ERROR: FindFirstChangeNotification function failed.\n")); //ExitProcess(GetLastError()); } // Watch the subtree for directory creation and deletion. dwChangeHandles[1] = FindFirstChangeNotification( lpDrive, // directory to watch TRUE, // watch the subtree FILE_NOTIFY_CHANGE_DIR_NAME); // watch dir name changes if (dwChangeHandles[1] == INVALID_HANDLE_VALUE) { UE_LOG(LogTemp, Warning, TEXT("\n ERROR: FindFirstChangeNotification function failed.\n")); //ExitProcess(GetLastError()); } // Make a final validation check on our handles. if ((dwChangeHandles[0] == NULL) || (dwChangeHandles[1] == NULL)) { UE_LOG(LogTemp, Warning, TEXT("\n ERROR: Unexpected NULL from FindFirstChangeNotification.\n")); //ExitProcess(GetLastError()); } const FString DrivePath = FString(lpDrive); FString FileString; FString DirectoryString; const UObject* WatcherDelegate = WatcherPtr->Delegate; //Wait while the runnable pointer hasn't been set TotalWatchers++; UE_LOG(LogTemp, Log, TEXT("\nStarting Watcher loop %d...\n"), TotalWatchers); while (WatcherPtr->ShouldRun) //Watcher.Runnable->Finished == false { // Wait for notification. //UE_LOG(LogTemp, Log, TEXT("\nWaiting for notification...\n")); dwWaitStatus = WaitForMultipleObjects(2, dwChangeHandles, FALSE, INFINITE); if (!WatcherPtr->ShouldRun) { UE_LOG(LogTemp, Log, TEXT("\nStop called while sleeping\n")); break; } if (!WatcherDelegate->IsValidLowLevel()) { UE_LOG(LogTemp, Warning, TEXT("\nInvalid Watcher Delegate, exiting watch\n")); break; } switch (dwWaitStatus) { case WAIT_OBJECT_0: ReadDirectoryChangesW(dwChangeHandles[0], (LPVOID)&strFileNotifyInfo, sizeof(strFileNotifyInfo), FALSE, FILE_NOTIFY_CHANGE_LAST_WRITE, &dwBytesReturned, NULL, NULL); //UE_LOG(LogTemp, Warning, TEXT("Received info about: %s"), strFileNotifyInfo->FileName); FileString = FString(strFileNotifyInfo[0].FileNameLength, strFileNotifyInfo[0].FileName); // A file was created, renamed, or deleted in the directory. // Refresh this directory and restart the notification. WFULambdaRunnable::RunShortLambdaOnGameThread([FullPath, FileString, WatcherDelegate]() { if (WatcherDelegate->GetClass()->ImplementsInterface(UWFUFolderWatchInterface::StaticClass())) { FString FilePath = FString::Printf(TEXT("%s\\%s"), *FullPath, *FileString); ((IWFUFolderWatchInterface*)WatcherDelegate)->Execute_OnFileChanged((UObject*)WatcherDelegate, FileString, FilePath); } }); if (FindNextChangeNotification(dwChangeHandles[0]) == FALSE) { UE_LOG(LogTemp, Warning, TEXT("\n ERROR: FindNextChangeNotification function failed.\n")); return; } break; case WAIT_OBJECT_0 + 1: // A directory was created, renamed, or deleted. // Refresh the tree and restart the notification. ReadDirectoryChangesW(dwChangeHandles[1], (LPVOID)&strFileNotifyInfo, sizeof(strFileNotifyInfo), FALSE, FILE_NOTIFY_CHANGE_LAST_WRITE, &dwBytesReturned, NULL, NULL); DirectoryString = FString(strFileNotifyInfo[0].FileNameLength, strFileNotifyInfo[0].FileName); WFULambdaRunnable::RunShortLambdaOnGameThread([FullPath, WatcherDelegate, DirectoryString]() { if (WatcherDelegate->GetClass()->ImplementsInterface(UWFUFolderWatchInterface::StaticClass())) { FString ChangedDirectoryPath = FString::Printf(TEXT("%s\\%s"), *FullPath, *DirectoryString); ((IWFUFolderWatchInterface*)WatcherDelegate)->Execute_OnDirectoryChanged((UObject*)WatcherDelegate, DirectoryString, ChangedDirectoryPath); } }); if (FindNextChangeNotification(dwChangeHandles[1]) == FALSE) { UE_LOG(LogTemp, Warning, TEXT("\n ERROR: FindNextChangeNotification function failed.\n")); return; } break; case WAIT_TIMEOUT: // A timeout occurred, this would happen if some value other // than INFINITE is used in the Wait call and no changes occur. // In a single-threaded environment you might not want an // INFINITE wait. UE_LOG(LogTemp, Warning, TEXT("\nNo changes in the timeout period.\n")); break; default: UE_LOG(LogTemp, Warning, TEXT("\n ERROR: Unhandled dwWaitStatus.\n")); return; break; } } TotalWatchers--; UE_LOG(LogTemp, Log, TEXT("\n Watcher loop stopped, total now: %d.\n"), TotalWatchers); } #include "HideWindowsPlatformTypes.h" #endif