[Delphi] Funcionalidade de Atualização Automática – Parte 2

[Delphi] Funcionalidade de Atualização Automática - Parte 2Estavam ansiosos pela segunda parte dessa série de artigos, não é? 🙂
Em continuidade ao artigo anterior, prosseguiremos com o tutorial de implementação de atualização automática em um projeto Delphi. A primeira parte abordou apenas os recursos de apoio, como o registro em um serviço de hospedagem, instalação do FileZilla e criação dos arquivos INI. Hoje, partiremos para a codificação!

 

Em primeiro lugar, um atualizador automático exige, claro, uma tela de atualização. Para exemplo, faremos uma tela simples. Crie um novo formulário e adicione os seguintes componentes:

  • TBitBtn
  • TLabel
  • TProgressBar

Além disso, para acessar o diretório FTP, utilizaremos o componente TIdFTP, da paleta IndyClients. Arraste este componente para a tela de atualização e configure as seguintes propriedades:

  • Host: endereço do FTP, fornecido no FreeWHA
  • Username: login do FTP, geralmente o mesmo que o host
  • Password: senha do FTP
  • Passive: True
  • TransferType: ftBinary

 

Declare também uma variável de classe chamada “FnTamanhoTotal”. Ela será útil nos eventos do TIdFTP:

private
  FnTamanhoTotal: integer;

 

Para manter o código limpo, precisaremos também de uma função que irá nos retornar o diretório em que se encontra o executável:

{ declaração }
function ObterDiretorioDoExecutavel: string;
 
{ implementação }
function TForm1.ObterDiretorioDoExecutavel: string;
begin
  result := ExtractFilePath(Application.ExeName);
end;

 

Para ignorar as exceções de “Connection Closed Gracefully”, disparada por alguns servidores, inclua a unit IdException na uses. Ela será necessária no bloco except.

uses
  IdException;

 

 

O próximo método a ser implementado é a conexão com o servidor FTP, envolvido em um bloco try..except para tratar possíveis exceções:

{ declaração }
function ConectarAoServidorFTP: boolean;
 
{ implementação }
function TForm1.ConectarAoServidorFTP: boolean;
begin
  // desconecta, caso tenha sido conectado anteriormente
  if IdFTP1.Connected then
    IdFTP1.Disconnect;
  try
    IdFTP1.Connect;
    result := True;
  except
    On E:Exception do
    begin
      ShowMessage('Erro ao acessar o servidor de atualização: ' + E.Message);
      result := False;
    end;
  end;
end;

 

Em seguida, implementaremos dois métodos: um para ler o número da versão instalada no cliente e outro para ler o número da versão mais recente da aplicação, disponível no FTP. Como essas informações estão armazenadas em arquivos INI, usaremos a classe TIniFile para abri-los e o método ReadString para retornar o valor da chave.

uses
  IniFiles;
 
{ declaração }
function ObterNumeroVersaoLocal: smallint;
 
{ implementação }
function TForm1.ObterNumeroVersaoLocal: smallint;
var
  sNumeroVersao: string;
  oArquivoINI: TIniFile;
begin
  // abre o arquivo "VersaoLocal.ini"
  oArquivoINI := TIniFile.Create(ObterDiretorioDoExecutavel + 'VersaoLocal.ini');
  try
    // lê o número da versão
    sNumeroVersao := oArquivoINI.ReadString('VersaoLocal', 'Numero', EmptyStr);
 
    // retira os pontos (exemplo: de "1.0.0" para "100")
    sNumeroVersao := StringReplace(sNumeroVersao, '.', EmptyStr, [rfReplaceAll]);
 
    // converte o número da versão para número
    result := StrToIntDef(sNumeroVersao, 0);
  finally
    FreeAndNil(oArquivoINI);
  end;
end;

{ declaração }
function ObterNumeroVersaoFTP: smallint;
 
{ implementação }
function TForm1.ObterNumeroVersaoFTP: smallint;
var
  sNumeroVersao: string;
  oArquivoINI: TIniFile;
begin
  // deleta o arquivo "VersaoFTP.ini" do computador, caso exista,
  // evitando erro de arquivo já existente
  if FileExists(ObterDiretorioDoExecutavel + 'VersaoFTP.ini') then
    DeleteFile(ObterDiretorioDoExecutavel + 'VersaoFTP.ini');
 
  // baixa o arquivo "VersaoFTP.ini" para o computador
  IdFTP1.Get('atualizacao/VersaoFTP.ini', ObterDiretorioDoExecutavel + 'VersaoFTP.ini', True, True);
 
  // abre o arquivo "VersaoFTP.ini"
  oArquivoINI := TIniFile.Create(ObterDiretorioDoExecutavel + 'VersaoFTP.ini');
  try
    // lê o número da versão
    sNumeroVersao := oArquivoINI.ReadString('VersaoFTP', 'Numero', EmptyStr);
 
    // retira os pontos (exemplo: de "1.0.1" para "101")
    sNumeroVersao := StringReplace(sNumeroVersao, '.', EmptyStr, [rfReplaceAll]);
 
    // converte o número da versão para número
    result := StrToIntDef(sNumeroVersao, 0);
  finally
    FreeAndNil(oArquivoINI);
  end;
end;

 

Agora, segue a definição do método que irá baixar o arquivo de atualização (executável) do servidor FTP. Para isso, atente-se ao uso do Get do TIdFTP, que consiste em buscar o arquivo do servidor e gravá-lo em um diretório no disco. Repare que o Get também foi utilizado no método anterior para baixar o arquivo INI.

{ declaração }
procedure BaixarAtualizacao;
 
{ implementação }
procedure TForm1.BaixarAtualizacao;
begin
  try
    // deleta o arquivo "Atualizacao.rar", caso exista,
    // evitando erro de arquivo já existente
    if FileExists(ObterDiretorioDoExecutavel + 'Atualizacao.rar') then
      DeleteFile(ObterDiretorioDoExecutavel + 'Atualizacao.rar');
 
    // obtém o tamanho da atualização e preenche a variável "FnTamanhoTotal"
    FnTamanhoTotal := IdFTP1.Size('atualizacao/Atualizacao.rar');
 
    // faz o download do arquivo "Atualizacao.rar"
    IdFTP1.Get('atualizacao/Atualizacao.rar',
      ObterDiretorioDoExecutavel + 'Atualizacao.rar', True, True);
  except
    On E:Exception do
    begin
      // ignora a exceção "Connection Closed Gracefully"
      if E is EIdConnClosedGracefully then
        Exit;
 
      ShowMessage('Erro ao baixar a atualização: ' + E.Message);
 
      // interrompe a atualização
      Abort;
    end;
  end;
end;

 

Quando a atualização é baixada, precisamos descompactá-la para substituir o executável existente pelo novo executável. Essa operação pode ser realizada através do método RenameFile, para renomear o executável desatualizado, e pelo ShellExecute, para descompactar a atualização utilizando a linha de comando do 7-Zip.

uses
  ShellAPI;
 
{ declaração }
procedure DescompactarAtualizacao;
 
{ implementação }
procedure TForm1.DescompactarAtualizacao;
var
  sNomeArquivoAtualizacao: string;
begin
  // deleta o backup anterior, ou melhor, da versão anterior,
  // evitando erro de arquivo já existente
  if FileExists(ObterDiretorioDoExecutavel + 'Sistema_Backup.exe') then
    DeleteFile(ObterDiretorioDoExecutavel + 'Sistema_Backup.exe');
 
  // Renomeia o executável atual (desatualizado) para "Sistema_Backup.exe"
  RenameFile(ObterDiretorioDoExecutavel + 'Sistema.exe',
    ObterDiretorioDoExecutavel + 'Sistema_Backup.exe');
 
  // armazena o nome do arquivo de atualização em uma variável
  sNomeArquivoAtualizacao := ObterDiretorioDoExecutavel + 'Atualizacao.rar';
 
  // executa a linha de comando do 7-Zip para descompactar o arquivo baixado
  ShellExecute(0, nil, '7z',  PWideChar(' e -aoa ' +
    sNomeArquivoAtualizacao + ' -o' + ObterDiretorioDoExecutavel), '', SW_SHOW);
end;

 

Por fim, é necessário atualizar o número da versão no arquivo INI local. Este procedimento mantém o usuário informado de que está com a versão mais recente da aplicação e garante que a mesma atualização não seja baixada novamente.

{ declaração }
procedure AtualizarNumeroVersao;
 
{ implementação }
procedure TForm1.AtualizarNumeroVersao;
var
  oArquivoLocal, oArquivoFTP: TIniFile;
  sNumeroNovaVersao: string;
begin
  // abre os dois arquivos INI
  oArquivoFTP := TIniFile.Create(ObterDiretorioDoExecutavel + 'VersaoFTP.ini');
  oArquivoLocal := TIniFile.Create(ObterDiretorioDoExecutavel + 'VersaoLocal.ini');
  try
    // obtém o número da nova versão no arquivo "VersaoFTP.ini"...
    sNumeroNovaVersao := oArquivoFTP.ReadString('VersaoFTP', 'Numero', EmptyStr);
 
    // ... e grava este número no arquivo "VersaoLocal.ini"
    oArquivoLocal.WriteString('VersaoLocal', 'Numero', sNumeroNovaVersao);
  finally
    FreeAndNil(oArquivoFTP);
    FreeAndNil(oArquivoLocal);
  end;
end;

 

Opa, mas espere aí… lembram-se que adicionamos um componente TProgressBar na tela de atualização? Pois bem, para aprimorar a usabilidade da nossa tela de atualização, exibiremos o progresso do download nesse componente e um contador de porcentagem. Que chique, hein?
Para isso, no evento OnWorkBegin do componente TIdFTP, implemente o código abaixo, responsável por ajustar o preenchimento da TProgressBar conforme o tamanho da atualização:

ProgressBar1.Max := FnTamanhoTotal div 1024;

E, no evento OnWork, atualizaremos a TProgressBar enquanto a transferência do arquivo estiver em andamento:

procedure TForm1.IdFTP1Work(ASender: TObject; AWorkMode: TWorkMode;
  AWorkCount: Int64);
var
  nTamanhoTotal, nTransmitidos, nPorcentagem: real;
begin
  if FnTamanhoTotal = 0 then
    Exit;
 
  Application.ProcessMessages;
 
  // obtém o tamanho total do arquivo em bytes
  nTamanhoTotal := FnTamanhoTotal div 1024;
 
  // obtém a quantidade de bytes já baixados
  nTransmitidos := AWorkCount div 1024;
 
  // calcula a porcentagem de download
  nPorcentagem := (nTransmitidos * 100) / nTamanhoTotal;
 
  // atualiza o componente TLabel com a porcentagem
  Label1.Caption := Format('%s%%', [FormatFloat('##0', nPorcentagem)]);
 
  // atualiza a barra de preenchimento do componente TProgressBar
  ProgressBar1.Position := AWorkCount div 1024;
end;

 

Agora, sim, estamos prontos! Hora de fazer toda essa mágica funcionar! No evento de clique do botão, utilizaremos todos os métodos criados acima para processar a atualização:

procedure TForm1.BitBtn1Click(Sender: TObject);
var
  nNumeroVersaoLocal, nNumeroVersaoFTP: smallint;
begin
  if not ConectarAoServidorFTP then
    Exit;
 
  nNumeroVersaoLocal := ObterNumeroVersaoLocal;
  nNumeroVersaoFTP := ObterNumeroVersaoFTP;
 
  if nNumeroVersaoLocal < nNumeroVersaoFTP then
  begin
    BaixarAtualizacao;
    DescompactarAtualizacao;
    AtualizarNumeroVersao;
 
    ShowMessage('O sistema foi atualizado com sucesso!');
  end
  else
    ShowMessage('O sistema já está atualizado.');
end;

 

Para fazer um teste, é simples: no FTP, faça o upload do arquivo “Atualizacao.rar” (executável compactado) e incremente o número da versão no arquivo “VersaoFTP.ini” (por exemplo, de “1.0.0” para “1.0.1”). Agora é só clicar no botão de atualização e aguardar o processo!

Houve algum problema ou dificuldade na implementação do código acima? Fique tranquilo! Clique neste link e baixe um exemplo dessa funcionalidade (com alguns aprimoramentos) desenvolvido em Delphi XE7. Mesmo assim, na terceira e última parte dessa série de artigos, apresentarei algumas ressalvas, dicas e observações que talvez possam esclarecer possíveis dúvidas sobre essa funcionalidade.

 

Espero vocês na próxima semana para fecharmos esse assunto com chave de ouro!
Grande abraço!


Confira as outras partes desse artigo:

[Delphi] Funcionalidade de Atualização Automática – Parte 1
[Delphi] Funcionalidade de Atualização Automática – Parte 2
[Delphi] Funcionalidade de Atualização Automática – Parte 3


 

Compartilhe!
Share on FacebookTweet about this on TwitterShare on LinkedInShare on Google+Pin on PinterestEmail this to someone

33 comentários

  1. E para atualiza o banco de dados junto com o executavel como Faria. Já procurei artigo nesse sentido mas nunca achei.

    1. Olá, Joabe, tudo certo?
      Ótima pergunta! Na verdade, eu vou tratar essa questão na terceira parte dessa série.
      Mesmo assim, adiantando, depende de qual banco de dados você está utilizando. No meu caso, como uso Firebird, executo uma linha de comando da ferramenta isql para rodar os scripts no banco de dados durante a atualização. Para outros bancos, o mecanismo de atualização provavelmente será diferente.
      Por conta dessa necessidade, também já encontrei desenvolvedores que criaram um “atualizador do banco de dados”, configurado para ser executado junto com a atualização do executável.

      Abraço!

  2. Fala André! Parabéns pela segunda parte do post! Muito bom, bem explicado e ilustrado. Infelizmente estou com um probleminha… kkkkkkk

    Can’t open atualizacao/VersaoFTP.ini: No such file or directory

    Esqueci de algum detalhe? O seu exemplo de projeto funciona perfeitamente, até mesmo compilado no Xe2. Copiei e colei as funções para ObterNumeroVersaoFTP e ObterNumeroVersaoLocal. Para ter certeza que não foi erro de digitação.

    Obrigado, no aguardo.

    1. Boa tarde. Desculpa André. Erro meu, já está tudo ok! Obrigado, ótimo post!!

    2. Opa, Jessé, já conseguiu identificar o erro?
      Estava faltando o arquivo “VersaoFTP.ini” na pasta do FTP, não é? 🙂
      Continue seguindo o tutorial. Se houver mais algum problema, me informe!

      Abraço!

    3. Boa tarde André! Realmente faltava o arquivo, muito descuido meu! kkk
      Então, fiz algumas alterações apenas na forma de uso, não alterei o código. No meu computador o esquema funciona perfeitamente, Já na máquina dos clientes num primeiro momento a atualização não saiu de 0%. E na segunda tentativa, quando ele baixa o arquivo não termina o processo, ele cria um Sistema_Backup, mais não descompacta e executa a nova versão. Praticamente fica um executável corrompido. Ai tenho que ir na máquia descompactar o Atualizacao.rar e substituir o executável corrompido do usuário.

      Apresenta o erro: “Invalid argument to date encode.”

      A .dll e o 7.exe estão na pasta raiz de todos os usuários junto com o sistema.

      O quê poderia ser? Pois na minha máquina atualiza perfeitamente.

  3. Prezado André,

    Acompanho sei site a algum tempo e gostaria de agradecer pelos excelentes artigos, exemplos em Delphi e dicas em geral.

    Quanto a implementação da atualização automática, gostaria de saber no próximos artigos, como atualizar outros arquivos além do exe. (como uma dll por exemplo) e como os colegas acima já lembraram, como seria sua solução para um DB Firebird.

    Abraço.

    1. Olá, Marcos, tudo certo?
      Primeiramente, agradeço por acompanhar e blog e também pelo feedback!
      Bom, Marcos, em um dos meus projetos, tenho justamente essas 3 necessidades: atualizar o executável, copiar alguns arquivos de configuração e executar alguns scripts no banco de dados.

      Para copiar os arquivos, utilizo um arquivo BAT com instruções em DOS, como, por exemplo:

      cmd /c copy C:\Atualizacao\Arquivo.dll C:\Aplicativo

      Para executar os scripts, utilizo a linha de comando abaixo da ferramenta isql do Firebird:

      isql.exe C:\Aplicativo\Banco.fdb -m -b -i C:\Atualizacao\Script.sql -q -u SYSDBA -p masterkey

      Tecnicamente, a aplicação realiza os seguintes passos:
      – Baixa e descompacta a atualização, que contém o executável, o arquivo BAT, os arquivos a serem copiados e o script;
      – Copia o executável;
      – Executa o arquivo BAT, copiando os arquivos que vieram com a atualização;
      – Executa a linha de comando do isql para rodar os scripts;

      Para que isso funcione, claro, o arquivo BAT e o script devem sempre estar com um nome padrão para que a aplicação possa encontrá-los durante a atualização. No meu caso, optei por “Atualizacao.bat” e “Script.sql”.
      Na prática, confira o corpo do método que processa a atualização:

      try
        BaixarAtualizacao;
        DescompactarArquivos;
        CriarBackupExecutavel;
        AtualizarExecutavel;
        ExecutarArquivoBAT;
        CriarBackupBancoDados;
        ExecutarScript;
        ExcluirArquivosTemporarios;
        AtualizarNumeroVersaoArquivoINI;
        EnviarLogPorEmail;
      except
        On E:Exception do
        begin
          raise Exception.Create('Erro ao atualizar: ' + E.Message);
        end;
      end;

      Abraço!

  4. Utilizo Delphi 7 (Utilizo o Indy 10.0.52) , e ao abrir o programa da a seguinte mensagem de erro:

    Error reading IdFTP1.IPVersion: Property IPVersion does not exist

    1. Olá, Jolielson, tudo bem?
      O exemplo do artigo foi desenvolvido em Delphi XE7, então há propriedades de alguns componentes que não existem em versões anteriores.
      Neste caso, eu sugiro que a implementação seja feita passo a passo conforme orientado no artigo, ok?

      Abraço!

  5. Boa noite André, fiz um porte do seu app para Lazarus e gostaria de compartilhar com você para análise e se quiser, para divulgar com os demais usuários do seu site. As poucas alterações que fiz estão anotas no fonte. Eu estava procurando algo assim para os meus projetos, principalmente sobre como utilizar a partir de um site que oferta o acesso ftp gratuito :).
    No aguardo…
    Abraço!

    1. Olá, Wilton, tudo certo?
      Opa, claro que pode compartilhar! Com certeza vai ajudar muitos desenvolvedores!
      Envie um e-mail para “contato@andrecelestino.com” com o projeto para que eu possa compartilhá-lo aqui!

      Obrigado pela colaboração! Abraço!

  6. Olá André, tudo bom?

    Sou novato no assunto programação

    Fiz o seu tuto e deu quase tudo certo. A única coisa que não estou conseguindo é descompactar o arquivo baixado do site através da linha de comando. O que será que pode estar errado?

    Obrigado pelas dicas!

    1. Olá, Ruddy, tudo certo?
      Talvez há um erro de sintaxe na linha de comando do 7z. A sintaxe correta é:

      7z e -aoa C:\Arquivo.rar -oC:\Arquivo.exe

      A opção “-o” fica junto com o nome do arquivo descompactado (sem espaços).
      Para testar, você pode executar essa linha via prompt de comando do Windows. Dessa forma será mais fácil identificar o erro.

      Além disso, existe a possibilidade da aplicação não estar encontrando os arquivos do 7z (7z.exe e 7z.dll). Lembre-se de colocá-los na mesma pasta em que está a aplicação.

      Obrigado pela visita!
      Abraço!

  7. Olá, boa noite
    Como disse, ainda sou novato no assunto. Estava declarando as function e procedure no private, assim que mudei para public, consegui descompactar. Me ajudou muito, fiz apenas um ajuste para o programa encerrar e reiniciar automaticamente após atualizado
    Obrigado!

  8. Porque esse comando não está dando certo ?

    ShellExecute(0, nil, ‘7z’, PWideChar(‘ e -aoa ‘ +
    sNomeArquivoAtualizacao+’-o’ +ObterDiretorioDoExecutavel), ”, SW_SHOW);

    1. Olá, Joabe, tudo bem?
      Identifiquei que falta um espaço antes do “-o”. Vide o exemplo do artigo.
      Se mesmo assim funcionar, experimente rodar este comando pelo prompt do DOS e visualizar a mensagem de erro.

      Abraço!

  9. André, boa tarde. Fiz o meu atualizador e está funcionando certinho, mas tem um erro aqui:

    EZeroDivide with message ‘Floating point division by zero’

    Vi que esse erro é na hora de mostrar a porcentagem do download. Usando o Delphi 7!

    1. Olá, Guilherme!

      Certifique-se de que a instrução abaixo está no início do evento OnWork do IdFTP:

      if FnTamanhoTotal = 0 then
        Exit;

      Se mesmo assim o problema persistir, envie a unit para “contato@andrecelestino.com”.
      Abraço!

    2. André, consegui!

      Achei o erro:

      FnTamanhoTotal := IdFTP1.Size(‘atualizacao/Atualizacao.rar’);

      Consertei o caminho do arquivo e deu certo! =D

    3. Mas agora um probleminha:

      Ele só chega até 98%, e já dá a mensagem kkkkkkk

      O que pode ser?

    1. Olá, João Pedro, tudo bem?
      Este problema está relacionado com a segurança do sistema operacional. Por padrão, o Windows não permite a criação/alteração de arquivos dentro da pasta “Arquivos de Programas” sem a intervenção do usuário ou sem os privilégios de administrador. Para evitar esse evento, muitos programadores geralmente configuram a pasta da aplicação no C:\.

      Abraço!

  10. Boa Noite André.
    Primeira mente quero agradecer todo o seu trabalho. Me ajudou e muito.
    Porém tenho um pequeno problema: eu quero descompactar todos os arquivos que estão dentro da pasta Atualização.
    Qual seria a linha de comando?

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Preencha o campo abaixo * Time limit is exhausted. Please reload CAPTCHA.