Fórum Live update na prática #340939

25/04/2007

0

Pessoal,

Há um certo tempo conversamos sobre algumas técnicas de como atualizar automaticamente o sistema usando um programa que faz o download da aplicação on-line. Bem, deu um pequeno problema na base de dados do fórum e perdemos a referência. Estou postando de novo pra gente voltar a discutir, pois embora esteja funcionando legal, quero compartilhar a idéia com todos e aprimorar se necessário. Ai vai:

[list:a6cbf17708]
[*:a6cbf17708]A imagem do aplicativo é a seguinte:
[URL=http://img77.imageshack.us/my.php?image=imagemgp8.jpg][img:a6cbf17708]http://img77.imageshack.us/img77/8798/imagemgp8.th.jpg[/img:a6cbf17708][/URL]

[*:a6cbf17708]Eu coloquei um image na parte superior pra mostrar o logotipo da empresa. No centro um componente do tipo TListView onde aparecerão os módulos necessários pra atualização. Depois alguns labels pra mostrar o progresso e a taxa de transferência de download. 1 TGauge, 1 TTimer e 1 TidFtp.
[*:a6cbf17708] Nas seções [b:a6cbf17708]public [/b:a6cbf17708]e [b:a6cbf17708]private[/b:a6cbf17708] eu declarei as variáveis, funções e procedimentos conforme abaixo:
...
const
  gsArqRemoto = ´Atualizacoes.amk´;
  gsArqLocal = ´Versoes.amk´;
...
type
   ...

  private
    procedure ApagarTemporarios;
    procedure LimparPastaTemp;
    procedure AtualizarINIVersoes(ASecao, ANomeVersao, AVersao: string);
    { Private declarations }
  public
    { Public declarations }
    gsPastaRemota : string;
    Tamanho_Arquivo: LongWord;
    STime: TDateTime;
    Tempo_Medio: Double;
    Bytes_Transf: Double;
    ModuloEmAtualizacao: string;
    gsPastaLocal: string;

    sArqLocal: string;
    sArqRemota: string;
    Modulo: string;
    Titulo: string;
    ModulosAbertos: TStringList;
    IniLocal: TIniFile;
    IniRemota: TIniFile;
    VersaoLocal: string;
    VersaoRemota: string;
    kbTotalDown: LongWord;
    procedure CriaArquivoVersoes;
    procedure PreencherModulosAbertos;
    procedure FecharModulosAbertos;
    function Conectar: Boolean;
    procedure LimpaLabels;
    procedure IncluiDownloads(AModulo, AVersaoAtual, AVersaoRemota: string;
      ATamanho: LongWord);
    procedure PosAtualizacao;
  end;

[*:a6cbf17708] A brincadeira é o seguinte: No diretório da aplicação mantenho um arquivo chamado Versoes.amk que contém a seguinte estrutura:
[Atualizacoes]
Modulo1=Operacao.exe
Versao1=vF6.1.04
Titulo1=Operacional

Modulo2=Cobranca.exe
Versao2=vF6.1.04
Titulo2=Faturamento/Cobrança

Modulo3=Comunica.exe
Versao3=vF6.1.04
Titulo3=Integração entre Filiais

Note que é um arquivo .INI renomeado para uma extensão qualquer. Declarei duas constantes pra dizer que o arquivo local contendo a informação de versão chama-se Versoes.amk e outra constante para dizer qual é o nome do arquivo que contem a mesma estrutura, mas que está no servidor FTP.
[*:a6cbf17708] Quando o meu usuário entra nesta ferramenta eu faço download do arquivo Atualizacoes.amk e saio comparando os dois arquivos e incluindo os programas que precisam ser atualizados em um listview, vejam:
procedure TfAtualiza.Timer1Timer(Sender: TObject);
var
  I: Integer;
begin
  try
    Screen.Cursor := crHourGlass;
    kbTotalDown := 0;
    Sleep(3000);
    lblListandoAtalizacoes.Caption := ´Verificando se há atualizações disponíveis´;
    lblListandoAtalizacoes.Font.Color := clBlue;
    Update;

    with idFtpAtualiza do
    begin
      {Tenta conectar-se ao servidor FTP da América}
      if Conectar then
      begin
        try
          IniLocal := TIniFile.Create(ExtractFilePath(Application.ExeName) + gsArqLocal);
          IniRemota := TIniFile.Create(ExtractFilePath(Application.ExeName) + gsArqRemoto);
          lstModulos.Items.Clear;
          {Faz o download do arquivo Remoto.ini pra comparar as versões}
          Get(sArqRemota, sArqLocal, True);

          {Faz as comparações necessárias e atualiza a lista de atualizações pendentes}
          for I := 1 to 50 do
          begin
            {Verifica se a chave existe, ou seja, se o "I" atual corresponde à algum módulo no arquivo. local.ini}
            if not IniRemota.ValueExists(´Atualizacoes´, ´Modulo´ + IntToStr(I)) then
              Break;

            {Nome e tamanho do Módulo}
            Modulo := IniRemota.ReadString(´Atualizacoes´, ´Modulo´ + IntToStr(I), Modulo);
            ModuloEmAtualizacao := Modulo;
            Tamanho_Arquivo := Size(gsPastaRemota + ´/´ + Modulo);

            VersaoLocal := IniLocal.ReadString(´Atualizacoes´, ´Versao´ + IntToStr(I), VersaoLocal);
            VersaoRemota := IniRemota.ReadString(´Atualizacoes´, ´Versao´ + IntToStr(I), VersaoRemota);

            {Verifica se há atualizações e então atualiza a lista de downloads}
            if (VersaoLocal <> VersaoRemota) or not
              (IniLocal.ValueExists(´Atualizacoes´, ´Modulo´ + IntToStr(I))) then
            begin
              IncluiDownloads(ModuloEmAtualizacao, VersaoLocal, VersaoRemota, Tamanho_Arquivo);
              kbTotalDown := kbTotalDown + Tamanho_Arquivo;
            end;
          end;
          idFtpAtualiza.Disconnect;

          IniLocal.Free;
          IniRemota.Free;

          if lstModulos.Items.Count > 0 then
          begin
            lstModulos.Visible := True;
            btnAtualizar.Enabled := True;
            lblTotalBytes.Caption := ´Total de bytes à baixar: ´ + FormatFloat(´#,,0´, kbTotalDown) + ´ Kb´;
          end
          else
          begin
            lstModulos.Visible := False;
            lblListandoAtalizacoes.Caption := ´Não há atualizações disponíveis´;
            lblListandoAtalizacoes.Font.Color := clRed;
            DeleteFile(gsPastaLocal + gsArqRemoto);
          end;
        except on E: Exception do
          begin
            MessageDlg(´Ocorreu um erro durante o processo.´ + 1313 + ´Mensagem original: ´ + 13 +
              E.Message + 1313 + ´O aplicativo será fechado.´, mtError, [mbOK], 0);
            Halt;
          end;
        end;
      end;
    end;
  finally
    Screen.Cursor := crDefault;
    Timer1.Enabled := False;
  end;
end;

Se não houverem atualizações disponíveis simplesmente deleto o arquivo Atualizacoes.amk e informo a mensagem de que não há necessidade de atualizar nada.
[*:a6cbf17708] Se houver atualizações disponíveis, basta o usuário clicar em [b:a6cbf17708]Atualizar agora[/b:a6cbf17708] que o sistema vai fazer o dowload de cada módulo:
procedure TfAtualiza.btnAtualizarClick(Sender: TObject);
var
  I: Integer;
  iContaAtualizacao: Integer;
begin
  try
    Screen.Cursor := crHourGlass;
    {Tenta conectar-se ao servidor FTP da América}
    if Conectar then
    begin
      with idFtpAtualiza do
      begin
        IniLocal := TIniFile.Create(ExtractFilePath(Application.ExeName) + gsArqLocal);
        IniRemota := TIniFile.Create(ExtractFilePath(Application.ExeName) + gsArqRemoto);
        {Re-ativa os eventos do TidFtp}
        OnWorkBegin := idFtpAtualizaWorkBegin;
        OnWork := idFtpAtualizaWork;
        try
          btnAtualizar.Enabled := False;
          btnFechar.Enabled := False;
          Tamanho_Arquivo := 1;

          {Aqui verifica se existem módulos abertos. Caso haja o usuário será alertado e os mesmos fechados na confirmação dele}
          PreencherModulosAbertos;
          if ModulosAbertos.Count > 0 then
          begin
            if MessageBox(Handle, ´Um ou mais módulos do sistema América encontram-se em execução.´ + #13 +
              ´Deseja fechá-los e continuar a atualização do sistema?´, ´Alerta´, MB_ICONQUESTION + MB_YESNO) = ID_YES then
              FecharModulosAbertos
            else
            begin
              btnAtualizar.Enabled := True;
              btnFechar.Enabled := True;
              Tamanho_Arquivo := 1;
              Exit;
            end;
          end;

          IniLocal := TIniFile.Create(ExtractFilePath(Application.ExeName) + gsArqLocal);
          IniRemota := TIniFile.Create(ExtractFilePath(Application.ExeName) + gsArqRemoto);
          {Aqui verifica se há atualizações e baixa o módulo}
          iContaAtualizacao := 0;
          for I := 1 to 20 do
          begin
            {Verifica se a chave existe, ou seja, se o "I" atual corresponde à algum módulo no arquivo. local.ini}
            if not IniRemota.ValueExists(´Atualizacoes´, ´Modulo´ + IntToStr(I)) then
              Break;

            {Nome e tamanho do Módulo}
            Modulo := IniRemota.ReadString(´Atualizacoes´, ´Modulo´ + IntToStr(I), Modulo);
            ModuloEmAtualizacao := Modulo;
            Tamanho_Arquivo := Size(gsPastaRemota + ´/´ + Modulo);

            VersaoLocal := IniLocal.ReadString(´Atualizacoes´, ´Versao´ + IntToStr(I), VersaoLocal);
            VersaoRemota := IniRemota.ReadString(´Atualizacoes´, ´Versao´ + IntToStr(I), VersaoRemota);

            {Efetua o download}
            if (VersaoLocal <> VersaoRemota) or not
              (IniLocal.ValueExists(´Atualizacoes´, ´Modulo´ + IntToStr(I))) then
            begin
              Inc(iContaAtualizacao);
              sArqRemota := gsPastaRemota + ´/´ + Modulo;
              sArqLocal := gsPastaLocal + ´\´ + Modulo;
              Tamanho_Arquivo := Size(sArqRemota);
              {Aqui verifica se o módulo está em execução.}
              {Em caso positivo avisa que não será possível fazer o download}

              Get(sArqRemota, sArqLocal, True);
              lstModulos.Items[iContaAtualizacao - 1].Checked := True;
              AtualizarINIVersoes(´Atualizacoes´, ´Versao´ + IntToStr(I), VersaoRemota);
            end;
          end;
          Disconnect;

          IniLocal.Free;
          IniRemota.Free;

          {Função desativada - Atualizando a cada download}
          //DeleteFile(gsPastaLocal + gsArqLocal);
          //RenameFile(gsPastaLocal + gsArqRemoto, gsPastaLocal + gsArqLocal);
          DeleteFile(gsPastaLocal + gsArqRemoto);

          lblListandoAtalizacoes.Caption := ´Todos os módulos listados foram atualizados com sucesso´;
          lblListandoAtalizacoes.Font.Color := clBlue;
          btnAtualizar.Enabled := False;
          btnFechar.Enabled := True;
          LimpaLabels;
          PosAtualizacao;
        except on E: Exception do
          begin
            MessageDlg(´Ocorreu um erro durante o processo.´ + #1313 +
              E.Message, mtError, [mbOK], 0);
            Halt;
          end;
        end;
      end;
    end;
  finally
    Screen.Cursor := crDefault;
  end;
end;

Antes de fazer o download eu verifico se existem módulos abertos. Faço isso procurando a referência da janela no Windows usando o FindWindow. Adiciono em uma TStringlist os módulos abertos e pergunto pro usuário se ele quer que o sistema mesmo feche ou se ele vai fechar manualmente.
A terceira chave de cada módulo é o título da janela do aplicativo. Aquela opção que preenchemos no Project >> Options. É por ai que sei se o módulo está ou não aberto. Daí preencho o TStringList:
procedure TfAtualiza.PreencherModulosAbertos;
var
  I: Integer;
begin
  IniRemota := TIniFile.Create(ExtractFilePath(Application.ExeName) + gsArqRemoto);
  ModulosAbertos.Clear;
  for I := 1 to 20 do
  begin
    {Verifica se a chave existe, ou seja, se o "I" atual corresponde à algum módulo no arquivo. local.ini}
    if not IniRemota.ValueExists(´Atualizacoes´, ´Modulo´ + IntToStr(I)) then
      Break;
    {Nome e tamanho do Módulo}
    Titulo := IniRemota.ReadString(´Atualizacoes´, ´Titulo´ + IntToStr(I), Titulo);
    if FindWindow(nil, PChar(Titulo)) > 0 then
      ModulosAbertos.Add(Titulo);
  end;
  IniRemota.Free;
end;

Se o usuário quiser que o sistema feche os módulos então rodo a função:
procedure TfAtualiza.FecharModulosAbertos;
var
  I: Integer;
begin
  IniRemota := TIniFile.Create(ExtractFilePath(Application.ExeName) + gsArqRemoto);
  for I := 0 to ModulosAbertos.Count - 1 do
    if FindWindow(nil, PChar(ModulosAbertos[I])) > 0 then
      PostMessage(FindWindow(nil, PChar(ModulosAbertos[I])), WM_CLOSE, 0, 0);
  IniRemota.Free;
end;

[*:a6cbf17708] Pra fazer o download primeiro conectamos no ftp e depois começamos o download de cada arquivo. Um detalhe, temos problemas com nosso atual servidor de FTP, então armazemos os arquivos em dois servidores. No atual e no disco virtual do Terra. O sistema é esperto, ele tenta conexão em um e se não conseguir redireciona para o Terra.
function TfAtualiza.Conectar: Boolean;
begin
  try
    with idFtpAtualiza do
    begin
      gsPastaRemota := ´/Executaveis/Atualizacoes´;
      gsPastaLocal := ExtractFilePath(Application.ExeName);
      sArqRemota := gsPastaRemota + ´/´ + gsArqRemoto;
      sArqLocal := gsPastaLocal + gsArqRemoto;
      Host := ´ftp.MEU-FTP.com.br´;
      Username := ´meu-usuario;
      Password := ´minha-senha´;
      Passive := True;

      if not Connected then
        Connect;

      {Desativa temporariamente os eventos do idFTP pra não mostrar o download do arquivo texto}
      OnWorkBegin := nil;
      OnWork := nil;

      try
        ChangeDir(gsPastaRemota);
      except
        Timer1.Enabled := False;
        Halt;
      end;

      List(nil);
      Result := True;
    end;
  except
    with idFtpAtualiza do
    begin
      gsPastaRemota := ´/´;
      gsPastaLocal := ExtractFilePath(Application.ExeName);
      sArqRemota := gsPastaRemota + ´/´ + gsArqRemoto;
      sArqLocal := gsPastaLocal + gsArqRemoto;
      lblListandoAtalizacoes.Caption := ´Tentando conexão com servidor alternativo...´;
      Host := ´ftp.discovirtual.terra.com.br´;
      Username := ´meu-usuario´;
      Password := ´minha-senha´;
      Passive := True;

      if not Connected then
        Connect;

      {Desativa temporariamente os eventos do idFTP pra não mostrar o download do arquivo texto}
      OnWorkBegin := nil;
      OnWork := nil;

      try
        ChangeDir(gsPastaRemota);
      except
        Timer1.Enabled := False;
        Halt;
      end;

      List(nil);
      Result := True;
    end;
    //Result := False;
  end;
end;

[*:a6cbf17708] Pra atualizar os labels e tudo mais eu escrevo nos eventos onWork e onWorkBegin do TidFtp. Os eventos estão abaixo:
procedure TfAtualiza.idFtpAtualizaWork(ASender: TObject; AWorkMode: TWorkMode;
  AWorkCount: Integer);

var
  Contador, kbTotal, kbTransmitidos, kbFaltantes: Integer;
  Status_transf: string;
  TotalTempo: TDateTime;
  H, M, Sec, MS: Word;
  DLTime, Media: Double;
begin
  btnAtualizar.Enabled := False;
  btnFechar.Enabled := False;
  kbTotal := Tamanho_Arquivo div 1024;
  TotalTempo := Now - STime;
  DecodeTime(TotalTempo, H, M, Sec, MS);
  Sec := Sec + M * 60 + H * 3600;
  DLTime := Sec + MS / 1000;
  KbTransmitidos := AWorkCount div 1024;
  kbFaltantes := kbTotal - kbTransmitidos;
  lblContador.Caption := ´Transmitidos: ´ + FormatFloat(´#,,0´, kbTransmitidos) +
    ´ Kb de ´ + FormatFloat(´,,0´, kbTotal) + ´ Kb´ + ´; Restam: ´ + FormatFloat(´,,0´, kbFaltantes) + ´ Kb´;
  Media := (100 / Tamanho_Arquivo) * AWorkCount;

  if DLTime > 0 then
  begin
    Tempo_Medio := (AWorkCount / 1024) / DLTime;
    Status_Transf := Format(´¬2d:¬2d:¬2d:´, [Sec div 3600, (Sec div 60) mod 60, Sec mod 60]);
    Status_Transf := ´Tempo de download ´ + Status_Transf;
  end;

  Status_Transf := ´Taxa de tranferência: ´ +
    FormatFloat(´0.00 Kb/s´, Tempo_Medio) + ´; ´ + Status_Transf;
  lblStatus.Caption := Status_Transf;
  lblNomeModulo.Caption := ModuloEmAtualizacao;
  Application.ProcessMessages;
  Contador := Trunc(Media);
  Gauge1.Progress := (contador);
end;

procedure TfAtualiza.idFtpAtualizaWorkBegin(ASender: TObject;
  AWorkMode: TWorkMode; AWorkCountMax: Integer);
begin
  STime := Now;
  Tempo_Medio := 0;
  pnlNomeModulo.Visible := True;
  Update;
end;

[*:a6cbf17708] O resto são funções pra ajudar em outras coisas. Por exemplo: Limpo as pastas temporárias do Windows, limpo os labels da tela e crio o arquivo Versoes.amk se for a primeira vez que o cara abre o sistema.
procedure TfAtualiza.ApagarTemporarios;
var
  lpEntryInfo: PInternetCacheEntryInfo;
  hCacheDir: LongWord;
  dwEntrySize: LongWord;
begin
  dwEntrySize := 0;
  FindFirstUrlCacheEntry(nil, TInternetCacheEntryInfo(nil^), dwEntrySize);
  GetMem(lpEntryInfo, dwEntrySize);
  if dwEntrySize > 0 then lpEntryInfo^.dwStructSize := dwEntrySize;
  hCacheDir := FindFirstUrlCacheEntry(nil, lpEntryInfo^, dwEntrySize);
  if hCacheDir <> 0 then
  begin
    repeat
      DeleteUrlCacheEntry(lpEntryInfo^.lpszSourceUrlName);
      FreeMem(lpEntryInfo, dwEntrySize);
      dwEntrySize := 0;
      FindNextUrlCacheEntry(hCacheDir, TInternetCacheEntryInfo(nil^), dwEntrySize);
      GetMem(lpEntryInfo, dwEntrySize);
      if dwEntrySize > 0 then lpEntryInfo^.dwStructSize := dwEntrySize;
    until not FindNextUrlCacheEntry(hCacheDir, lpEntryInfo^, dwEntrySize);
  end;
  FreeMem(lpEntryInfo, dwEntrySize);
  FindCloseUrlCache(hCacheDir);
end;

procedure TfAtualiza.LimparPastaTemp;
var
  Lng: DWORD;
  ThePath: string;
  I: integer;
  flbArquivos: TFileListBox;
begin
  SetLength(thePath, MAX_PATH);
  Lng := GetTempPath(MAX_PATH, PChar(ThePath));
  SetLength(ThePath, Lng);
  flbArquivos := TFileListBox.Create(fAtualiza);
  flbArquivos.Parent := fAtualiza;
  flbArquivos.Directory := ThePath;
  flbArquivos.Visible := False;
  for I := flbArquivos.Items.Count - 1 downto 0 do
    DeleteFile(flbArquivos.Items[I]);
  flbArquivos.Free;
end;

procedure TfAtualiza.AtualizarINIVersoes(ASecao, ANomeVersao, AVersao: string);
var
  Ini : TIniFile;
begin
  Ini := TIniFile.Create(ExtractFilePath(Application.ExeName)+´Versoes.amk´);
  Ini.WriteString(ASecao, ANomeVersao, AVersao);
  Ini.Free;
end;
procedure TfAtualiza.CriaArquivoVersoes;
var
  StringList: TStringList;
begin
  StringList := TStringList.Create;
  with StringList do
  begin
    Add(´[Atualizacoes]´);
    Add(´Modulo1=Operacao.exe´);
    Add(´Versao1=vF1.1.01´);
    Add(´Titulo1=Operacional´);
    Add(´´);
    Add(´Modulo2=Cobranca.exe´);
    Add(´Versao2=vF1.1.01´);
    Add(´Titulo2=Faturamento/Cobrança´);
    Add(´´);
    Add(´Modulo3=Comunica.exe´);
    Add(´Versao3=vF1.1.01´);
    Add(´Titulo3=Integração entre Filiais´);
    Add(´´);
    Add(´Modulo4=Armazem.exe´);
    Add(´Versao4=vF1.1.01´);
    Add(´Titulo4=Armazenagem e Logistica´);
    Add(´´);
    Add(´Modulo5=Frota.exe´);
    Add(´Versao5=vF1.1.01´);
    Add(´Titulo5=Frota´);
    Add(´´);
    Add(´Modulo6=InstalarRelatorios.exe´);
    Add(´Versao6=vF1.1.01´);
    Add(´Titulo6=Instalador de relatórios´);
    Add(´´);
    Add(´Modulo6=InstalarBPLs.exe´);
    Add(´Versao6=vF1.1.01´);
    Add(´Titulo6=Instalador de complementos´);
    Add(´´);
  end;
  StringList.SaveToFile(ExtractFilePath(Application.ExeName) + gsArqLocal);
  StringList.Free;
end;


[/list:u:a6cbf17708]

Bom, o código fonte completo eu coloquei pra download no meu FTP. Dêem uma olhada no meu blog que tem o link e todos podem entrar em contato comigo.
http://www.delphitodelphi.blogspot.com


Adriano Santos

Adriano Santos

Responder

Posts

26/04/2007

Nerdex

Adriano blz? Achei legal seu trabalho, e é uma coisa da qual eu vou precisar, mas pretendo implementar de outras formas. Só um detalhe inicial: minha aplicação não é modulada.

Pois bem, vou criar uma Unit somente para este fim dentro da aplicação mesmo, e não utilizarei arquivos .INI. A comparação (nº de versão) prefiro efetuar simplesmente via ´ping´ recorrendo a nomes de arquivos no lado FTP ou HTTP. Após baixar o download aplico uma verificação de integridade no arquivo via MD5 e/ou CRC, espero o usuário finalizar a aplicação para assim, de fato, ativar a atualização, para não ter que pedir, incovenientemente, que o usuário feche a aplicação corrente.

Não sei se dá para fazer assim como eu penso :cry: , mas é só a minha idéia... :wink:

flw?


Responder

Gostei + 0

30/11/2007

Facc

Bom, o código fonte completo eu coloquei pra download no meu FTP. Dêem uma olhada no meu blog que tem o link e todos podem entrar em contato comigo. http://www.delphitodelphi.blogspot.com



tentei pegar o arquivo, mas ele não vem


Responder

Gostei + 0

23/03/2008

Paullsoftware

Adriano só uma implementação no código...

Depois de listar não seria boa dar um

Quit; Disconnect
para que o Software não ficar pendurado no Servidor!!!


Responder

Gostei + 0

25/02/2009

Kripton

fiz todos os processos do tutorial porem quando ele baixa o arquivos é como se ele tive-se dado problema o arquivo com como DOS e o icone do arquivo .exe some. vou postar as imagem com sequencia do que estou fazendo.

primeira imagem quando o TESTE.exe está lá funcionando 100¬

[img:57848e8911]http://www.megaconcursos.com.br/imagem1.JPG[/img:57848e8911]

agora quando clico no Atualizador.exe

[img:57848e8911]http://www.megaconcursos.com.br/imagem2.JPG[/img:57848e8911]

Agora depois de atualizar vejam como ficou o TESTE.exe

[img:57848e8911]http://www.megaconcursos.com.br/imagem3.JPG[/img:57848e8911]

ele fica assim como se fosse DOS e ai quando clica nele para abrir ele não abre, não sei o que acontece. Por favor alguem pode me ajudar a resolver esse problema...

[/img]


Responder

Gostei + 0

Utilizamos cookies para fornecer uma melhor experiência para nossos usuários, consulte nossa política de privacidade.

Aceitar