Spaces:
Sleeping
Sleeping
fast server for auditory of acts
Browse files- Dockerfile +19 -0
- index.js +78 -0
- lib/remote.js +86 -0
- package.json +11 -0
- public/index.html +61 -0
Dockerfile
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM node:20
|
| 2 |
+
|
| 3 |
+
# Instalamos unzip para los paquetes
|
| 4 |
+
RUN apt-get update && apt-get install -y unzip && rm -rf /var/lib/apt/lists/*
|
| 5 |
+
|
| 6 |
+
WORKDIR /app
|
| 7 |
+
|
| 8 |
+
COPY package*.json ./
|
| 9 |
+
RUN npm install
|
| 10 |
+
|
| 11 |
+
COPY . .
|
| 12 |
+
|
| 13 |
+
# Creamos las carpetas necesarias con permisos
|
| 14 |
+
RUN mkdir -p storage temp && chmod 777 storage temp
|
| 15 |
+
|
| 16 |
+
EXPOSE 7860
|
| 17 |
+
ENV PORT=7860
|
| 18 |
+
|
| 19 |
+
CMD ["node", "index.js"]
|
index.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import 'dotenv/config';
|
| 2 |
+
import express from 'express';
|
| 3 |
+
import path from 'path';
|
| 4 |
+
import fs from 'fs';
|
| 5 |
+
import { fileURLToPath } from 'url';
|
| 6 |
+
import { execSync } from 'child_process';
|
| 7 |
+
import RemoteFetcher from './lib/remote.js';
|
| 8 |
+
|
| 9 |
+
const __filename = fileURLToPath(import.meta.url);
|
| 10 |
+
const __dirname = path.dirname(__filename);
|
| 11 |
+
|
| 12 |
+
const app = express();
|
| 13 |
+
const PORT = process.env.PORT || 7861; // Puerto diferente para no chocar
|
| 14 |
+
|
| 15 |
+
const STORAGE_DIR = path.join(__dirname, 'storage');
|
| 16 |
+
const TEMP_DIR = path.join(__dirname, 'temp');
|
| 17 |
+
|
| 18 |
+
if (!fs.existsSync(STORAGE_DIR)) fs.mkdirSync(STORAGE_DIR, { recursive: true });
|
| 19 |
+
if (!fs.existsSync(TEMP_DIR)) fs.mkdirSync(TEMP_DIR, { recursive: true });
|
| 20 |
+
|
| 21 |
+
app.use(express.static('public'));
|
| 22 |
+
app.use('/view', express.static(STORAGE_DIR));
|
| 23 |
+
|
| 24 |
+
// Endpoint ultra simple: Listar archivos en disco
|
| 25 |
+
app.get('/api/files', (req, res) => {
|
| 26 |
+
const files = fs.readdirSync(STORAGE_DIR).filter(f => !f.startsWith('.'));
|
| 27 |
+
res.json(files);
|
| 28 |
+
});
|
| 29 |
+
|
| 30 |
+
app.listen(PORT, () => {
|
| 31 |
+
console.log(`🚀 Servidor SIMPLE corriendo en puerto ${PORT}`);
|
| 32 |
+
startSimpleWorker();
|
| 33 |
+
});
|
| 34 |
+
|
| 35 |
+
async function startSimpleWorker() {
|
| 36 |
+
const config = {
|
| 37 |
+
host: process.env.REMOTE_HOST,
|
| 38 |
+
username: process.env.REMOTE_USER,
|
| 39 |
+
password: process.env.REMOTE_PASS,
|
| 40 |
+
port: parseInt(process.env.REMOTE_PORT || 22)
|
| 41 |
+
};
|
| 42 |
+
|
| 43 |
+
const fetcher = new RemoteFetcher(config);
|
| 44 |
+
try {
|
| 45 |
+
await fetcher.connect();
|
| 46 |
+
console.log('✅ Conexión SSH lista.');
|
| 47 |
+
|
| 48 |
+
const remoteRoot = '/home/user110426@vi/ACTAS';
|
| 49 |
+
const regions = await fetcher.listDirectories(remoteRoot);
|
| 50 |
+
|
| 51 |
+
for (const region of regions) {
|
| 52 |
+
console.log(`🌍 Entrando a: ${region}`);
|
| 53 |
+
const regionPath = path.posix.join(remoteRoot, region);
|
| 54 |
+
const zips = await fetcher.listFiles(regionPath, '.zip');
|
| 55 |
+
|
| 56 |
+
for (const zipName of zips) {
|
| 57 |
+
const localZipPath = path.join(TEMP_DIR, zipName);
|
| 58 |
+
const remoteZipPath = path.posix.join(regionPath, zipName);
|
| 59 |
+
|
| 60 |
+
console.log(`📦 Descargando ${zipName}...`);
|
| 61 |
+
await fetcher.fetchFile(remoteZipPath, localZipPath);
|
| 62 |
+
|
| 63 |
+
console.log(`📂 Descomprimiendo ${zipName}...`);
|
| 64 |
+
try {
|
| 65 |
+
execSync(`unzip -o "${localZipPath}" -d "${STORAGE_DIR}"`);
|
| 66 |
+
} catch (e) {
|
| 67 |
+
console.log(`❌ Error unzip: ${e.message}`);
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
// Borramos el zip para no llenar el disco
|
| 71 |
+
fs.unlinkSync(localZipPath);
|
| 72 |
+
console.log(`✨ ${zipName} listo.`);
|
| 73 |
+
}
|
| 74 |
+
}
|
| 75 |
+
} catch (err) {
|
| 76 |
+
console.error(`❌ Error SimpleWorker: ${err.message}`);
|
| 77 |
+
}
|
| 78 |
+
}
|
lib/remote.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Client } from 'ssh2';
|
| 2 |
+
import fs from 'fs';
|
| 3 |
+
import path from 'path';
|
| 4 |
+
|
| 5 |
+
export default class RemoteFetcher {
|
| 6 |
+
constructor(config) {
|
| 7 |
+
this.config = {
|
| 8 |
+
host: config.host,
|
| 9 |
+
port: config.port || 22,
|
| 10 |
+
username: config.username,
|
| 11 |
+
password: config.password,
|
| 12 |
+
readyTimeout: 40000,
|
| 13 |
+
keepaliveInterval: 10000
|
| 14 |
+
};
|
| 15 |
+
this.conn = new Client();
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
connect() {
|
| 19 |
+
return new Promise((resolve, reject) => {
|
| 20 |
+
this.conn.on('ready', () => {
|
| 21 |
+
this.conn.sftp((err, sftp) => {
|
| 22 |
+
if (err) return reject(err);
|
| 23 |
+
this.sftp = sftp;
|
| 24 |
+
resolve();
|
| 25 |
+
});
|
| 26 |
+
})
|
| 27 |
+
.on('error', (err) => reject(err))
|
| 28 |
+
.connect(this.config);
|
| 29 |
+
});
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
// Volvemos a fastGet pero con configuración optimizada para velocidad/estabilidad
|
| 33 |
+
async fetchFile(remotePath, localPath, onProgress) {
|
| 34 |
+
return new Promise((resolve, reject) => {
|
| 35 |
+
this.sftp.fastGet(remotePath, localPath, {
|
| 36 |
+
concurrency: 6, // 6 descargas en paralelo para mayor velocidad
|
| 37 |
+
chunkSize: 32768, // Bloques de 32KB
|
| 38 |
+
step: (transferred, chunk, total) => {
|
| 39 |
+
if (onProgress) onProgress(transferred, total);
|
| 40 |
+
}
|
| 41 |
+
}, (err) => {
|
| 42 |
+
if (err) return reject(err);
|
| 43 |
+
resolve();
|
| 44 |
+
});
|
| 45 |
+
});
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
async listFiles(remoteDir, extension = '') {
|
| 49 |
+
return new Promise((resolve, reject) => {
|
| 50 |
+
this.sftp.readdir(remoteDir, (err, list) => {
|
| 51 |
+
if (err) return reject(err);
|
| 52 |
+
const files = list
|
| 53 |
+
.filter(f => f.attrs.isFile())
|
| 54 |
+
.map(f => f.filename);
|
| 55 |
+
|
| 56 |
+
if (extension) {
|
| 57 |
+
resolve(files.filter(f => f.toLowerCase().endsWith(extension.toLowerCase())));
|
| 58 |
+
} else {
|
| 59 |
+
resolve(files);
|
| 60 |
+
}
|
| 61 |
+
});
|
| 62 |
+
});
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
async listDirectories(remoteDir) {
|
| 66 |
+
return new Promise((resolve, reject) => {
|
| 67 |
+
this.sftp.readdir(remoteDir, (err, list) => {
|
| 68 |
+
if (err) return reject(err);
|
| 69 |
+
resolve(list.filter(f => f.attrs.isDirectory() && f.filename !== '.' && f.filename !== '..').map(f => f.filename));
|
| 70 |
+
});
|
| 71 |
+
});
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
async getFileStats(remotePath) {
|
| 75 |
+
return new Promise((resolve, reject) => {
|
| 76 |
+
this.sftp.stat(remotePath, (err, stats) => {
|
| 77 |
+
if (err) return reject(err);
|
| 78 |
+
resolve(stats);
|
| 79 |
+
});
|
| 80 |
+
});
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
disconnect() {
|
| 84 |
+
this.conn.end();
|
| 85 |
+
}
|
| 86 |
+
}
|
package.json
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "votolibre-simpleserver",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"type": "module",
|
| 5 |
+
"main": "index.js",
|
| 6 |
+
"dependencies": {
|
| 7 |
+
"dotenv": "^16.4.5",
|
| 8 |
+
"express": "^4.19.2",
|
| 9 |
+
"ssh2": "^1.15.0"
|
| 10 |
+
}
|
| 11 |
+
}
|
public/index.html
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="es">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<title>VotoLibre | Fast Audit</title>
|
| 6 |
+
<style>
|
| 7 |
+
body { font-family: sans-serif; padding: 20px; background: #f4f4f9; color: #333; }
|
| 8 |
+
.container { max-width: 800px; margin: 0 auto; background: white; padding: 30px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
|
| 9 |
+
h1 { color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 10px; }
|
| 10 |
+
input { width: 100%; padding: 10px; margin: 20px 0; border: 1px solid #ddd; border-radius: 5px; }
|
| 11 |
+
ul { list-style: none; padding: 0; }
|
| 12 |
+
li { padding: 10px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; }
|
| 13 |
+
li:hover { background: #f9f9f9; }
|
| 14 |
+
a { color: #3498db; text-decoration: none; font-weight: bold; }
|
| 15 |
+
a:hover { text-decoration: underline; }
|
| 16 |
+
.count { font-size: 0.9em; color: #666; }
|
| 17 |
+
</style>
|
| 18 |
+
</head>
|
| 19 |
+
<body>
|
| 20 |
+
<div class="container">
|
| 21 |
+
<h1>VotoLibre FastTrack Audit</h1>
|
| 22 |
+
<p>Lista simple de actas descargadas en tiempo real.</p>
|
| 23 |
+
<div class="count">Actas disponibles: <span id="count">0</span></div>
|
| 24 |
+
<input type="text" id="search" placeholder="Buscar acta por número..." onkeyup="filter()">
|
| 25 |
+
<ul id="list">
|
| 26 |
+
<li>Cargando archivos...</li>
|
| 27 |
+
</ul>
|
| 28 |
+
</div>
|
| 29 |
+
|
| 30 |
+
<script>
|
| 31 |
+
let allFiles = [];
|
| 32 |
+
async function load() {
|
| 33 |
+
try {
|
| 34 |
+
const res = await fetch('/api/files');
|
| 35 |
+
allFiles = await res.json();
|
| 36 |
+
document.getElementById('count').innerText = allFiles.length;
|
| 37 |
+
render(allFiles);
|
| 38 |
+
} catch (e) {}
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
function render(list) {
|
| 42 |
+
const ul = document.getElementById('list');
|
| 43 |
+
ul.innerHTML = list.map(f => `
|
| 44 |
+
<li>
|
| 45 |
+
<span>${f}</span>
|
| 46 |
+
<a href="/view/${f}" target="_blank">Abrir Imagen</a>
|
| 47 |
+
</li>
|
| 48 |
+
`).join('');
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
function filter() {
|
| 52 |
+
const q = document.getElementById('search').value.toLowerCase();
|
| 53 |
+
const filtered = allFiles.filter(f => f.toLowerCase().includes(q));
|
| 54 |
+
render(filtered);
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
setInterval(load, 5000);
|
| 58 |
+
load();
|
| 59 |
+
</script>
|
| 60 |
+
</body>
|
| 61 |
+
</html>
|