ZLIB Framework – Parte 01

Introdução

Vamos ver um pouco sobre Bibliotecas de Funções e Framework, com destaque para as funcionalidades em implementação no projeto ZLIB.

Bibliotecas e Frameworks

Com as funções básicas da linguagem, conseguimos criar qualquer programa. Alguns programas podem dar mais trabalho que outros, tudo depende de quantas funcionalidades serão implementadas. Porém, quando você precisa implementar muitas funcionalidades parecidas, é mais eficiente isolar o código comum em classes ou funções parametrizáveis, para não ter que escrever tudo de novo ou copiar-e-colar, replicando código desnecessariamente. Neste ponto, começa o nascimento de uma Biblioteca de funções.

Na ciência da computaçãobiblioteca é uma coleção de subprogramas utilizados no desenvolvimento de software. Bibliotecas contém código e dados auxiliares, que provém serviços a programas independentes, o que permite o compartilhamento e a alteração de código e dados de forma modular. Alguns executáveis são tanto programas independentes quanto bibliotecas, mas a maioria das bibliotecas não são executáveis.

Quando falamos em Framework, não apenas estamos usando funções genéricas de uma biblioteca, mas sim uma abstração de nível mais alto, que impõe um fluxo de controle na aplicação.

Um framework em desenvolvimento de software, é uma abstração que une códigos comuns entre vários projetos de software provendo uma funcionalidade genérica. Um framework pode atingir uma funcionalidade específica, por configuração, durante a programação de uma aplicação. Ao contrário das bibliotecas, é o framework quem dita o fluxo de controle da aplicação, chamado de Inversão de Controle.[1]

Projeto ZLIB

A ideia — necessidade — de uma LIB (Biblioteca) de componentes surgiu com os posts da série do CRUD em AdvPL, que acabou virando uma Agenda de Contatos, feita originalmente atrelada a interface do SmartClient, e depois implementada em uma interface WEB/HTTP.

Muito daquele código é comum a aplicações de mesma funcionalidade — cadastro simples. Inclusão, Alteração, Exclusão, Consulta ordenada, consulta por filtro. Outras funcionalidades, como exibição e cadastro de imagem, envio de email e mapa do endereço não necessariamente são usadas em todos os cadastros, mas podem ser colocadas em componentes de uma biblioteca para reaproveitamento.

A ideia da ZLIB é ser uma Biblioteca de Funções, que vai servir de base para construir um Framework. Ela já está versionada no GITHUB, mas ainda em desenvolvimento e com pouca (nenhuma) documentação, e como os componentes ainda estão nascendo, muitas alterações drásticas estão sendo feitas a cada atualização.

Orientação a Objetos e Abstração

Estas são duas chaves importantes no reaproveitamento de código e desenvolvimento modular. A orientação a objetos nos permite criar classes com uma finalidade (abstração) e implementar para múltiplos cenários ou recursos.

Por exemplo, as classes implementadas para acesso a arquivos DBF e arquivos em memória. Ambas possuem a mesma declaração de métodos para implementar as suas funcionalidades. Logo, o mesmo programa que insere um registro em uma tabela da classe ZDBFFILE pode realizar a mesma operação usando um objeto da ZMEMFILE.

Uma classe de geração de LOG de operação ou execução não precisa saber onde o log será gravado, ou mesmo conhecer a interface de gravação. Ela pode receber como parâmetro um objeto de uma classe de gravação de LOG. Ele pode ser de uma classe que grave os registros emitidos de log em um arquivo TXT, ou em um banco de dados, ou ainda seja um encapsulamento de uma interface “client” de log, que envia os dados gerados para serem gravados remotamente por um Log Server.

Criação de Componentes

Um dos primeiros mandamentos da criação de componentes é : A CRIAÇÃO DE QUALQUER COMPONENTE DEVE SER MOTIVADA PELA NECESSIDADE. Criar componentes adicionais ou agregar funcionalidades demais a um componente só por que vai ser “legal” só engorda código. Limite-se a uma funcionalidade por classe, e coloque nela o que realmente é comum a todos. Exceções são tratadas na implementação, a abstração é genérica.

Quando aos níveis de implementação — ou camadas — normalmente os componentes de alto nível são construídos para usar os de mais baixo nível. Na prática eles são construídos para usar todas as implementações feitas sobre uma abstração. Por exemplo, um componente de CRUD feito para usar a abstração ZISAMFILE pode usar qualquer implementação feita sobre ela, como a ZDBFFILE, ZTOPFILE, ZMEMFILE…

Como a implementação está por baixo da abstração, eu posso por exemplo criar uma abstração de exportação de arquivo, e implementar uma exportação para cada formato, a mesma coisa para importação.

Objetivo Final

Criar um conjunto de funções e funcionalidades que, permitam escrever programas, funções e rotinas, separando totalmente o processamento da interface, focando em SOA utilizando micro-serviços, filas e controladores, com foco em desempenho, escalabilidade, resiliência e alta disponibilidade.

Conclusão

Por hora, a primeira missão das funções em desenvolvimento é permitir a reescrita do programa de Agenda para SmartClient, usando componentes destacados, que permitam um elevado índice de reaproveitamento de código, e uma forma de declarar e executar as validações e procedimentos de cada operação que torne a codificação mais fácil e rápida, usando uma abordagem que permita aproveitar o CORE de cada componente em integrações encapsuladas por APIs (RPC Advpl, REST, SOAP) para serem consumidas por interfaces criadas em AdvPL ou qualquer outra linguagem ou plataforma.

Referências

Manipulação de arquivos em AdvPL – Parte 01

Introdução

Faz algum tempo, eu publiquei um post sobre desempenho em AdvPL, criando uma classe em AdvPL para leitura de arquivos de texto simples (Acelerando o AdvPL – Lendo arquivos TXT). Porém, o exemplo em si já parte da premissa que o programador conhece o que e como funcionam as funções de manipulação de arquivos funcionam. Neste post, vamos ver o que elas fazem “por dentro”, para entendermos como podemos utilizá-las.

Um arquivo no disco

Qualquer arquivo gravado em uma unidade de disco ou em um dispositivo com sistema de arquivos (Drive USB, CD-ROM,etc…) não passa de uma sequência finita de bytes, que representam um conteúdo. Um arquivo (no Windows) normalmente possui um nome e uma extensão, separados pelo caractere “.” (ponto), onde a extensão indica ao sistema operacional qual é o tipo do conteúdo deste arquivo. O arquivo possui um tamanho alocado na unidade na qual ele está gravado — quantos bytes têm o arquivo — além da data e hora da última gravação feita no arquivo, e ainda pode conter atributos especiais.

Uma imagem gravada em disco possui uma representação digital da imagem real, em uma determinada resolução (ou grid de pontos em linhas x colunas), onde o formato utilizado indica ao programa como as sequencias de bytes devem ser tratadas para representar a imagem que o arquivo contém.

Arquivos TEXTO

Um arquivo de texto simples — por exemplo com a extensão “.txt”, nada mais é do que a representação em bytes de todas as letras, números, espaços, pontuação, letras acentuadas e quebras de linha. Normalmente um arquivo texto simples utiliza a representação da tabela ASCII — onde uma faixa de bytes com os valores de 0 a 31 representam caracteres de controle ( quebra de linha, tabulação e outros), do 32 as 127 representam letras maiúsculas e minúsculas, números — representação do número como texto –, espaço em branco, caracteres e símbolos gráficos comuns (exclamação, arroba, dólar, percentual, asterisco, hífen, colchetes, aspas simples e duplas, parênteses, e afins; e por fim a faixa de 128 a 255 é utilizada para representar letras acentuadas, caracteres gráficos e especiais. Esta representação é conhecida como TABELA ASCII.

Existe uma outra padronização mundialmente conhecida de conversão de valores de bytes para caracteres. Chama-se EBCDIC — vide mais detalhes nas referências no final do post — criada pela IBM na década de 60, praticamente junto da padronização do ASCII. Esta tabela foi usada nos equipamentos IBM a partir do Mainframe System/360, usada ainda hoje em Mainframes e Mid-Range Computers da IBM.

Outros formatos de arquivo

Existem vários outros formatos de arquivo, cada formato na verdade é uma especificação de como o seu conteúdo deve ser tratado. A Tabela ASCII é convenção de representação de códigos numéricos para caracteres, criada para padronizar a troca de informações entre computadores. Na prática, dentro do arquivo, eu tenho uma sequência de bytes com valores de 0 a 255.

Texto e Bytes

Vamos partir de alguns exemplos, para deixar mais claro o que tem dentro dos arquivos, e das variáveis do tipo “C” Caractere do AdvPL, usando algumas funções. Por exemplo, se eu quero criar em AdvPL uma variável caractere com as letras A, B e C (maiúsculas, sem acento), eu posso simplesmente fazer uma atribuição direta a uma variável, usando um conteúdo literal.

cTexto := "ABC"

Na área de memória usada para armazenar estes três caracteres, é usado um byte para cada caractere. O valor guardado dentro do Byte é o valor numérico da representação da letra na TABELA ASCII. Por exemplo, a letra “A” é o número 65“B” é 66 e “C” é 67.

Eu poderia escrever isso de outra forma. Por exemplo, usando a função CHR(), que recebe o número do byte e retorna a representação em string do mesmo. Por exemplo:

cTexto := chr(65) + chr(66) + chr(67)

O conteúdo da variável cTexto será exatamente igual nas duas formas de atribuição. Um arquivo texto pode ter algumas diferenças entre sistemas operacionais. Por exemplo, no Windows uma quebra de linha do arquivo é representada pela sequência de bytes chr(13)+chr(10). Já em Linux, é usado apenas chr(10). Logo, eu posso montar a representação de uma linha de um arquivo texto na memória usando:

cLinha := "Uma linha de texto." + chr(13) + chr(10)

Funções de baixo nível de manipulação de arquivo

Existem algumas funções da linguagem AdvPL, projetadas para ler e gravar bytes de qualquer arquivo. Porém, nenhuma destas funções fazem qualquer crítica ao tipo de conteúdo do arquivo — isto é, não importa se o arquivo tem uma imagem, um texto ou um vídeo: Para estas funções, o arquivo é simplesmente uma sequência de bytes. São elas:

  • FCreate – Cria um arquivo.
  • FOpen – Abre um arquivo.
  • FSeek – Reposiciona o ponteiro de dados do arquivo.
  • FRead – Lê bytes do arquivo.
  • FWrite – Grava bytes no arquivo.
  • FClose – Fecha o arquivo
  • FError – Retorna o status de execução da última instrução de arquivo

As funções FCreate() e FOpen() retornam um número identificador, chamado de “Handler de Arquivo”, e todas as demais funções, enquanto o handler não foi fechado pela função FCLose(), recebem este identificador como parâmetro para realizar as operações descritas.

Ponteiro de Dados do Arquivo

O ponteiro de dados é uma espécie de marcador que aponta para um determinado byte do conteúdo arquivo, em uma determinada posição — que também pode ser chamado de “Offset“. O Offset inicia em 0 (zero), correspondendo ao primeiro byte do arquivo. Em um arquivo de tamanho N bytes, o ponteiro de dados pode apontar para uma determinada posição dentro do arquivo, de 0 a N-1, ou pode apontar para o final do arquivo (EOF), correspondendo a posição N.

Qualquer operação de leitura ou gravação sempre é iniciada na posição onde o ponteiro de dados está apontando, e a operação é sempre feito da posição atual em direção ao final do arquivo. Imagine um arquivo no disco como sendo uma sequência de bytes, iniciando da esquerda para a direita.

Função FCreate()

Primeiro vamos criar um arquivo. Usamos para isso a função FCreate(), onde informamos no primeiro parâmetro o caminho e nome do arquivo a ser criado. Em caso de sucesso, a função FCreate() retorna um identificador numérico, chamado de “Handler” do arquivo. Agora, vamos ver alguns detalhes sobre os comportamentos da função FCreate().

  • Caso o arquivo informado como parâmetro contenha uma unidade de disco (drive), o Application Server entende que a criação da tabela será feita pelo cliente AdvPL — SmartClient.
  • Caso o arquivo não contenha o drive ou unidade de disco, o arquivo será criado na máquina onde o Application Server está sendo executado, em uma pasta ou sub-pasta a partir do RootPath (pasta raiz de arquivos do ambiente).
  • Caso o arquivo a ser criado já exista no disco, e não esteja sendo aberto e/ou usado por outro processo, o conteúdo anterior do arquivo é perdido — ele torna-se um arquivo vazio, com 0 (zero) bytes.
  • Caso o arquivo a ser criado não exista, ele é criado vazio — com 0 byte(s).
  • Uma vez que o arquivo tenha sido criado (ou recriado) com sucesso, ele será mantido aberto em modo exclusivo, para leitura e escrita. Isto é, enquanto este programa não fechar o arquivo usando a função FClose(), o arquivo permanece aberto para o processo atual ler e gravar bytes no arquivo, e nenhum outro processo vai conseguir abrir este arquivo, mesmo que seja apenas para leitura.
  • Em caso de falha na criação (ou recriação) do arquivo, o handler retornado é o valor -1 (menos um) , indicando erro. Podemos recuperar a causa do erro chamando a função FError() após executar o FCreate(). A função FError() retorna o código do erro de criação/recriação da tabela, e retorna 0 (zero) em caso de sucesso.
User Function TSTFILE()
Local nHnd 
Local cFile := '\temp\teste.txt'
Local cLine
nHnd := FCreate(cFile)
If nHnd == -1
  MsgStop("Falha ao criar arquivo ["+cFile+"]","FERROR "+cValToChar(fError()))
  Return
Endif

Com o fonte acima, criamos um arquivo vazio, na pasta “\temp\”, que deve ser criada a partir do RootPath do ambiente atual, Esta configuração está presente nos ambientes do Protheus Server, na chave “RootPath” do ambiente.

Como  o arquivo está vazio, o ponteiro de dados do arquivo neste momento não aponta para lugar algum . Agora, vamos gravar uma linha de texto em formato Windows dentro desse arquivo, com quebra de linha.

cLine := "Olá sistema de arquivos" + CRLF

Para quem não conhece, CRLF escrito desta forma no AdvPL — junto e com letras maiúsculas — é um #translate, que retorna os dois bytes ( chr(13) e chr(10) que indicam em um arquivo texto do Windows que deve ser realizada uma quebra de linha. Este recurso está disponível no AdvPL desde que seja utilizado o #include “protheus.ch” — ou o seu equivalente, “totvs.ch”.

Agora, vamos gravar esta informação três vezes no arquivo, em três linhas, uma embaixo da outra, e em seguida fechar o arquivo

FWrite(nHnd,cLine)
FWrite(nHnd,cLine)
FWrite(nHnd,cLine)
FClose(nHnd)

Uma operação de escrita no arquivo é feita sempre a partir de onde está apontando o ponteiro de dados. Como o arquivo não tem nada após ser criado, o ponteiro de dados está em EOF (Final de Arquivo), a operação de escrita no arquivo insere os bytes/caracteres informados no final do arquivo, aumentando o seu tamanho. Após gravar os caracteres, o ponteiro de dados aponta novamente para EOF,

Feito isto, vamos ver exatamente o que foi gravado. Cada linha contém 23 caracteres (ou bytes), mais dois caracteres de controle ( chr(13)  e chr(10) ). Logo, cada linha possui na verdade 25 bytes de informação. Após gravar as três linhas e as respectivas quebras, o arquivo resultante deve ter exatamente 75 byte(s).

Função FOPEN

A função fOpen() é usada para abrir um arquivo. Podemos especificar parâmetros opcionais para definir se o arquivo será aberto para leitura, escrita, ou ambos (leitura e escrita), e ainda é possível especificar o modo de compartilhamento do arquivo — exclusivo, compartilhado, bloqueio de leitura ou bloqueio de escrita. Por default, o arquivo é aberto para leitura exclusiva — nenhum outro processo pode abrir o arquivo para fazer nada, e o processo atual pode apenas ler dados do arquivo.

Seu tratamento de erro é idêntico ao FCreate() — em caso de retorno -1 (menos um), a função FError() contém um código com mais detalhes da falha encontrada.

Veja os detalhes da documentação da função FOpen na TDN. O modo de abertura e compartilhamento da tabela é definido pela soma de dois números das listas de modo de acesso e modo de compartilhamento — apenas um elemento de cada lista. Para usarmos as constantes — que são mais “elegantes” para ler o fonte, devemos fazer um #include “fileio.ch” no início do nosso fonte AdvPL. No nosso exemplo, vamos abrir a tabela no modo default.

nHnd := FOpen(cFile)
If nHnd == -1
   MsgStop("Falha ao abrir ["+cFile+"]","Ferror " + cValToChar(fError()) )
Endif

Caso eu queira, por exemplo, abrir o arquivo em modo compartilhado de leitura e escrita, podemos usar os seguintes parâmetros:

nHnd := FOpen(cFile,FO_READWRITE + FO_SHARED)

Vale lembrar que, ao usar um modo compartilhado que permita escrita no arquivo, dois processos podem tentar escrever na mesma área de dados do arquivo. Se isto acontecer, a última gravação é a que vale. Cabe a aplicação gerenciar os acessos concorrentes ao mesmo Offset do arquivo.

Função FSEEK

Usamos a função FSeek() para mover o ponteiro de Offset de dados do arquivo. Esta função não mexe no arquivo, apenas movimenta o ponteiro de dados do arquivo o parâmetro nOffset informado, e a partir de que ponto do arquivo — ou origem — este Offset deve ser considerado. Veja a documentação oficial completa da FSeek no TDN.

Para entender melhor seu funcionamento, vamos das um exemplo de cada movimentação a partir de uma origem distinta. A origem padrão / default é 0 (zero), indicando que o nOffSet informado é considerado a partir do início do arquivo — posição 0 (zero). Veja abaixo os parâmetros a serem informados para a função.

FSeek( < nHandle >, < nOffSet >, [ nOrigem ] )

O parâmetro nOrigem é opcional. Ele indica a partir de que ponto do arquivo o parâmetro nOffset deve ser considerado. Seu valor default é 0 (zero). Ele pode ser:

  • 0 ( zero) — DEFAULT — Movimenta o ponteiro de dados para a posição informada em nOffSet, a partir do início do arquivo.
  • 1 (um) – Considera o nOffSet como sendo o número de bytes para movimenta o ponteiro de dados a partir de sua posição atual. Neste caso, nOffSet pode ser positivo ( mover o ponteiro para frente) ou negativo (mover o ponteiro para traz).
  • 2 (dois) – Movimenta o ponteiro de dados considerando nOffSet como sendo a quantidade de bytes para deslocar o ponteiro de dados a partir do final do arquivo. Neste caso, nOffSet deve ser 0 (zero) ou um valor negativo.

O retorno da função, independente dos parâmetros utilizados, será a nova posição (OffSet) do arquivo, a partir do início do arquivo. Vamos a alguns exemplos

FSeek( nHnd , 0  [ ,0 ] ) – Posiciona no primeiro byte do arquivo. Retorna o OffSet após o movimento  (zero).

FSeek( nHnd , 3 [ ,0 ]) – Posicional no quarto byte do arquivo. Retorna o OffSet após o movimento (três).

FSeek( nHnd , 0 , 2 ) – Posiciona o ponteiro de dados no final do arquivo (EOF). Neste caso o offset atual corresponde ao tamanho do arquivo — pois para um arquivo de N bytes, o final de arquivo (EOF) é justamente a posição N.

FSeek( nHnd , 5 , 1 ) – Avança o ponteiro de dados 5 bytes para frente da posição atual.

FSeek( nHnd , -5 , 1 )  – Volta o ponteiro de dados 5 bytes para traz da posição atual.

Quando utilizamos o #include “fileio.ch”, ao invés de usar os números de 0 a 2 como terceiro parâmetro da função fSeek(), podemos usar as constantes abaixo:

Número 0       Constante FS_SET         
Número 1       Constante FS_RELATIVE    
Número 2       Constante FS_END

Continuando o programa de exemplo, vamos determinar o tamanho do arquivo e mover o ponteiro de dados para a primeira posição (Offset 0) do arquivo novamente.

nTamFile := FSeek(nHnd,0,FS_END)
FSeek(nHnd,0,FS_SET)

Função FRead()

Agora, vamos ler os primeiros 25 bytes do arquivo. Para isso, usamos a função FRead(). A Documentação oficial está no TDN – FRead. Veja abaixo os parâmetros da função:

FRead( < nHandle >, < cBufferVar >, < nQtdBytes > )

A função recebe em nHandle o identificador do arquivo obtido através da função FCreate() ou FOpen(), uma variável string onde os bytes lidos do arquivo a partir da posição atual do ponteiro de dados, e a quantidade de bytes que devem ser lidas.

Por exemplo, logo após determinar o tamanho do arquivo, vamos ler os primeiros 25 bytes do arquivo:

cBuffer := ""
nRead := FRead( nHnd, @cBuffer, 25 )
conout("Bytes Lidos ... "+cValToChar(nRead))

Primeiro inicializamos uma variável do tipo “C” Caractere no AdvPL com uma string vazia, depois a passamos por referência na função FRead(), seguido do número de bytes que eu quero ler a partir da posição atual do ponteiro de dados do arquivo. A função retorna o número de bytes que efetivamente foram lidos. Caso a leitura termine devido ao final do arquivo ter sido atingido, ou caso tenha havido algum erro, o valor de retorno de FRead() pode ser 0 ou um valor menor que o tamanho que foi solicitado para ler. Em caso de erro na leitura, a função FError() deve conter maiores informações. Se ferror() retornar zero, e o conteúdo lido for menor que o solicitado, então o arquivo efetivamente chegou ao final.

Como eu gravei três linhas de 25 bytes no arquivo, inserindo as quebras de linha, eu seu que ao carregar os primeiros 25 bytes ao arquivo, eu li a primeira linha de dados do arquivo inclusive com a quebra de linha. E, se eu fizer mais duas leituras da mesma forma, eu vou ler fatalmente as linhas 2 e 3 inteiras, respectivamente.

Função FWrite()

Caso eu queira escrever alguma informação no arquivo, eu posso utilizar a função FWrite(), desde que eu tenha aberto o arquivo para escrita. A função FWrite() recebe o identificador ou Handler do arquivo como parâmetro, seguido de uma variável “C” Caractere do AdvPL, contendo uma sequência de caracteres / bytes a serem gravados no arquivo, e pode ainda receber um último parâmetro opcional, permitindo informar quantos bytes do buffer de caracteres informado deve ser salvo. Caso este último parâmetro não seja informado, a string inteira é gravada no arquivo.

De forma similar a FRead(), que começa a leitura a partir da posição atual do ponteiro de dados do arquivo, a função FWrite() começa a gravar os dados a partir da posição atual do ponteiro de dados, em direção ao final do arquivo. Caso o ponteiro de dados esteja situado em um ponto do arquivo com informações já gravadas, estas informações serão perdidas, pois deste ponto em diante a nova string informada como parâmetro será salva. Caso o ponteiro de dados atinga EOF, ou já esteja em EOF, os dados são acrescentados a partir de então, sempre no final do arquivo, aumentando o tamanho do arquivo.

Não há função ou API direta que permita inserir novos dados no meio do arquivo deslocando os demais dados para a frente. E não há função ou API direta que diminua o tamanho de um arquivo. Vamos ao exemplo, continuando o fonte anterior:

nGravado := fWrite( nHnd , replicate('-',23) )

Após ler a primeira linha do arquivo, o ponteiro de dados estará apontando para o primeiro caractere da segunda linha, que é igual a primeira? 23 bytes de dados e os dos bytes finais de quebra de linha. Da forma feita acima, eu literalmente passo por cima da linha escrevendo vários “hifens” sobre a segunda linha, sem invadir o espaço ocupado pela quebra de linha. Se formos ver o arquivo após todas estas execuções, seu conteúdo deve ser:

Olá sistema de arquivos
-----------------------
Olá sistema de arquivos

Se eu escrever por exemplo mais 4 hífens, eu passaria por cima da quebra de linha, e por cima das duas primeiras letras da terceira linha. Sem a quebra de linha, o arquivo ficaria apenas com duas linhas, com o seguinte conteúdo:

Olá sistema de arquivos
---------------------------á sistema de arquivos

Observações finais

  • Se eu abrir um arquivo no modo de escrita, mesmo assim eu ainda consigo ler dados do arquivo.
  • Se eu abrir um arquivo apenas para leitura, a função FWrite() não vão conseguir gravar nada, vai retornar 0 (zero), e a função FError() retornará o código de erro correspondente.
  • Ao usar a função FOpen(), procure especificar o modo de abertura e o modo de acesso, sempre.

Conclusão

Por incrível que pareça, esta é apenas a primeira parte sobre as funções de baixo nível de arquivos em AdvPL. Ainda têm mais coisas para ver no próximo post desta sequência, porém com essa introdução já dá para se ter uma ideia do que é possível fazer em AdvPL.

Agradeço novamente a audiência, as curtidas, compartilhamentos, dúvidas e afins. Desejo a todos TERABYTES DE SUCESSO !!! 

Referências

 

Protheus e FTP Client – Parte 03

Introdução

A ideia era fazer um fonte mais detalhado de exemplo de uso da Classe TFTPClient(), mas o resultado acabou virando um mini WinSCP em AdvPL 😀 Vamos aos prints e aos códigos !!!

O Programa

Existe uma aplicação chamada WINSCP, uma ferramenta de código aberto que permite gerenciamento e sincronização de arquivos, entre a máquina local e um servidor de FTP ou mesmo SFTP (SSH File Transfer Protocol). As operações básicas são realizadas em dois painéis lado-a-lado, onde o lado esquerdo permite navegar na estrutura de pastas da máquina local, e no lado direito é possível conectar em um FTP / SFTP Server, navegar pela estrutura de pastas do servidor FTP, e realizar operações em ambos os lados, como criar pastas, apagar pastas, e copiar pastas e arquivos de um lado para o outro — Download e Upload do FTP.

Este programa de exemplo em AdvPL acabou crescendo, e tornou-se também um exemplo avançado de utilização de interface. Não se trata de um programa muito complexo, ele acaba ficando parecido com o CRUD — Uma máquina de estado de interface onde o disparo das operações dependem do estado de um ou de ambos os painéis de arquivos.

O programa ainda está sendo codificado, a parte mais trabalhosa foi fazer a “área de trabalho” da Interface do programa, onde uma parte as ações são disparadas por um menu superior, e a outra é disparada interativamente dentro dos painéis de navegação. Neste ponto, estou escrevendo as operações para serem feitas entre os painéis, e terminei apenas a cópia (Download/Upload). Porém, com o que já está escrito e operacional, já é possível lançar a versão 1.0 😀

Entrada

Após compilado em um ambiente, o programa deve ser chamado diretamente do SmartClient, através da função U_FTPManager. Ao ser iniciado, o programa abre uma Janela em tela cheia, trazendo os arquivos do RootPath do ambiente “\” no componente de navegação do lado esquerdo. OS detalhes de cada arquivo posicionado podem ser vistos no painel inferior do mesmo lado, tais como Tamanho, Data e Hora de criação (ou última alteração), e atributos do arquivo e/ou pasta.

Uma vez na tela inicial, podemos usar as setas para cima ou para baixo para mudar o arquivo ou pasta em foco, bem como usando o mouse, com um clique no botão esquerdo sobre o arquivo. Caso seja pressionado ENTER ou um Duplo Clique do mouse sobre uma pasta, isto fará com que a aplicação abra esta pasta e mostre os arquivos dentro dela,e permite navegar entre as pastas. Vamos ao fonte inicial do programa:

USER Function FTPManager()
Local oDlg, oFont
Local cTitle := "FTP Client Manager"
Local oSplitter
Local oPLeft , oPRight
Local nOpLeft := 1
Local oLbxLeft,oLbxRight
Local aLList := {} , aLFiles := {}
Local cLPath := "\"
Local nOpRight := 1
Local aRList := {}, aRFiles := {}
Local cRPath := "FTP Client (Não Conectado)"
Local oMenuBar
Local oTMenu1, oTMenu2, oTMenu3, oTMenu4
Local oFtp
Local lConnected := .F.
Local aGetsL := {} , aGetsR := {}
Local cFname := space(50)
Local cFAttr := space(50)
Local cFDate := space(18)
Local nFSize := 0
Local cRFname := space(50)
Local cRFAttr := space(50)
Local cRFDate := space(18)
Local nRFSize := 0
Local aFTPInfo := {}
Local oLSay,oRSay

aadd(aFTPInfo,space(50)) // 1 FTP Addr
aadd(aFTPInfo,21) // 2 FTP Port
aadd(aFTPInfo,5) // 3 FTP TimeOut (sec)
aadd(aFTPInfo,.T.) // 4 Firewall Mode ( passive )
aadd(aFTPInfo,.F.) // 5 Use IP Conn
aadd(aFTPInfo,.T.) // 6 Anonymous Login
aadd(aFTPInfo,"anonymous") // 7 User
aadd(aFTPInfo,"") // 8 Password

// Define Formato de data DD/MM/AAAA
SET DATE BRITISH
SET CENTURY ON

// Usa uma fonte Fixed Size, tamanho 10
oFont := TFont():New('Courier new',,-10,.T.)
SETDEFFONT(oFont)

// Cria o objeto Client de FTP
oFtp := tFtpClient():New()

// Cria a janela principal da Agenda como uma DIALOG
DEFINE WINDOW oDlg FROM 0,0 TO 600,800 PIXEL ;
  TITLE (cTitle) NOSYSMENU

// Permite ESC fechar a janela
oDlg:lEscClose := .T.

// Primeiro cria a barra superior de Menus da Aplicação na Janela Principal
oMenuBar := tMenuBar():New(oDlg)
oMenuBar:SetColor(CLR_BLACK,CLR_WHITE)

// Agora cria a tela inicial
oSplitter := tSplitter():New( 00,00,oDlg,400,280 )
oSplitter:ALIGN := CONTROL_ALIGN_ALLCLIENT

@ 0,0 MSPANEL oPLeft LOWERED SIZE 130, 36 OF oSplitter
@ 0,0 MSPANEL oPRight LOWERED SIZE 130, 36 OF oSplitter

oPLeft:ALIGN := CONTROL_ALIGN_ALLCLIENT
oPRight:ALIGN := CONTROL_ALIGN_ALLCLIENT

@ 0,0 MSPANEL oPLeftUp SIZE 130, 10 OF oPLeft
@ 0,0 MSPANEL oPLeftMid SIZE 130, 15 OF oPLeft
@ 0,0 MSPANEL oPLeftBottom SIZE 130, 65 OF oPLeft

oPLeftUp:ALIGN := CONTROL_ALIGN_TOP
oPLeftBottom:ALIGN := CONTROL_ALIGN_BOTTOM
oPLeftMid:ALIGN := CONTROL_ALIGN_ALLCLIENT

@ 1,1 SAY oLSay PROMPT cLPath SIZE 120,15 COLOR CLR_BLACK,CLR_WHITE ;
  of oPLeftUp PIXEL

@ 0,0 MSPANEL oPRightUp SIZE 130, 10 OF oPRight
@ 0,0 MSPANEL oPRightMid SIZE 130, 15 OF oPRight
@ 0,0 MSPANEL oPRightBottom SIZE 130, 65 OF oPRight

oPRightUp:ALIGN := CONTROL_ALIGN_TOP
oPRightBottom:ALIGN := CONTROL_ALIGN_BOTTOM
oPRightMid:ALIGN := CONTROL_ALIGN_ALLCLIENT

@ 1,1 SAY oRSay PROMPT cRPAth SIZE 120,15 COLOR CLR_BLACK,CLR_WHITE ;
  of oPRightUp PIXEL

// ListBox lado esquerdo
// Arquivos do servidor a partir do RootPath 

aLList := GetLFiles(cLPath,@aLFiles)

@0,0 LISTBOX oLbxLeft VAR nOpLeft;
  ITEMS aLList ;
  ON CHANGE ( doLChange(aGetsL,oLbxLeft,@aLList,@aLFiles) ) ;
  ON DBLCLICK ( EnterLeft(oLbxLeft,@aLList,@aLFiles,@cLPath) ) ;
  OF oPLeftMid

oLbxLeft:ALIGN := CONTROL_ALIGN_ALLCLIENT

aRList := {}

@ 15,15 LISTBOX oLbxRight VAR nOpRight;
  SIZE 300, 300 ;
  OF oPRightMid ;
  ITEMS aRList ;
  ON CHANGE ( doRChange(aGetsR,oLbxRight,@aRList,@aRFiles) ) ;
  ON DBLCLICK ( EnterRight(oFtp,aFtpInfo,oLbxRight,oRSay,@aRList,;
  @aRFiles,@cRPath) ) ;
  PIXEL

oLbxRight:ALIGN := CONTROL_ALIGN_ALLCLIENT

oLSay:ALIGN := CONTROL_ALIGN_TOP
oRSay:ALIGN := CONTROL_ALIGN_TOP

// Insere os gets com os dados do arquivo atual
// do lado esquerdo. Os dados são atualizados conforme
// é feita a navegação na lista

@ 05+3,02 SAY oSay PROMPT "Arquivo" SIZE 40,10 ;
  COLOR CLR_BLACK,CLR_WHITE OF oPLeftBottom PIXEL
@ 05,50 GET oGetFName VAR cFname SIZE CALCSIZEGET(40),10 ;
  OF oPLeftBottom PIXEL
oGetFName:SETENABLE(.F.)
aadd(aGetsL,oGetFName)

@ 20+3,02 SAY oSay PROMPT "Tamanho" SIZE 40,10 ;
  COLOR CLR_BLACK,CLR_WHITE OF oPLeftBottom PIXEL
@ 20,50 GET oGetFSize VAR nFSize PICTURE "999999999999999999" ;
  SIZE CALCSIZEGET(18),10 OF oPLeftBottom PIXEL
oGetFSize:SETENABLE(.F.)
aadd(aGetsL,oGetFSize)

@ 35+3,02 SAY oSay PROMPT "Data" SIZE 40,10 ;
  COLOR CLR_BLACK,CLR_WHITE OF oPLeftBottom PIXEL
@ 35,50 GET oGetFDate VAR cFDate SIZE CALCSIZEGET(18),10 ; 
  OF oPLeftBottom PIXEL
oGetFDate:SETENABLE(.F.)
aadd(aGetsL,oGetFDate)

@ 50+3,02 SAY oSay PROMPT "Atributos" SIZE 40,10 ; 
  COLOR CLR_BLACK,CLR_WHITE OF oPLeftBottom PIXEL
@ 50,50 GET oGetFAttr VAR cFAttr SIZE CALCSIZEGET(5),10 ;
  OF oPLeftBottom PIXEL
oGetFAttr:SETENABLE(.F.)
aadd(aGetsL,oGetFAttr)

// Insere dados e detalhes dos arquivos do FTP
// Os dados são atualizados conforme
// é feita a navegação na lista

@ 05+3,02 SAY oSay PROMPT "Arquivo" SIZE 40,10 ;
  COLOR CLR_BLACK,CLR_WHITE OF oPRightBottom PIXEL
@ 05,50 GET oRFName VAR cRFname SIZE CALCSIZEGET(40),10 ;
  OF oPRightBottom PIXEL
oRFName:SETENABLE(.F.)
aadd(aGetsR,oRFName)

@ 20+3,02 SAY oSay PROMPT "Tamanho" SIZE 40,10 ;
  COLOR CLR_BLACK,CLR_WHITE OF oPRightBottom PIXEL
@ 20,50 GET oRFSize VAR nRFSize PICTURE "999999999999999999" ;
  SIZE CALCSIZEGET(18),10 OF oPRightBottom PIXEL
oRFSize:SETENABLE(.F.)
aadd(aGetsR,oRFSize)

@ 35+3,02 SAY oSay PROMPT "Data" SIZE 40,10 ;
  COLOR CLR_BLACK,CLR_WHITE OF oPRightBottom PIXEL
@ 35,50 GET oRFDate VAR cRFDate SIZE CALCSIZEGET(18),10 ;
  OF oPRightBottom PIXEL
oRFDate:SETENABLE(.F.)
aadd(aGetsR,oRFDate)

@ 50+3,02 SAY oSay PROMPT "Atributos" SIZE 40,10 ;
  COLOR CLR_BLACK,CLR_WHITE OF oPRightBottom PIXEL
@ 50,50 GET oRFAttr VAR cRFAttr SIZE CALCSIZEGET(5),10 ;
  OF oPRightBottom PIXEL
oRFAttr:SETENABLE(.F.)
aadd(aGetsR,oRFAttr)

// Insere as opções de Menu

oTMenu1 := TMenu():New(0,0,0,0,.T.,,oDlg,CLR_BLACK,CLR_WHITE)
oMenuBar:AddItem('&Local' , oTMenu1, .T.)

oTMenu1:Add( TMenuItem():New(oDlg,'&Path',,,,;
  {|| LocalPath(oLbxLeft,@aLList,@aLFiles,oLSay,@cLPath) },,,,,,,,,.T.))
oTMenu1:Add( TMenuItem():New(oDlg,'Sai&r',,,,;
  {||oDlg:End()},,,,,,,,,.T.))

oTMenu2 := TMenu():New(0,0,0,0,.T.,,oDlg,CLR_BLACK,CLR_WHITE)
oMenuBar:AddItem('&FTP' , oTMenu2, .T.)

oTMenu2:Add( TMenuItem():New(oDlg,'&Conectar' ,,,,;
  {|| FTPConn(oDlg,oPRight,oFtp,@aFTPInfo,oLbxRight,@aRList,@aRFiles,oRSay,@cRPath) },,,,,,,,,.T.))
oTMenu2:Add( TMenuItem():New(oDlg,'&Desconectar',,,,;
  {|| FTPDesConn(oDlg,oPRight,oFtp,@aFTPInfo,oLbxRight,@aRList,@aRFiles,oRSay,@cRPath) },,,,,,,,,.T.))

oTMenu4 := TMenu():New(0,0,0,0,.T.,,oDlg,CLR_WHITE,CLR_BLACK)
oMenuBar:AddItem('&Ajuda', oTMenu4, .T.)

oTMenu4:Add(TMenuItem():New(oDlg,'&Sobre',,,,{|| About()},,,,,,,,,.T.))

// Ajusta o tamanho do Menu com o Tamanho da Janela
// PAra nao "suprimir" opções com ">>"
oMenuBar:NWIDTH := oDlg:nWidth

// Posiciona no primeiro arquivo
oLbxLeft:Gotop()

// Copiar Arquivo
SETKEY( VK_F5 , {|| IIF(_KeyRunning , NIL , ;
   ( _KeyRunning := .T. , ;
     CallKey(VK_F5 ,oLbxLeft,oLbxRight,aLFiles,aLList,;
             aRFiles,aRList,cLPath,cRPath,oFtp),;
     _KeyRunning := .F. ))})

ACTIVATE WINDOW oDlg MAXIMIZED ;
  ON INIT DoInit(oDlg) ;
  VALID CanQuit()

// Fecha a conexao com o FTP caso esteja aberta
oFtp:Close()

Return

Estrutura da Interface

A primeira escolha da interface foi usar uma WINDOW para a criação do programa, pois ela permite o redimensionamento da janela. E, para que o redimensionamento mão me obrigasse a fazer cálculos rebuscados de alinhamento de componentes, a grande sacada foi usar um objeto visual tSplitter() para dividir a tela principal em dois painéis, cada um ocupando automaticamente 50% da tela, e dentro de cada painél, mais três painéis de alinhamento automático, para permitir um header e um footer em cada painel, onde a área central contém um tListBox(), que será populado do lado esquerdo com os arquivos locais, e do lado direito com os arquivos da pasta atual do FTP Server conectado.

Para executar o programa, basta iniciar um SmartClient, e chamar diretamente a função U_FTPMANAGER.

FTP Client 1

Ao entrar, o programa ainda não está conectado a nenhum FTP Server, então ele mostra do lado esquerdo a estrutura de pastas a partir do RootPath do Ambiente no Protheus Server, e do lado direito uma lista sem itens.

FTP Client 2

Menu Local

No menu superior chamado “Local”, a opção “Path” permite trocar diretamente a pasta atual do painel do lado esquerdo. Para isso, é aberta uma interface de navegação (função cGetFile) que permite escolher inclusive navegar pela estrutura de pastas do Protheus Server — a partir do Rootpath do ambiente — para escolher um diretório de trabalho.

FTP Client 3

Segue abaixo o fonte da função de menu “LocalPath()

/* ----------------------------------------------------------------------
Permite trocar a pasta atual navegando diretamente
na estrutura de pastas do Servidor a partir do RootPath
---------------------------------------------------------------------- */

STATIC Function LocalPath(oLbxLeft,aLList,aLFiles,oLSay,cLPath)
Local cRet

cRet := cGetFile( 'Todos os Arquivos|*.*' , ;
   'Path', 1, cLPath, .F., GETF_RETDIRECTORY ,.T., .T. )

If !empty(cRet)
  // Troca o path e atualiza a lista de arquivos na tela
  cLPath := cRet
  aLList := GetLFiles(cLPath,aLFiles)
  oLbxLeft:SetArray(aLList)
  oLbxLeft:GoTop()
  oLSay:SetText(cLPath)
Endif

Return

Também temos a opção de Sair, que também pode ser acionada com a tecla ESC.

Menu FTP

No menu superior chamado “FTP”, a opção “Conectar” permite especificar as informações de conexão com um FTP Server, através de uma caixa de diálogo. Normalmente as informações mínimas necessárias — pelo menos para acessar um servidor publico de download — são apenas HOST ou IP do FTP Server, e o número da porta. As demais informações já foram determinadas no programa como DEFAULT.

FTP Client 4

Segue abaixo o fonte da função de conexão com o FTP — “FTPConn()” — Esta função acaba fazendo um pouco mais do que apenas conectar, mas também atualizar o endereço atual do FTP no painel superior, e os arquivos da pasta atual no tListBox() do lado direito.

/* ----------------------------------------------------------------------
Diálogo de Conexão com FTP
Armazema parametros de conexao, e em caso de sucesso,
já alimenta a lista de arquivos do lado direito
---------------------------------------------------------------------- */

STATIC Function FTPConn(oDlg,oPRight,oFtp,aFTPInfo,oLbxRight,aRList,aRFiles,oRSay,cRPath)
Local cTitle := 'Conexão com FTP'
Local oSay1,oSay2
Local lGo := .F.
Local cFTPAddr := padr(aFTPInfo[1],40)
Local nFtpPort := aFTPInfo[2]
Local nTimeOut := aFTPInfo[3]
Local bPasv := aFTPInfo[4]
Local bUseIP := aFTPInfo[5]
Local bAnonymous := aFTPInfo[6]
Local cUser := padr(aFTPInfo[7],40)
Local cPass := padr(aFTPInfo[8],40)
Local nStat

DEFINE DIALOG oDlgConn TITLE (cTitle) ;
  FROM 0,0 TO 220,450 PIXEL;
  OF oDlg ;
  COLOR CLR_WHITE, CLR_BROWN

@ 05+3,05 SAY oSay1 PROMPT "FTP" RIGHT SIZE 40,12 OF oDlgConn PIXEL
@ 05,50 GET oGetFTP VAR cFtpAddr SIZE CALCSIZEGET(40) ,12 OF oDlgConn PIXEL

@ 20+3,05 SAY oSay2 PROMPT "Porta" RIGHT SIZE 40,12 OF oDlgConn PIXEL
@ 20,50 GET oGetPorta VAR nFtpPort PICTURE "99999" ;
  SIZE CALCSIZEGET(5) ,12 OF oDlgConn PIXEL

@ 20+3,100 SAY oSay3 PROMPT "TimeOut" RIGHT SIZE 40,12 OF oDlgConn PIXEL
@ 20,145 GET oGetTimeOut VAR nTimeOut PICTURE "999" ;
  SIZE CALCSIZEGET(3) ,12 OF oDlgConn PIXEL

@ 35,50 CHECKBOX oCkh1 VAR bPasv PROMPT "Passive Mode" ;
  SIZE 80,12 OF oDlgConn PIXEL

@ 45,50 CHECKBOX oCkh2 VAR bUseIP PROMPT "Use IP Conn" ;
  SIZE 80,12 OF oDlgConn PIXEL

@ 55,50 CHECKBOX oCkh3 VAR bAnonymous PROMPT "Anonymous Login" ;
  SIZE 80,12 OF oDlgConn PIXEL

@ 65+3,05 SAY oSay1 PROMPT "Usuário" RIGHT SIZE 40,12 OF oDlgConn PIXEL
@ 65,50 GET oGetUsr VAR cUser SIZE CALCSIZEGET(40) ,12 ;
  WHEN !bAnonymous OF oDlgConn PIXEL

@ 80+3,05 SAY oSay2 PROMPT "Senha" RIGHT SIZE 40,12 OF oDlgConn PIXEL
@ 80,50 GET oGetPsw VAR cPass SIZE CALCSIZEGET(40) ,12 ;
  WHEN !bAnonymous OF oDlgConn PIXEL
oGetPsw:LPASSWORD := .T.

@ 95, CALCSIZEGET(40) - 10 BUTTON oBtnOk PROMPT "Ok" SIZE 60,15 ;
  ACTION (lGo := .T. , oDlgConn:End()) OF oDlgConn PIXEL

ACTIVATE DIALOG oDlgConn CENTER

If lGo

  // Fecha qqer conexão existente anteriormente
  oFTP:Close()

  // Ajusta os parametros
  cFTPAddr := alltrim(cFTPAddr)
  cUser := alltrim(cUser)
  cPass := alltrim(cPass)

  // Guarda os parâmetros utilizados
  aFTPInfo[1] := cFTPAddr
  aFTPInfo[2] := nFtpPort
  aFTPInfo[3] := nTimeOut
  aFTPInfo[4] := bPasv
  aFTPInfo[5] := bUseIP
  aFTPInfo[6] := bAnonymous
  aFTPInfo[7] := cUser
  aFTPInfo[8] := cPass

  // Seta parâmetros na classe antes de conectar
  oFtp:BFIREWALLMODE := bPasv
  oFtp:NCONNECTTIMEOUT := nTimeOut
  oFtp:BUSESIPCONNECTION := bUseIP
 
  // Conecta no FTP
  If !bAnonymous
    MsgRun("FTP Connect",cFtpAddr,;
      {|| nStat := oFtp:FtpConnect(cFtpAddr,nFTPPort,cUser,cPass) })
  Else
    MsgRun("FTP Connect",cFtpAddr,;
      {|| nStat := oFtp:FtpConnect(cFtpAddr,nFTPPort,"anonymous","anonymous") })
  Endif

  If nStat == 0
    cCurrDir := ''
    nStat := oFtp:GETCURDIR(@cCurrDir)
    If nStat <> 0
      cRPath := "ftp://"+cFtpAddr+"/"
      oRSay:SetText(cRPath)
      oRSay:Refresh()
      MsgStop("Falha ao recuperar executar GetCurDir() - Erro "+;
        cValtoChar(nStat),oFtp:CERRORSTRING)
    Else
      // Atualiza pasta atual do FTP
      cRPath := "ftp://"+cFtpAddr+cCurrDir
      oRSay:SetText(cRPath)
      oRSay:Refresh()
    Endif
    // Limpa lado direito
    aSize(aRFiles,0)
    aSize(aRList,0)
    oLbxRight:SetArray(aRList)

    // Conectou com sucesso, recupera pasta atual e lista de arquivos
    MsgRun("Obtendo lista de arquivos",cRPath,;
      {|| aRFiles := oFtp:Directory("*",.T.) })
    aSize(aRList,0)

    aEval(aRFiles,{|x| aadd( aRList , alltrim(x[1]) )})
    oLbxRight:SetArray(aRList)

  Else

    aSize(aRFiles,0)
    aSize(aRList,0)
    oLbxRight:SetArray(aRList)

    MsgStop("Falha de Conexão -- Erro "+cValToChar(nStat),;
      oFtp:CERRORSTRING)
    cRPath := "FTP Client (Não Conectado)"
    oRSay:SetText(cRPath)
  Endif

  oLbxRight:GoTop()
  oPRight:Refresh()

Endif

Return

Reparem que algumas chamadas do FTP foram encapsuladas pela função MsgRun() — isto foi proposital, pois se você está conectando com um servidor de FTP mais “longe”, com latência de rede, as operações podem demorar um pouco, e dessa forma sabemos que função está sendo executada — enquanto ela está sendo executada.

Navegação e Funções

Para navegar entre as pastas, tanto de arquivos locais como do FTP, utilize ENTER ou Duplo Clique nas pastas. Para voltar para a pasta anterior, utilize o primeiro arquivo da lista, chamado “..”. Estas regras de navegação valem para ambos os painéis de arquivos – lado direito e esquerdo.

Por hora, as únicas funções disponíveis — além da navegação nas pastas — é a tecla F5. Caso você esteja com o foco em um arquivo local, e pressionar F5, a aplicação permite fazer UPLOAD deste arquivo na pasta atual do FTP mostrada no painel direito. E, caso você esteja com o foco em um arquivo do FTP, no painel direito, e pressionar F5, a aplicação permite fazer o Download do arquivo para a pasta local sendo mostrada no painel esquerdo.

A tecla ENTER ou o Duplo Clique em um arquivo ou pasta do painel esquerdo dispara a função EnterLeft() — vista abaixo:

/* ----------------------------------------------------------------------
Funcao disparada em caso de [ENTER] ou Duplo Click em um arquivo
na lista de arquivos locais -- lado esquerdo. Permite a navegação
entre os diretorios.
---------------------------------------------------------------------- */

STATIC Function EnterLeft(oLbxLeft,aLList,aLFiles,cLPath)
Local cFile
Local aTmp, nI
Local nOp := oLbxLeft:GetPos()

If nOp > 0
  cFile := alltrim(aLList[nOp])
  If cFile == '..'
    // Tenta ir para o nivel anterior
    aTmp := StrTokarr(cLPath,'\')
    cLPath := ''
    For nI := 1 to len(aTmp)-1
      cLPath += ( aTmp[nI] + '\')
    Next
    if empty(cLPath)
      cLPath := '\'
    Endif
    aLList := GetLFiles(cLPath,aLFiles)
    oLbxLeft:SetArray(aLList)
    oLbxLeft:GoTop()
  Else
    // SE for um diretorio, entra nele
    aTmp := aLFiles[nOp]
    if 'D' $ aTmp[5]
      // Se for um diretorio , entra
      cLPath += ( cFile+'\' )
      aLList := GetLFiles(cLPath,aLFiles)
      oLbxLeft:SetArray(aLList)
      oLbxLeft:GoTop()
    Endif
  Endif
Endif
Return

Esta função usa algumas funções auxiliares, que serão vistas no código completo — a ser disponibilizado no GITHUB. Por hora, vamos dar uma olhada também na função de navegação do lado direito — pasta do FTP Server conectado.

/* ----------------------------------------------------------------------
Função disparada em caso de [ENTER] ou Duplo Click em um arquivo
na lista de arquivos de FTP - Lado direito -- Permite navegar
entre os diretórios.
---------------------------------------------------------------------- */
STATIC Function EnterRight(oFTP,aFtpInfo,oLbxRight,oRSay,aRList,aRFiles,cRPath)
Local cFile
Local aTmp, nI
Local nOp := oLbxRight:GetPos()
Local cCurrDir

If nOp > 0
  cFile := alltrim(aRList[nOp])
    If cFile == '..'
    // Volta ao nivel anterior
    nStat := oFTP:CDUP()
    If nStat != 0
      MsgStop("Falha ao mudar de Diretorio - Erro "+cValToChar(nStat),oFtp:CERRORSTRING)
    Else
      cCurrDir := ''
      nStat := oFtp:GETCURDIR(@cCurrDir)
      cRPath := "ftp://"+aFtpInfo[1]+cCurrDir
      oRSay:SetText(cRPath)
      oRSay:Refresh()
      // Pega os arquivos do diretorio atual
      MsgRun("Obtendo lista de arquivos",cRPath,{|| aRFiles := oFtp:Directory("*",.T.) })
      aSize(aRList,0)
      // Acrescenta um atalho para voltar para o nivel anterior
      // SE eu nao estiver no niver RAIZ ...
      IF !(cCurrDir == '/')
        aadd(aRFiles,{"..",0,ctod(""),"",""})
        aSort(aRFiles,,,{|x1,x2| lower(x1[1]) < lower(x2[1]) })
      Endif
      aEval(aRFiles,{|x| aadd( aRList , alltrim(x[1]) )})
      oLbxRight:SetArray(aRList)
      oLbxRight:GoTop()
    Endif
  Else
    // SE for um diretorio, entra nele
    aTmp := aRFiles[nOp]
    if 'D' $ aTmp[5]
      // Se for um diretorio , entra
      // Troca o diretorio atual
      nStat := oFTP:CHDIR(cFile)
      If nStat != 0
        MsgStop("Falha ao mudar de Diretorio - Erro "+cValToChar(nStat),oFtp:CERRORSTRING)
      Else
        cRPath += ( cFile+'/' )
        oRSay:SetText(cRPath)
        // Pega os arquivos do diretorio atual
        MsgRun("Obtendo lista de arquivos",cRPath,{|| aRFiles := oFtp:Directory("*",.T.) })
        aSize(aRList,0)
        // Acrescenta um atalho para voltar para o nivel anterior
        aadd(aRFiles,{"..",0,ctod(""),"",""})
        aSort(aRFiles,,,{|x1,x2| lower(x1[1]) < lower(x2[1]) })
        aEval(aRFiles,{|x| aadd( aRList , alltrim(x[1]) )})
        oLbxRight:SetArray(aRList)
        oLbxRight:GoTop()
      Endif
    Endif
  Endif
Endif
Return

Pulos do Gato

Alguns pulos do gato neste fonte, além dos alinhamentos, foram a escolha dos arrays. Para cada tListBox, existem 2 arrays que trabalham em “paralelo”, um deles apenas com o nome dos arquivos, para ser mostrado na tela, e o outro array com 5 colunas, contendo o nome, tamanho, atributos e detalhes do arquivo, tanto da pasta local como do FTP. Trabalhar com estes arrays de forma sincronizada permite as validações para a navegação entre pastas, por exemplo, para ignorar um Enter ou Duplo Clique em um arquivo — ao invés de uma pasta.

Outra sacada está no controle do disparo da tecla de atalho F5, para a cópia dos arquivos. Primeiro, a forma de setar o bloco de código usando uma variável STATIC, inicializada com o valor .F. :

SETKEY( VK_F5 , {|| IIF(_KeyRunning , NIL , ;
  ( _KeyRunning := .T. , ;
    CallKey(VK_F5 ,oLbxLeft,oLbxRight,aLFiles,aLList,;
    aRFiles,aRList,cLPath,cRPath,oFtp) , ;
    _KeyRunning := .F. ))})

Na prática, isso evita o duplo disparo da tecla F5,  e ainda mais, quando forem acrescentadas novas teclas de função ou atalho, esta proteção vai fazer com que o primeiro atalho disparado não permita nenhuma outra tecla de atalho ser executada. O tratamento da ação da tecla será determinado internamente dentro da função CallKey(), que recebe todos os parâmetros necessários para ela obter os dados que ela precisa, e fazer uma atualização da interface se ou quando necessário.

A outra grande sacada é descobrir em qual componente eu estou com o FOCO, para eu saber se, quando eu pressionar F5, eu devo copiar o arquivo Local para o FTP, ou baixar o arquivo do FTP para a pasta local ? Vamos ao fonte:

/* ----------------------------------------------------------------------
Teclas de Atalho de funcionalidades do FTP
F5 = Copiar Arquivo ( Download ou Upload ) 
---------------------------------------------------------------------- */

STATIC Function CallKey(nKey,oLbxLeft,oLbxRight,aLFiles,aLList,aRFiles,aRList,cLPath,cRPath,oFtp)
Local hHndFocus
Local nPos
Local cFile
Local cSource
Local cTarget
Local cCurrDir
Local lExist
Local lRun
// Pega Handle do componente de interface que estava com o foco
// quando a tecla de atalho foi pressionada
hHndFocus := GETFOCUS()
If hHndFocus == oLbxLeft:HWND
  // Caso o foco esteja na lista de arquivos locais
  // E exista um arquivo posicionado ... 
  nPos := oLbxLeft:GetPos()
  If nPos > 0
    cFile := alltrim(aLFiles[nPos][1])
    cAttr := aLFiles[nPos][5]
    If cFile == '.' .or. cFile == '..'
      MsgStop("Operação com pasta não implementada. Selecione um arquivo.","Atenção")
      return
    ElseIf 'D'$cAttr
      MsgStop("Operação com pasta não implementada. Selecione um arquivo.","Atenção")
      return
    Endif
    If nKey == VK_F5
      // Copia de arquivo Local para o FTP
      cSource := cLPath+cFile
      cTarget := cRPath+cFile
      If MsgYEsNo("Seseja copiar o arquivo local ["+cSource+"] para o FTP ["+cTarget+"] ?")
        MsgRun("FTP Upload",cFile,{|| nStat := oFTP:SENDFILE(cSource,cFile) })
        If nStat <> 0
          MsgStop("Falha no UPLOAD de Arquivo - Erro "+cValToChar(nStat),oFtp:CERRORSTRING)
        Else
          MsgInfo("Upload realizado com sucesso.")
          cCurrDir := ''
          oFtp:GETCURDIR(@cCurrDir)
          // Pega os arquivos do diretorio atual
          MsgRun("Obtendo lista de arquivos",cRPath,{|| aRFiles := oFtp:Directory("*",.T.) })
          aSize(aRList,0)
          // Acrescenta um atalho para voltar para o nivel anterior
          // SE eu nao estiver no nivel RAIZ ...
          IF !(cCurrDir == '/')
            aadd(aRFiles,{"..",0,ctod(""),"",""})
            aSort(aRFiles,,,{|x1,x2| lower(x1[1]) < lower(x2[1]) })
          Endif
          aEval(aRFiles,{|x| aadd( aRList , alltrim(x[1]) )})
          oLbxRight:SetArray(aRList)
        Endif
      Endif
    Else
      MsgInfo("Operação com Arquivo Local ainda não implementada.")
    Endif
  Endif
ElseIf hHndFocus == oLbxRight:HWND
  // Copia arquivo do FTP para pasta Local
  // e exista algum arquivo posicionado
  nPos := oLbxRight:GetPos()
  IF nPos > 0
    cFile := alltrim(aRFiles[nPos][1])
    cAttr := aRFiles[nPos][5]
    If cFile == '.' .or. cFile == '..'
      MsgStop("Operação com pasta não implementada. Selecione um arquivo.","Atenção")
      return
    ElseIf 'D'$cAttr
      MsgStop("Operação com pasta não implementada. Selecione um arquivo.","Atenção")
      return
    Endif
    // Ajusta o nome vindo do FTP 
    AdjustFTP(@cFile)
    If nKey == VK_F5
      // Copia de arquivo do FTP para a pasta local 
      cSource := cRPath+cFile
      cTarget := cLPath+cFile
      lExist := File(cLPath+cFile)
      lRun := .F. 
      IF lExist
        If MsgYesNo("O Arquivo local já existe. Deseja continuar o Download ? ")
          lRun := .T.
          MsgRun("FTP Resume Download",cFile,{|| nStat := oFTP:RESUMERECEIVEFILE(cFile,cTarget) })
        ElseIf MsgYesNo("Apaga o arquivo local e reinicia o Download ?")
          lRun := .T.
          Ferase(cLPath+cFile)
          MsgRun("FTP Download",cFile,{|| nStat := oFTP:RECEIVEFILE(cFile,cTarget) })
        Endif
      Else
        If MsgYEsNo("Deseja baixar o arquivo do FTP ["+cSource+"] para a pasta local ["+cTarget+"] ?")
          lRun := .T.
          MsgRun("FTP Download",cFile,{|| nStat := oFTP:RECEIVEFILE(cFile,cTarget) })
        Endif
      Endif
      If lRun
        If nStat <> 0
          MsgStop("Falha no DOWNLOAD de Arquivo - Erro "+cValToChar(nStat),oFtp:CERRORSTRING)
        Else
          MsgInfo("Download realizado com sucesso.")
          // Atualiza lista de arquivos 
          aLList := GetLFiles(cLPath,aLFiles)
          oLbxLeft:SetArray(aLList)
        Endif
      Endif
    Else
      MsgInfo("Operação com Arquivo do FTP ainda não implementada.")
    Endif
  Endif
Endif

Return

A mágica do foco é feita inicialmente usando a função GetFocus(), que retorna o ID ou “Handler” do componente de interface que está em foco no momento. Como eu estou iniciando o processamento de uma tecla de atalho que não está relacionada a nenhum componente de interface, pressionar F5 não muda o foco do componente de interface atual.

hHndFocus := GETFOCUS()

A segunda parte da mágica, é eu verificar SE o componente em foco é a lista de arquivos do lado direito ou do lado esquerdo. No momento, estes são os únicos componentes da minha interface — fora o Menu — que permitem foco. Logo, eu devo estar posicionado em um deles. Cada componente da interface visual possui um ID ou “Handler”, dado no momento da criação deste componente, e você pode consultá-lo através da propriedade HWND.

If hHndFocus == oLbxLeft:HWND

Desta forma, eu sei se o foco está no tListBox do lado esquerdo, e realizando o mesmo teste com o objeto oLbxRight, eu sei se ele está com foco no lado direito. Se nenhuma das alternativas for a correta, eu assumo que foi pressionado F5 quando o foco não estava em nenhum componente válido para realizar a execução desta funcionalidade.

Detalhes dos Arquivos em Foco

Cada tListBox() foi parametrizado para disparar um evento ONCHANGE, na mudança da seleção ou foco em um item. Este evento é usado pelas funções  doLChange() e doRChange() para atualizar os detalhes dos arquivos em foco nos painéis, vide fonte abaixo:

/* ----------------------------------------------------------------------
Função disparada na troca de posição da lista de arquivos
do lado esquerdo -- arquivos locais
Atualiza as informações do arquivo selecionado no painel inferior
---------------------------------------------------------------------- */

STATIC Function doLChange(aGetsL,oLbxLeft,aLList,aLFiles)
Local cFname , cFDate, nFSize , cFAttr
Local nOp := oLbxLeft:GetPos()
If nOp > 0 .and. nOp <= Len(aLList)
  cFname := aLFiles[nOp][1]
  nFSize := aLFiles[nOp][2]
  cFDate := dtoc(aLFiles[nOp][3])+' ' +aLFiles[nOp][4]
  cFattr := aLFiles[nOp][5]
  Eval(aGetsL[1]:bSetGet,cFname)
  Eval(aGetsL[2]:bSetGet,nFSize)
  Eval(aGetsL[3]:bSetGet,cFDate)
  Eval(aGetsL[4]:bSetGet,cFattr)
Else
  Eval(aGetsL[1]:bSetGet,"")
  Eval(aGetsL[2]:bSetGet,0)
  Eval(aGetsL[3]:bSetGet,"")
  Eval(aGetsL[4]:bSetGet,"")
Endif
aGetsL[1]:Refresh()
aGetsL[2]:Refresh()
aGetsL[3]:Refresh()
aGetsL[4]:Refresh()
return


/* ----------------------------------------------------------------------
Função disparada na troca de posição da lista de arquivos FTP
do lado direito.
Atualiza as informações do arquivo selecionado no painel inferior
---------------------------------------------------------------------- */
STATIC Function doRChange(aGetsR,oLbxRight,aRList,aRFiles)
Local cFname , cFDate, nFSize , cFAttr
Local nOp := oLbxRight:GetPos()
If nOp > 0 .and. nOp <= Len(arList)
  cFname := aRFiles[nOp][1]
  nFSize := aRFiles[nOp][2]
  cFDate := dtoc(aRFiles[nOp][3])+' ' +aRFiles[nOp][4]
  cFattr := aRFiles[nOp][5]
  Eval(aGetsR[1]:bSetGet,cFname)
  Eval(aGetsR[2]:bSetGet,nFSize)
  Eval(aGetsR[3]:bSetGet,cFDate)
  Eval(aGetsR[4]:bSetGet,cFattr)
Else
  Eval(aGetsR[1]:bSetGet,"")
  Eval(aGetsR[2]:bSetGet,0)
  Eval(aGetsR[3]:bSetGet,"")
  Eval(aGetsR[4]:bSetGet,"")
Endif
aGetsR[1]:Refresh()
aGetsR[2]:Refresh()
aGetsR[3]:Refresh()
aGetsR[4]:Refresh()
return

Conclusão

Muitas vezes o fonte cresce em trabalho, não necessariamente em complexidade — desde que ele já tenha nascido usando um mínimo de boas práticas de programação. Se você olhar o fonte com uma lupa, vai ver que ainda existe códigos duplicados e algumas “pontas soltas”, que serão vistas na continuação desse post! O Fonte completo está no link do GITHUB logo abaixo, nas referências.

Agradeço novamente a todos pela audiência e lhes desejo TERABYTES DE SUCESSO !!! 

Referências

 

Protheus e FTP Client – Parte 02

Introdução

No post anterior (Protheus e FTP Client), vimos um exemplo básico de identificação da existência de um arquivo, e como fazer para baixar o arquivo do FTP em uma pasta local a partir do RootPath do ambiente Protheus. Agora, vamos ver com mais detalhes algumas propriedades interessantes da classe TFTPClient.

Propriedades da classe TFTPClient

Todas as propriedades e métodos da classe estão documentadas na TDN, vide link nas referências, no final do post, porém vamos ver algumas delas uma riqueza maior de detalhes, para entender onde é preciso utilizá-las.

bFireWallMode

Esta propriedade indica se a conexão com o FTP será Ativa ou Passiva. Para esta propriedade ter efeito, ela deve ser setada antes de estabelecermos a conexão com o FTP Server.

Em poucas palavras, o FTP usa duas conexões entre o Cliente e o Servidor FTP, uma conexão de dados e outra de controle. Quando usado o modo Ativo (bFireWallMode = .F.  — DEFAULT) , o FTP Server estabelece a conexão de dados em uma porta aberta e indicada pelo Cliente, que abre primeiro a conexão de controle na porta 21 do FTP Server. Pela perspectiva do Firewall do Cliente FTP, uma sistema externo está tentando conectar-se com um cliente interno, o que normalmente é restrito.

Quando habilitamos a propriedade bFireWallMode para .T., indicamos ao Cliente de FTP que ele deve estabelecer a conexão em “Modo Passivo”. Uma vez que o FTP Server seja capaz de trabalhar com a conexão em modo passivo, o Cliente de FTP abre as duas conexões — controle e dados — no FTP Server, na seguinte sequência: Após abrir a conexão de controle no FTP Server, o cliente informa ao FTP Server que a conexão deve ser passiva, então o FTP Server retorna um número de uma segunda porta — acima de 1024 — para o Cliente abrir a conexão de dados, sem precisar abrir um range de portas no Cliente FTP.

— Observação – O Protheus como FTP Server não suporta o modo passivo.  —

bUsesIPConnection

Esta configuração pode ser necessária quando o servidor onde o Protheus Server está sendo executado possua mais de uma interface de rede. Quando o FTP Client não está em modo passivo, e deve receber a conexão de dados de “volta”do FTP Server, normalmente o FTP Client busca o IP da máquina atual para enviar ao FTP Server, porém quando a máquina têm mais de uma interface de rede, não é garantido que o IP retornado seja por exemplo o IP “Externo”, que aceite a conexão. Quando habilitamos a propriedade bUsesIPConnection para .T., o FTP Client pega o IP da Interface de rede que estabeleceu a conexão de controle com o FTP, para informar ao FTP Server onde ele deve fazer a conexão de dados.

cErrorString

Esta é uma propriedade de consulta. Normalmente os métodos da classe cliente de FTP retornam “0” (zero) em caso de sucesso, e em caso de falha um código de erro. A lista de código de erros está documentada neste link da TDN, porém quando um método retorna erro, a propriedade cErrorString é alimentada com a descrição do erro retornado. Isto facilita a montagem de uma mensagem ou LOG de erro com mais detalhes.

nConnectTimeout

Por default, o time-out de tentativa de conexão com o FTP Server é de 5 segundos. Para alterar este tempo (em segundos) antes de estabelecer a conexão, defina o valor desejado nesta propriedade. Alguns servidores — dependendo da velocidade e latência de rede — podem precisar de um tempo um pouco maior.

nControlPort

Caso já exista uma conexão estabelecida com o FTP Server, esta propriedade informa a porta que foi usada para a conexão de controle (DEFAULT=21). Se esta propriedade for setada antes da conexão estabelecida, ela define a porta default de conexão com o FTP Server. O método FTPConnect(), usado para estabelecer a conexão, permite opcionalmente receber a porta de conexão no segundo parâmetro. Caso este não seja informado, será usada a porta definida na propriedade nControlPort.

nDataPort

Caso já exista uma conexão estabelecida com o FTP Server, esta propriedade informa qual é a porta TCP do FTP Client usada para estabelecer a conexão de dados. Quando não usamos o modo passivo, o FTP Client usa uma porta randomicamente sorteada entre 10 e 30 mil.

nDirInfo

Esta propriedade, quando consultada, realiza uma busca da lista de arquivos da pasta atual setada na conexão com o FTP Server. retornando em caso de sucesso o valor 0 (zero), caso contrário retorna o código do erro ocorrido. Normalmente realizamos a busca dos arquivos disponíveis na pasta atual usando o método Directory(), que internamente também realiza a busca de arquivos da pasta atual do FTP Server, permitindo inclusive um retorno filtrado por um arquivo específico ou o uso de máscaras (* e ?).

nDirInfoCount

Após uma consulta à propriedade nDirInfo,  a propriedade nDirInfoCount informa quantos arquivos foram encontrados na pasta atual do FTP Server.

nTransferMode

Esta propriedade indica e permite alterar o modo interno de transferência de dados entre o FTP Client e o FTP Server. Este valor deve ser setado após estabelecida a conexão com o FTP Server. Existem três modos de transferência: 0=Stream, 1=Block e 2=Compressed. O modo Stream é o DEFAULT. A RFC 959 explica em detalhes cada um dos modos, porém vejamos uma síntese de cada um.

Stream significa que os dados do arquivo são transmitidos em um modo contínuo, usando algumas sequencias de escape internas de controle, é o modo mais comum de transmissão; Block indica o uso de uma sequência de blocos de dados formatados em cabeçalho e conteúdo; e Compressed indica o uso de um algoritmo simples de controle para transmissão de bytes repetidos — economizando no tráfego de rede quando existem sequências do mesmo byte repetidas no arquivo a ser transmitido — veja mais detalhes sobre Run-length Encoding.

nTransferStruct

Permite alterar o modo como os dados são tratados na transferência. Os modos disponíveis são: 0=File (DEFAULT), 1=Record e 2=Page. A implementação de arquivo (File) é normalmente a mais utilizada, pois não interfere em seu conteúdo. Já as estruturas de transferência orientadas a registro (Record) e página (Page) podem ter interferências e sofrerem ajustes em uma das pontas da conexão, dependendo da plataforma em uso. Recomenda-se o uso da opção default (0=File), salvo em necessidades de integrações específicas.

nTransferType

Permite definir o tipo de transferência de dados usada na conexão. Por padrão, a transferência é do tipo 1=Image. Isto significa que os dados do arquivo são transmotidos em modo binário, isto é, a sqeuência de Bytes que compõe o arquivo, independente de seu conteúdo, é transmitida e recebida sem alteração.

Quando usado o tipo 0=ASCII, feito exclusivamente para a transmissão de arquivos texto (Texto Simples, sem UTF-8 ou caracteres especiais), podem haver alterações do conteúdo do arquivo entre plataformas, inclusive em casos onde isto seja desejável. Por exemplo, um arquivo texto puro no Windows usa dois bytes (CRLF) para indicar final de linha. Ao ser transmitido por FTP para uma estação Linux, usando o modo 0=ascii, as quebras de linha serão salvas no linux apenas com LF. Porém, se você setar o modo ascii e acidentalmente transmitir um arquivo de imagem ou outro arquivo de conteúdo binário, fatalmente ele vai ser “corrompido” no processo de gravação de quem estiver recebendo o arquivo.

Conclusão

Por hora, este post fica apenas como referência destas propriedades. No próximo sobre este assunto, vamos montar algo um pouco “maior” com a classe client de FTP.

Agradeço novamente a audiência, e desejo a todos TERABYTES DE SUCESSO !!! 

Referências

Protheus e FTP Client

Introdução

No post Protheus como Servidor de FTP, vimos como configurar um Servidor Protheus como FTP Server. Agora, vamos ver uma classe AdvPL que permite estabelecer uma conexão com um servidor FTP, e fazer operações como Download e Upload de arquivos — a classe tFtpClient.

Protocolos FTP, FTPS e SFTP

FTP, acrônimo de File Transfer Protocol, é um protocolo criado com a finalidade de transferência de arquivos entre estações. Para isso, uma estação atua como Cliente, estabelecendo uma conexão TCP/IP com a estação servidora. A porta padrão para os servidores de FTP é a porta 21.

O FTPS nada mais é do que uma extensão do protocolo FTP, que acrescenta o suporte a conexão criptografada usando TLS e/ou SSL.

O SFTP normalmente é confundido com o FTPS.  Na verdade seu nome vêm apenas da finalidade de transferência de arquivos, porém sua origem e natureza de implementação é bem diferente do FTP. Acrônimo de SSH File Transfer Protocol, na verdade é uma extensão do protocolo SSH (Secure Shell Protocol)  versão 2.0, concebido para transferência segura de arquivos.

Classe TFTPClient

Através da classe TFTPClient() do AdvPL, podemos criar uma conexão com um servidor FTP, bastando ter em mãos no mínimo o IP — ou nome do host — e a porta do servidor. Caso o servidor exija autenticação, precisamos ter um nome de usuário e uma senha para ser autenticada pelo servidor e realizar as operações.

Vale mencionar que, por hora, a classe TFTPClient() suporta apenas o protocolo FTP. Não há (ainda) suporte nativo em AdvPL para conexão cliente com FTPS e/ou SFTP. Quando existe a necessidade de uma integração automatizada com um servidor de arquivos implementado sobre um destes protocolos, a alternativa atual é executar uma aplicação externa mediante script ou similar, que faça a conexão e as tarefas necessárias.

Funcionalidades

Basicamente, as operações de um Cliente de FTP são as equivalentes a uma navegação em uma estrutura de pastas ou diretórios. Ao estabelecer a conexão com um servidor de FTP, normalmente o nosso diretório de trabalho remoto é a pasta “raiz” de publicação do FTP Server. A partir desse ponto, podemos executar operações como “listar arquivos da pasta atual”, “entrar em uma pasta”, “voltar para a pasta anterior”, “copiar um arquivo da pasta do FTP para a estação atual”, “copiar um arquivo da estação atual para a pasta atual do FTP”, “apagar um arquivo” e assim por diante.

Através das propriedades e métodos da classe TFTPClient(), podemos consultar ou parametrizar — de acordo com as capacidades do Servidor de FTP — o modo de transferência de arquivos, o tipo de transferência, o modo de conexão (Ativo ou Passivo), entre outras particularidades. Será mais fácil entender o funcionamento da classe partindo de um exemplo.

Exemplo AdvPL

O primeiro exemplo de uso será bem simples. Sua função é identificar a existência de um determinado arquivo na pasta Raiz de um servidor de FTP, e caso o arquivo exista, a aplicação AdvPL fará o download deste arquivo para uma pasta local do ambiente (environment) a partir do rootpath, chamada “downloads”. O Servidor de FTP utilizado foi um IIS em um Windows 10, com um site de FTP configurado para permitir acesso anônimo.

#include 'protheus.ch'

User Function TSTFTP()
Local oFtp, nStat
Local aFtpFile 
Local cFtpSrv := 'note-juliow-ssd'
Local nFTPPort := 21

SET DATE BRITISH
SET CENTURY ON

// Cria o objeto Client
oFtp := tFtpClient():New()

// Estabelece a conexão com o FTP Server 
nStat := oFtp:FtpConnect(cFtpSrv,nFTPPort)
If nStat != 0
  conout("FTPClient - Erro de Conexao "+cValToChar(nStat))
  QUIT
Endif

// Procura pelo arquivo leiame.txt
aFtpFile := oFtp:Directory( "leiame.txt", .T. )

if len(aFtpFile) > 0
  // Arquivo encontrado - Mostra os detalhes do arquivo 
  conout( cValToChar(aFtpFile[1][1])+" | "+; // nome
    cValToChar(aFtpFile[1][2])+" | "+; // tamanho
    cValToChar(aFtpFile[1][3])+" | "+; // data
    cValToChar(aFtpFile[1][4])+" | "+; // horario
    cValToChar(aFtpFile[1][5]) ) // Atributo . D = Diretorio
  // Faz o download do arquivo para a pasta local de downloads
  nStat := oFtp:ReceiveFile('leiame.txt','\downloads\leiame.txt' ) 
  If nStat != 0 
    conout("*** Falha no recebimento do arquivo - Erro "+cValToChar(nStat))
  Else
    conout("Arquivo recebido com sucesso.")
  Endif
Else
  conout("*** Arquivo nao encontrado.")
Endif
oFtp:Close()
Return

Caso ocorra falha de conexão, o programa não continua. Ao determinar a existência do arquivo no Servidor de FTP — através do método ::Directory() — fazemos o download do arquivo usando o método ::ReceiveFile(). Caso o arquivo na pasta local já exista, ele será sobrescrito.

Nas referências deste post, verifiquem todas as propriedades e métodos disponíveis na classe TFTPClient() na documentação dela na TDN. Para uma primeira versão, nosso exemplo será bem “arroz com feijão” mesmo, acredito que somente com um programa mais extenso, ou com mais programas de tamanho reduzido, será possível exemplificar as demais funcionalidades da classe TFTPClient()

Observações

Os primeiros testes das funcionalidades básicas foram feitos configurando o programa Cliente de exemplo usando o Protheus como FTP Server. E, para meu espanto, o método ::Directory()  não encontrava o arquivo, na verdade mesmo que a máscara de busca informada fosse  “*”  — para identificar todos os arquivos e sub-pastas a partir da pasta atual, não localizavam nada. Em um dos testes, eu resolvi acessar — para consulta — a propriedade chamada nDirInfo da engine Client de FTP, e para minha surpresa, após acessar esta propriedade, o método ::Directory(“leiame.txt”) localizou o arquivo, e o download / recebimento foi feito com sucesso. Como houveram falhas também em outras funcionalidades da API client, porém somente quando usado o Protheus como FTP Server, por hora os exemplos usados para demonstração das funcionalidades da classe TFTPClient serão testados com o FTP do Microsoft IIS, e posteriormente com um FTP Server no Linux,

Conclusão

Ainda vamos explorar mais esta classe, inclusive acessando um FTP Server na Internet. Porém, esta abordagem fica para o próximo post.

Agradeço novamente a audiência, curtidas, compartilhamentos, comentários e sugestões, e desejo a todos TERABYTES DE SUCESSO 😀

Referências

 

 

O Protheus como servidor HTTP – Parte 02

Introdução

Continuando de onde paramos no post anterior, vamos agora acrescentar mais alguns detalhes e falar sobre algumas configurações mais avançadas do Protheus como Servidor HTTP.

Porta TCP 80

Quando falamos de Internet e HTTP, a porta padrão deste protocolo é a porta TCP 80. Isto quer dizer que, o Servidor WEB deve estar configurado para receber conexões HTTP na porta 80.

Porém, quando estamos por exemplo testando um serviço, ou mesmo ciando um serviço para WEB usando WebServices sobre HTTP (REST ou SOAP), podemos especificar outra porta para disponibilizar o serviço. Na prática, o que muda é a forma de você escrever a URL.

Partindo do exemplo anterior, onde configuramos o Protheus como HTTP Server na porta 80, usamos a URL http://localhost/index.html. Ao mudarmos a porta de publicação do serviço — e consequentemente reiniciar o Servidor — devemos colocar a porta depois do nome do host, usando o separador “:” dois pontos. Por exemplo, ao subir o serviço de HTTP na porta 8000, a nossa URL passa a ser http://localhost:8000/index.html

Porta SSL 443

A porta 443 é a porta default para acesso ao WEB Server usando uma conexão chamada de “segura”, que nada mais é do que uma conexão criptografada usando o protocolo SSL (Secure Socket Layer) sobre o TCP. Se, no nosso exemplo, você trocar a URL http://localhost/index.html para https://localhost/index.html, o Browse vai tentar abrir uma conexão segura na porta 443, pois esta é a porta DEFAULT para o protocolo HTTPS — e como o nosso Protheus Server ainda não está configurado para SSL, esta conexão ainda não será possível. Caso seja usada outra porta, a forma de especificar é a mesma: Após o nome do host, deve ser colocado “:” mais o número da porta desejada. O Protheus como servidor WEB também suporta HTTPS, porém vamos ver este recurso mais para frente.

Falha ao subir o serviço de HTTP

Normalmente um Protheus pode falhar na subida do serviço HTTP, caso já exista outra aplicação na máquina usando (ou em outros termos, “fazendo bind”) da porta 80. Existem alguns programas de mensagens e aplicativos que podem subir um serviço de HTTP na porta 80, para uso na máquina local. Caso já exista algum outro serviço qualquer usando a porta configurada, o Protheus Server emite a seguinte mensagem no log de console:

[WARN ][SERVER] [Thread 12628] Trying acquire port 80 (Bind error #10048:Address already in use.)

Neste caso, você pode pode mudar a porta do Protheus, para preferencialmente usar uma porta a partir da 5024, ou tentar identificar qual é a aplicação que está usando a porta 80. Uma das formas mais simples — no Windows — é usar o aplicativo gráfico RESMON.EXE (Resource Monitor), abrir a aba NETWORK, e expandir a seção “Listening Ports”. Veja um exemplo abaixo, onde o appserver.exe (Protheus Server) está com o servidor HTTP habilitado na porta 80:

AdvPL ASP - Server 80

Falha ao acessar o servidor HTTP

Vamos pegar o nosso exemplo de acesso, http://localhost/index.html — e enumerar as possibilidades desta requisição, ao ser feita pelo Browser, não ser completada com sucesso.

  1. Erro na digitação da URL  — qualquer coisa alterada nessa URL pode fazer a conexão TCP feita pelo Browser não chegar ao servidor certo, ou não localizar o arquivo desejado.
  2. O Serviço HTTP do Protheus não está no ar.  — ou está no ar mas em outra porta.
  3. O Serviço do Protheus não foi iniciado, ou falhou na subida ou inicialização.

Agora, se estamos acessando esta página pelo IP da máquina, a partir de outro computador dentro da mesma rede, além de todos os problemas enumerados acima, ainda podemos ter:

  1. Windows Firewall endo executado na máquina onde o HTTP Server está sendo executado, bloqueando o aceite ou retorno de conexões vindas de outra máquina na porta 80.
  2. Um Proxy Client configurado para acesso de internet na máquina onde está sendo executado o Browser , onde faltou configurar que o IP da máquina de destino, onde está o Protheus Server, não deve ser acessado através do PROXY. — vamos ver depois  o que é um “proxy”.

Formas de Acesso ao HTTP Server

Nestes exemplos, já vimos duas formas de escrever uma URL para abrir a página HTML de testes “index.html”, uma usando a palavra reservada “localhost”, e outra usando o IP da máquina em questão. Ainda podemos usar o IP reservado 127.0.0.1 — que na verdade é a mesma coisa que “localhost”, e caso a máquina onde está o Protheus Server HTTP tiver um nome de host — ou nome do equipamento na rede — visível pelos outros computadores da rede, podemos colocar o nome da máquina na URL, ao invés do IP. Por exemplo, ao invés de http://192.168.0.12/index.html, eu poderia usar http://note-juliow-ssd/index.html, que o Browser abriria a página corretamente.

Quando queremos que o HTTP Server esteja publicado na Internet, para que pessoas de qualquer parte do mundo conectadas na rede possam acessar, o buraco é um pouco mais em baixo… vamos ver primeiro o acesso através de uma rede local.

Configurações HTTP adicionais

Para o nosso teste, foi necessário informar na URL o protocolo (http://), o host (localhost ou IP), e a página ou arquivo a ser aberto (index.html), certo? Normalmente ninguém precisa saber qual é a página principal de um site, você entra apenas com a URL contendo protocolo e host, e o próprio website já abre a página inicial (ou DEFAULT).

Para configurar uma página DEFAULT para o PRotheus Server como HTTP, usamos a chave DEFAULTPAGE. Vide exemplo abaixo:

[http]
enable=1
port=80
path=c:\Protheus12LG\Http
defaultpage=index.html

Desta forma, agora podemos abrir o site usando apenas http://localhost , que o arquivo index.html será revolvido ao Browser, sem que você precise informar o nome dele na URL .

Sub-Pastas de publicação WEB

No nosso PATH de publicações WEB, a pasta “raiz” de publicação fica no disco, mais exatamente no drive C:, na pasta \Protheus12LG\Http . Logo, qualquer arquivo que a gente coloque dentro desta pasta, pode ser acessado pela URL http://host-ou-ip/nomedoarquivo.extensao&#8221;

Caso você crie no disco, dentro da pasta HTTP, uma pasta chamada “imagens”, e dentro dela você colocar a imagem “foto.jpg”, para acessá-la do Browser, você deverá usar a URL http://host-ou-ip/imagens/foto,jpg&#8221;

Mais de um site no mesmo HTTP Server

Quando utilizamos no Browse o IP da máquina, ou o nome dela na rede local, ambas as requisições chegam no mesmo HTTP Server, na mesma porta. O que muda entre elas é a URL através da qual foi feito o pedido da página.

Usando o HTTP Server do Protheus, podemos configurar uma ou mais seções no arquivo de configuração, para permitir direcionar URLs diferentes que chegam no mesmo serviço, para diferentes pastas raiz de publicação. Por exemplo, minha máquina na rede chama-se note-juliow-ssd, com o IP 192.168.0.12, e eu quero colocar dois sites diferentes, com pastas de publicação WEB diferentes. Basta eu acrescentar as chaves abaixo no appserver.ini:

[note-juliow-ssd]
enable=1
Path=c:\Protheus12LG\WebSite1
DefaultPage=index.html

[192.168.0.12]
enable=1
Path=c:\Protheus12LG\WebSite2
DefaultPage=index.html

Considerando que eu ainda tenho todas as demais configurações da seção HTTP (abaixo), agora nós temos o seguinte cenário:

[http]
enable=1
port=80
path=c:\Protheus12LG\Http
defaultpage=index.html

Deste modo, o HTTP Server vai se comportar da seguinte forma, para cada uma das URLs de acesso:

http://192.168.0.12/ — O HTTP Server encontra uma seção de configuração com este IP, verifica que está habilitada (Enable=1) e que o PATH raiz de publicação é a pasta c:\Protheus12LGWebSite1. Logo, ele vai procurar a página default (index.html) dentro dessa pasta do disco, e retorná-la ao Browse.

http://note-juliow-ssd/ — Mesmo caso anterior, porém com outra pasta raiz de publicação configurada — c:\Protheus12LG\WebSite2. Logo, o HTTP Server vai retornar o arquivo index.html deste diretório configurado.

http://localhost/ — Inicialmente o HTTP Server verifica se tem uma seção de configuração chamada de [localhost] no arquivo de configuração. Como ele não encontra nada, ele vai ler as configurações default da seção [HTTP], e vai retornar o arquivo index.html da pasta raiz default — c:\Protheus12LG\Http

FAQ

  • É possível subir o Protheus como  HTTP Server em mais de uma porta ao mesmo tempo ? 

R: Não. A configuração de porta do HTTP Server é única, e é definida apenas na seção HTTP. O que dá para ser feito é subir o HTTP Server e o HTTPS Server — conexão SSL — no mesmo serviço, porém são protocolos distintos, cada um sendo acessado pela porta configurada.

  •  É possível subir mais de um Protheus como HTTP Server na mesma porta e na mesma máquina?

R: Sim, mediante configuração específica. Quando subimos um HTTP Server Protheus em uma porta, ele passa a atender as requisições que chegam nesta porta, por qualquer interface de rede da máquina. Se você configurar, através da chave IP=nnn.nnn.nnn.nnn, na seção HTTP, indicando o IP de uma interface de rede para o HTTP Server atender as requisições, você consegue configurar outro HTTP Server na mesma porta, bastando especificar o IP de outra interface de rede da sua máquina.

  • Eu consigo criar pastas virtuais no HTTP Server ?

R: Sim, usando o mesmo recurso para configurar múltiplos sites na mesma máquina — criando uma chave no arquivo appserver.ini com o nome do host de entrada da URL, seguido por “/” e uma ou mais pastas, separadas por “/”. Por exemplo, os dois sites acima possuem uma pasta chamada “common”, cujo conteúdo é idêntico para os dois sites. Você pode gravar estes arquivos em apenas uma pasta — por exemplo c:\Protheus12LG\WebCommon, e criar as seguintes configurações:

[note-juliow-ssd/common]
enable=1
Path=c:\Protheus12LG\WebCommon

[192.168.0.12/common]
enable=1
Path=c:\Protheus12LG\WebCommon

Com isso, tanto faz se o usuário está navegando no site 1 ou no site 2, quando o Browse pedir um arquivo da pasta “/common/” através de qualquer uma das URLs, apenas uma pasta será usada para atender estas requisições, evitando duplicidades de arquivo desnecessárias.

Conclusão

Acho que até aqui já está bom como introdução ao HTTP Server do Protheus para páginas estáticas. No próximo post, vamos ver o uso de uma conexão segura (SSL), e depois vamos partir para o AdvPL ASP !!!

Agradeço novamente a audiência, e desejo a todos TERABYTES DE SUCESSO !!!

Referências

Identificando Problemas – Queries lentas – Parte 04

Introdução

Continuando o assunto de identificação de problemas, vamos ver agora o que e como lidar com queries que não apresentam um bom desempenho. Antes de chamar um DBA, existem alguns procedimentos investigativos e algumas ações que ajudam a resolver uma boa parte destas ocorrências.

Queries, de onde vêm?

Quando utilizamos um Banco de Dados relacional homologado com o ERP Microsiga / Protheus, todas as requisições de acesso a dados passam pela aplicação DBAccess. Como eu havia mencionado em um post anterior, o DBAccess serve de “gateway” de acesso ao Banco de Dados, e serve para emular as instruções ISAM do AdvPL em um banco relacional.

Logo, as queries submetidas pelo DBAccess ao Banco de Dados em uso podem ser de de dois tipos:

  • Queries emitidas (geradas) pelo DBAccess para atender a uma requisição ISAM — DBGoTop(), DBGoBottom(), DbSkip(), DBSeek().
  • Queries abertas pela aplicação AdvPL para recuperar dados diretamente do Banco de Dados.

Quando utilizamos o DBAccess Monitor, para realizar um Trace de uma conexão do Protheus com o DBAccess, podemos visualizar as operações solicitadas do Protheus ao DBAccess, e as respectivas queries submetidas ao Banco de Dados.

Como identificar uma query “lenta”?

Normalmente encontramos uma ou mais queries com baixo desempenho quando estamos procurando a causa da demora de um determinado processo. A descoberta acaba sendo realizada durante a análise de um Log Profiler obtido durante a execução da rotina, ou também através de um trace da conexão do DBAccess usando o DBMonitor.

Agora, podemos também usar o próprio DBAccess para gerar um log das queries que demoraram mais para serem abertas pelo Banco de Dados. Basta utilizar a configuração MaxOperationTimer, onde podemos especificar um número de segundos limite, a partir do qual um log de advertência deve ser gerado pelo DBAccess, caso o retorno da abertura de uma Query ultrapasse o tempo definido.

O que é uma query lenta?

Definir lentidão normalmente não está ligado apenas a medida absoluta de tempo, mas sim é relativa a urgência ou necessidade de obter a informação rapidamente, versus a quantidade de informações a serem avaliadas e retornadas.

Por exemplo, quando a aplicação AdvPL executa uma instrução como DBGoto(N), onde N é o número de um registro da base de dados, o DBAccess vai montar e submeter uma Query contra o banco de dados, para selecionar todas as colunas da tabela em questão, onde o campo R_E_C_N_O_ é igual ao número N informado.

SELECT CPO1,CPO2,CPO3,...N FROM TABELA WHERE R_E_C_N_O_ = N

Meu chapa, essa Query deve rodar normalmente em menos de 1 milissegundo no banco de dados, por duas razões: A primeira é que a coluna R_E_C_N_O_ é a chave primária (Primary Key) de todas as tabelas criadas pelo DBAccess, então naturalmente existe um índice no Banco de Dados usando internamente para achar em qual posição do Banco estão gravadas as colunas correspondentes a esta linha. E a segunda é que, apenas uma linha será retornada.

Se não existisse um índice para a coluna R_E_C_N_O_, o Banco de Dados teria que sair lento a tabela inteira, sequencialmente, até encontrar a linha da tabela que atendesse esta condição de busca. Esta busca na tabela de dados inteira sem uso de índice é conhecida pelo termo “FULL TABLE SCAN”.

Agora, imagine um SELECT com UNION de mais quatro selects, finalizado com um UNION ALL, onde cada Query faz SUB-SELECTS e JOINS no Banco de Dados, e o resultado disso não vai ser pequeno … Mesmo em condições ideais de configuração do Banco de Dados, não é plausível exigir que uma operação deste tamanho seja apenas alguns segundos.

Causas mais comuns de degradação de desempenho em Queries

Entre as mais comuns, podemos mencionar:

  1. Ausência de um ou mais índices — simples ou compostos — no Banco de Dados, que favoreçam um plano de execução otimizado do Banco de Dados para recuperar as informações desejadas.
  2. Estatísticas do Banco de Dados desatualizadas.
  3. Picos de CPU , Disco ou Rede, na máquina onde está o DBAccess e/ou na máquina onde está o Banco de Dados.
  4. Problemas de hardware na máquina do Banco de Dados ou em algum dos componentes da infra-estrutura.
  5. Problemas de configuração ou de comportamento do próprio Banco de Dados sob determinadas condições.
  6. Excesso de operações ou etapas do plano de execução, relacionadas a complexidade da Query ou da forma que a Query foi escrita para chegar ao resultado esperado.

Recomendo adicionalmente uma pesquisa sobre “Full Table Scan” e outras causas possíveis de baixo desempenho em Queries. Quanto mais infirmação, melhor. E, a propósito, mesmo que a tabela não tenha um índice adequado para a busca, se ela for  realmente pequena (poucas linhas) , o Banco de Dados internamente acaba fazendo CACHE da tabela inteira em, memória, então um Full Table Scan acaba sendo muito rápido, quando a tabela é pequena. Esta é mais uma razão por que muitas ocorrências de desempenho relacionados a este evento somente são descobertas após a massa de dados crescer representativamente no ambiente.

Troubleshooting e boas práticas

Normalmente as melhores ferramentas que podem dar pistas sobre as causas do baixo desempenho de uma Query são ferramentas nativas ou ligadas diretamente ao Banco de Dados, onde a ferramenta é capaz de desenhar e retornar — algumas inclusive em modo gráfico — o PLANO DE EXECUÇÃO da Query. Neste plano normalmente as ferramentas de diagnóstico informam quando está havendo FULL TABLE SCAN, e quais são as partes da Query que consomem mais recursos no  plano de execução. Algumas destas ferramentas inclusive são capazes de sugerir a criação de um ou mais índices para optimizar a busca dos dados desejados.

Mesmo sem ter uma ferramenta destas nas mãos, normalmente necessária para analisar queries grandes e mais complexas, podemos avaliar alguns pontos em queries menores “apenas olhando”, por exemplo:

  1. Ao estabelecer os critérios de busca — condições e comparações usadas na cláusula WHERE — procure usar diretamente os nomes dos campos, comparando com um conteúdo fixo,  evitando o uso de funções. É clado, vão existir exceções, mas via de regra procure estar atento neste ponto.
  2. Evite concatenações de campos nas expressões condicionais de busca. Imagine que você tem uma tabela com dois campos, e você tem nas mãos, para fazer a busca, uma string correspondendo a concatenação destes dois valores. Muto prático você fazer SELECT X FROM TABELA WHERE CPO1 || CPO2 = ‘0000010100’, certo ? Sim, mas mesmo que você tenha um índice com os campos CPO1 e CPO2, o Banco de Dados não vai conseguir usar o índice para ajudar nesta Query — e corre o risco de fazer FULLSCAN. Agora, se ao inves disso, você quebrar a informação para as duas colunas, e escrever SELECT X FROM TABELA WHERE CPO1 = ‘000001’ AND CPO2  = ‘01000’ , o Banco de Dados vai descobrir durante a montagem do plano de execução que ele pode usar um índice para estas buscas, e vai selecionar as linhas que atendem esta condição rapidinho.
  3. O Banco de Dados vai analisar a sua Query, e tentar criar um plano de acesso (ou plano de execução) para recuperar as informações desejadas o mais rápido possível. Se todas as condições usadas na cláusula WHERE forem atendidas por um mesmo índice, você ajuda o Banco de Dados a tomar a decisão mais rapidamente de qual índice internamente usar, se você fizer as comparações com os campos na ordem de declaração do índice. Por exemplo, para o índice CPO1, CPO2, CPO3, eu sugiro  uma Query com SELECT XYZ from TABELA WHERE CPO1 = ‘X’ AND CPO2 = ‘Y’ AND CPOC3 >=  ‘Z’

Queries emitidas pelo DBAccess

As queries emitidas pelo DBAccess no Banco de Dados para emular o comportamento de navegação ISAM são por natureza optimizadas. Para emular a navegação de dados em uma determinada ordem de índice, o DBAccess emite queries para preencher um cache de registros — não de dados — usando os dados dos campos como condições de busca, na mesma sequência da criação do índice. E, para recuperar o conteúdo (colunas) de um registro (linha), ele usa um statement preparado, onde o Banco faz o parser da requisição apenas na primeira chamada, e as demais usam o mesmo plano de execução.

Porém, isto não impede de uma ou mais queries emitidas pelo DBAccess acabem ficando lentas. Normalmente isso acontece quando é realizada uma condição de filtro na tabela em AdvPL, onde nem todos — ou nenhum —  os campos utilizados não possuem um índice que favoreça uma busca indexada, fazendo com que o Banco acabe varrendo a tabela inteira — FULL SCAN — para recuperar os dados desejados.

Este tipo de ocorrência também é solúvel, uma vez determinado qual seria a chave de índice que tornaria a navegação com esta condição de filtro optimizada, é possível de duas formas criar este índice.

Criando um índice auxiliar

A forma recomendada de se criar um índice auxiliar é acrescentá-lo via Configurador no arquivo de índices do ERP (SIX), para que ele seja criado e mantido pelas rotinas padrão do sistema. Porém, para isso este índice não pode conter campos de controle do DBAccess no meio das colunas do índice, e para se adequar ao padrão do ERP, seu primeiro campo deveria sempre ser o campo XX_FILIAL da tabela.

Quando esta alternativa não for possível, existe a possibilidade de criar este índice diretamente no Banco de Dados. Porém, a existência desse índice não deve interferir na identificação de índices de emulação ISAM que o DBAccess faz quando qualquer tabela é aberta. Para isso, a escolha do NOME DO ÍNDICE é fundamental.

Um índice criado diretamente no Banco de Dados para estes casos deve ter um nome que seja alfabeticamente POSTERIOR aos índices declarados no dicionário de dados (SIX). Por exemplo, existe uma tabela ABC990, que possui 8 índices. O ERP Microsiga nomeia os índices da tabela no padrão usando o nome da tabela e mais um número ou letra, em ordem alfabética. Logo, os oito primeiros indices da tabela chamam-se, respectivamente, ABC9901, ABC9902 … ABC9908.

Nomeie seu primeiro índice customizado com o nome de ABC990Z1. Caso seja necessário mais um índice, ABC990Z2, e assim sucessivamente. Chegou no 9, precisa de mais um índice, coloque a letra “A” — ABC990ZA. Dessa forma, estes índices ficarão por último na identificação do DBAccess, e por eles provavelmente não se encaixarem no padrão do ERP, você não vai — e não deve — usá-los dentro de fontes customizados AdvPL — Estes índices vão existir apenas no Banco de Dados, para favorecer a execução de queries especificas ou filtros específicos de navegação.

Precauções

Uma vez que um ou mais índices sejam criados dentro do Banco de Dados, sem usar o dicionário ativo de dados (SIX), qualquer alteração estrutural na tabela feita por uma atualização de versão ou outro programa pode apagar estes índices a qualquer momento, caso seja necessário, e isso vai novamente impactar o desempenho da aplicação. Para evitar isto, é possível escrever uma rotina em AdvPL — customização — para verificar usando Queries no banco de dados se os índices auxiliares criados por fora ainda existem, e até recriá-los se for o caso. Pode ser usado para isso o Ponto de Entrada CHKFILE() , junto das funções TCCanOpen() — para testar a existência do índice — e TcSqlExec() — para criar o índice customizado usando uma instrução SQL diretamente no banco de dados.

Outras causas de degradação de desempenho

Existem ocorrências específicas, que podem estar relacionadas desde a configuração do Banco de Dados, até mesmo problemas no mecanismo de montagem ou cache dos planos de acesso criados pelo Banco para resolver as Queries. Outas ocorrências podem estar relacionadas a execução de rotinas automáticas ou agendadas no próprio servidor de Banco de Dados. Normalmente o efeito destas interferências são percebidos como uma lentidão momentânea porém generalizada, por um período determinado de tempo.

Conclusão

Use a tecnologia ao seu favor, e em caso de pânico, chame um DBA! Muitas vezes existe uma forma mais simples ou com menos etapas para trazer as informações desejadas. Outras vezes é mais elegante, rápido e prático escrever mais de uma query menor, do que uma Super-Query-Megazord.

Desejo a todos um ótimo final de semana, e muitos TERABYTES DE SUCESSO 😀

Referências