Delphi 和 Indy TIdFTP:将所有文件从服务器上的一个文件夹复制到另一个文件夹

Delphi and Indy TIdFTP: Copy all files from one folder on the server to another

我正在使用 TIdFTP (Indy 10.6) 作为客户端应用程序,我需要能够将所有文件从服务器上的一个文件夹复制到另一个文件夹。这能做到吗?

我知道如何重命名或移动文件,我们可以使用 TIdFTP.Rename(Src, Dst)。 副本呢?我是否需要将 Get()Put() 与新路径/名称一起使用,因为服务器中的文件数量可能超过 500,000 个文件。

在我们公司,我们有一些文件的大小超过了 1.5 GB。通过使用我的代码,它会消耗大量内存并且文件不会从一个目录复制到另一个目录:在更少的代码中,源目录被命名为 "Fichiers" 并且目标目录被命名为 "Sauvegardes".

这是我的代码:

var
  S , directory  : String;
  I: Integer;
  FichierFTP : TMemoryStream;
begin

  IdFTP1.Passive := True;

  idftp1.ChangeDir('/Fichiers/'); 
  IdFTP1.List();

  if IdFTP1.DirectoryListing.Count > 0 then begin

    IdFTP1.List();

    for I := 0 to IdFTP1.DirectoryListing.Count-1 do begin

      with IdFTP1.DirectoryListing.Items[I] do begin

        if ItemType = ditFile then begin

          FichierFTP := TMemoryStream.Create;

          S := FileName;

          idftp1.Get( FileName , FichierFTP , false );

          Application.ProcessMessages
          idftp1.ChangeDir('/Sauvegardes/' ); 
          idftp1.Put(FichierFTP , S );

          Application.ProcessMessages;
          FichierFTP.Free;  
        end;
      end;
    end;
    IdFTP1.Disconnect;
  end;

有没有人有这方面的经验?我怎样才能更改我的代码来解决这个问题?

FTP 协议中没有规定,因此 TIdFTP 中没有方法,一次 copy/move 多个 文件.一次只能copy/move个人个文件。

将文件从一个 FTP 文件夹移动 到另一个文件夹很容易,可以使用 TIdFTP.Rename() 方法完成。但是,复制 文件通常需要发出单独的命令,先将文件下载到本地,然后再将其重新上传到新路径。

一些 FTP 服务器支持 复制 文件的自定义命令,因此您不需要在本地 download/upload 它们。例如,ProFTPD 的 mod_copy 模块为此实现了 SITE CPFR/CPTO 命令。如果你的FTP服务器支持这样的命令,你可以使用TIdFTP.Site()方法,eg:

Item := IdFTP1.DirectoryListing[I];
if Item.ItemType = ditFile then
begin
  try
    IdFTP1.Site('CPFR ' + Item.FileName);
    IdFTP1.Site('CPTO /Sauvegardes/' + Item.FileName);
  except
    // fallback to another transfer option, see further below...
  end;
end;

如果这不起作用,另一种避免必须在本地复制每个文件的方法是在到同一 FTP 服务器的 2 个单独的 TIdFTP 连接之间使用站点到站点传输。如果服务器允许,可以使用TIdFTP.SiteToSiteUpload()TIdFTP.SiteToSiteDownload()方法让服务器给自己传输文件,eg:

IdFTP2.Connect;
...
Item := IdFTP1.DirectoryListing[I];
if Item.ItemType = ditFile then
begin
  try
    IdFTP1.SiteToSiteUpload(IdFTP2, Item.FileName, '/Sauvegardes/' + Item.FileName);
  except
    try
      IdFTP2.SiteToSiteDownload(IdFTP1, Item.FileName, '/Sauvegardes/' + Item.FileName);
    except
      // fallback to another transfer option, see further below...
    end;
  end;
end;
...
IdFTP2.Disconnect;

但是,如果根本无法使用此类命令,那么您将不得不求助于将每个文件下载到本地,然后重新上传。当以这种方式复制 一个大文件时,您应该使用TFileStream(或类似的)而不是TMemoryStream。不要在内存中存储大文件。如果内存管理器不能分配足够的内存来保存整个文件,你不仅会冒内存错误的风险,而且一旦内存被分配和释放,内存管理器将保留它以供以后重用,它不会得到回到了OS。这就是为什么在传输大文件时内存使用率如此之高,即使在所有传输都已完成之后也是如此。

如果您真的想使用 TMemoryStream,请仅将其用于较小的文件。您可以在下载文件之前检查服务器上每个文件的大小(通过 TIdFTPListItem.Size 如果可用,否则通过 TIdFTP.Size()),然后选择合适的 TStream-派生 class用于该传输,例如:

const
  MaxMemoryFileSize: Int64 = ...; // for you to choose...
var
  ...
  FichierFTP : TStream;
  LocalFileName: string;
  RemoteFileSize: Int64;

Item := IdFTP1.DirectoryListing[I];
if Item.ItemType = ditFile then
begin
  LocalFileName := '';
  if Item.SizeAvail then
    RemoteFileSize := Item.Size
  else
    RemoteFileSize := IdFTP1.Size(Item.FileName);
  if (RemoteFileSize >= 0) and (RemoteFileSize <= MaxMemoryFileSize) then
  begin
    FichierFTP := TMemoryStream.Create;
  end else
  begin
    LocalFileName := MakeTempFilename;
    FichierFTP := TFileStream.Create(LocalFileName, fmCreate);
  end;
  try
    IdFTP1.Get(Item.FileName, FichierFTP, false);
    IdFTP1.Put(FichierFTP, '/Sauvegardes/' + Item.FileName, False, 0);
  finally
    FichierFTP.Free;
    if LocalFileName <> '' then
      DeleteFile(LocalFileName);
  end;
end;

您还可以对此进行其他优化,例如创建一个具有预先设定大小 Capacity 的单个 TMemoryStream,然后将其重复用于不会超过 [=29] 的多次传输=].


因此,将所有这些放在一起,您最终可能会得到如下内容:

var
  I: Integer;
  Item: TIdFTPListItem;
  SourceFile, DestFile: string;
  IdFTP2: TIdFTP;
  CanAttemptRemoteCopy: Boolean;
  CanAttemptSiteToSite: Boolean;

  function CopyFileRemotely: Boolean;
  begin
    Result := False;
    if CanAttemptRemoteCopy then
    begin
      try
        IdFTP1.Site('CPFR ' + SourceFile);
        IdFTP1.Site('CPTO ' + DestFile);
      except
        CanAttemptRemoteCopy := False;
        Exit;
      end;
      Result := True;
    end;
  end;

  function CopyFileSiteToSite: Boolean;
  begin
    Result := False;
    if CanAttemptSiteToSite then
    begin
      try
        if IdFTP2 = nil then
        begin
          IdFTP2 := TIdFTP.Create(nil);
          IdFTP.Host := IdFTP1.Host;
          IdFTP.Port := IdFTP1.Port;
          IdFTP.UserName := IdFTP1.UserName;
          IdFTP.Password := IdFTP1.Password;
          // copy other properties as needed...
          IdFTP2.Connect;
        end;
        try
          IdFTP1.SiteToSiteUpload(IdFTP2, SourceFile, DestFile);
        except
          IdFTP2.SiteToSiteDownload(IdFTP1, SourceFile, DestFile);
        end;
      except
        CanAttemptSiteToSite := False;
        Exit;
      end;
      Result := True;
    end;
  end;

  function CopyFileManually: Boolean;
  const
    MaxMemoryFileSize: Int64 = ...;
  var
    FichierFTP: TStream;
    LocalFileName: String;
    RemoteFileSize: Int64;
  begin
    Result := False;
    try
      if Item.SizeAvail then
        RemoteFileSize := Item.Size
      else
        RemoteFileSize := IdFTP1.Size(SourceFile);
      if (RemoteFileSize >= 0) and (RemoteFileSize <= MaxMemoryFileSize) then
      begin
        LocalFileName := '';
        FichierFTP := TMemoryStream.Create;
      end else
      begin
        LocalFileName := MakeTempFilename;
        FichierFTP := TFileStream.Create(LocalFileName, fmCreate);
      end;
      try
        IdFTP1.Get(SourceFile, FichierFTP, false);
        IdFTP1.Put(FichierFTP, DestFile, False, 0);
      finally
        FichierFTP.Free;
        if LocalFileName <> '' then
          DeleteFile(LocalFileName);
      end;
    except
      Exit;
    end;
    Result := True;
  end;

begin
  CanAttemptRemoteCopy := True;
  CanAttemptSiteToSite := True;

  IdFTP2 := nil;
  try
    IdFTP1.Passive := True;

    IdFTP1.ChangeDir('/Fichiers/'); 
    IdFTP1.List;

    for I := 0 to IdFTP1.DirectoryListing.Count-1 do
    begin
      Item := IdFTP1.DirectoryListing[I];

      if Item.ItemType = ditFile then
      begin
        SourceFile := Item.FileName;
        DestFile := '/Sauvegardes/' + Item.FileName;

        if CopyFileRemotely then
          Continue;

        if CopyFileSiteToSite then
          Continue;

        if CopyFileManually then
          Continue;

        // failed to copy file! Do something...
      end;
    end;
  finally
    IdFTP2.Free;
  end;

  IdFTP1.Disconnect;
end;