Skip to content

Commit

Permalink
Merge pull request #7 from filiperochalopes/develop
Browse files Browse the repository at this point in the history
Add database cloud backup feature [Google Drive]
  • Loading branch information
filiperochalopes authored Nov 22, 2022
2 parents 38ca75a + b1b4754 commit 3be0447
Show file tree
Hide file tree
Showing 17 changed files with 268 additions and 7 deletions.
2 changes: 1 addition & 1 deletion .env
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ POSTGRES_PASSWORD='esus'
POSTGRESQL_PORT=54351
APP_PORT=88
PGWEB_PORT=8099
TIMEZONE='America/Bahia'
TIMEZONE='America/Bahia'
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
POSTGRES_DB='esus'
POSTGRES_USER='postgres'
POSTGRES_PASSWORD='esus'
POSTGRESQL_PORT=54351
APP_PORT=88
PGWEB_PORT=8099
TIMEZONE='America/Bahia'
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,9 @@ data
__pycache__
*.pyc
*.backup
.webassets-cache
.webassets-cache
.env
client_secret_*.json
credentials.json
token.json
venv
2 changes: 0 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,4 @@ COPY ./run.sh run.sh
RUN chmod +x /var/www/html/install.sh
RUN chmod +x /var/www/html/run.sh

EXPOSE 8080

CMD "/var/www/html/run.sh"
13 changes: 12 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,15 @@ dev-update:
terminal:
docker exec -it esus_app bash
db-terminal:
docker exec -it esus_psql bash
docker exec -it esus_psql bash
cloud-backup:
docker exec -it esus_cron sh -c "curl localhost:5000/backup-database"
google-oauth:
cd cron/app; \
python3 --version; \
pip3 install virtualenv; \
virtualenv cron/app/venv; \
chmod +x ./venv/bin/activate; \
./venv/bin/activate; \
pip install -r requirements.txt; \
python googleoauth.py; \
27 changes: 25 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# eSUS PEC

É um sistema bastante utilizado por profissionais de saúde da Atenção Básica para registros de pacientes e dados de saúde. Esse repositório se propõe a criar uma estrutura docker com linux para viabilizar o deploy do sistema em qualquer ambiente que tenha docker e facilitar a instalação e atualização.
Compatível e testado com
![version](https://img.shields.io/badge/version-5.0.12-blue) ![version](https://img.shields.io/badge/version-5.0.8-blue) ![version](https://img.shields.io/badge/version-4.5.5-blue) ![version](https://img.shields.io/badge/version-4.2.6-blue) ![version](https://img.shields.io/badge/version-5.0.14-blue)

É um sistema bastante utilizado por profissionais de saúde da Atenção Básica para registros de pacientes e dados de saúde. Esse repositório se propõe a criar uma estrutura docker com linux para viabilizar o deploy do sistema em qualquer ambiente que tenha docker e facilitar a instalação e atualização do sistema [e-SUS PEC](https://sisaps.saude.gov.br/esus/)

## Preparando pacotes

Expand Down Expand Up @@ -82,11 +85,31 @@ pg_restore -U "postgres" -d "esus" -1 /home/seu_arquivo.backup
Fora do container, na pasta raiz do projeto execute, substituindo o nome do pacote `eSUS-AB-PEC-5.0.8-Linux64.jar` para a versão que você vai instalar em sua máquina.

```sh
sh build.sh -f eSUS-AB-PEC-5.0.8-Linux64.jar
sh build.sh -f eSUS-AB-PEC-5.0.14-Linux64.jar
```

## Known Issues (Bugs Conhecidos)

- Testes realizados com versão `4.2.7` e `4.2.8` não foram bem sucedidos
- A versão 4.2.8 está com erro no formulário de cadastro, nas requisições ao banco de dados, pelo endpoint graphql, retorna "Não autorizado"
- Verificar sempre a memória caso queira fazer depois em servidor. Senão ele trará no console um `Killed` inesperado https://stackoverflow.com/questions/37071106/spring-boot-application-quits-unexpectedly-with-killed
- Não instale a versão `5.0.8`, do de cabeça, não carrega alguns exames e atendimentos de forma aparentemente aleatória, corrigido após instalar versão `5.0.14`

## Serviço de backup

Para fazer funcionar o serviço de backup na nuvem pelo [Google Drive](https://developers.google.com/drive/api/v3/reference) ela deve estar relacionada a um Google Drive, se assim não for irá armazenar os backups apenas localmente. Para o uso de backup na nuvem é necessário:

1. [Criar uma chave de Client ID Google](https://developers.google.com/drive/api/quickstart/python)
2. Salve o arquivo json baixado com segurança e cole na pasta como `cron/app/credentials.json`
3. Execute
```sh
make google-oauth
```
Para iniciar autenticação Google e permitir acessos. É necessário para criar o `token.json` que guardará as credenciais. Caso o comando não funcione, provavelmente você não está no linux ou não tem o `python3` instalado no seu linux. **É importante que ao executar o comando você esteja dentro da pasta raiz do projeto.**

Para realizar um teste de backup execute:
```
make cloud-backup
```

As configurações de tempo de expiração de backup estão disponíveis em `env.py`
29 changes: 29 additions & 0 deletions cron/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
FROM python:alpine3.16

# instalando dependências para o container
RUN apk add --no-cache postgresql-client tzdata curl

ARG TIMEZONE

# Configurando timezone
RUN ls /usr/share/zoneinfo
RUN cp "/usr/share/zoneinfo/${TIMEZONE}" /etc/localtime
RUN echo "${TIMEZONE}" > /etc/timezone
RUN date
RUN apk del tzdata

# Copiando arquivos de uso do cron
ADD crontab.txt /crontab.txt
ADD script.sh /script.sh
COPY entry.sh /entry.sh
RUN chmod 755 /script.sh /entry.sh
RUN /usr/bin/crontab /crontab.txt

# Criando o cenário para rodar a aplicação python
WORKDIR /home
COPY app .
COPY wsgi.py /
RUN pip install -r requirements.txt

WORKDIR /
CMD ["/entry.sh"]
Empty file added cron/app/__init__.py
Empty file.
91 changes: 91 additions & 0 deletions cron/app/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
from __future__ import print_function
from datetime import datetime

import os
import sys
from flask import Flask, Response

import os.path
from googleapiclient.discovery import build
from googleapiclient.http import MediaFileUpload
from googleapiclient.errors import HttpError

from .env import BACKUP_EXPIRATION_TIME, DATABASE_HOST, DATABASE_NAME, DATABASE_USER, FILENAME, GOOGLE_DRIVE_FOLDER_ID, FILE_EXTENSION
from .googleoauth import get_google_credentials

app = Flask(__name__)

def get_file_in_path(filename):
return os.path.join(os.path.dirname(os.path.realpath(__file__)), filename)

def upload_file(service, filename, mime_type, folder_id):
try:
file_metadata = {
'name': filename,
'parents': [folder_id],
'mimeType': mime_type
}

media = MediaFileUpload(get_file_in_path(filename), mimetype=mime_type, resumable=True)

file = service.files().create(
body=file_metadata,
media_body=media,
fields='id'
).execute()
except HttpError as error:
print(f'An error occurred: {error}')
file = None

return file.get('id')

@app.route("/backup-database")
def backup_database():
from sh import pg_dump

print('Realizando backup do banco de dados...', file=sys.stderr)
pg_dump('--host', DATABASE_HOST, '--port', '5432', '-U', DATABASE_USER, '-w', '--format', 'custom', '--blobs', '--encoding',
'UTF8', '--no-privileges', '--no-tablespaces', '--no-unlogged-table-data', '--file', f'/home/{FILENAME}', DATABASE_NAME)


# upload de arquivo
print('Autenticando no Google...', file=sys.stderr)
service = build('drive', 'v3', credentials=get_google_credentials())

print('Realizando upload para Google Drive...', file=sys.stderr)
file_id = upload_file(service=service, filename=FILENAME, mime_type='application/octet-stream', folder_id=GOOGLE_DRIVE_FOLDER_ID)
print('File uploaded:', file_id)

# Google Drive API: https://developers.google.com/drive/api/v3/reference
print('Listando arquivos na pasta de backup...', file=sys.stderr)
# fields props: https://developers.google.com/drive/api/v3/reference/files
# query props: https://developers.google.com/drive/api/guides/search-files#python
results = service.files().list(q=f'"{GOOGLE_DRIVE_FOLDER_ID}" in parents and trashed = false', orderBy='createdTime desc',
pageSize=20, fields="nextPageToken, files(id, name, mimeType, description, trashed)").execute()
items = results.get('files', [])

if not items:
print('No files found.', file=sys.stderr)
return
else:
for item in items:
print('{:<40} {:<20} {:<20}'.format(item['name'], item['mimeType'], item['trashed']), file=sys.stderr)

print('Excluindo arquivos antigos...', file=sys.stderr)
for item in items:
filename_datetime = item['name'].replace(FILE_EXTENSION, '')
try:
print(datetime.now())
print(BACKUP_EXPIRATION_TIME)
if datetime.strptime(filename_datetime, '%Y_%m_%d_%H_%M_%S') < BACKUP_EXPIRATION_TIME:
filename = f'{filename_datetime}{FILE_EXTENSION}'
print(f'Excluindo {filename}', file=sys.stderr)
os.remove(get_file_in_path(filename))
service.files().delete(fileId=item['id']).execute()
except Exception as e:
print('error', e)

return Response('Backup realizado', status=201)

if __name__ == '__main__':
get_google_credentials()
13 changes: 13 additions & 0 deletions cron/app/env.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from datetime import datetime, timedelta
import os

DATABASE_HOST = os.getenv('POSTGRES_HOST', 'psql')
DATABASE_NAME = os.getenv('POSTGRES_DB', 'esus')
DATABASE_USER = os.getenv('POSTGRES_USER', 'postgres')

NOW = datetime.now()
FILE_EXTENSION = '.backup'
FILENAME = f'{NOW.strftime("%Y_%m_%d_%H_%M_%S")}{FILE_EXTENSION}'

GOOGLE_DRIVE_FOLDER_ID = os.getenv('GOOGLE_DRIVE_FOLDER_ID', '1osoeAhww2IM2V2W_xbgcRoROHEk_DAPw')
BACKUP_EXPIRATION_TIME = datetime.now() - timedelta(days=7)
43 changes: 43 additions & 0 deletions cron/app/googleoauth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from __future__ import print_function

import os.path

from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow

# If modifying these scopes, delete the file token.json.
SCOPES = [
'https://www.googleapis.com/auth/drive.appdata',
'https://www.googleapis.com/auth/drive.file',
'https://www.googleapis.com/auth/drive.install',
'https://www.googleapis.com/auth/drive',
'https://www.googleapis.com/auth/drive.metadata.readonly'
]

CREDEDNTIALS = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'credentials.json')
TOKEN = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'token.json')

def get_google_credentials():
creds = None
# The file token.json stores the user's access and refresh tokens, and is
# created automatically when the authorization flow completes for the first
# time.
if os.path.exists(TOKEN):
creds = Credentials.from_authorized_user_file(TOKEN, SCOPES)
# If there are no (valid) credentials available, let the user log in.
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(CREDEDNTIALS, SCOPES)
creds = flow.run_local_server(host='localhost', port=8080, open_browser=False)
# Save the credentials for the next run
with open(TOKEN, 'w') as token:
token.write(creds.to_json())

return creds


if __name__ == '__main__':
get_google_credentials()
6 changes: 6 additions & 0 deletions cron/app/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Flask==2.2.2
gunicorn==20.1.0
sh==1.14.3
google-api-python-client
google-auth-httplib2
google-auth-oauthlib
3 changes: 3 additions & 0 deletions cron/crontab.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# m h dom mon dow user command
* 0 * * * /script.sh >> /var/log/script.log

6 changes: 6 additions & 0 deletions cron/entry.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/bin/sh

# start python app
gunicorn --bind 0.0.0.0:5000 -D --enable-stdio-inheritance --capture-output --log-level debug --reload wsgi:app
# start cron
/usr/sbin/crond -f -l 8
5 changes: 5 additions & 0 deletions cron/script.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/sh

# code goes here.
curl http://localhost:5000/backup-database
echo "This is a script, run by cron!"
4 changes: 4 additions & 0 deletions cron/wsgi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from home.app import app

if __name__ == "__main__":
app.run()
17 changes: 17 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,20 @@ services:
- "${APP_PORT}:8080"
depends_on:
- psql
cron:
container_name: esus_cron
restart: always
build:
context: cron
dockerfile: Dockerfile
args:
- TIMEZONE=${TIMEZONE}
volumes:
- ./cron/crontab.txt:/crontab.txt
- ./cron/app:/home
environment:
- POSTGRES_DB=${POSTGRES_DB}
- POSTGRES_USER=${POSTGRES_USER}
- PGPASSWORD=${POSTGRES_PASSWORD}
depends_on:
- psql

0 comments on commit 3be0447

Please sign in to comment.