#!/usr/bin/env node /* * Uploads the latest signed release artifacts in releases// via FTP/FTPS using curl. * Reads configuration from .env (auto-loaded) or process.env. * Env vars: * FTP_PROTOCOL=ftp|ftps (default: ftp) * FTP_HOST=example.com (required) * FTP_PORT=21 (optional) * FTP_USER=username (required) * FTP_PASS=password (required) * FTP_REMOTE_DIR=/remote/path (required) */ const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); const root = path.join(__dirname, '..'); // Auto-load .env if present (() => { try { const envPath = path.join(root, '.env'); if (fs.existsSync(envPath)) { const content = fs.readFileSync(envPath, 'utf8'); for (const rawLine of content.split(/\r?\n/)) { const line = rawLine.trim(); if (!line || line.startsWith('#')) continue; const cleaned = line.startsWith('export ') ? line.slice('export '.length).trim() : line; const eq = cleaned.indexOf('='); if (eq === -1) continue; const key = cleaned.slice(0, eq).trim(); let val = cleaned.slice(eq + 1).trim(); if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) { val = val.slice(1, -1); } if (!(key in process.env)) process.env[key] = val; } console.log('Loaded environment from .env'); } } catch (e) { console.warn('Warning: Failed to load .env:', e.message); } })(); function mask(str) { if (!str) return str; return '***'; } function run(cmd, secrets = []) { let shown = cmd; for (const s of secrets) { if (s) shown = shown.split(String(s)).join('***'); } console.log('> ' + shown); execSync(cmd, { stdio: 'inherit' }); } const pkg = require(path.join(root, 'package.json')); const version = pkg.version; if (!version) { console.error('package.json is missing version'); process.exit(1); } const artifactsDir = path.join(root, 'releases', version); if (!fs.existsSync(artifactsDir) || !fs.statSync(artifactsDir).isDirectory()) { console.error(`Artifacts directory not found: ${artifactsDir}. Run npm run release:sign first.`); process.exit(1); } const files = fs.readdirSync(artifactsDir).filter(f => fs.statSync(path.join(artifactsDir, f)).isFile()); if (files.length === 0) { console.error(`No files found in ${artifactsDir}`); process.exit(1); } const protocol = (process.env.FTP_PROTOCOL || 'ftp').toLowerCase(); const host = process.env.FTP_HOST; const port = process.env.FTP_PORT; const user = process.env.FTP_USER; const pass = process.env.FTP_PASS; let remoteDir = process.env.FTP_REMOTE_DIR || '/'; if (!host || !user || !pass || !remoteDir) { console.error('Missing FTP config. Required: FTP_HOST, FTP_USER, FTP_PASS, FTP_REMOTE_DIR'); process.exit(1); } // Normalize remoteDir if (!remoteDir.startsWith('/')) remoteDir = '/' + remoteDir; if (remoteDir.endsWith('/')) remoteDir = remoteDir.slice(0, -1); // Construct base URL const baseUrl = `${protocol}://${host}${port ? `:${port}` : ''}${remoteDir}`; console.log(`Uploading ${files.length} file(s) from ${artifactsDir} to ${protocol}://${host}${port ? `:${port}` : ''}${remoteDir}/`); for (const file of files) { const localPath = path.join(artifactsDir, file); const url = `${baseUrl}/${encodeURIComponent(file)}`; // --ftp-create-dirs ensures remote dirs are created; --fail fails on server errors. const cmd = [ 'curl', '--fail', '--ftp-create-dirs', `--user`, `${user}:${pass}`, '--upload-file', JSON.stringify(localPath), JSON.stringify(url), ].join(' '); run(cmd, [user, pass]); } console.log('Upload complete.');