19 de outubro de 2010

Fazendo Download e Upload usando FTP com Delphi

O FTP é um protocolo bastante antigo, anterior ao advento da internet mas largamente usado nela para transferência de arquivos - os populares download e upload. Até publiquei um post em julho explicando o funcionamento teórico do FTP. Neste post eu pretendo mostrar algo mais prático, incluindo num programa em Delphi a capacidade de fazer transferência de arquivos usando este protocolo.

O Delphi vem incluindo há bastante tempo em sua paleta de componentes o tratamento para um amplo conjunto de protocolos. Esses componentes são o produto de um projeto Open Source chamado Indy Internet Direct e estão disponíveis para várias plataformas de desenvolvimento - Delphi, C++, C#, etc. Eles encapsulam os detalhes de mais baixo nível do funcionamento de cada protocolo, simplificando bastante a sua utilização. Há componentes para considerar ambos os lados de uma comunicação, permitindo a criação tanto de aplicações servidoras em relação a um protocolo quanto aquelas que serão apenas Clients, isto é, consumidoras do serviço provido por um servidor.

No caso do FTP, vou me ater ao componente TIdFTP, que implementa o lado Client de uma comunicação para transferência de arquivos. O componente tem métodos para tratar os comandos inerentes a um Client FTP; portanto, a mesma instância pode tratar tanto o envio quanto o recebimento de arquivos, dentre outras possibilidades.

Qualquer que seja a operação desejada, o primeiro passo tem que ser a conexão com um servidor. O exemplo abaixo preenche as propriedades mais importantes do FTP:
IdFTP1.Disconnect();

IdFTP1.Host := 'ftp.abc71.com.br';
IdFTP1.Port := 21;
IdFTP1.Username := 'usuario_para_login';
IdFTP1.Password := 'senha';
IdFTP1.Passive := false; { usa modo ativo }
IdFTP1.RecvBufferSize := 8192;
try
{ Espera até 10 segundos pela conexão }
IdFTP1.Connect(true, 10000);
except
on E: Exception do
_Erro = E.Message;
end;
Os parâmetros Host e Port identificam o endereço do servidor FTP com o qual queremos conectar. O Username e a Password servem para fazer o login naquele servidor; são informações que você deve conhecer previamente - aqui, os dados fornecidos são apenas exemplos. Alguns servidores permitem que se conecte anonimamente; para isso é recomendado que se informe um endereço de email em Password. Caso você esteja tentando fazer a conexão a partir de um computador que só se conecta na rede através de um proxy, use a propriedade ProxySettings para complementar as configurações.

O valor de RecvBufferSize indica o tamanho do buffer interno para a recepção de dados; normalmente, ele não precisa ser alterado. Finalmente, a função Connect faz todo o trabalho para estabelecer a conexão, aguardando um tempo máximo especificado na própria função. O primeiro parâmetro passado a ela indica se o login deve ocorrer junto com a conexão. Se optar por não fazer o login neste momento, você terá que chamar manualmente a função Login.

Com a conexão estabelecida e o login feito com sucesso, enviar um arquivo (fazer o upload) é bastante simples. Basta chamar a função Put do TIdFTP:
IdFTP1.Put (AFileName, ADstFileName, false);
O primeiro parâmetro é o nome completo do arquivo que você quer mandar, incluindo o caminho. O segundo parâmetro é como o arquivo será nomeado no servidor, isto é, o seu arquivo pode ser enviado com um nome diferente. Para mantê-lo com o mesmo nome no servidor, simplesmente passe um texto vazio no lugar. Para mandar para uma pasta específica no servidor, informe o nome dela como parte do nome do arquivo. O componente FTP também possui métodos para navegar pela árvore de pastas, criar uma nova pasta e listar as pastas atualmente existentes, caso seja necessário. O último parâmetro passado ao Put sinaliza ao servidor o que fazer caso um arquivo com o mesmo nome já exista. Informar true fará com que o servidor inclua o conteúdo do seu arquivo no final do arquivo já existente.

Fazer o download de um arquivo também é simples. Basicamente, chame a função Get do TIdFTP:
IdFTP1.Get (AFileName, ADstFileName, true, false);
Aqui a situação é invertida em relação ao Put. O nome de arquivo passado no primeiro parâmetro identifica um arquivo no servidor, enquanto o segundo parâmetro é o nome com o qual queremos salvá-lo localmente. O terceiro parâmetro determina se o download sobreporá ou não um arquivo local que tenha o mesmo nome. Por fim, o último parâmetro indica se o download deve tentar prosseguir a partir do ponto onde uma tentativa anterior tenha sido interrompida.

Quando terminar o uso do componente, é conveniente chamar o método Quit para encerrar a conexão e devolver os recursos usados pelo sistema operacional.

As operações Get e Put podem ser bastante demoradas, dependendo do tamanho do arquivo sendo transferido. Então, para evitar que a aplicação pareça travada, é possível responder a alguns eventos no TIdFTP para dar ao usuário um retorno sobre o andamento da operação. O evento OnStatus notifica o programa a respeito de mudanças no estado da conexão. Já os eventos OnWorkBegin, OnWork e OnWorkEnd tratam do progresso da transferência do arquivo, informando o tamanho esperado do trabalho, a evolução da quantidade de bytes transferida enquanto a operação está em andamento e, por fim, sinalizando a conclusão da transferência. Com isso, podemos, por exemplo, incluir uma barra de progresso na tela para acompanhar toda a operação.

44 comentários :

Anônimo disse...

Meu amigo Parabéns muito bem explicado gostei .
Continue assim , o pessoal fala que acha de tudo na internet no Google sim é correto se acha de tudo mas nao com uma boa explicação .
E com bom conteudo.
Voce esta de parabéns.
Somos vizinhos de cidade nao sei se vc é de Bauru.

Fabiano Couto disse...

Muito bom artigo. Me ajudou muito.

Jamil Alves Fonseca disse...

Bom dia, cara adorei esse exemplo com ele consegui desenvolver minha aplicação sem problemas só que quando faço a funçao put nem sempre da certo por exemplo to enviando um arquivo de texto e um arquivo com extensao .v35 que é de uma aplicacao nossa dae ele da um erro "file successfuly uploaded" e nao envia nada da essa mensagem e ja para tudo isso quando começa a enviar arquivo e tem outros computadores que ele vai até quase o fim e da '0,5 não é valor inteiro valido' ja testei enviar outros arquivos como documento do word, etc.. e da o mesmo erro sera que pode me ajudar nao encontrei nada sobre isso

Luís Gustavo Fabbro disse...

Jamil

Pela descrição que você faz, tenho a impressão que o problema está no tratamento do progresso da transferência do arquivo. Revise o código que você está usando nos eventos OnWorkBegin, OnWork e OnWorkEnd. Depure-os ou vá comentando trechos desses códigos pra tentar descobrir o que há de errado.

Att.

PregaçãoAqui disse...

parabens, muito bom seu post.

PregaçãoAqui disse...

muito bom seu post. me ajudou muito.

Anônimo disse...

Muito bom consegui fácil, só gostaria de saber como baixar mais de um arquivo, na mesma function

Adriano

Luís Gustavo Fabbro disse...

Adriano

Não sei se entendi bem sua dúvida; pra baixar mais arquivos, é só chamar o Get para cada um deles. Se o problema está no acompanhamento do download, vc pode, por exemplo, mostrar janelas diferentes com a evolução de cada um ou colocar uma barra de progresso extra para indicar a qtde de arquivos sendo baixados.

[]s

Fabio Schunig disse...

Obrigado pela dica, funcionou 100% aqui.

O único problema que estava dando para mim era o erro "Illegal PORT command".
Para resolver, só precisei mudar para modo "passivo", alterando a linha:
IdFTP1.Passive := true; { usa modo passivo }

Abraços!
Fabio

Anônimo disse...

Bom dia,
Existe alguma forma de fazer o upload continuar de onde parou por ultimo, por ex. caso a maquina reinicie ou caia a conexao com a internet, o sistema continuar em vez de reiniciar o processo do zero?
Grato
Rodrigo

Luís Gustavo Fabbro disse...

Rodrigo
Vi na versão 10.5 do Indy do Delphy XE2 que há uma sobrecarga da função Put que permite informar se o conteúdo do "stream" sendo enviado deve ser anexado ao arquivo existente no Servidor.

Obviamente, isso só funcionará se o servidor FTP em questão der suporte ao "resume" de upload.

[]s

Anônimo disse...

Oi Luis,

A linha _Erro = E.Message; está dando erro. Substituir por ShowMessage...
O problema é que não tô conseguindo a conexao e não tenho
mensagem de erro específica.
O que faço ?

Luís Gustavo Fabbro disse...

Há alguns componentes de log do Indy que vc pode incluir em sua aplicação para ajudar a depurá-la. Eles são conhecidos como "interceptadores" (intercepts). Veja, por exemplo, o TIdLogFile, onde toda a comunicação com o servidor pode ser interceptada e gravada num arquivo.

[]s

Anônimo disse...

Luis, Bom dia,
Estou utilizando o IDFTP para upload de arquivos. Quando o arquivo chega no FTP o tamanho do mesmo aumenta consideravelmente, de 500 kb para mais de 1 mb. Você imagina o que possa estar acontecendo?
Obrigada!

Luís Gustavo Fabbro disse...

Verifique se não existe um arquivo com o mesmo nome no servidor. Nessa situação, a função put acrescentará o novo conteúdo no final do arquivo existente - se você passou TRUE no último parâmetro.

[]s

Anônimo disse...

Muito obrigada! Até mais!

Anônimo disse...

Luís:
Criei uma aplicação dessa forma e não está realizando o download.
Ao debugar constatei que a conexão é estabelecida normalmente, mas na hora de do download com o FTP1.Get não anda, simplesmente para, tentei monitorar pelo evento OnStatus e a última mensagem que ele exibe é: Starting FTP Transfer. Mas ai ele fica parado e não baixa o arquivo.
Em apenas uma das vezes apresentou o erro: SocketError # 10053 Só que não consegui identificar o que é.
Caso possa auxiliar fico agradecido.

Luís Gustavo Fabbro disse...

O erro 10053 está associado ao encerramento prematuro da conexão, forçado por algum outro software. Veja, por exemplo, se não há um firewall barrando a comunicação ou se há um proxy que sua aplicação tenha que levar em conta.

Dependendo do cenário, você pode obter um erro mais preciso adicionando log do próprio Indy ao seu programa. Veja, por exemplo, o TIdLogFile.

[]s

Lucas Carvalho disse...

Valeu pelo artigo.

Rogerio Kirschner disse...

Parabéns pelo blog! Minha aplicação esta funcionando direitinho usando suas orientações; porem em algumas máquinas esta dando time out no envio do arquivo (Put). Ele cria o arquivo no diretório destino mas com tamanho zero bytes.

Oque pode estar acontecendo pois em duais máquinas iguais com mesma configuração, numa funciona e na outra não - mesmo com firewall desativado.

Obrigado,

Rogério Kirschner

Luís Gustavo Fabbro disse...

Rogério

É bastante provável a existência de algum problema de infraestrutura nessas máquinas que você citou. Você pode usar, por exemplo, o PING para ver o tempo de resposta do servidor a partir dessas máquinas. Há ferramentas também para analisar eventuais redirecionamentos que as requisições dessas máquinas possam estar sofrendo.

Em todo caso, se for incontornável, ajuste a propriedade TransferTimeout do TIdFTP para compatibilizar com a comunicação nessas máquinas.

[]s

Paulo disse...

Gostaria de saber como fazer isso em uma thread utilizando as exceções caso ocorra erro de usuário, senha ou inatividade do servidor FTP, pois até agora não consegui fazer isso.

Luís Gustavo Fabbro disse...

Paulo

Que tipo de problema você encontrou para colocar o FTP numa thread separada? Há alguma mensagem de erro? Lembre-se que a atualização de componentes visuais VCL deve ser feita na thread principal, através do Synchronize.

Quanto a detectar exceções e outras situações de erro, você pode conectar o componente de FTP a um dos componentes de interceptação de comunicação do Indy, como o TIdLogStream.

[]s

Paulo disse...

Olá Luís, obrigado pelo retorno.

Na verdade só estou utilizando uma thread em meu sistema. A questão é com relação à conexão. Não estou conseguindo utilizar as exceções nas etapas de conexão do Indy FTP.

Por exemplo, se caso o servidor FTP não estiver online ou ativo, como criar uma exceção e pular ou fechar as tentativas de conexão?

A mesma coisa com o usuário, caso a conta não existir mais, como fazer o tratamento de exceção.

Como a thread que criei é executada no carregamento do sistema, caso ocorra algum erro de conexão, a tela do sistema acaba ficando congelada para o usuário e ele não consegue fazer mais nada.

Não sei se deu para entender, qualquer coisa eu explico melhor...

Abraços.

Luís Gustavo Fabbro disse...

Paulo

Em princípio, basta fazer a conexão dentro de um try/except para capturar qualquer exceção e, então poder tratá-las de forma apropriada. No caso de um erro ter ocorrido, a propriedade Connected do ftp estará FALSE; vc pode checar isso antes de tentar usar a conexão.

O exemplo do post usa um timeout de 10 segundos antes de uma exceção reportar a indisponibilidade do servidor. Nesse caso, o usuário ficará com a tela "congelada" por esse período. Diminua o timeout para dar uma resposta mais rápida ao usuário ou postergue a conexão até o momento em que ela realmente for usada.

Se vc não conseguir implementar assim, envie seu código para o email do blog. mostrando o ponto que está dando problema.

[]s

Paulo disse...

Olá Luís, mais uma vez obrigado pelo feedback.

Há duas questões a considerar, a primeira é que ao rodar a aplicação pelo compilador, uma mensagem de exceção é mostrada ou com erro de soquete ou domínio, ou ainda de usuário.
A segunda é que ao rodar a aplicação pelo executável após compilado, não é mostrada nenhuma mensagem de exceção e ao final da execução da thread a conexão é encerrada e a aplicação funciona normalmente sem problemas.

Portanto, apenas inseri uma verificação (if, else) para constatar que a conexão esteja False, assim como você sugeriu. De momento está atendendo às minhas necessidades. Quem sabe mais adiante eu me aprofunde nesse assunto e tente aperfeiçoar esse código que fiz aqui, pois ao meu ver deve haver alguma maneira de captar a resposta enviada pelo servidor FTP com o erro de conexão seja ele por usuário inválido, login errado, etc., seria mais ou menos isso:

try
IdFTP.Host:= 'ftp.com.br';
except
código a ser colocado aqui.
end;

try
IdFtp.UserName:= 'nomeusuario';
except
outro código.
end;

try
IdFtp.Password:= 'senha';,
except
outro código.
end;

try
IdFtp.Port:= 21;
except
outro código.
end;

De momento eu agradeço a atenção e a disponibilidade em me ajudar.

Um grande abraço e sucesso.

Luís Gustavo Fabbro disse...

Paulo

Os pontos do código onde você envolveu com um try/except são apenas atribuições de valores; a validação de todas essas informações é feita somente quando você comanda o Connect. Veja no primeiro quadro do post que essas atribuições podem ficar de fora do tratamento de exceção e que qualquer erro que ocorra na conexão será capturado pelo except, não importa o tipo do erro. Caso queira mostrar para o usuário a mensagem retornada, use o ShowMessage ou equivalente.

[]s

Anônimo disse...

Oi Luís... Muito legal a sua dica !!!

Na versão XE3 que estou usando, precisei alterar algumas coisas no código. Por exemplo: o método Connect não tem parâmetros, o método RecvBufferSize não existe e o método Quit está obsoleto.

Mesmo assim funcionou. Porém, fiquei com algumas dúvidas e se você puder me ajudar, eu fico muito grato.

1. Testei 5 vezes e acontece sempre a mesma coisa: o download não é feito progressivamente. São baixados aproximadamente 650Kb por vez. Aí pára por 6 segundos e, em seguida baixa mais 650Kb. E assim sucessivamente (o arquivo tem 3,5Mb). Será que isso é do componente mesmo ?.

2. Minha Progressbar não é atualizada porque o parâmetro AWorkCountMax do método WorkBegin não está retornado valor algum. Onde será que estou errando ?

Código:

procedure TForm1.IdFTP1Work(ASender: TObject; AWorkMode: TWorkMode;
AWorkCount: Int64);
begin
lblTaxa.Caption := IntToStr(AWorkCount) + ' bytes de ' + TotalBytes + ' bytes';
Progressbar1.Position := aWorkCount;
Progressbar1.Update;
Application.ProcessMessages;
end;

procedure TForm1.IdFTP1WorkBegin(ASender: TObject; AWorkMode: TWorkMode;
AWorkCountMax: Int64);
begin
// O tamanho total do arquivo - parâmetro AWorkCountMax - também não é recebido
TotalBytes := IntToStr(AWorkCountMax);
ProgressBar1.Max := AWorkCountMax;
lblTaxa.Caption := '0 bytes de ' + TotalBytes + ' bytes';
end;

procedure TForm1.IdFTP1WorkEnd(ASender: TObject; AWorkMode: TWorkMode);
begin
lblTaxa.Caption := 'Transferência completa.';
end;

Abraço e obrigado.

Adalberto.

Luís Gustavo Fabbro disse...

Adalberto

Fiz um teste com o XE2, onde a versão do indy tb é a 10, e realmentre o AWorkCountMax não está vindo alimentado no evento WorkBegin para operações de Get. Você pode contornar esse problema obtendo o tamanho do arquivo diretamente do servidor através da função do componente de FTP.

Quanto ao problema da demora, é provável que esteja relacionado com o tamanho do buffer. A propriedade RecvBufferSize não existe mais pois o buffer é controlado por uma instância de IOHandler (Ex: TIdIOHandlerStack) e pode ser plugada diretamente no TIdFTP. Pode ser tb alguma questão com sua infraestrutura - firewalls, etc.

[]s

Cezar Lopes disse...

Bom dia Luís Gustavo!

Estou tentando fazer download através de uma lista (TStringList) mas os nomes dos arquivos estão aparecendo com a data e a hora o que gera exceção ao tentar gravar ( como eles deveriam aparecer "exemplo.Txt" como eles aparecem "11-20-13 10:20PM exemplo.Txt" ). Então eu pergunto, tem como configurar o componente para que isso não aconteça ou tenho de tratar pelo código?

Obrigado.

Luís Gustavo Fabbro disse...

Cezar

Vc provavelmente está usando a função List sem parâmetros em conjunto com a propriedade LisResult. Há duas soluções possíveis:
1) Use a versão da função List que te permite omitir os detalhes de cada arquivo, obtendo apenas os nomes dos arquivos e diretórios.
2) Ou use a propriedade DirectoryListing para acessar os nomes sem adereços indicativos de data.

[]s

Marlon degra disse...
Este comentário foi removido pelo autor.
Luís Gustavo Fabbro disse...

Marlon

Que tipo de sistema de arquivos você tem no seu servidor que recebe os arquivos? No sistema FAT32, arquivos podem ter no máximo 4GB; no NTFS do Windows 8 e Server 2012, o máximo é implementado como 256TB. Embora eu não tenha feito testes, creio que o Indy seja capaz de lidar com arquivos grandes assim.

Há alguma mensagem de erro reportada?

[]s

Marlon degra disse...

Desculpe Luis por ter removido é que eu ia melhorar a Pergunta

Boa Tarde Luis Gustavo

tenho uma aplicação nos meus clientes que manda o banco de dados para meu ftp, consegui fazer isso tranquilamente até hj, pois os bancos de dados não ultrapassava 2gb, só que agora tenho dois clientes que dá um erro quando executa o comando 'PUT' pois os arquivo já estão mais ou menos em 3,5GB, não sei o que esta acontecendo pois no servidor de ftp não tem nenhum regra pra barrar os arquivos, e acredito que seja minha aplicação o problema, Você sabe como eu consigo resolver isso?

Marlon degra disse...

Sobre o tópico acima.......
Pergunta: Há alguma mensagem de erro reportada?

Resposta:
Assim que executa o comando PUT o erro que retorna é:
"value must be between 1 and 2147483647"

conversei com o pessoal que administra o FTP e ele falaram que não tem nenhuma regra pra bloquear arquivos grandes, logo em seguida fiz o teste enviando o arquivo através do FileZila e Windows Explorer e funciona certinho, portanto acredito que não deve ter nenhuma regra de bloqueio de arquivos grandes.

E vale lembrar que o sistema operacional é Windows Server..

Luís Gustavo Fabbro disse...

Marlon

Parece que a versão do componente Indy usada por vc não está suportando transferir arquivos tão grandes. Você pode se certificar disso usando uns dos "interceptadores" (intercepts) do Indy para gerar um arquivo de log. Use, por exemplo, o TIdLogFile; com ele, toda a comunicação com o servidor pode ser interceptada e gravada num arquivo. Assim, você pode ver em que ponto a comunicação está falhando.

Se for esse mesmo o caso, use um IOHanlder com a propriedade LageStream ligada para permitir arquivos maiores.

[]s

Marlon degra disse...

Bom Dia Luís Gustavo
coloquei o interceptador e o resultado foi este abaixo,
me parece que foi tudo tranquilo, apesar da mensagem de "Transfer complete" o erro ainda persistiu.

Sent 24/06/2014 11:22:42: PORT 10,1,1,5,218,10
Recv 24/06/2014 11:22:42: 200 PORT command successful.
Sent 24/06/2014 11:22:42: STOR SISTEMA-TERÇA.ZIP
Recv 24/06/2014 11:22:42: 150 Opening ASCII mode data connection for SISTEMA-TERÇA.ZIP.
Recv 24/06/2014 11:22:55: 226 Transfer complete.
Sent 24/06/2014 11:22:55: QUIT
Recv 24/06/2014 11:22:55: 221
Stat Disconnected.

Vou tentar usar o IOHandler, qualquer coisa te aviso
Por acaso você tem alguma sugestão de como usar esse IOHandler?

Luís Gustavo Fabbro disse...

Em tese, não há nada especial.... Basta criar o IOHandler, associá-lo ao idFTP e alimentar o valor do Stream dele no evento OnGetStreams.

[]s

[]s

Marlon degra disse...

Só para Esclarecimento
Pessoal, como vcs perceberam,estava com um problema que no momento de enviar um arquivo muito grande, mais que 2.147.483.647 Gb, exatamente no momento do "Idftp.Put(arquivo)" estava dando um problema, mais o que realmente aconteceu foi que eu estava trabalhando com um componente ProgressBar e que na verdade ele vai dar o status do envio do arquivo, certo! Bom mais ai é o seguinte quando eu informava o tamanho do ProgressBar.Max = TamanhoArquivo que é maior que 2.147.483.647, isso ultrapassa o valor da variável Inteira, que retornava um erro apenas no momento que o arquivo estava sendo enviado, portanto fica ai a dica a todos quando for trabalhar com ProgressBar, faça uma regra de três para não ultrapassar o valor ProgressBar=100, Certo. Ok um grande abraço

boynet disse...

Fiz tudo de acordo com o seu código mas retornou o seguinte erro na hora de conectar com o server FTP: Socket Error # 10049

uso o delphi 7

Luís Gustavo Fabbro disse...

O erro 10049 significa que seu programa está tentando acessar um endereço que não é válido no contexto do seu computador.

Você pode se certificar que o endereço que você quer acessar está correto testando-o como outro cliente FTP (Ex: Filezilla Client).

[]s

boynet disse...

testei com o Filezilla e deu certo sem erro algum mas quando testo com o delphi sempre retorna esse erro. mas tudo bem consegui resolver o problema baixando o delphi XE3 e escrevendo o mesmo código para ele :D

Turbo Drive disse...

Bom dia. Muito bom o artigo. Amigo eu consegui fazer a conexão FTP usando o delphi 7. Mas ao migrar a mesma aplicação para o XE5, está dando o erro: Failed to change directory.

with IdFtp2 do
begin
Disconnect;

Host := edtServFTP2.Text;
Port := StrToInt(edtPorta2.Text);
ReadTimeout := 0;
Username := edtUser2.Text;
Password := edtSenha2.Text;
Connect;
Passive := true;
ChangeDir('/'+edit1.text+'/'); <--- erro ocorre aqui
Put(ListBox1.Items[i]);

Disconnect;
end;

Não mudei nada no código apenas mudei do delphi 7 para o delphi XE5.

Você saberia dizer o que pode estar ocorrendo ?

Desde já agradeço a atenção.

Luís Gustavo Fabbro disse...

A versão mais nova que tenho do delphi é o XE2; nesta versão, o ChangeDir funcionou como esperado. A pasta informada no edit1 realmente existe? Alguns caracteres no texto podem influenciar o mal funcionamento: espaço em branco, barras, acentuação, etc.

[]s

Postar um comentário

OBS: Os comentários enviados a este Blog são submetidos a moderação. Por isso, eles serão publicados somente após aprovação.

Observação: somente um membro deste blog pode postar um comentário.