Recursively delete files and skip files that are in use? - delphi

I'm using Windows API to recursively delete many files and folders. I'm using it without a UI and suppressing errors. The problem is, it completely fails if one of those files is in use. I expect that possibility, and want this to continue anyway, skipping any such cases. The one file which fails is actually the same EXE which is calling this delete command (which will be deleted after it's all done anyway.
Here's what I'm doing now:
procedure DeleteDirectory(const DirName: string);
var
FileOp: TSHFileOpStruct;
begin
FillChar(FileOp, SizeOf(FileOp), 0);
FileOp.wFunc := FO_DELETE;
FileOp.pFrom := PChar(DirName+#0);//double zero-terminated
FileOp.fFlags := FOF_SILENT or FOF_NOERRORUI or FOF_NOCONFIRMATION;
SHFileOperation(FileOp);
end;
How can I make this skip any event of a file being in use? I looked at the documentation but can't find anything that can do this.

Here is just an idea you can implement in your function to validate if there is any file in use:
function IsFileInUse(FileName: TFileName): Boolean;
var
HFileRes: HFILE;
begin
Result := False;
if not FileExists(FileName) then
Exit;
HFileRes := CreateFile(PChar(FileName),
GENERIC_READ or GENERIC_WRITE,
0,
nil,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
0);
Result := (HFileRes = INVALID_HANDLE_VALUE);
if not Result then
CloseHandle(HFileRes);
end;
Maybe it can help you.

Why don't you try this:
procedure DeleteFiles(const DirName: String);
var
SR: TSearchRec;
i: Integer;
begin
//get all files in directory
i := FindFirst(DirName +'\*.*', faAnyFile, SR);
while i = 0 do
begin
if (SR.Attr and faDirectory) <> faDirectory then
DeleteFile(DirName +'\'+ SR.Name);
i := FindNext(SR);
end;
FindClose(SR);
end;
It's another way to do it.

Related

Using Modern IFileDialog Open/Save dialog with Delphi 7 apps under Win10/11

I would like to modernize the GUI of my Delphi 7 App, I have already .manifest file and it looks quite good, but the Fileopen dialogs are terrible. How the make them modern?
I am using this code at the moment.
What would it require to use e.g. IFileOpenDialog instead, how to compile the header for that, or any tweaks to dialogs.pas ?
FileOpenDialog := TOpenDialog.create(parent);
FileOpenDialog.DefaultExt := '*.x';
FileOpenDialog.Filter := 'my|*.x|Text File (CSV)|*.csv';
FileOpenDialog.options := [ofHideReadOnly,ofFileMustExist ,ofNoChangeDir,ofPathMustExist ];
if FileOpenDialog.Execute then begin
// do my tricks with FileOpenDialog.filename
FormUpdate;
end;
The following example code of IFileDialog cannot be compiled with D7:
var
FolderDialog : IFileDialog;
hr: HRESULT;
IResult: IShellItem;
FileName: PChar;
Settings: DWORD;
begin
if Win32MajorVersion >= 6 then
begin
hr := CoCreateInstance(CLSID_FileOpenDialog,
nil,
CLSCTX_INPROC_SERVER,
IFileDialog,
FolderDialog);
if hr = S_OK then
begin
FolderDialog.SetOkButtonLabel(PChar('Select'));
FolderDialog.SetTitle(PChar('Select a Directory'));
hr := FolderDialog.Show(Handle);
if hr = S_OK then
begin
hr := FolderDialog.GetResult(IResult);
if hr = S_OK then
begin
IResult.GetDisplayName(SIGDN_FILESYSPATH,FileName);
ConfigPathEdit.Text := FileName;
end;
end;
end;
end;
I used this one, I tested it with D7.
// uses commdlg
function OpenSaveFileDialog( Parent: TWinControl;
const DefExt,Filter,InitialDir,Title: string;
var FileName: string;
MustExist,OverwritePrompt,NoChangeDir,DoOpen: Boolean): Boolean;
var ofn: TOpenFileName;
szFile: array[0..MAX_PATH] of Char;
begin
Result := False;
FillChar(ofn, SizeOf(TOpenFileName), 0);
with ofn do
begin
lStructSize := SizeOf(TOpenFileName);
hwndOwner := Parent.Handle;
lpstrFile := szFile;
nMaxFile := SizeOf(szFile);
if (Title <> '') then
lpstrTitle := PChar(Title);
if (InitialDir <> '') then
lpstrInitialDir := PChar(InitialDir);
StrPCopy(lpstrFile, FileName);
lpstrFilter := PChar(StringReplace(Filter, '|', #0,[rfReplaceAll, rfIgnoreCase])+#0#0);
if DefExt <> '' then
lpstrDefExt := PChar(DefExt);
end;
if MustExist then
ofn.Flags := ofn.Flags or OFN_FILEMUSTEXIST;
if OverwritePrompt then
ofn.Flags := ofn.Flags or OFN_OVERWRITEPROMPT;
if NoChangeDir then
ofn.Flags := ofn.Flags or OFN_NOCHANGEDIR;
if DoOpen
then begin
if GetOpenFileName(ofn) then
begin
Result := True;
FileName := StrPas(szFile);
end;
end
else begin
if GetSaveFileName(ofn) then
begin
Result := True;
FileName := StrPas(szFile);
end;
end;
end;
procedure TForm1.Button1Click(Sender: TObject);
VAR FilSelez : String;
begin
If OpenSaveFileDialog(Form1,'','*.*','c:\windows','',FilSelez,False,False,True,True) Then
Edit1.Text := FilSelez;
end;
Instead of using the IFileDialog interface you can also just modify Delphi's 7 Dialogs.pas file to display the "modern" dialogs.
First make a backup copy of the Dialogs.pas file in the Source\VCL folder under the Delphi installation directory. Then search the file for the term OFN_ENABLEHOOK. The complete line should be Flags := OFN_ENABLEHOOK;. Comment out the line. Add a new line Flags := 0; directly below.
Now search for the term OFN_ENABLETEMPLATE. Two lines above this should be an if Template <> nil then statement. Comment out this statement and all following ones up to and including hWndOwner := Application.Handle; and add the line hWndOwner := Screen.ActiveForm.Handle;.
Now make sure to replace the precompiled units Dialogs.dcu in the Lib and SLib directory under the Delphi installation directory with newly compiled Dialogs.dcu containing the changes. In the Lib directory you store a version without debug information, while the SLib directory contains a version with debug information. Make backup copies of the files before replacing them.
Please take note that the instructions given above only apply to Delphi 7. Furthermore, the code disables event handling for the common dialog components.

CreateProcess call returns error code 50

Try to use Createprocess to start windows Explorer in a given path, but I keep getting
System Error. Code 50. The request is not supported.
What am i doing wrong?
procedure TfrmProjectManager.OpenFolderinExplorer(const aPath: string);
function GetWinDir: String;
var
Buffer: array[0..MAX_PATH] of Char;
begin
GetWindowsDirectory(Buffer, SizeOf(Buffer));
SetString(Result, Buffer, StrLen(Buffer));
end;
var
strCmdLine : String;
fStartInfo : TStartupInfo;
fProcessInfo : TProcessInformation;
begin
try
if sysutils.DirectoryExists(aPath) or
(MessageDlg('Folder [%s] not found. Create it?', mtConfirmation, mbYesNo, 0)=mrYes) then
begin
sysutils.ForceDirectories(aPath);
FillChar(fStartInfo,sizeof(fStartInfo),0);
FillChar(fPRocessInfo, Sizeof(fProcessInfo),0);
fStartInfo.cb:=sizeof(fStartInfo);
fStartInfo.lpReserved := nil;
fStartInfo.lpDesktop := nil;
fStartInfo.lpTitle := nil;
fStartInfo.dwFlags := STARTF_USESHOWWINDOW ;
fStartInfo.wShowWindow := SW_SHOW;
fStartInfo.cbReserved2 := 0;
fStartInfo.lpReserved2 := nil;
strCmdLine := '"' + GetWinDir + '\explorer.exe"';
if not CreateProcess(nil,PChar(strCmdLine),nil,nil,False, 0,nil,PChar(aPath),fStartInfo,fProcessInfo) then
RaiseLastOSError;
end
except
on E:TObject do
if not IsAbortException(E) then
raise;
end;
end;
I tried various combinations of parameters in CreateProcess, but just don't seem able to find the correct one.
I'd say that you shouldn't be using CreateProcess here. Rather than debugging your CreateProcess I'll offer you what I believe to be the right way to open a shell view onto a folder. Call ShellExecute.
ShellExecute(0, '', PChar(aPath), '', '', SW_SHOWNORMAL);
This way you let the shell decide on the appropriate way to display the folders contents to the user.

Getting output from a shell/dos app into a Delphi app

I have a commandline application coded in delphi that I need to call from a normal desktop application (also coded in delphi). In short, I want to call the commandline app and display the text it outputs "live" in a listbox.
It's been ages since I have played around with the shell, but I distinctly remember that in order to grab the text from a commandline app - I have to use the pipe symbol ">". Like this:
C:/mycmdapp.exe >c:/result.txt
This will take any text printed to the shell (using writeLn) and dump it to a textfile called "result.txt".
But.. (and here comes the pickle), I want a live result rather than a backlog file. A typical example is the Delphi compiler itself - which manages to report back to the IDE what is going on. If my memory serves me correctly, I seem to recall that I must create a "pipe" channel (?), and then assign the pipe-name to the shell call.
I have tried to google this but I honestly was unsure of how to formulate it. Hopefully someone from the community can point me in the right direction.
Updated: This question might be identical to How do I run a command-line program in Delphi?. Some of the answers fit what I'm looking for, although the title and question itself is not identical.
As ever so often Zarco Gajic has a solution: Capture the output from a DOS (command/console) Window. This is a copy from his article for future reference:
The example runs 'chkdsk.exe c:\' and displays the output to Memo1.
Put a TMemo (Memo1) and a TButton (Button1) on your form. Put this code in the OnCLick event procedure for Button1:
procedure RunDosInMemo(DosApp: string; AMemo:TMemo);
const
READ_BUFFER_SIZE = 2400;
var
Security: TSecurityAttributes;
readableEndOfPipe, writeableEndOfPipe: THandle;
start: TStartUpInfo;
ProcessInfo: TProcessInformation;
Buffer: PAnsiChar;
BytesRead: DWORD;
AppRunning: DWORD;
begin
Security.nLength := SizeOf(TSecurityAttributes);
Security.bInheritHandle := True;
Security.lpSecurityDescriptor := nil;
if CreatePipe({var}readableEndOfPipe, {var}writeableEndOfPipe, #Security, 0) then
begin
Buffer := AllocMem(READ_BUFFER_SIZE+1);
FillChar(Start, Sizeof(Start), #0);
start.cb := SizeOf(start);
// Set up members of the STARTUPINFO structure.
// This structure specifies the STDIN and STDOUT handles for redirection.
// - Redirect the output and error to the writeable end of our pipe.
// - We must still supply a valid StdInput handle (because we used STARTF_USESTDHANDLES to swear that all three handles will be valid)
start.dwFlags := start.dwFlags or STARTF_USESTDHANDLES;
start.hStdInput := GetStdHandle(STD_INPUT_HANDLE); //we're not redirecting stdInput; but we still have to give it a valid handle
start.hStdOutput := writeableEndOfPipe; //we give the writeable end of the pipe to the child process; we read from the readable end
start.hStdError := writeableEndOfPipe;
//We can also choose to say that the wShowWindow member contains a value.
//In our case we want to force the console window to be hidden.
start.dwFlags := start.dwFlags + STARTF_USESHOWWINDOW;
start.wShowWindow := SW_HIDE;
// Don't forget to set up members of the PROCESS_INFORMATION structure.
ProcessInfo := Default(TProcessInformation);
//WARNING: The unicode version of CreateProcess (CreateProcessW) can modify the command-line "DosApp" string.
//Therefore "DosApp" cannot be a pointer to read-only memory, or an ACCESS_VIOLATION will occur.
//We can ensure it's not read-only with the RTL function: UniqueString
UniqueString({var}DosApp);
if CreateProcess(nil, PChar(DosApp), nil, nil, True, NORMAL_PRIORITY_CLASS, nil, nil, start, {var}ProcessInfo) then
begin
//Wait for the application to terminate, as it writes it's output to the pipe.
//WARNING: If the console app outputs more than 2400 bytes (ReadBuffer),
//it will block on writing to the pipe and *never* close.
repeat
Apprunning := WaitForSingleObject(ProcessInfo.hProcess, 100);
Application.ProcessMessages;
until (Apprunning <> WAIT_TIMEOUT);
//Read the contents of the pipe out of the readable end
//WARNING: if the console app never writes anything to the StdOutput, then ReadFile will block and never return
repeat
BytesRead := 0;
ReadFile(readableEndOfPipe, Buffer[0], READ_BUFFER_SIZE, {var}BytesRead, nil);
Buffer[BytesRead]:= #0;
OemToAnsi(Buffer,Buffer);
AMemo.Text := AMemo.text + String(Buffer);
until (BytesRead < READ_BUFFER_SIZE);
end;
FreeMem(Buffer);
CloseHandle(ProcessInfo.hProcess);
CloseHandle(ProcessInfo.hThread);
CloseHandle(readableEndOfPipe);
CloseHandle(writeableEndOfPipe);
end;
end;
procedure TForm1.Button1Click(Sender: TObject);
begin {button 1 code}
RunDosInMemo('chkdsk.exe c:\',Memo1);
end;
Update:
The above example reads the output in one step. Here is another example from DelphiDabbler showing how the output can be read while the process is still running:
function GetDosOutput(CommandLine: string; Work: string = 'C:\'): string;
var
SA: TSecurityAttributes;
SI: TStartupInfo;
PI: TProcessInformation;
StdOutPipeRead, StdOutPipeWrite: THandle;
WasOK: Boolean;
Buffer: array[0..255] of AnsiChar;
BytesRead: Cardinal;
WorkDir: string;
Handle: Boolean;
begin
Result := '';
with SA do begin
nLength := SizeOf(SA);
bInheritHandle := True;
lpSecurityDescriptor := nil;
end;
CreatePipe(StdOutPipeRead, StdOutPipeWrite, #SA, 0);
try
with SI do
begin
FillChar(SI, SizeOf(SI), 0);
cb := SizeOf(SI);
dwFlags := STARTF_USESHOWWINDOW or STARTF_USESTDHANDLES;
wShowWindow := SW_HIDE;
hStdInput := GetStdHandle(STD_INPUT_HANDLE); // don't redirect stdin
hStdOutput := StdOutPipeWrite;
hStdError := StdOutPipeWrite;
end;
WorkDir := Work;
Handle := CreateProcess(nil, PChar('cmd.exe /C ' + CommandLine),
nil, nil, True, 0, nil,
PChar(WorkDir), SI, PI);
CloseHandle(StdOutPipeWrite);
if Handle then
try
repeat
WasOK := ReadFile(StdOutPipeRead, Buffer, 255, BytesRead, nil);
if BytesRead > 0 then
begin
Buffer[BytesRead] := #0;
Result := Result + Buffer;
end;
until not WasOK or (BytesRead = 0);
WaitForSingleObject(PI.hProcess, INFINITE);
finally
CloseHandle(PI.hThread);
CloseHandle(PI.hProcess);
end;
finally
CloseHandle(StdOutPipeRead);
end;
end;
You probably have the code on your harddisk already: the Execute function in the JclSysUtils unit of the JCL (JEDI Code Library) does what you need:
function Execute(const CommandLine: string; OutputLineCallback: TTextHandler;
RawOutput: Boolean = False; AbortPtr: PBoolean = nil): Cardinal;
You can supply it with a callback procedure:
TTextHandler = procedure(const Text: string) of object;
Did an answer too for better understanding:
{type TTextHandler =} procedure TTextHandlerQ(const aText: string);
begin
memo2.lines.add(atext);
end;
writeln(itoa(JExecute('cmd /C dir *.*',#TTextHandlerQ, true, false)));
You have to use /C then cmd /c is used to run commands in MS-DOS and terminate after command or process completion, otherwise it blocks output to memo.

How to Search a File through all the SubDirectories in Delphi

I implemented this code but again i am not able to search through the subdirectories .
procedure TFfileSearch.FileSearch(const dirName:string);
begin
//We write our search code here
if FindFirst(dirName,faAnyFile or faDirectory,searchResult)=0 then
begin
try
repeat
ShowMessage(IntToStr(searchResult.Attr));
if (searchResult.Attr and faDirectory)=0 then //The Result is a File
//begin
lbSearchResult.Items.Append(searchResult.Name)
else
begin
FileSearch(IncludeTrailingBackSlash(dirName)+searchResult.Name);
//
end;
until FindNext(searchResult)<>0
finally
FindClose(searchResult);
end;
end;
end;
procedure TFfileSearch.btnSearchClick(Sender: TObject);
var
filePath:string;
begin
lbSearchResult.Clear;
if Trim(edtMask.Text)='' then
MessageDlg('EMPTY INPUT', mtWarning, [mbOK], 0)
else
begin
filePath:=cbDirName.Text+ edtMask.Text;
ShowMessage(filePath);
FileSearch(filePath);
end;
end;
I am giving the search for *.ini files in E:\ drive. so initially filePath is E:*.ini.
But the code does not search the directories in E:\ drive. How to correct it?
Thanks in Advance
You can't apply a restriction to the file extension in the call to FindFirst. If you did so then directories do not get enumerated. Instead you must check for matching extension in your code. Try something like this:
procedure TMyForm.FileSearch(const dirName:string);
var
searchResult: TSearchRec;
begin
if FindFirst(dirName+'\*', faAnyFile, searchResult)=0 then begin
try
repeat
if (searchResult.Attr and faDirectory)=0 then begin
if SameText(ExtractFileExt(searchResult.Name), '.ini') then begin
lbSearchResult.Items.Append(IncludeTrailingBackSlash(dirName)+searchResult.Name);
end;
end else if (searchResult.Name<>'.') and (searchResult.Name<>'..') then begin
FileSearch(IncludeTrailingBackSlash(dirName)+searchResult.Name);
end;
until FindNext(searchResult)<>0
finally
FindClose(searchResult);
end;
end;
end;
procedure TMyForm.FormCreate(Sender: TObject);
begin
FileSearch('c:\windows');
end;
I'd recommend doing as follows:
uses
System.Types,
System.IOUtils;
procedure TForm7.Button1Click(Sender: TObject);
var
S: string;
begin
Memo1.Lines.Clear;
for S in TDirectory.GetFiles('C:\test', '*.bmp', TSearchOption.soAllDirectories) do
Memo1.Lines.Add(S);
Showmessage('Finished!');
end;
I hate those recursive solutions with FindFirst/FindNext and I consider it troublesome that some even forget to use FindClose to clean up resources. So, for the fun of it, a non-recursive solution that should be practical to use...
procedure FindDocs(const Root: string);
var
SearchRec: TSearchRec;
Folders: array of string;
Folder: string;
I: Integer;
Last: Integer;
begin
SetLength(Folders, 1);
Folders[0] := Root;
I := 0;
while (I < Length(Folders)) do
begin
Folder := IncludeTrailingBackslash(Folders[I]);
Inc(I);
{ Collect child folders first. }
if (FindFirst(Folder + '*.*', faDirectory, SearchRec) = 0) then
begin
repeat
if not ((SearchRec.Name = '.') or (SearchRec.Name = '..')) then
begin
Last := Length(Folders);
SetLength(Folders, Succ(Last));
Folders[Last] := Folder + SearchRec.Name;
end;
until (FindNext(SearchRec) <> 0);
FindClose(SearchRec);
end;
{ Collect files next.}
if (FindFirst(Folder + '*.doc', faAnyFile - faDirectory, SearchRec) = 0) then
begin
repeat
if not ((SearchRec.Attr and faDirectory) = faDirectory) then
begin
WriteLn(Folder, SearchRec.Name);
end;
until (FindNext(SearchRec) <> 0);
FindClose(SearchRec);
end;
end;
end;
While it seems to eat a lot of memory because it uses a dynamic array, a recursive method will do exactly the same but recursion happens on the stack! Also, with a recursive method, space is allocated for all local variables while my solution only allocates space for the folder names.
When you check for speed, both methods should be just as fast. The recursive method is easier to remember, though. You can also use a TStringList instead of a dynamic array, but I just like dynamic arrays.
One additional trick with my solution: It can search in multiple folders! I Initialized the Folders array with just one root, but you could easily set it's length to 3, and set Folders[0] to C:\, Folders[1] to D:\ and Folders[2] to E:\ and it will search on multiple disks!
Btw, replace the WriteLn() code with whatever logic you want to execute...
This is worked for me with multi-extension search support:
function GetFilesPro(const Path, Masks: string): TStringDynArray;
var
MaskArray: TStringDynArray;
Predicate: TDirectory.TFilterPredicate;
begin
MaskArray := SplitString(Masks, ',');
Predicate :=
function(const Path: string; const SearchRec: TSearchRec): Boolean
var
Mask: string;
begin
for Mask in MaskArray do
if MatchesMask(SearchRec.Name, Mask) then
exit(True);
exit(False);
end;
Result := TDirectory.GetFiles(Path, Predicate);
end;
Usage:
FileList := TStringList.Create;
FileSearch(s, '.txt;.tmp;.exe;.doc', FileList);
The problem with this file search is that it will loop infinitely, FindClose is like it does not exist.
procedure FindFilePattern(root:String;pattern:String);
var
SR:TSearchRec;
begin
root:=IncludeTrailingPathDelimiter(root);
if FindFirst(root+'*.*',faAnyFile,SR) = 0 then
begin
repeat
Application.ProcessMessages;
if ((SR.Attr and faDirectory) = SR.Attr ) and (pos('.',SR.Name)=0) then
FindFilePattern(root+SR.Name,pattern)
else
begin
if pos(pattern,SR.Name)>0 then Form1.ListBox1.Items.Add(Root+SR.Name);
end;
until FindNext(SR)<>0;
end;
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
FindFilePattern('C:\','.exe');
end;
This searches recursively to all folders displaying filenames that contain a certain pattern.

I cannot delete files to Recycle Bin

I cannot delete files to Recycle Bin.
VAR SHFileOpStruct: TSHFileOpStruct;
begin
with SHFileOpStruct do
begin
wnd := Handle;
wFunc := FO_DELETE;
pFrom := PChar(FileName);
fFlags:= 0;
pTo := NIL;
hNameMappings:= NIL;
lpszProgressTitle:= NIL;
end;
Result:= SHFileOperation(SHFileOpStruct);
end;
I can delete files in this format: '1.xyz' but not in this format '12.xyz' (file name is longer than 1 character).
According to the documentation of SHFileOperation you should not use GetLastError to see if the operation succeeds. Check the Result of the function and use the documentation to figure out the error it returns. That should give you a better clue what the problem is.
EDIT:
Best guess from reading the documentation:
pFrom
Although this member is declared as a
single null-terminated string, it is
actually a buffer that can hold
multiple null-delimited file names.
Each file name is terminated by a
single NULL character. The last file
name is terminated with a double NULL
character ("\0\0") to indicate the end
of the buffer
So you should make sure pFrom is ended with a double 0. Try the following
pFrom := PChar(FileName + #0);
Also, what Delphi version are you using?
EDIT2:
Also make sure the structure is properly initialized to 0. Uncomment the FillChar
This works for me:
function DeleteToRecycleBin(WindowHandle: HWND; Filename: string; Confirm: Boolean): Boolean;
var
SH: TSHFILEOPSTRUCT;
begin
FillChar(SH, SizeOf(SH), 0);
with SH do
begin
Wnd := WindowHandle;
wFunc := FO_DELETE;
pFrom := PChar(Filename + #0);
fFlags := FOF_SILENT or FOF_ALLOWUNDO;
if not Confirm then
begin
fFlags := fFlags or FOF_NOCONFIRMATION
end;
end;
Result := SHFileOperation(SH) = 0;
end;
You may want to set the fFlags := FOF_SILENT + FOF_ALLOWUNDO + FOF_NOCONFIRMATION

Resources