diff --git a/source/components/DriveView.cpp b/source/components/DriveView.cpp index 07142ee35..5aa36b232 100644 --- a/source/components/DriveView.cpp +++ b/source/components/DriveView.cpp @@ -3,9 +3,16 @@ #pragma hdrstop #include +#include +#include +#include "CoreMain.h" #include "DriveView.h" #include "IEDriveInfo.h" #include "DirView.h" +#ifdef DESIGN_ONLY +#undef AppLogFmt +#define AppLogFmt(S, F) +#endif //--------------------------------------------------------------------------- #pragma package(smart_init) //--------------------------------------------------------------------------- @@ -73,10 +80,16 @@ void __fastcall TDriveView::CreateWnd() void __fastcall TDriveView::ReadSubDirs(TTreeNode * Node) { auto NodeData = static_cast(Node->Data); + TDateTime Start = Now(); UnicodeString Path = NodePath(Node); TSearchRec SRec; if (!FindFirstSubDir(IncludeTrailingBackslash(Path) + L"*.*", SRec)) { + int Sec = static_cast(SecondsBetween(Now(), Start)); + if (Sec > 2) + { + AppLogFmt(L"Took %d s to find that '%s' cannot be read", (Sec, Path)); + } Node->HasChildren = false; } else @@ -88,7 +101,7 @@ void __fastcall TDriveView::ReadSubDirs(TTreeNode * Node) Limit /= 10; CheckInterval /= 10; } - if (!ReadSubDirsBatch(Node, SRec, CheckInterval, Limit)) + if (!ReadSubDirsBatch(Node, SRec, CheckInterval, Limit, Start)) { NodeData->DelayedSrec = SRec; NodeData->DelayedExclude = new TStringList(); @@ -108,7 +121,8 @@ void __fastcall TDriveView::ReadSubDirs(TTreeNode * Node) //--------------------------------------------------------------------------- bool __fastcall TDriveView::DoScanDir(TTreeNode * FromNode) { - return !static_cast(FromNode->Data)->IsRecycleBin; + auto NodeData = static_cast(FromNode->Data); + return !NodeData->IsRecycleBin && !NodeData->IsRemote; } //--------------------------------------------------------------------------- void __fastcall TDriveView::AddChildNode(TTreeNode * ParentNode, UnicodeString ParentPath, const TSearchRec & SRec) @@ -116,19 +130,32 @@ void __fastcall TDriveView::AddChildNode(TTreeNode * ParentNode, UnicodeString P auto NodeData = new TNodeData(); NodeData->Attr = SRec.Attr; NodeData->DirName = SRec.Name; + UnicodeString Path = IncludeTrailingBackslash(ParentPath) + SRec.Name; NodeData->IsRecycleBin = FLAGSET(SRec.Attr, faSysFile) && (ParentNode->Parent == nullptr) && (SameText(SRec.Name, L"RECYCLED") || SameText(SRec.Name, L"RECYCLER") || SameText(SRec.Name, L"$RECYCLE.BIN")); + + #ifndef DESIGN_ONLY + if (FLAGSET(SRec.Attr, faSymLink)) + { + String Target; + NodeData->IsRemote = + FileGetSymLinkTargetSafe(Path, Target) && + TPath::IsUNCRooted(Target); + } + #endif + NodeData->Scanned = false; TTreeNode * NewNode = Items->AddChildObject(ParentNode, EmptyStr, NodeData); NewNode->Text = GetDisplayName(NewNode); NewNode->HasChildren = true; - if (GetDriveTypetoNode(ParentNode) != DRIVE_REMOTE) + if ((GetDriveTypetoNode(ParentNode) != DRIVE_REMOTE) && + !NodeData->IsRemote) { - FSubDirReaderThread->Add(NewNode, IncludeTrailingBackslash(ParentPath) + SRec.Name); + FSubDirReaderThread->Add(NewNode, Path); } } diff --git a/source/core/Common.cpp b/source/core/Common.cpp index 0d0ab2292..e39ce764b 100644 --- a/source/core/Common.cpp +++ b/source/core/Common.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #pragma clang diagnostic push @@ -1057,6 +1058,7 @@ bool __fastcall IsReservedName(UnicodeString FileName) // Inspired by // https://stackoverflow.com/q/18580945/850848 // This can be reimplemented using PathCchCanonicalizeEx on Windows 8 and later +// There's also TPath.GetPosAfterExtendedPrefix. enum PATH_PREFIX_TYPE { PPT_UNKNOWN, @@ -1589,6 +1591,73 @@ void __fastcall ProcessLocalDirectory(UnicodeString DirName, } } //--------------------------------------------------------------------------- +// Similar structure might be a part of WINAPI eventually (VCLCOPY - sic) +struct REPARSE_DATA_BUFFER +{ + ULONG ReparseTag; + USHORT ReparseDataLength; + USHORT Reserved; + union + { + struct + { + USHORT SubstituteNameOffset; + USHORT SubstituteNameLength; + USHORT PrintNameOffset; + USHORT PrintNameLength; + ULONG Flags; + WCHAR PathBuffer[1]; + } SymbolicLink; + struct + { + USHORT SubstituteNameOffset; + USHORT SubstituteNameLength; + USHORT PrintNameOffset; + USHORT PrintNameLength; + WCHAR PathBuffer[1]; + } MountPoint; + } ReparseBuffer; +}; +//--------------------------------------------------------------------------- +// Contrary to VCL's FileGetSymLinkTarget, this does not try to open the target folder and does not throw an exception. +// Also it would call CreateFile without FILE_FLAG_OPEN_REPARSE_POINT, what also tries to access the remote directory, +// potentially stalling. And with FILE_FLAG_OPEN_REPARSE_POINT, the GetFinalPathNameByHandle does not work. +bool FileGetSymLinkTargetSafe(const String & FileName, String & Target) +{ + bool Result = false; + HANDLE Handle = + CreateFile( + FileName.c_str(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, NULL); + if (Handle != INVALID_HANDLE_VALUE) + { + DWORD bytesReturned = 0; + char Buffer[MAXIMUM_REPARSE_DATA_BUFFER_SIZE]; + auto Reparse = reinterpret_cast(Buffer); + if (DeviceIoControl(Handle, FSCTL_GET_REPARSE_POINT, NULL, 0, Reparse, sizeof(Buffer), &bytesReturned, NULL)) + { + BYTE * NamePtr = nullptr; + USHORT NameSize = 0; + if (Reparse->ReparseTag == IO_REPARSE_TAG_SYMLINK) + { + NamePtr = reinterpret_cast(Reparse->ReparseBuffer.SymbolicLink.PathBuffer) + Reparse->ReparseBuffer.SymbolicLink.PrintNameOffset; + NameSize = Reparse->ReparseBuffer.SymbolicLink.PrintNameLength; + } + else if (Reparse->ReparseTag == IO_REPARSE_TAG_MOUNT_POINT) + { + NamePtr = reinterpret_cast(Reparse->ReparseBuffer.MountPoint.PathBuffer) + Reparse->ReparseBuffer.MountPoint.PrintNameOffset; + NameSize = Reparse->ReparseBuffer.MountPoint.PrintNameLength; + } + if (NamePtr != nullptr) + { + Target = String(reinterpret_cast(NamePtr), NameSize / sizeof(wchar_t)); + Result = true; + } + } + } + return Result; +} +//--------------------------------------------------------------------------- int __fastcall FileGetAttrFix(const UnicodeString & FileName) { // Already called with ApiPath @@ -1610,6 +1679,11 @@ int __fastcall FileGetAttrFix(const UnicodeString & FileName) // On Samba, InternalGetFileNameFromSymLink fails and returns true but empty target. // That confuses FileGetAttr, which returns attributes of the parent folder instead. // Using FileGetSymLinkTarget solves the problem, as it returns false. + + // Might want to use FileGetSymLinkTargetSafe here for consistency, + // but flaws/limitations of VCL FileGetSymLinkTarget probably does not matter here. + // The FileGetSymLinkTarget does not return the target when it cannot open it. + // But then GetFileAttributes would fail anyway too, so we return -1 is either case. if (!FileGetSymLinkTarget(FileName, TargetName)) { // FileGetAttr would return faInvalid (-1), but we want to allow an upload from Samba, diff --git a/source/core/Common.h b/source/core/Common.h index 7b763ecdb..87b3f28da 100644 --- a/source/core/Common.h +++ b/source/core/Common.h @@ -269,6 +269,7 @@ int __fastcall FindNextChecked(TSearchRecChecked & F); int __fastcall FindNextUnchecked(TSearchRecChecked & F); void __fastcall ProcessLocalDirectory(UnicodeString DirName, TProcessLocalFileEvent CallBackFunc, void * Param = NULL, int FindAttrs = -1); +bool FileGetSymLinkTargetSafe(const UnicodeString & FileName, UnicodeString & Target); int __fastcall FileGetAttrFix(const UnicodeString & FileName); //--------------------------------------------------------------------------- extern const wchar_t * DSTModeNames; diff --git a/source/packages/filemng/DriveViewInt.pas b/source/packages/filemng/DriveViewInt.pas index 90ec62b18..8d08c3be4 100644 --- a/source/packages/filemng/DriveViewInt.pas +++ b/source/packages/filemng/DriveViewInt.pas @@ -80,6 +80,7 @@ type FScanned: Boolean; FData: Pointer; FIsRecycleBin: Boolean; + FIsRemote: Boolean; FIconEmpty: Boolean; FSchedule: TSubDirReaderSchedule; @@ -95,6 +96,7 @@ type property Scanned: Boolean read FScanned write FScanned; property Data: Pointer read FData write FData; property IsRecycleBin: Boolean read FIsRecycleBin write FIsRecycleBin; + property IsRemote: Boolean read FIsRemote write FIsRemote; property IconEmpty: Boolean read FIconEmpty write FIconEmpty; property Schedule: TSubDirReaderSchedule read FSchedule write FSchedule; end; @@ -183,7 +185,7 @@ type procedure ReadSubDirs(Node: TTreeNode); virtual; abstract; procedure CancelDelayedNode(Node: TTreeNode); procedure DelayedNodeTimer(Sender: TObject); - function ReadSubDirsBatch(Node: TTreeNode; var SRec: TSearchRec; CheckInterval, Limit: Integer): Boolean; + function ReadSubDirsBatch(Node: TTreeNode; var SRec: TSearchRec; CheckInterval, Limit: Integer; Start: TDateTime): Boolean; procedure UpdateDelayedNodeTimer; function DoSearchSubDirs( ParentNode: TTreeNode; Path: string; Level: Integer; ExistingOnly: Boolean; @@ -436,6 +438,7 @@ begin FScanned := False; FDirName := ''; FIsRecycleBin := False; + FIsRemote := False; FIconEmpty := True; FSchedule := nil; DelayedExclude := nil; @@ -785,6 +788,7 @@ begin NewData.Scanned := SourceData.Scanned; NewData.Data := SourceData.Data; NewData.FIsRecycleBin := SourceData.FIsRecycleBin; + NewData.FIsRemote := SourceData.FIsRemote; NewData.IconEmpty := SourceData.IconEmpty; TTreeNode(Source).Data := NewData; end; @@ -1853,15 +1857,13 @@ begin Result := (FindNext(SRec) = 0) and GetSubDir(SRec); end; -function TDriveViewInt.ReadSubDirsBatch(Node: TTreeNode; var SRec: TSearchRec; CheckInterval, Limit: Integer): Boolean; +function TDriveViewInt.ReadSubDirsBatch(Node: TTreeNode; var SRec: TSearchRec; CheckInterval, Limit: Integer; Start: TDateTime): Boolean; var - Start: TDateTime; Cont: Boolean; Path: string; Count: Integer; DelayedExclude: TStringList; begin - Start := Now; Path := NodePath(Node); Result := True; Count := 0; @@ -1916,7 +1918,7 @@ begin else begin NodeData := TNodeData(Node.Data); - if ReadSubDirsBatch(Node, NodeData.DelayedSrec, 10, 50) then + if ReadSubDirsBatch(Node, NodeData.DelayedSrec, 10, 50, Now) then begin FreeAndNil(NodeData.DelayedExclude); FDelayedNodes.Delete(0);