Adiciona backup geral no painel master
This commit is contained in:
@@ -16,6 +16,10 @@ public final class DataSourceProvider {
|
|||||||
return DriverManager.getConnection(config.masterJdbcUrl(), config.dbUser(), config.dbPassword());
|
return DriverManager.getConnection(config.masterJdbcUrl(), config.dbUser(), config.dbPassword());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Connection getConnection(String databaseName) throws SQLException {
|
||||||
|
return DriverManager.getConnection(config.jdbcUrl(databaseName), config.dbUser(), config.dbPassword());
|
||||||
|
}
|
||||||
|
|
||||||
public void testarConexaoMaster() throws SQLException {
|
public void testarConexaoMaster() throws SQLException {
|
||||||
try (Connection conn = getMasterConnection();
|
try (Connection conn = getMasterConnection();
|
||||||
Statement stmt = conn.createStatement()) {
|
Statement stmt = conn.createStatement()) {
|
||||||
|
|||||||
@@ -14,11 +14,16 @@ import com.sun.net.httpserver.HttpExchange;
|
|||||||
import com.sun.net.httpserver.HttpHandler;
|
import com.sun.net.httpserver.HttpHandler;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
import java.net.URLDecoder;
|
import java.net.URLDecoder;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.sql.*;
|
import java.sql.*;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
public final class MasterHandlers implements HttpHandler {
|
public final class MasterHandlers implements HttpHandler {
|
||||||
private final DataSourceProvider dataSourceProvider;
|
private final DataSourceProvider dataSourceProvider;
|
||||||
@@ -131,9 +136,145 @@ public final class MasterHandlers implements HttpHandler {
|
|||||||
listarLogsEnvio(ex);
|
listarLogsEnvio(ex);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (parts.length == 1 && "backup".equals(parts[0])) {
|
||||||
|
if ("GET".equals(method)) gerarBackup(ex);
|
||||||
|
else HttpResponses.methodNotAllowed(ex);
|
||||||
|
return;
|
||||||
|
}
|
||||||
HttpResponses.json(ex, 404, Map.of("erro", "endpoint_nao_encontrado"));
|
HttpResponses.json(ex, 404, Map.of("erro", "endpoint_nao_encontrado"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void gerarBackup(HttpExchange ex) throws Exception {
|
||||||
|
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss"));
|
||||||
|
List<Map<String,Object>> bancos = new ArrayList<>();
|
||||||
|
try (Connection conn = dataSourceProvider.getMasterConnection(); PreparedStatement ps = conn.prepareStatement("SELECT id,nome,slug,database_name,status FROM empresas WHERE status<>'excluida' ORDER BY id")) {
|
||||||
|
bancos.add(Map.of("nome", "master", "database_name", currentDatabase(conn), "tipo", "master"));
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
while (rs.next()) {
|
||||||
|
Map<String,Object> b = new LinkedHashMap<>();
|
||||||
|
b.put("id", rs.getLong("id"));
|
||||||
|
b.put("nome", rs.getString("nome"));
|
||||||
|
b.put("slug", rs.getString("slug"));
|
||||||
|
b.put("database_name", rs.getString("database_name"));
|
||||||
|
b.put("status", rs.getString("status"));
|
||||||
|
b.put("tipo", "cliente");
|
||||||
|
bancos.add(b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String filename = "seconectai-backup-" + timestamp + ".zip";
|
||||||
|
ex.getResponseHeaders().set("Content-Type", "application/zip");
|
||||||
|
ex.getResponseHeaders().set("Content-Disposition", "attachment; filename=\"" + filename + "\"");
|
||||||
|
ex.sendResponseHeaders(200, 0);
|
||||||
|
try (OutputStream out = ex.getResponseBody(); ZipOutputStream zip = new ZipOutputStream(out, StandardCharsets.UTF_8)) {
|
||||||
|
zip.putNextEntry(new ZipEntry("manifest.json"));
|
||||||
|
zip.write(backupManifest(timestamp, bancos).getBytes(StandardCharsets.UTF_8));
|
||||||
|
zip.closeEntry();
|
||||||
|
for (Map<String,Object> banco : bancos) {
|
||||||
|
String db = String.valueOf(banco.get("database_name"));
|
||||||
|
String prefixo = "master".equals(banco.get("tipo")) ? "master" : "clientes/" + safeName(String.valueOf(banco.get("slug"))) + "-" + banco.get("id");
|
||||||
|
zip.putNextEntry(new ZipEntry(prefixo + "/" + db + ".sql"));
|
||||||
|
try (Connection conn = dataSourceProvider.getConnection(db)) {
|
||||||
|
escreverBackupSql(conn, db, zip);
|
||||||
|
} catch (Exception err) {
|
||||||
|
zip.write(("-- Falha ao gerar backup de " + db + ": " + err.getMessage() + "\n").getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
zip.closeEntry();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String backupManifest(String timestamp, List<Map<String,Object>> bancos) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("{\"gerado_em\":\"").append(timestamp).append("\",\"bancos\":[");
|
||||||
|
for (int i = 0; i < bancos.size(); i++) {
|
||||||
|
if (i > 0) sb.append(',');
|
||||||
|
Map<String,Object> b = bancos.get(i);
|
||||||
|
sb.append('{');
|
||||||
|
int j = 0;
|
||||||
|
for (var e : b.entrySet()) {
|
||||||
|
if (j++ > 0) sb.append(',');
|
||||||
|
sb.append('"').append(jsonEscape(e.getKey())).append("\":\"").append(jsonEscape(String.valueOf(e.getValue()))).append('"');
|
||||||
|
}
|
||||||
|
sb.append('}');
|
||||||
|
}
|
||||||
|
return sb.append("]}").toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void escreverBackupSql(Connection conn, String databaseName, OutputStream out) throws Exception {
|
||||||
|
out.write(("-- Backup SeConectai - " + databaseName + "\n").getBytes(StandardCharsets.UTF_8));
|
||||||
|
out.write(("SET client_encoding = 'UTF8';\nSET session_replication_role = replica;\n\n").getBytes(StandardCharsets.UTF_8));
|
||||||
|
for (String table : tabelasPublicas(conn)) {
|
||||||
|
out.write(("-- Tabela " + table + "\n").getBytes(StandardCharsets.UTF_8));
|
||||||
|
try (Statement st = conn.createStatement(); ResultSet rs = st.executeQuery("SELECT * FROM " + ident(table))) {
|
||||||
|
ResultSetMetaData md = rs.getMetaData();
|
||||||
|
while (rs.next()) {
|
||||||
|
StringBuilder sql = new StringBuilder("INSERT INTO ").append(ident(table)).append(" (");
|
||||||
|
for (int i = 1; i <= md.getColumnCount(); i++) {
|
||||||
|
if (i > 1) sql.append(',');
|
||||||
|
sql.append(ident(md.getColumnName(i)));
|
||||||
|
}
|
||||||
|
sql.append(") VALUES (");
|
||||||
|
for (int i = 1; i <= md.getColumnCount(); i++) {
|
||||||
|
if (i > 1) sql.append(',');
|
||||||
|
sql.append(sqlValue(rs.getObject(i)));
|
||||||
|
}
|
||||||
|
sql.append(") ON CONFLICT DO NOTHING;\n");
|
||||||
|
out.write(sql.toString().getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.write("\n".getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
for (String seq : sequenciasPublicas(conn)) {
|
||||||
|
String sql = "SELECT setval('" + seq.replace("'", "''") + "', COALESCE((SELECT last_value FROM " + ident(seq) + "), 1), true);\n";
|
||||||
|
out.write(sql.getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
out.write("SET session_replication_role = DEFAULT;\n".getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String currentDatabase(Connection conn) throws SQLException {
|
||||||
|
try (Statement st = conn.createStatement(); ResultSet rs = st.executeQuery("SELECT current_database()")) {
|
||||||
|
rs.next();
|
||||||
|
return rs.getString(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> tabelasPublicas(Connection conn) throws SQLException {
|
||||||
|
List<String> tables = new ArrayList<>();
|
||||||
|
try (PreparedStatement ps = conn.prepareStatement("SELECT tablename FROM pg_tables WHERE schemaname='public' ORDER BY tablename"); ResultSet rs = ps.executeQuery()) {
|
||||||
|
while (rs.next()) tables.add(rs.getString(1));
|
||||||
|
}
|
||||||
|
return tables;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> sequenciasPublicas(Connection conn) throws SQLException {
|
||||||
|
List<String> seqs = new ArrayList<>();
|
||||||
|
try (PreparedStatement ps = conn.prepareStatement("SELECT sequencename FROM pg_sequences WHERE schemaname='public' ORDER BY sequencename"); ResultSet rs = ps.executeQuery()) {
|
||||||
|
while (rs.next()) seqs.add(rs.getString(1));
|
||||||
|
}
|
||||||
|
return seqs;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String ident(String value) {
|
||||||
|
return "\"" + value.replace("\"", "\"\"") + "\"";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String sqlValue(Object value) {
|
||||||
|
if (value == null) return "NULL";
|
||||||
|
if (value instanceof Number) return value.toString();
|
||||||
|
if (value instanceof Boolean) return (Boolean)value ? "TRUE" : "FALSE";
|
||||||
|
if (value instanceof byte[] bytes) return "'\\\\x" + HexFormat.of().formatHex(bytes) + "'";
|
||||||
|
return "'" + value.toString().replace("'", "''") + "'";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String safeName(String value) {
|
||||||
|
return value == null ? "cliente" : value.replaceAll("[^A-Za-z0-9._-]", "_");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String jsonEscape(String value) {
|
||||||
|
return value == null ? "" : value.replace("\\", "\\\\").replace("\"", "\\\"");
|
||||||
|
}
|
||||||
|
|
||||||
private void listarEvolutionInstancias(HttpExchange ex, long empresaId) throws Exception {
|
private void listarEvolutionInstancias(HttpExchange ex, long empresaId) throws Exception {
|
||||||
try (Connection conn = dataSourceProvider.getMasterConnection(); PreparedStatement ps = conn.prepareStatement("SELECT id, empresa_id, instance_url, numero_whatsapp, status, solicitado_em, aprovado_em, aprovado_por FROM evolution_instancias WHERE empresa_id=? ORDER BY solicitado_em DESC, id DESC")) {
|
try (Connection conn = dataSourceProvider.getMasterConnection(); PreparedStatement ps = conn.prepareStatement("SELECT id, empresa_id, instance_url, numero_whatsapp, status, solicitado_em, aprovado_em, aprovado_por FROM evolution_instancias WHERE empresa_id=? ORDER BY solicitado_em DESC, id DESC")) {
|
||||||
ps.setLong(1, empresaId);
|
ps.setLong(1, empresaId);
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
window.Layout={mount(scope,active){Auth.require(scope);const label=localStorage.getItem('auth_label')||(scope==='master'?'Master':'Escritorio');const master=[['Clientes','/master/clientes.html','clientes'],['Planos','/master/planos.html','planos'],['Financeiro','/master/financeiro.html','financeiro'],['Configuracoes','/master/configuracoes.html','configuracoes']];let cliente=[['Cadastro','/cliente/condominios.html','condominios'],['Lembrete','/cliente/lembretes.html','lembretes'],['Financeiro','/cliente/financeiro.html','financeiro'],['WhatsApp','/cliente/configuracoes.html','configuracoes']];if(scope==='empresa'&&!Auth.isAdminEmpresa())cliente=cliente.filter(x=>x[2]!=='configuracoes');const menu=(scope==='master'?master:cliente).map(([n,h,k])=>`<a class="${k===active?'active':''}" href="${h}">${n}</a>`).join('');document.body.insertAdjacentHTML('afterbegin',`<aside class="sidebar"><div><div class="brand">SeConectai</div><div class="user">${label}</div></div><nav class="menu">${menu}</nav><button class="btn" onclick="Auth.logout()">Sair</button></aside>`);document.body.classList.add('app');document.querySelector('main')?.classList.add('content')}};
|
window.Layout={mount(scope,active){Auth.require(scope);const label=localStorage.getItem('auth_label')||(scope==='master'?'Master':'Escritorio');const master=[['Clientes','/master/clientes.html','clientes'],['Planos','/master/planos.html','planos'],['Financeiro','/master/financeiro.html','financeiro'],['Configuracoes','/master/configuracoes.html','configuracoes'],['Backup','/master/backup.html','backup']];let cliente=[['Cadastro','/cliente/condominios.html','condominios'],['Lembrete','/cliente/lembretes.html','lembretes'],['Financeiro','/cliente/financeiro.html','financeiro'],['WhatsApp','/cliente/configuracoes.html','configuracoes']];if(scope==='empresa'&&!Auth.isAdminEmpresa())cliente=cliente.filter(x=>x[2]!=='configuracoes');const menu=(scope==='master'?master:cliente).map(([n,h,k])=>`<a class="${k===active?'active':''}" href="${h}">${n}</a>`).join('');document.body.insertAdjacentHTML('afterbegin',`<aside class="sidebar"><div><div class="brand">SeConectai</div><div class="user">${label}</div></div><nav class="menu">${menu}</nav><button class="btn" onclick="Auth.logout()">Sair</button></aside>`);document.body.classList.add('app');document.querySelector('main')?.classList.add('content')}};
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<!doctype html><html lang="pt-BR"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Backup</title><link rel="stylesheet" href="/css/app.css"><script src="/js/auth.js"></script><script src="/js/util.js"></script><script src="/js/api.js"></script><script src="/js/layout.js"></script></head><body><main><div class="topbar"><h1>Backup</h1></div><section class="panel"><h2>Backup geral</h2><p class="muted">Baixe um arquivo ZIP com o banco Master e os bancos de todos os clientes cadastrados.</p><button id="backup-btn" class="btn" type="button">Gerar e baixar backup</button><div id="backup-status" class="panel-block muted"></div></section></main><script>Layout.mount('master','backup');</script><script src="/master/backup.js"></script></body></html>
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
const btn=document.querySelector('#backup-btn'),statusEl=document.querySelector('#backup-status');
|
||||||
|
btn.onclick=async()=>{
|
||||||
|
if(!confirm('Gerar backup geral do sistema e de todos os clientes?'))return;
|
||||||
|
btn.disabled=true;
|
||||||
|
statusEl.textContent='Gerando backup...';
|
||||||
|
try{
|
||||||
|
const headers={};
|
||||||
|
const token=Auth.token();
|
||||||
|
if(token)headers.Authorization=`Bearer ${token}`;
|
||||||
|
const res=await fetch('/api/master/backup',{headers});
|
||||||
|
if(res.status===401){Auth.logout();return}
|
||||||
|
if(!res.ok){
|
||||||
|
const data=await res.json().catch(()=>({}));
|
||||||
|
throw new Error(data.mensagem||data.erro||`Erro HTTP ${res.status}`);
|
||||||
|
}
|
||||||
|
const blob=await res.blob();
|
||||||
|
const cd=res.headers.get('Content-Disposition')||'';
|
||||||
|
const match=cd.match(/filename="?([^"]+)"?/);
|
||||||
|
const filename=match?match[1]:'seconectai-backup.zip';
|
||||||
|
const url=URL.createObjectURL(blob);
|
||||||
|
const a=document.createElement('a');
|
||||||
|
a.href=url;
|
||||||
|
a.download=filename;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
a.remove();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
statusEl.textContent='Backup baixado com sucesso.';
|
||||||
|
Util.toast('Backup baixado');
|
||||||
|
}catch(e){
|
||||||
|
statusEl.textContent='';
|
||||||
|
Util.toast(e.message,true);
|
||||||
|
}finally{
|
||||||
|
btn.disabled=false;
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user