Downloading directory from FTP server - delphi

I am developing FTP client by RAD Studio (IdFTP). How I can dowload directory from server?
Delphi or C++. Thanks.

You need to call TIdFTP.ChangeDir() to go to the desired starting directory, then call TIdFTP.List() to retrieve the names of its files and subdirectories, then loop through the TIdFTP.DirectoryListing calling TIdFTP.Get() on each filename and store each subdirectory name into your own local list, then finally repeat the above steps on each subdirectory in your local list.
For example:
Procedure DownloadFolder(ARemoteFolder, ALocalFolder: string);
Var
SubFolders: TStringList;
I: Integer;
Begin
ALocalFolder := IncludeTrailingPathDelimiter(ALocalFolder);
ForceDirectories(ALocalFolder);
SubFolders := TStringList.Create;
Try
FTP.ChangeDir(ARemoteFolder);
FTP.List;
For I := 0 to FTP.DirectoryListing.Count-1 do
Begin
If FTP.DirectoryListing[I].ItemType = ditFile then
Begin
FTP.Get(FTP.DirectoryListing[I].FileName, ALocalFolder + FTP.DirectoryListing[I].FileName);
End
Else if FTP.DirectoryListing[I].ItemType = ditDirectory then
Begin
if (FTP.DirectoryListing[I].FileName <> '.') and FTP.DirectoryListing[I].FileName <> '..') then
SubFolders.Add(FTP.DirectoryListing[I].FileName);
End;
End;
For I := 0 to SubFolders.Count-1 do
Begin
DownloadFolder(ARemoteFolder + '/' + SubFolders[I], ALocalFolder + SubFolders[I]);
End;
Finally
SubFolders.Free;
End;
End;
DownloadFolder('/StartingDir', 'C:\Downloaded');

It is necessary to add the condition:
Else if ((IdFTP.DirectoryListing[I].ItemType = ditDirectory) and
(Length(IdFTP.DirectoryListing[I].FileName) > 2)) then
to avoid ".." as the directory name

Related

How do I populate a Treeview via FTP

Scenario
I'm trying to duplicate the standard way to fill a Treeview with directories/folders from a folder structure, starting at the root, but using IdFTP to get the structure from a remote server instead of my local hard drive. I'd like the result to look similar to clients like Filezilla.
I used this reasonably standard code from the Swiss Delphi Centre (which works to display my hard drive's structure) and then modified it to use IdFTP.ChangeDir(Directory) and IdFTP.List instead of FindFirst() and FindNext().
Problem
I seem to have got myself in a muddle as it is not correctly 'unwinding' the recursion so that once it traverses down the /cpanel/cashes/config directories on the remote server it doesn't return and traverse all the other directories hanging off the root but exits the procedure without displaying anything else. Also it doesn't seem to show all the top level folders but this could be simply due to the order that IdFTP.List returns them in
Can anyone tell me what I have done wrong here?
If you can also tell me how I should get the root (/) shown as well that would be very helpful
(I've commented out displaying non directories as I only want folders at this stage)
What I expected to see Copied from Filezilla
What I did see Using a Ttreeview in Delphi
My Code
procedure TForm2.Button1Click(Sender: TObject);
var StartingDir : string;
begin
TreeView1.Items.BeginUpdate;
try
StartingDir := '/';
Screen.Cursor := crHourGlass;
TreeView1.Items.Clear;
FTPconnect; //procedure to connect to remote server
GetDirectories(TreeView1, StartingDir, nil, True);
FTPDisconnect; //procedure to disconnect from remote server
finally
TreeView1.Items.EndUpdate;
Screen.Cursor := crDefault;
end;
end;
procedure TForm2.GetDirectories(Tree: TTreeView; Directory: string; Item: TTreeNode; IncludeFiles: Boolean);
var
ItemTemp: TTreeNode;
DirItemType : TIdDirItemType ;
Filename , NewStartingDirectory: string;
i : Integer;
begin
Tree.Items.BeginUpdate;
IdFTP.ChangeDir(Directory);
IdFTP.List; //get directory of remote folder
i:=0;
repeat
DirItemType := IdFTP.DirectoryListing[I].ItemType;
Filename := IdFTP.DirectoryListing[I].FileName;
If (DirItemType = ditDirectory) and (Filename <> '.') and (Filename <> '..')then
begin
if DirItemType = ditDirectory then
Item := Tree.Items.AddChild(Item, Filename);
ItemTemp := Item.Parent;
if Directory = '/' then
NewStartingDirectory := Directory + Filename
else
NewStartingDirectory := Directory + '/' +Filename;
GetDirectories(Tree, NewStartingDirectory, Item, IncludeFiles);
Item := ItemTemp;
end
else
if IncludeFiles then
begin //this bit commented out as we only want to see directories
// if (Filename <> '.') and (Filename <> '..') then
// Tree.Items.AddChild(Item, Filename);
end;
inc(i);
until i = IdFTP.DirectoryListing.Count;
Tree.Items.EndUpdate;
end;
Swiss Delhpi Centre's code (for comparison)
procedure TForm1.Button1Click(Sender: TObject);
var
Node: TTreeNode;
Path: string;
Dir: string;
begin
Dir := 'c:\temp';
Screen.Cursor := crHourGlass;
TreeView1.Items.BeginUpdate;
try
TreeView1.Items.Clear;
GetDirectories(TreeView1, Dir, nil, True);
finally
Screen.Cursor := crDefault;
TreeView1.Items.EndUpdate;
end;
end;
procedure TForm1.GetDirectories(Tree: TTreeView; Directory: string; Item: TTreeNode; IncludeFiles: Boolean);
var
SearchRec: TSearchRec;
ItemTemp: TTreeNode;
begin
Tree.Items.BeginUpdate;
if Directory[Length(Directory)] <> '\' then Directory := Directory + '\';
if FindFirst(Directory + '*.*', faDirectory, SearchRec) = 0 then
begin
repeat
if (SearchRec.Attr and faDirectory = faDirectory) and (SearchRec.Name[1] <> '.') then
begin
if (SearchRec.Attr and faDirectory > 0) then
Item := Tree.Items.AddChild(Item, SearchRec.Name);
ItemTemp := Item.Parent;
GetDirectories(Tree, Directory + SearchRec.Name, Item, IncludeFiles);
Item := ItemTemp;
end
else if IncludeFiles then
if SearchRec.Name[1] <> '.' then
Tree.Items.AddChild(Item, SearchRec.Name);
until FindNext(SearchRec) <> 0;
FindClose(SearchRec);
end;
Tree.Items.EndUpdate;
end;
I've looked on SO here - too complicated and wrong language and here - similar to the Swiss Delphi Centre and here - wrong language and not sure what its doing.
if it's better to use a TlistView, can you please show me the equivalent code to use that instead?
Untested:
I made the TIdFTP variable a parameter, since TTreeView was also one and it should be done consistently, not archaic.
Using for loops instead of repeat until.
Eliminating IncludeFiles when it wasn't used anyway.
Eliminating weird logic to always get the new TreeNode's parent.
Not locking the TreeView anymore - do this once before calling this method and unlock it after calling - otherwise you do that dozens of times in vain.
Basic logic is as I wrote in the comments:
Store all folder strings into your own list and avoid recursion at this point.
Fix the path to be concatenated once, not with every iteration of a loop.
Go through that list to do the recursion - at this point the state of FTP is irrelevant and you won't mess up listings at different levels.
Of course, release the created instance of the StringList.
procedure TForm2.GetFolders
( Ftp: TIdFTP // The source, from which we read the content
; Tree: TTreeView // The destination, which we want to fill
; ParentNode: TTreeNode // Node under which all new child nodes should be created
; Path: String // Starting directory
);
var
NewNode: TTreeNode; // New child in the tree
Filename: String; // Check against unwanted folder entries
i: Integer; // Looping over both lists
sl: TStringList; // Collect folders only
begin
FTP.ChangeDir( Path );
FTP.List; // Entire remote listing
sl:= TStringList.Create; // Collect all entries we're interested in
try
for i:= 0 to FTP.DirectoryListing.Count- 1 do begin // For each entry
Filename:= FTP.DirectoryListing[i].FileName;
if (FTP.DirectoryListing[i].ItemType= ditDirectory) // Only folders
and (Filename<> '.')
and (Filename<> '..') then begin
sl.Add( Filename ); // Only the name, not the full path
end;
end;
// Do this only once
if Path<> '/' then Path:= '/'+ Path+ '/';
for i:= 0 to sl.Count- 1 do begin // All collected folders
NewNode:= Tree.Items.AddChild( ParentNode, sl[i] ); // Populate tree
GetFolders( Ftp, Tree, NewNode, Path+ sl[i] ); // Recursion of folder name + current path
end;
finally
sl.Free;
end;
end;
Untested, but should compile.

ICS FTP - Function to check if folder exists on the ftp server

I'm trying to create a function to check if a folder exists using Overbyte ICS FTP component.Using the DIR command from the icsftp does not display anything in my memo log.
I'm interested in parsing the result of the dir command into a stringlist in order to search for a specific folder.
For the moment I use an indy function like this. How can I make the same thing with ICS?
function exista_textul_in_stringlist(const stringul_pe_care_il_caut:string; stringlistul_in_care_efectuez_cautarea:Tstringlist):boolean;
begin
if stringlistul_in_care_efectuez_cautarea.IndexOf(stringul_pe_care_il_caut) = -1 then
begin
result:=false;
//showmessage('Textul "'+text+'" nu exista!' );
end
else
begin
result:=true;
//showmessage('Textul "'+text+'" exista la pozitia '+ inttostr(ListBox.Items.IndexOf(text)));
end;
end;
function folder_exists_in_ftp(folder_name_to_search_for,ftp_hostname,ftp_port,ftp_username,ftp_password,ftp_root_folder:string;memo_loguri:Tmemo):boolean;
Var
DirList : TStringList;
ftp:Tidftp;
antifreeze:TidAntifreeze;
var i,k:integer;
begin
dateseparator:='-';
Result := False;
DirList := TStringList.Create;
ftp:=tidftp.Create;
antifreeze:=TidAntifreeze.Create;
try
antifreeze.Active:=true;
ftp.Host:=ftp_hostname;
ftp.Port:=strtoint(ftp_port);
ftp.username:=ftp_username;
ftp.password:=ftp_password;
ftp.Passive:=true;
ftp.Connect;
ftp.ChangeDir(ftp_root_folder);
ftp.List(DirList, folder_name_to_search_for, True);
if DirList.Count > 0 then begin
k := DirList.Count;
DirList.Clear; // DIRLIST will hold folders only
for i := 0 to k - 1 do begin
if (ftp.DirectoryListing.Items[i].FileName <> '.') and (ftp.DirectoryListing.Items[i].FileName <> '..') then begin
if ftp.DirectoryListing.Items[i].ItemType = ditDirectory then begin
DirList.Add(ftp.DirectoryListing.Items[i].FileName);
end;
end;
end;
end;
if exista_textul_in_stringlist(folder_name_to_search_for,DIRLIST) then
begin
Result := True;
memo_loguri.Lines.Add(datetimetostr(now)+' - caut folderul "'+folder_name_to_search_for+'" in directorul ftp "'+ftp_root_folder+'" => EXISTS!');
end
ELSE
begin
result:=false;
memo_loguri.Lines.Add(datetimetostr(now)+' - caut folderul "'+folder_name_to_search_for+'" in directorul ftp "'+ftp_root_folder+'" => NOT exists!');
end;
finally
ftp.Free;
antifreeze.Free;
DirList.Free;
end;
end;
I assume you are using the latest released version of OverbyteIcs (ICS-V8.16 (Apr, 2015)).
If you just need to check if a remote directory exists its a good recommendation mentioned in the other answer to avoid a list (it could be a time consuming operation if a lot of files and folders are returned).
I suggest you just try to be "optimistic" and change to the remote dir you wish to investigate using FTP.Cwd. If this call return true the folder of course exists, and if you plan to continue with the same client you have to change back to the original dir. On the other hand, if the call fails, the directory does not exist if the ftp server reponds with code 550.
I have included a simple sample doing the above (however, it does not provide the "change-back-to-original-dir-on-success" feature):
uses
...
OverbyteIcsFtpCli;
function FtpRemoteDirExists(
HostName: String;
UserName: String;
Password: String;
HostDirToCheck : String ) : Boolean;
const
cFtpCode_FileOrDirNotExists = 550;
var
FTP: TFtpClient;
begin
FTP := TFtpClient.Create(nil);
try
FTP.HostName := HostName;
FTP.Passive := True;
FTP.Binary := True;
FTP.Username := UserName;
FTP.Password := Password;
FTP.Port := '21';
if not FTP.Open then
raise Exception.Create('Failed to connect: ' + FTP.ErrorMessage);
if (not FTP.User) or (not FTP.Pass) then
raise Exception.Create('Failed to login: ' + FTP.ErrorMessage);
FTP.HostDirName := HostDirToCheck;
if FTP.Cwd then
Result := True
else
begin
if FTP.StatusCode = cFtpCode_FileOrDirNotExists then
Result := False
else
raise Exception.Create('Failed to change dir: ' + FTP.ErrorMessage);
end;
finally
FTP.Free;
end;
end;
You better use a command like SIZE (TFtpClient.Size) or MLST (TFtpClient.Mlst) to check for file existence.
Using LIST is quite an overkill.

Can't delete folder from a different partition

I'm having a problem when deleting folders that are on a different partition (E:/) from my software. I can delete files, using the DeleteFile function, but I'm not able to delete a folder using the code below:
function RemoveDirectory(strDir : String) : Boolean;
var
SearchRec : TSearchRec;
strFile : String;
nResult : Integer;
begin
try
Result := false;
nResult := FindFirst(strDir + '*', faAnyFile, SearchRec);
while (nResult = 0) do
begin
if (SearchRec.Name <> '.') and (SearchRec.Name <> '..') then
begin
strFile := strDir + SearchRec.Name;
if FileExists(strFile) then
DeleteFile(strFile)
else if DirectoryExists(strFile) then
RemoveDirectory(strFile);
end;
nResult := FindNext(SearchRec);
end;
Result := RemoveDir(strDir);
finally
FindClose(SearchRec);
end;
end;
With this code I can delete folders that are on the same partition from my software. Somebody knows what's going on? Is it because it's on a different partition?
You are trying to remove directories while you still have open search handles. Since this is a recursive function, if the directory hierarchy is deep, you would have multiple search handles open at a time and that is a lot of system resources being used when the deeper folders are reached.
It is better to collect the immediate subfolders into a temp list, then you can close the current search handle before iterating that list. This way, there is ever only 1 search handle active at a time, and there is no search handle active when each folder is actually being deleted.
Try this:
function RemoveDirectory(strDir : String) : Boolean;
var
SearchRec : TSearchRec;
nResult,i : Integer;
SubFolders: TStringList;
begin
SubFolders := nil;
try
strDir := IncludeTrailingPathDelimiter(strDir);
nResult := FindFirst(strDir + '*', faAnyFile, SearchRec);
if (nResult = 0) then
try
repeat
if (SearchRec.Attr and faDirectory) = 0 then
DeleteFile(strDir + SearchRec.Name)
else
begin
if (SearchRec.Name <> '.') and (SearchRec.Name <> '..') then
begin
if not Assigned(SubFolders) then SubFolders := TStringList.Create;
SubFolders.Add(strDir + SearchRec.Name);
end;
end;
until FindNext(SearchRec) <> 0;
finally
FindClose(SearchRec);
end;
if Assigned(SubFolders) then
begin
for i := 0 to SubFolders.Count-1 do
RemoveDirectory(SubFolders[i]);
end;
finally
SubFolders.Free;
end;
Result := RemoveDir(strDir);
end;
If this still fails, then someone else outside of your app/loop is actually using the directories, and you can use a tool like SysInternals Process Explorer to check that.
DeleteFile() is boolean function and you can receive only information was it successful or not. If you want more details return to the plain old Erase():
var f: file;
begin
AssignFile(f,strFile);
Erase(f);
end;
Here, if Erase() is not completed, an exception will be raised and you can receive more info, especially in the debugging phase.

Determining which folder in a directory is the latest by “modified date”?

How to determine which folder in a windows directory is the latest by 'modified date' ?
Like this one, but for folder not file.
I need to create a function like GetLastModifiedFolderName('D:\LogFolder\'):string;
After some suggestion below (from MBo) then reading some findfirst reference. I modified the answered link become like this one :
function TForm1.GetLastModifiedFolderName(AFolder: String): string;
var
sr: TSearchRec;
aTime: Integer;
begin
Result := '';
aTime := 0;
if FindFirst(IncludeTrailingPathDelimiter(AFolder)+'*',faDirectory, sr) = 0 then
begin
// directory found
repeat
if (sr.Attr and faDirectory)=faDirectory then
begin
// directory only
if (sr.Name <> '.') and (sr.name<>'..') then
begin
// exclude '.' and '..' directory
if sr.Time > aTime then
begin
aTime := sr.Time;
Result := sr.Name;
end;
end;
end;
until FindNext(sr) <> 0;
FindClose(sr);
end else
begin
// not found
Result:='-1';
end;
end;
You can use slightly modified function from linked answer. Because you need folders only, just check that filesystem object (found by FindXX functions) is a directory:
if (sr.Attr and faDirectory) = faDirectory ...
P.S. Note that fresh Delphi versions include System.IOUtils unit with variety of useful methods.

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;

Resources