Access denied in RenameFile after FileExists - delphi

I´m regularly importing files from a path into an application with Delphi 10.2. After each successful import I want to move the current file to a new path(SuccessPath). It´s possible to have file names that already exist in the SuccessPath. That´s why I first check if the file name exists. If thats the case I append an index to the filename (e.g. test.txt changes to test_2.txt).
In some cases RenameFile returns false and GetLastError returns Access denied. It´s probably an unclosed file handle. Do I need to close a handle after calling FileExists? How would I do so?
Here´s my example code:
procedure TDemo.Execute;
begin
MoveFileWithRenameDuplicates('C:\temp\test.txt', 'C:\temp\new\');
end;
procedure TDemo.MoveFileWithRenameDuplicates(oldFile, newPath: string);
var
lNewFile: string;
i: integer;
begin
lNewFile := newPath + TPath.GetFileName(oldFile);
i := 0;
repeat
if FileExists(lNewFile) then
begin
i := i + 1;
lNewFile := newPath + TPath.GetFileNameWithoutExtension(oldFile) + '_' + IntToStr(i) + TPath.GetExtension(oldFile);
end;
until not FileExists(lNewFile);
WriteLn('lNewFile=' + lNewFile);
if not RenameFile(oldFile, lNewFile) then
WriteLn(SysErrorMessage(GetLastError));
end;

I changed my loop like Remy suggested. I also realized that the appended index didn´t actually increment so I changed that aswell:
procedure TDemo.MoveFileWithRenameDuplicates(oldFile, newPath: string);
var
lNewFile: string;
i: integer;
begin
lNewFile := newPath + TPath.GetFileName(oldFile);
i := 0;
while not RenameFile(oldFile, lNewFile) do
begin
if FileExists(lNewFile) then
begin
lNewFile := newPath
+ TRegEx.Match(TPath.GetFileNameWithoutExtension(oldFile), '/^(?''name''[^_]*)_\d*$', [roIgnoreCase]).Groups['name'].Value
+ '_' + IntToStr(i) + TPath.GetExtension(oldFile);
end
else
begin
WriteLn(SysErrorMessage(GetLastError));
break;
end;
end;
end;
This seems to have fixed my problem.

Related

Why could CreateFile fail on a network drive

I have one installation that behaves VERY oddly... Every time we try to copy something on a network drive we check accessibility with code like this:
procedure TForm1.TestAccess;
var fn : string;
hdl : THandle;
res : boolean;
dir : string;
flags : Cardinal;
begin
dir := edDir.Text;
flags := FILE_FLAG_DELETE_ON_CLOSE or FILE_FLAG_NO_BUFFERING or FILE_ATTRIBUTE_HIDDEN;
fn := FindUnusedFileName( IncludeTrailingPathDelimiter( dir ) + IntToStr( Random(10000) ) + '.tst' );
memLog.Lines.Add('Try to create file: ' + fn);
hdl := CreateFile( PChar(fN), GENERIC_READ or GENERIC_WRITE, FILE_SHARE_DELETE, nil, CREATE_NEW,
flags, 0 );
res := hdl <> INVALID_HANDLE_VALUE;
if not res then
begin
memLog.Lines.Add('Error: ' + SysErrorMessage(GetLastError));
end
else
memLog.Lines.Add('Success');
if res then
CloseHandle(hdl);
end;
where memLog is a TMemo and edDir is simply an edit field.
Now here is the strange part... I get an access denied on that system meaning uploading will fail most of the time (most of the time is the strange part here).
Another thing is that in a first attempt I used the JvDirectoryEdit control. In that case the result is twofold... If I enter the directory (UNC Path) there without a backslash I get the access denied too. If I enter a final backslash and the combo box window pops up showing the content (aka directories) in that folder it finally works!!!
So... First: has anyone a clue what the problem might be and do I something wrong here?
I have used this procedure in the past to check if a directory is read-only, maybe it can be useful:
FUNCTION DirRO(NomeCartella : String) : Boolean;
VAR VarFile : TextFile;
NomCart : String;
BEGIN
Result := False;
If NomeCartella[Length(NomeCartella)]='\'
Then NomCart := NomeCartella
Else NomCart := NomeCartella+'\';
Try AssignFile(VarFile,NomCart+'^ghi.kol');
{$I-}
Rewrite(VarFile);
{$I-}
If IOResult<>0 Then
Begin
Result := True;
Exit;
End;
CloseFile(VarFile);
Erase(VarFile);
Except Result := True;
End;
END;

FindfirstEx using recursion

It drives me nuts (I'll should learn about recursion though)
But I'll can't make it work, something I'm doing wrong.
I was trying to make an exact copy of the FindFirst code below just with FindFirstEx but on the recursion I'll got stackoverflow and a lot of strange things happen so I'll guess that must be the wrong way using it so I'll removed it and replace it with the comment I'm stuck here...
So basically I'll need a procedure where I'm able to specify a directory and a file mask thats it.
procedure FetchFilesAndFolders(aPath, AMask; lbSearchResult: TMemo);
const
Find_First_Ex_Large_Fetch = 2;
var
hFile: Cardinal;
searchResult: Win32_Find_DataW;
begin
lbSearchResults.Lines.BeginUpdate;
hFile := FindFirstFileEx(PChar(aPath + aMask), FindExInfoStandard, #searchResult,
FindExSearchNameMatch, nil, Find_First_Ex_Large_Fetch);
If (hFile <> INVALID_HANDLE_VALUE) Then
begin
try
repeat
If (searchResult.dwFileAttributes And faDirectory = faDirectory) Then
begin
lbSearchResult.Lines.Append(IncludeTrailingBackSlash(aPath) +
string(searchResult.cFileName));
end
else
begin
// I'm stuck here...
end;
until (not FindNextFile(hFile, searchResult))
finally Winapi.Windows.FindClose(hFile);;
lbSearchResult.Lines.EndUpdate;
end;
Something like this but with FindFirstFileEx instead:
procedure FileSearch(const dirName, aMask: string; lbSearchResult:
TMemo);
var
searchResult: TSearchRec;
begin
lbSearchResult.Lines.BeginUpdate;
if FindFirst(dirName+ aMask, faAnyFile, searchResult) = 0 then
begin
try
repeat
if (searchResult.Attr and faDirectory) = 0 then
begin
lbSearchResult.Lines.Append(IncludeTrailingBackSlash(dirName) +
searchResult.Name);
end
else if (searchResult.Name <> '.') and (searchResult.Name <> '..') then
begin
FileSearch(IncludeTrailingBackSlash(dirName) + searchResult.Name,
lbSearchResult);
end;
until FindNext(searchResult) <> 0 finally FindClose(searchResult);
lbSearchResult.Lines.EndUpdate;
end;
end;
end;
So I'll need someone to show me doing it right.
Thank you!
Version 3 (still not working but no exception):
procedure FileSearch(const aPath, aMask: string; lbSearchResult: TMemo);
const
Find_First_Ex_Large_Fetch = 2;
var
hFile: Cardinal;
searchResult: Win32_Find_DataW;
begin
lbSearchResult.Lines.BeginUpdate;
hFile := FindFirstFileEx(PChar(aPath + aMask), FindExInfoStandard,
#searchResult, FindExSearchNameMatch, nil, Find_First_Ex_Large_Fetch);
If (hFile <> INVALID_HANDLE_VALUE) Then
begin
try
repeat
If (0 = (searchResult.dwFileAttributes and FILE_ATTRIBUTE_DIRECTORY)) then
begin
lbSearchResult.Lines.Add(IncludeTrailingBackSlash(aPath) +
string(searchResult.cFileName));
end
else
begin
If (searchResult.cFileName <> String('.')) And
(searchResult.cFileName <> '..') Then
lbSearchResult.Lines.Add(IncludeTrailingBackSlash(aPath) +
string(searchResult.cFileName));
// the following is not good, infinite!!!
// FileSearch(aPath, aMask, lbSearchResult);
end;
until (not FindNextFile(hFile, searchResult))
finally Winapi.Windows.FindClose(hFile);;
lbSearchResult.Lines.EndUpdate;
end;
end;
end;
All files and folders are shown which are inside the test folder but not any deeper so no recursion...
FileSearch('D:\projects\Test\', '*.*', Memo1);
So for example:
D:\projects\Test\Blubb
is shown but not the files inside or any directories from Test\Blubb\
The cause of your infinite recursion appears to be that you are omitting the test for the files '.' and '..'.
in the second code there is the line
else if (searchResult.Name <> '.') and (searchResult.Name <> '..') then
These two values are special directories that mean 'current directory' and 'parent directory' respectively. So while analysing the current directory it will find a 'subdirectory' called '.', which is actually a pointer to the directory that you are currently analysing. Therefore a recursive call to this subdirectory will analyse the same directory again, during which it will again find the directory '.' and again attempt to analyse the same directory, and so on for ever.
A similar situation arises for '..' which would analyse the parent directory.
It is critical that this test not be omitted.
Note on your version 3
One of my pet hates is not including begin...end blocks in if statements, and your 3rd version seems to illustrate why. You still get the infinite loop because your if statement does not apply to the recursion - which is exactly where it must apply.
The second, more serious issue is that the parameters of the recursive call are exactly the same as the original call. I can think of no circumstances where it is ever valid to do that (although I am sure that someone will correct me). Instead you must call with the name of the child directory. I have added a new variable fNewPath to simplify things.
procedure FileSearch(const aPath, aMask: string; lbSearchResult: TMemo);
const
Find_First_Ex_Large_Fetch = 2;
var
hFile: Cardinal;
searchResult: Win32_Find_DataW;
fNewPath : string;
begin
lbSearchResult.Lines.BeginUpdate;
hFile := FindFirstFileEx(PChar(aPath + aMask), FindExInfoStandard,
#searchResult, FindExSearchNameMatch, nil, Find_First_Ex_Large_Fetch);
If (hFile <> INVALID_HANDLE_VALUE) Then
begin
try
repeat
If (0 = (searchResult.dwFileAttributes and FILE_ATTRIBUTE_DIRECTORY)) then
begin
lbSearchResult.Lines.Add(IncludeTrailingBackSlash(aPath) +
string(searchResult.cFileName));
end
else
begin
If (searchResult.cFileName <> String('.')) And
(searchResult.cFileName <> '..') Then
begin // <-------------------
fNewPath := IncludeTrailingBackSlash(aPath) +
string(searchResult.cFileName);
lbSearchResult.Lines.Add( fNewPath );
FileSearch(fNewPath, aMask, lbSearchResult);
end; //<--------------- The recurstion *must* be covered by the if statement
end;
until (not FindNextFile(hFile, searchResult));
finally Winapi.Windows.FindClose(hFile);
lbSearchResult.Lines.EndUpdate;
end;
end;
end;

Excluding folder and files from search [duplicate]

This question already has an answer here:
Delphi function, Not allowing files and folders from main directory during compression
(1 answer)
Closed 9 years ago.
I surrender, I spend my time almost 12hours to get what I want, but I can't.
This code search all folders and filenames, but I want to exclude some folders including the sub directory of folders I want to exclude from searching.
I wish there's someone can help.
procedure TForm1.CombineDir(InDir : string; OutStream : TStream);
var AE : TArchiveEntry;
dFound:boolean;
procedure RecurseDirectory(ADir : string);
var sr : TSearchRec;
TmpStream : TStream;
begin
if FindFirst(ADir + '*', faAnyFile, sr) = 0 then begin
repeat
if (sr.Attr and (faDirectory or faVolumeID)) = 0 then begin
//ShowMessage('Filename is :>'+ ADir + sr.Name);
if (NotThisPath.IndexOf(ADir + sr.Name)>=0) or dFound then begin
ShowMessage('DO NOT INCLUDE THIS FILENAME :>'+ ADir + sr.Name);
end else begin
ShowMessage('>>> INCLUDE THIS FILENAME :>'+ ADir + sr.Name);
// We have a file (as opposed to a directory or anything
// else). Write the file entry header.
AE.EntryType := aeFile;
AE.FileNameLen := Length(sr.Name);
AE.FileLength := sr.Size;
OutStream.Write(AE, SizeOf(AE));
OutStream.Write(sr.Name[1], Length(sr.Name));
// Write the file itself
TmpStream := TFileStream.Create(ADir + sr.Name, fmOpenRead or fmShareDenyWrite);
OutStream.CopyFrom(TmpStream, TmpStream.Size);
TmpStream.Free;
end;
end;
if (sr.Attr and faDirectory) > 0 then begin
if (sr.Name <> '.') and (sr.Name <> '..') then begin
//ShowMessage('DIR is:>'+ ADir + sr.Name);
//if (Pos(ADir, NotThisPath.Text)>0) then
if (NotThisPath.IndexOf(ADir + sr.Name)>=0) then begin
ShowMessage('DO NOT INCLUDE THIS DIR:>'+ ADir + sr.Name);
dFound:=True;
end else begin
ShowMessage('>>> INCLUDE THIS DIR:>'+ ADir + sr.Name);
// Write the directory entry
AE.EntryType := aeDirectory;
AE.DirNameLen := Length(sr.Name);
OutStream.Write(AE, SizeOf(AE));
OutStream.Write(sr.Name[1], Length(sr.Name));
end;
// Recurse into this directory
RecurseDirectory(IncludeTrailingPathDelimiter(ADir + sr.Name));
end;
end;
until FindNext(sr) <> 0;
FindClose(sr);
end;
// Show that we are done with this directory
AE.EntryType := aeEOD;
OutStream.Write(AE, SizeOf(AE));
end;
begin
RecurseDirectory(IncludeTrailingPathDelimiter(InDir));
end;
NotThisPath is a TStringList;
I think your fundamental problem is that you have mixed together file enumeration, file name filtering, and your GUI into one unholy blob of goo. You simply should not see FindFirst being called from a method of a form. Code that calls FindFirst belongs in helper classes or functions.
I'm not going to attempt to answer your question directly, not least because you did not actually ask a question. What I'm going to attempt is to show you how to separate the concerns of enumerating files and filtering for names.
First of all, I'm going to implement this function:
procedure EnumerateFiles(Dir: string;
const EnumerateFileName: TEnumerateFileNameMethod);
This function is passed a directory in the Dir parameter and it proceeds to enumerate all files within that directory, its sub-directories, and so on recursively. Each file that is found is passed to the callback method EnumerateFileName. This is defined like so:
type
TEnumerateFileNameMethod = procedure(const FileName: string) of object;
The implementation is very simple indeed. It's just the standard FindFirst based repeat loop. The function rejects the special directories . and ... It will recurse into any directories that it encounters.
procedure EnumerateFiles(Dir: string;
const EnumerateFileName: TEnumerateFileNameMethod);
var
SR: TSearchRec;
begin
Dir := IncludeTrailingPathDelimiter(Dir);
if FindFirst(Dir + '*', faAnyFile, SR) = 0 then
try
repeat
if (SR.Name = '.') or (SR.Name = '..') then
continue;
if (SR.Attr and faDirectory) <> 0 then
EnumerateFiles(Dir + SR.Name, EnumerateFileName)
else
EnumerateFileName(Dir + SR.Name);
until FindNext(SR) <> 0;
finally
FindClose(SR);
end;
end;
Now, this should be simple enough to follow I hope. The next issue is filtering. You can implement that in the callback method that you provide. Here's a complete demo that illustrates filtering that picks out Delphi source files with the .pas extension.
program EnumerateFilesDemo;
{$APPTYPE CONSOLE}
uses
SysUtils;
type
TEnumerateFileNameMethod = procedure(const FileName: string) of object;
procedure EnumerateFiles(Dir: string;
const EnumerateFileName: TEnumerateFileNameMethod);
var
SR: TSearchRec;
begin
Dir := IncludeTrailingPathDelimiter(Dir);
if FindFirst(Dir + '*', faAnyFile, SR) = 0 then
try
repeat
if (SR.Name = '.') or (SR.Name = '..') then
continue;
if (SR.Attr and faDirectory) <> 0 then
EnumerateFiles(Dir + SR.Name, EnumerateFileName)
else
EnumerateFileName(Dir + SR.Name);
until FindNext(SR) <> 0;
finally
FindClose(SR);
end;
end;
type
TDummyClass = class
class procedure EnumerateFileName(const FileName: string);
end;
class procedure TDummyClass.EnumerateFileName(const FileName: string);
begin
if SameText(ExtractFileExt(FileName), '.pas') then
Writeln(FileName);
end;
procedure Main;
begin
EnumerateFiles('C:\Users\heff\Development', TDummyClass.EnumerateFileName);
end;
begin
try
Main;
Readln;
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
end.
Now, I know that's not the type of filtering that you want to do, but the point is that we now have generality. You can replace the call to SameText with whatever filtering you want. And once you have picked out the files that you want to deal with, you can do what you like with them.
I used a class method for convenience. I did not want my demo to be laden down with the boiler-plate of instantiating an object. But for your needs you would want to create a class to handle the enumeration callback. That class would encapsulate the file archiving operation that you are performing. That class would own an instance of the output stream. And the callback method would be an instance method that would write to the archive.
Now, I've not implemented a complete solution to your problem, but I hope I've done something better. Namely to show you how to factor code to make solving your problem simple.

Delete Directory with non empty subdirectory and files

How to delete one directory having some files and some non empty sub directory.
I have tried SHFileOperation Function. It has some compatibility issue in Windows 7.
Then I have tried IFileOperation Interface. But it is not compatible in Windows XP.
Then I have tried the following codes as suggested by David Heffernan :
procedure TMainForm.BitBtn01Click(Sender: TObject);
var
FileAndDirectoryExist: TSearchRec;
ResourceSavingPath : string;
begin
ResourceSavingPath := (GetWinDir) + 'Web\Wallpaper\';
if FindFirst(ResourceSavingPath + '\*', faAnyFile, FileAndDirectoryExist) = 0 then
try
repeat
if (FileAndDirectoryExist.Name <> '.') and (FileAndDirectoryExist.Name <> '..') then
if (FileAndDirectoryExist.Attr and faDirectory) <> 0 then
//it's a directory, empty it
ClearFolder(ResourceSavingPath +'\' + FileAndDirectoryExist.Name, mask, recursive)
else
//it's a file, delete it
DeleteFile(ResourceSavingPath + '\' + FileAndDirectoryExist.Name);
until FindNext(FileAndDirectoryExist) <> 0;
//now that this directory is empty, we can delete it
RemoveDir(ResourceSavingPath);
finally
FindClose(FileAndDirectoryExist);
end;
end;
But it does not get compiled mentioning error as Undeclared Identifier at ClearFolder, mask and recursive. My requirement is to that "If any sub folder exist under WALLPAPER folder it will be deleted". The same sub folder may contain any number of non empty sub folder or files.
Well, for starters, SHFileOperation has no compatibility issues on Windows 7 or Windows 8. Yes, you are now recommended to use IFileOperation instead. But if you want to support older operating systems like XP, then you can and should just call SHFileOperation. It works and will continue to work. It's pefectly fine to use it on Windows 7 and Windows 8 and I'll eat my hat if it's ever removed from Windows. Microsoft go to extraordinary lengths to maintain backwards compatibility. So, SHFileOperation is your best option in my view.
Your FindFirst based approach fails because you need to put it in a separate function in order to allow recursion. And the code I posted in that other answer is incomplete. Here is a complete version:
procedure DeleteDirectory(const Name: string);
var
F: TSearchRec;
begin
if FindFirst(Name + '\*', faAnyFile, F) = 0 then begin
try
repeat
if (F.Attr and faDirectory <> 0) then begin
if (F.Name <> '.') and (F.Name <> '..') then begin
DeleteDirectory(Name + '\' + F.Name);
end;
end else begin
DeleteFile(Name + '\' + F.Name);
end;
until FindNext(F) <> 0;
finally
FindClose(F);
end;
RemoveDir(Name);
end;
end;
This deletes a directory and its contents. You'd want to walk the top level directory and then call this function for each subdirectory that you found.
Finally I have implemented the following Code:
uses
ShellAPI;
...
...
function GetWinDir: string;
var
WindowsDirectory: array[0..MAX_PATH] of Char;
begin
GetWindowsDirectory(WindowsDirectory, MAX_PATH - 1);
SetLength(Result, StrLen(WindowsDirectory));
Result := IncludeTrailingPathDelimiter(WindowsDirectory);
end;
...
...
procedure DeleteDirectory(const DirName: string);
var
FileFolderOperation: TSHFileOpStruct;
begin
FillChar(FileFolderOperation, SizeOf(FileFolderOperation), 0);
FileFolderOperation.wFunc := FO_DELETE;
FileFolderOperation.pFrom := PChar(ExcludeTrailingPathDelimiter(DirName) + #0);
FileFolderOperation.fFlags := FOF_SILENT or FOF_NOERRORUI or FOF_NOCONFIRMATION;
SHFileOperation(FileFolderOperation);
end;
...
...
procedure TMainForm.BitBtn01Click(Sender: TObject);
begin
DeleteDirectory((GetWinDir) + '\Web\Wallpapers\');
end
...
...
Please don't mention anything regarding 'TrailingPathDelimiter', I have intentionally implemented. I works successfully having one problem that the files or folder successfully deleted without going to 'Recycle Bin' in case of Windows XP, but in case of Vista and higher those files goes to 'Recycle Bin' and I don't have any option for directly deletion without sending to 'Recycle Bin' in case of Vista or Higher.
This is a pretty complete function that works both with files and folders.
It allows you to specify the following parameters:
DeleteToRecycle
ShowConfirm
TotalSilence
{---------------------------------------------------------------
DELETE FILE
Deletes a file/folder to RecycleBin.
----------------------------------------------------------------}
function RecycleItem(CONST ItemName: string; CONST DeleteToRecycle: Boolean= TRUE; CONST ShowConfirm: Boolean= TRUE; CONST TotalSilence: Boolean= FALSE): Boolean;
VAR
SHFileOpStruct: TSHFileOpStruct;
begin
FillChar(SHFileOpStruct, SizeOf(SHFileOpStruct), #0);
SHFileOpStruct.wnd := Application.MainForm.Handle; { Others are using 0. But Application.MainForm.Handle is better because otherwise, the 'Are you sure you want to delete' will be hidden under program's window }
SHFileOpStruct.wFunc := FO_DELETE;
SHFileOpStruct.pFrom := PChar(ItemName+ #0);
SHFileOpStruct.pTo := NIL;
SHFileOpStruct.hNameMappings := NIL;
if DeleteToRecycle
then SHFileOpStruct.fFlags:= SHFileOpStruct.fFlags OR FOF_ALLOWUNDO;
if TotalSilence
then SHFileOpStruct.fFlags:= SHFileOpStruct.fFlags OR FOF_NO_UI
else
if NOT ShowConfirm
then SHFileOpStruct.fFlags:= SHFileOpStruct.fFlags OR FOF_NOCONFIRMATION;
Result:= SHFileOperation(SHFileOpStruct)= 0;
end;

File Masking in delphi

I am trying to find all files that have the extenstion .cbr or .cbz
If i set my mask to *.cb?
it finds *.cbproj files. How can i set the mask to only find .cbr and .cbz files?
here is code i am using.
I have two edit boxes EDIT1 is the location to search, EDIT2 is where i put my mask. A listbox to show what it found and a Search button.
edit1 := c:\
edit2 := mask (*.cb?)
space
procedure TFAutoSearch.FileSearch(const PathName, FileName : string; const InDir : boolean);
var Rec : TSearchRec;
Path : string;
begin
Path := IncludeTrailingBackslash(PathName);
if FindFirst(Path + FileName, faAnyFile - faDirectory, Rec) = 0 then
try
repeat
ListBox1.Items.Add(Path + Rec.Name);
until FindNext(Rec) <> 0;
finally
FindClose(Rec);
end;
If not InDir then Exit;
if FindFirst(Path + '*.*', faDirectory, Rec) = 0 then
try
repeat
if ((Rec.Attr and faDirectory) <> 0) and (Rec.Name<>'.') and (Rec.Name<>'..') then
FileSearch(Path + Rec.Name, FileName, True);
until FindNext(Rec) <> 0;
finally
FindClose(Rec);
end;
end; //procedure FileSearch
procedure TFAutoSearch.Button1Click(Sender: TObject);
begin
FileSearch(Edit1.Text, Edit2.Text, CheckBox1.State in [cbChecked]);
end;
end.
The easiest way is to use ExtractFileExt against the current filename and check to see if it matches either of your desired extensions.
Here's a fully-rewritten version of your FileSearch routine which does exactly what you're trying to do (according to your question, anyway):
procedure TFAutoSearch.FileSearch(const ARoot: String);
var
LExt, LRoot: String;
LRec: TSearchRec;
begin
LRoot := IncludeTrailingPathDelimiter(ARoot);
if FindFirst(LRoot + '*.*', faAnyFile, LRec) = 0 then
begin
try
repeat
if (LRec.Attr and faDirectory <> 0) and (LRec.Name <> '.') and (LRec.Name <> '..') then
FileSearch(LRoot + LRec.Name)
else
begin
LExt := UpperCase(ExtractFileExt(LRoot + LRec.Name));
if (LExt = '.CBR') or (LExt = '.CBZ') then
ListBox1.Items.Add(LRoot + LRec.Name);
end;
until (FindNext(LRec) <> 0);
finally
FindClose(LRec);
end;
end;
end;
While the other answer suggesting the use of multiple extensions as a mask *.cbr;*.cbz should (in principal anyway) work, I've noted through bitter experience that the FindFirst and FindNext methods in Delphi tend not to accept multiple extensions in a mask!
The code I've provided should work just fine for your needs, so enjoy!
UPDATED: To allow the use of multiple extensions in a Mask dynamically at runtime (as indicated by the OP's first comment to this answer).
What we're going to do is take a String from your TEdit control (this String is one or more File Extensions as you would expect), "Explode" the String into an Array, and match each file against each Extension in the Array.
Sounds more complicated than it is:
type
TStringArray = Array of String; // String Dynamic Array type...
// Now let's provide a "Mask Container" inside the containing class...
TFAutoSearch = class(TForm)
// Normal stuff in here
private
FMask: TStringArray; // Our "Mask Container"
end;
This code will populate FMask with each individual mask extension separated by a ; such as .CBR;.CBZ.
Note this method will not accept Wildcard characters or any other Regex magic, but you can modify it as you require!
procedure TFAutoSearch.ExplodeMask(const AValue: String);
var
LTempVal: String;
I, LPos: Integer;
begin
LTempVal := AValue;
I := 0;
while Length(LTempVal) > 0 do
begin
Inc(I);
SetLength(FMask, I);
LPos := Pos(';', LTempVal);
if (LPos > 0) then
begin
FMask[I - 1] := UpperCase(Copy(LTempVal, 0, LPos - 1));
LTempVal := Copy(LTempVal, LPos + 1, Length(LTempVal));
end
else
begin
FMask[I - 1] := UpperCase(LTempVal);
LTempVal := EmptyStr;
end;
end;
end;
We now need a function to determine if the nominated file matches any of the defined Extensions:
function TFAutoSearch.MatchMask(const AFileName: String): Boolean;
var
I: Integer;
LExt: String;
begin
Result := False;
LExt := UpperCase(ExtractFileExt(LExt));
for I := Low(FMask) to High(FMask) do
if (LExt = FMask[I]) then
begin
Result := True;
Break;
end;
end;
Now here's the modified FileSearch procedure:
procedure TFAutoSearch.FileSearch(const ARoot: String);
var
LRoot: String;
LRec: TSearchRec;
begin
LRoot := IncludeTrailingPathDelimiter(ARoot);
if FindFirst(LRoot + '*.*', faAnyFile, LRec) = 0 then
begin
try
repeat
if (LRec.Attr and faDirectory <> 0) and (LRec.Name <> '.') and (LRec.Name <> '..') then
FileSearch(LRoot + LRec.Name)
else
begin
if (MatchMask(LRoot + LRec.Name)) then
ListBox1.Items.Add(LRoot + LRec.Name);
end;
until (FindNext(LRec) <> 0);
finally
FindClose(LRec);
end;
end;
end;
Finally, here's how you initiate your search:
procedure TFAutoSearch.btnSearchClick(Sender: TObject);
begin
ExplodeMask(edMask.Text);
FileSearch(edPath.Text);
end;
Where edMask is defined in your question as Edit2 and edPath is defined in your question as Edit1. Just remember that this method doesn't support the use of Wildcard or other Special Chars, so edMask.Text should be something like .CBR;.CBZ
If you use the Regex library for Delphi, you could easily modify this method to support all of the Expression Cases you could ever imagine!
Dorin's suggestion to replace your mask with *.cbr;*.cbz should work. That is, it won't match cbproj anymore. It would, however, still match cbzy or any other extension that starts with cbr or cbz. The reason for this is that FindFirst/FindNext match both the long form and the legacy short forms (8.3) of file names. So the short forms will always have truncated extensions where cbproj is shortened to cbp, and therefore matches cb?.
This is supposed to be avoidable by using FindFirstEx instead, but this requires a small rewrite of your search function and actually didn't work for me. So instead I just double checked all matches with the MatchesMask function.

Resources