Não é engraçado como habilidades de computação, tais como os próprios computadores, evoluem a um ritmo rápido? Alguns de nós que nos encontramos fazendo programação pesada de computadores hoje, começamos com HTML e CGI nos cativantes dias do início da web. Eu sou uma dessas pessoas. Se você também está interessado no admirável mundo do pseudo-código chamado "web design", você sem dúvida reconhece que, atualmente, a maior parte dos designers caíram em um de dois grupos. O primeiro grupo usa um editor WYSIWYG como o Dreamweaver para criar e publicar uma página web. O segundo grupo usa um editor de texto como Emacs ou Vim para codificar HTML na mão e um cliente de FTP para fazer upload da página finalizada para um servidor web, para o mundo ver e, esperançosamente, apreciar. O primeiro grupo sacrifica flexibilidade por conveniência, e o segundo, conveniência por flexibilidade. Nenhuma maneira está errada, mas também não está totalmente correta.
Durante meus primeiros anos de web design caí no primeiro grupo. Mais recentemente tenho abraçado meu destino de habilidade com Notepad e aderido às fileiras daqueles que preferem "fazer tudo a mão". Eu tenho apreciado a flexibilidade adicional oferecida por fazê-lo desta maneira, mas o custo em facilidade de uso não tem sido baixo. Instalar um servidor web localmente e editar os arquivos diretamente, consumia tempo e não se portava bem; então normalmente eu me encontrava modificando uma página, mudando para o meu cliente de FTP, fazendo upload do arquivo, mudando para o meu navegador, e o atualizando para ver a página atualizada. Não é uma coisa rápida de fazer, e ficou muito antigo após algum tempo. Era um processo que praticamente gritava "AUTOMATIZE-ME!" Portanto, no verão passado, iniciei meu editor de texto favorito e decidi fazer exatamente isto.
Eu queria um programa que fizesse tudo que eu estava fazendo a mão, mas de forma mais rápida e precisa. Decidi usar Ruby para escrever um script de automação. Eu queria meu código curto e fácil de manter, já que provavelmente eu adicionaria funcionalidades complementares a ele mais tarde. Ruby (sendo dinamicamente tipada) torna fácil escrever código compacto que pode ser estendido com um mínimo de ruido. É uma linguagem de script, mas é também orientada a objetos. Isto me permitiu evitar a duplicação de código mais elegantemente do que eu poderia ter com uma linguagem procedural. Ruby também tem disponível uma ótima biblioteca open source de SFTP (Net:: SFTP [0]), assim eu não seria forçado a escrever a minha própria. (SFTP é um protocolo de rede que permite que os arquivos sejam transferidos com segurança.)
Neste artigo, vou guiá-lo passo a passo através do processo de criação da sua própria versão deste programa. O código fonte completo dos exemplos serão incluídos, com analise linha por linha do que o código está fazendo. Convido você a participar e experimentar o quão facilmente Ruby pode automatizar partes de rotinas de seu trabalho diário.
Requisitos
Nosso programa tem um requisito básico: ele precisa se conectar a um servidor de SFTP remoto e enviar nossos arquivos. No entanto, também gostaríamos que ele transferisse apenas arquivos que foram alterados localmente, e automaticamente recursivo em subdiretórios enquanto procura por arquivos para upload.
O fluxo planejado para o script é:
- Estabeleça uma conexão SFTP com um servidor remoto.
- Liste todos os arquivos e subdiretórios em um diretório local.
- Compare o timestamps de arquivos em diretórios locais com o timestamp de arquivos em diretórios remotos e transfira somente dos arquivos que tiveram mudanças locais.
- Entre recursivamente em quaisquer subdiretórios e repita a partir do passo dois, criando subdiretórios remotos conforme necessário.
É claro que os passos um a três são facilmente manipulados por objetos já incluso no Ruby e Net::SFTP. O passo quatro é muito interessante. Enquanto a classe Dir do Ruby oferece uma forma de recursividade em subdiretórios, não é tão óbvia como gostaríamos. Já que Ruby permite fácil extensão da linguagem, por que não escrever nosso próprio método? Não somente vai ser muito divertido, mas também vamos aprender como é fácil estender Ruby.
Dependências
Além de Ruby em si, nossas únicas dependências são as bibliotecas Net::SFTP e Net::SSH [1]. Felizmente para nós, ambos são empacotados como Gems[2]. Assumindo que você tem Ruby instalado em sua máquina local e Gem está no path, execute um prompt de comando e digite:
gem install net-ssh --include-dependencies
gem install net-sftp --include-dependencies
Vamos codificar!
Agora estamos prontos para começar a escrever algum código. Vamos tentar conectar ao servidor remoto, listar todos os arquivos em um determinado diretório local, e fechar a conexão. Iremos fazer isso usando as interfaces Net::SSH e Net::SFTP e a classe Dir do Ruby.
1: require 'net/ssh'
2: require 'net/sftp'
3: Net::SSH.start('server', 'username', 'password') do |ssh|
4: ssh.sftp.connect do |sftp|
5: Dir.foreach('.') do |file|
6: puts file
7: end
8: end
9: end
Vamos passar por este código linha por linha:
- Requer a biblioteca Net::SSH.
- Requer a biblioteca Net::SFTP, uma vez que usaremos as duas.
- Estabelece uma sessão SSH com um dado nome de usuário e senha. (Argumentos adicionais tal como um servidor de proxy também podem ser informados aqui. Veja a documentação da API para maiores informações.)
- Abre uma conexão SFTP para o servidor remoto.
- A classe Dir lista todos os arquivos em um diretório de trabalho corrente.
- Imprime cada nome de arquivo.
- Sai do laço de listagem de arquivos.
- -
- Fecha as conexões SFTP e SSH.
Depois de executar o script no meu sistema, a seguinte saída é produzida:
.
..
cgi-bin
etc
logs
public_html
temp
Olhando para nossa lista original de requisitos, conseguimos finalizar os passos um e dois com nove linhas de código.
Vamos passar ao passo três, comparar os timestamps dos arquivos listados com os timestamps remotos, e transferir apenas os arquivos que foram alterados. (Para os nossos propósitos, definimos que um arquivo tenha sido modificado se o timestamp local é maior ou igual ao timestamp no servidor remoto.) Comparar timestamps com Ruby é realmente fácil. Na verdade, a própria comparação pode ser feita com apenas uma linha de código.
Vamos dar uma olhada no script agora:
1: require 'net/ssh'
2: require 'net/sftp'
3: Net::SSH.start('server', 'username', 'password') do |ssh|
4: ssh.sftp.connect do |sftp|
5: Dir.foreach('.') do |file|
6: next if File.stat(file).directory?
7: begin
8: local_file_changed = File.stat(file).mtime > Time.at(sftp.stat(file).mtime)
9: rescue Net::SFTP::Operations::StatusException
10: not_uploaded = true
11: end
12: if not_uploaded or local_file_changed
13: puts "#{file} has changed and will be uploaded"
14: sftp.put_file(file, file)
15: end
16: end
17: end
18: end
Vamos passar por este código linha a linha:
1. - 2. Requer Net::SSH e Net::SFTP.
3. - 4. Estabelece nossa sessão SSH e conexão SFTP.
5. Percorre os arquivos no diretório de trabalho corrente.
6. Já que não podemos lidar com recursividade em diretórios ainda, verifica se o arquivo corrente realmente é um diretório, em caso afirmativo, salta para a próxima iteração do laço.
7. - 11. Já que arquivos remotos podem não existir ainda, precisamos capturar exceções que Net::SFTP poderá lançar quando tentarmos determinar seu timestamp. Definimos duas flags: uma indica se o arquivo local foi modificado e precisa ser transferido, e outra que indica se o arquivo remoto ainda não existe.
12.- 13. Se o arquivo local ainda não foi transferido ou é mais novo que o arquivo remoto, imprime uma linha indicando que o arquivo será transferido.
14. Transfere o arquivo local para o servidor remoto.
15.- 18. Fecha a sentença If, o laço pelos arquivos, a conexão SFTP, e a sessão SSH.
Agora completamos o requisito três. Temos um script que irá fazer login em um servidor remoto e fazer o upload de todos os arquivos que tenham mudado no sistema local, mas o script só irá fazer isso para um único diretório. Ele não pode navegar em subdiretórios para encontrar arquivos adicionais para transferir. Também não pode lidar a com a criação de diretórios que não existam no servidor remoto. Precisamos cobrir ambas as situações antes de dizer que o nosso script está completo.
Recursividade
Vamos finalizar o nosso script descendo por subdiretórios e lidando com situações onde o diretório que contem o arquivo a ser transferido pode não existir no servidor remoto:
1: require 'net/ssh'
2: require 'net/sftp'
3: require 'dir'
4:
5: local_path = 'C:\public_html'
6: remote_path = '/usr/jsmith/public_html'
7: file_perm = 0644
8: dir_perm = 0755
9:
10: puts 'Connecting to remote server'
11: Net::SSH.start('server', 'username', 'password') do |ssh|
12: ssh.sftp.connect do |sftp|
13: puts 'Checking for files which need updating'
14: Find.find(local_path) do |file|
15: next if File.stat(file).directory?
16: local_file = "#{dir}/#{file}"
17: remote_file = remote_path + local_file.sub(local_path, '')
18:
19: begin
20: remote_dir = File.dirname(remote_file)
21: sftp.stat(remote_dir)
22: rescue Net::SFTP::Operations::StatusException => e
23: raise unless e.code == 2
24: sftp.mkdir(remote_dir, :permissions => dir_perm)
25: end
26:
27: begin
28: rstat = sftp.stat(remote_file)
29: rescue Net::SFTP::Operations::StatusException => e
30: raise unless e.code == 2
31: sftp.put_file(local_file, remote_file)
32: sftp.setstat(remote_file, :permissions => file_perm)
33: next
34: end
35:
36: if File.stat(local_file).mtime > Time.at(rstat.mtime)
37: puts "Copying #{local_file} to #{remote_file}"
38: sftp.put_file(local_file, remote_file)
39: end
40: end
41: end
42:
43: puts ‘Disconnecting from remote server'
44: end
45: end
46:
47: puts 'File transfer complete'
Wow! Este é substancialmente mais longo do que as nossas revisões anteriores, mas isso deve-se principalmente a verificar a ressalva do que deve ser feito para lidar com diretórios remotos que podem não existir. Esta é uma limitação da biblioteca Net::SFTP. O método put_file lança uma exceção desagradável se tentarmos fazer upload para um diretório remoto que não existe. Seria ideal para o método put_file tratar este caso criando automaticamente as partes inexistentes da árvore de diretórios do arquivo. Modificar o método está fora do escopo deste artigo, no entanto, eu o deixo como um exercício para você.
Vamos passar por nossas novas linhas de código, uma por uma:
1. - 4. Requer Net::SSH e Net::SFTP.
5. - 6. Define variáveis para diretórios local e remoto, os quais serão comparados e transferidos de um para o outro.
7. - 8. Define variáveis para permissão padrão para arquivos e diretórios que precisarão ser associadas a arquivos e diretório os quais ainda não existem no servidor remoto.
9. - 13. Estabelece uma sessão SSH e uma conexão SFTP para o servidor remoto.
14. Começa a descida por cada subdiretório.
15. Percorre cada item no diretório corrente.
14. Salta para a próxima iteração se o item corrente é um diretório e não um arquivo.
15. Modifica a variável local_file para ser igual ao caminho para o arquivo sobre o qual estamos iterando, relativo ao diretório corrente onde estamos localizados.
16. Modifica a variável remote_file para ser igual ao diretório/arquivo destino no servidor remoto, prefixado com o valor do remote_dir assim, o arquivo que transferiremos será copiado na localização correta e não só no diretório home do usuário.
17.- 26. Este é outro pedaço de código desagradável que não precisaríamos fazer aqui se Net::SFTP fosse um pouco mais inteligente quanto a tratamento de arquivos. Precisamos verificar se o diretório remoto que estamos transferindo já existe. Para fazer isso, chamamos sftp.stat(..), passando-lhe o nome do diretório para verificar. Se stat lançar uma exceção com a propriedade code igual a 2, o diretório remoto não existe, então o criamos, e associamos a ele a permissão correta.
27.- 35. Mais código desagradável para verificar se já existe remotamente o arquivo que faremos upload. Embora não precisemos fazer essa verificação para que possamos criar o arquivo remoto, porque ele será criado automaticamente quando transferirmos o arquivo local. Precisamos fazer isso para que possamos configurar a permissão apropriada no arquivo remoto, se ele for novo. Se não fizermos isso, por padrão permissões UNIX serão usadas, as quais podem nos impedir de fazer upload do arquivo mais tarde.
36.- 40. Por último, se é que fizemos até aqui, isso significa que ambos, diretório e arquivo remoto, que estamos tentando transferir existem. Comparamos o horário do arquivo local com o horário do arquivo remoto e, se o arquivo local for mais novo, fazemos o upload.
40.- 48. Finaliza todos os laços que abrimos, fechando nossa conexão SFTP e nossa sessão SSH no servidor remoto.
Isto é o que a saída desse script nos mostra quando ele é executado:
Connecting to remote server
Checking for files which need updating
Copying D:/html/index.php to /home/public_html/index.php
Copying D:/html/media.php to /home/public_html/media.php
Copying D:/html/contact.php to /home/public_html/contact.php
Copying D:/html/images/go.gif to /home/public_html/images/go.gif
Copying D:/html/images/stop.gif to /home/public_html/images/stop.gif
Copying D:/html/include/menu.php to /home/public_html/include/menu.php
Disconnecting from remote server
File transfer complete
Terminamos! Temos agora uma maneira rápida e fácil de fazer upload de árvores de diretórios local, de qualquer profundidade, para um servidor remoto. O script é inteligente o suficiente para criar diretórios que não existem, e também é esperto o suficiente para transferir apenas arquivos que realmente tenham mudado.
Melhorias Futuras
Enquanto a versão "puro osso" do script que desenvolvemos é completamente útil como é, há várias melhorias que poderiam ser feitas no script com apenas um pouco mais de trabalho:
- Lidar com a deleção de arquivos e diretórios. O nosso script atual não lida com deleção de arquivos e diretórios remotamente no caso de terem sido deletados localmente. Pode ser útil, opcionalmente, perguntar ao usuário antes de remover arquivos remotos para se certificar de que algo importante não é acidentalmente apagado.
- Adicionar verificações complementares para determinar se um arquivo foi modificado. Comparar timestamps é bom e elegante, mas porque não comparar tamanhos também? Esta seria uma verificação útil em cópias, quando conectarmos a servidores que podem não ter um sistema de tempo apurado.
- Ter um registro de log de que arquivos foram transferidos. Eu adicionei uma sentença para imprimir na saída padrão, mas por que não usar um mecanismo de logging mais sofisticado para manter um rastro de que arquivos foram transferidos e quando? (Isto seria importante se você decidisse agendar o script para executar em uma base diária ou semanal.)
- Reescreve o script usando Capistrano[4]. O excelente framework de Jamis Buck para escrita de “receitas” de deployment seria uma boa escolha para uma solução mais permanente que poderia ser reutilizada em outros projetos.
Além disso, embora possa não ser verdadeiramente uma melhora, a biblioteca Net::SSH suporta o uso de chave pública de autenticação. Inicie a aplicação Pageant do PuTTY (veja http://www.chiark.greenend.org.uk/~sgtatham/putty/download.html), adicione a sua chave, e remova a sua senha da sentença Net::SSH.start em nosso script. Agora você pode fazer upload de arquivos sem armazenar sua senha em texto simples e sem ser obrigado a introduzi-la toda vez que conectar ao servidor remoto. Brilhante!
Conclusão
Ao longo do tempo, esse script já me salvou dezenas de horas que eu teria gasto manualmente alternando com uma GUI de FTP, ou mudando de diretórios constantemente através de uma linha de comando chata de um programa de FTP. Eu espero que você ache o script igualmente útil no seu próprio trabalho. Caso ache, o convido a me contatar através do meu site (www.matthewbass.com) e me avisar. Eu também estou interessado em te ouvir se você tiver sugestões complementares de melhorias, ou se você viu uma boa refatoração que poderia eliminar uma ou duas linhas de código. Feliz Ruby-ing!
Notas de rodapé
1. Net::SFTP é uma Ruby API para transferência de arquivos de forma segura sobre SFTP. Ela é parte da Net::SSH.
2. Net::SSH é uma API Ruby para acessar recursos através de um envelope seguro. http://rubyforge.org/projects/net-ssh/
3. Gem é um gerenciador de pacotes do Ruby. http://www.rubygems.org
4. Automatizador de deployment de aplicações com Ruby. http://manuals.rubyonrails.com/read/book/17