/** * Publica el instalador NSIS en MinIO y actualiza latest.json, versions.json y DATA.txt. * * Uso: * set MINIO_ACCESS_KEY=... * set MINIO_SECRET_KEY=... * npm run publish:minio * * Opcional: * MINIO_ENDPOINT=https://s3.builker.xyz * MINIO_BUCKET=apps-versions * MINIO_PREFIX=IPSExtractor * RELEASE_DATE=11/07/2026 (formato DATA.txt DD/MM/YYYY; default: fecha de hoy) */ import { createHash } from "node:crypto"; import { createReadStream, existsSync, readFileSync, statSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { S3Client, PutObjectCommand, GetObjectCommand } from "@aws-sdk/client-s3"; import { getReleaseDir, projectRoot } from "./release-dir.mjs"; const root = projectRoot; /** Carga KEY=VALUE desde un archivo local (sin dependencia dotenv). */ function loadEnvFile(filePath) { if (!existsSync(filePath)) return; const text = readFileSync(filePath, "utf8"); for (const line of text.split(/\r?\n/)) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith("#")) continue; const eq = trimmed.indexOf("="); if (eq <= 0) continue; const key = trimmed.slice(0, eq).trim(); let value = trimmed.slice(eq + 1).trim(); if ( (value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")) ) { value = value.slice(1, -1); } if (!(key in process.env) || process.env[key] === "") { process.env[key] = value; } } } loadEnvFile(join(root, ".env.minio")); loadEnvFile(join(root, ".env")); const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8")); const version = pkg.version; const endpoint = (process.env.MINIO_ENDPOINT || "https://s3.builker.xyz").replace(/\/$/, ""); const bucket = process.env.MINIO_BUCKET || "apps-versions"; const prefix = (process.env.MINIO_PREFIX || "IPSExtractor").replace(/^\/|\/$/g, ""); const accessKey = process.env.MINIO_ACCESS_KEY || process.env.AWS_ACCESS_KEY_ID; const secretKey = process.env.MINIO_SECRET_KEY || process.env.AWS_SECRET_ACCESS_KEY; if (!accessKey || !secretKey) { console.error( "Faltan credenciales. Define MINIO_ACCESS_KEY y MINIO_SECRET_KEY (o AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY).", ); process.exit(1); } const setupName = `IPSExtractor-Desktop-${version}-Setup.exe`; const releaseDir = getReleaseDir(); const setupPath = join(releaseDir, setupName); if (!existsSync(setupPath)) { console.error(`No se encontró ${setupPath}. Ejecuta antes: npm run dist`); console.error(`(directorio de release: ${releaseDir})`); process.exit(1); } function todayDisplay() { const d = new Date(); const dd = String(d.getDate()).padStart(2, "0"); const mm = String(d.getMonth() + 1).padStart(2, "0"); const yyyy = d.getFullYear(); return `${dd}/${mm}/${yyyy}`; } function isoFromDisplay(display) { // DD/MM/YYYY → YYYY-MM-DD const m = String(display).trim().match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/); if (!m) return null; const [, dd, mm, yyyy] = m; return `${yyyy}-${mm.padStart(2, "0")}-${dd.padStart(2, "0")}`; } function resolveReleaseDateDisplay() { // Prioridad: RELEASE_DATE explícita → fecha de hoy al publicar. if (process.env.RELEASE_DATE) return String(process.env.RELEASE_DATE).trim(); return todayDisplay(); } async function sha256File(filePath) { return new Promise((resolve, reject) => { const hash = createHash("sha256"); const stream = createReadStream(filePath); stream.on("data", (chunk) => hash.update(chunk)); stream.on("error", reject); stream.on("end", () => resolve(hash.digest("hex"))); }); } async function upload(client, key, body, contentType) { const payload = Buffer.isBuffer(body) ? body : typeof body === "string" ? Buffer.from(body, "utf8") : body; const params = { Bucket: bucket, Key: key, Body: payload, ContentType: contentType, }; // MinIO exige Content-Length; con Body string/stream el SDK a veces no lo envía. if (Buffer.isBuffer(payload)) { params.ContentLength = payload.byteLength; } else if (typeof payload === "object" && payload && "path" in payload) { // ReadStream desde createReadStream(path) try { params.ContentLength = statSync(payload.path).size; } catch { /* ignore — el stream del .exe suele ir bien */ } } await client.send(new PutObjectCommand(params)); console.log(` ↑ s3://${bucket}/${key}`); } function compareSemver(a, b) { const pa = String(a).replace(/^v/i, "").split(".").map((x) => Number.parseInt(x, 10) || 0); const pb = String(b).replace(/^v/i, "").split(".").map((x) => Number.parseInt(x, 10) || 0); const len = Math.max(pa.length, pb.length); for (let i = 0; i < len; i++) { const x = pa[i] ?? 0; const y = pb[i] ?? 0; if (x < y) return -1; if (x > y) return 1; } return 0; } async function streamToString(body) { if (!body) return ""; if (typeof body.transformToString === "function") return body.transformToString(); const chunks = []; for await (const chunk of body) chunks.push(chunk); return Buffer.concat(chunks).toString("utf8"); } /** Lee versions.json existente del bucket (o [] si no existe). */ async function loadExistingVersions(client) { try { const res = await client.send( new GetObjectCommand({ Bucket: bucket, Key: `${prefix}/versions.json` }), ); const text = await streamToString(res.Body); const parsed = JSON.parse(text); return Array.isArray(parsed?.versions) ? parsed.versions : []; } catch (err) { const code = err?.name || err?.Code || err?.$metadata?.httpStatusCode; if (code === "NoSuchKey" || code === 404 || err?.$metadata?.httpStatusCode === 404) { return []; } console.warn("No se pudo leer versions.json previo:", err?.message || err); return []; } } async function main() { const releaseDateDisplay = resolveReleaseDateDisplay(); const releaseDateIso = isoFromDisplay(releaseDateDisplay) || new Date().toISOString().slice(0, 10); const sha256 = await sha256File(setupPath); const { size } = statSync(setupPath); const publicBase = `${endpoint}/${bucket}/${prefix}`; const objectKeyExe = `${prefix}/${version}/${setupName}`; const objectKeyData = `${prefix}/${version}/DATA.txt`; const objectKeyLatest = `${prefix}/latest.json`; const objectKeyVersions = `${prefix}/versions.json`; const publicUrl = `${publicBase}/${version}/${setupName}`; const entry = { version, releaseDate: releaseDateIso, releaseDateDisplay, fileName: setupName, url: publicUrl, sha256, size, }; // Actualiza version.json local para alinear la UI con la fecha publicada writeFileSync( join(root, "src/shared/version.json"), `${JSON.stringify({ releaseDate: releaseDateIso }, null, 2)}\n`, "utf8", ); const client = new S3Client({ region: process.env.MINIO_REGION || "us-east-1", endpoint, forcePathStyle: true, credentials: { accessKeyId: accessKey, secretAccessKey: secretKey, }, }); const existing = await loadExistingVersions(client); const byVersion = new Map(existing.map((v) => [String(v.version), v])); byVersion.set(version, entry); const versions = [...byVersion.values()].sort((a, b) => compareSemver(b.version, a.version)); console.log(`Publicando v${version} → ${endpoint}/${bucket}/${prefix}/`); await upload(client, objectKeyExe, createReadStream(setupPath), "application/octet-stream"); // Re-upload metadata even if exe already exists (reintento tras fallo parcial). await upload( client, objectKeyData, Buffer.from(`${releaseDateDisplay}\n`, "utf8"), "text/plain; charset=utf-8", ); await upload( client, objectKeyLatest, Buffer.from(`${JSON.stringify(entry, null, 2)}\n`, "utf8"), "application/json", ); await upload( client, objectKeyVersions, Buffer.from(`${JSON.stringify({ versions }, null, 2)}\n`, "utf8"), "application/json", ); console.log("\nListo."); console.log(` latest.json: ${publicBase}/latest.json`); console.log(` versions.json: ${publicBase}/versions.json (${versions.length} versiones)`); console.log(` DATA.txt: ${publicBase}/${version}/DATA.txt → ${releaseDateDisplay}`); console.log(` Setup: ${publicUrl}`); } main().catch((err) => { console.error(err); process.exit(1); });