| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315 |
- // .build/deploy-android-to-server.js - Android APK 部署脚本
- const { NodeSSH } = require('node-ssh');
- const path = require('path');
- const fs = require('fs');
- /**
- * 部署 Android APK 到远程服务器
- * @param {Object} options 部署选项
- * @param {string} options.privateKey SSH 私钥内容
- * @param {string} options.host 服务器地址
- * @param {string} options.username 用户名
- * @param {string} options.remotePath H5 部署路径(会自动转换为 android-releases)
- * @param {string} options.apkPath 本地 APK 文件路径
- * @param {string} options.appVersion 应用版本号(如 1.0.0)
- * @param {boolean} options.testOnly 是否仅测试连接
- */
- async function deployAndroidToServer(options) {
- const {
- privateKey,
- host,
- username,
- remotePath,
- apkPath,
- appVersion,
- testOnly = false
- } = options;
- const ssh = new NodeSSH();
-
- try {
- console.log('🔐 正在连接到服务器...');
- console.log(` 主机: ${username}@${host}`);
-
- // 连接到服务器
- await ssh.connect({
- host,
- username,
- privateKey,
- // 兼容多种 SSH 密钥算法
- algorithms: {
- key: [
- 'ssh-rsa',
- 'ecdsa-sha2-nistp256',
- 'ecdsa-sha2-nistp384',
- 'ecdsa-sha2-nistp521',
- 'ssh-ed25519'
- ],
- serverHostKey: [
- 'ssh-rsa',
- 'ecdsa-sha2-nistp256',
- 'ecdsa-sha2-nistp384',
- 'ecdsa-sha2-nistp521',
- 'ssh-ed25519'
- ]
- }
- });
-
- console.log('✅ SSH 连接成功!');
-
- // 如果是测试模式,执行测试命令后返回
- if (testOnly) {
- console.log('🧪 测试模式:执行测试命令...');
- const result = await ssh.execCommand('echo "SSH key works!"');
- console.log(` 输出: ${result.stdout}`);
- ssh.dispose();
- console.log('✅ SSH 连接测试完成');
- return;
- }
-
- // 检查本地 APK 文件是否存在
- if (!fs.existsSync(apkPath)) {
- throw new Error(`本地 APK 文件不存在: ${apkPath}`);
- }
-
- // 路径转换:/var/www/html/releases -> /var/www/html/android-releases
- const androidRemotePath = remotePath.replace(/\/releases$/, '/android-releases');
-
- console.log(`📱 Android APK 部署`);
- console.log(` H5 路径: ${remotePath}`);
- console.log(` Android 路径: ${androidRemotePath}`);
-
- // 生成时间戳
- const timestamp = new Date().toISOString()
- .replace(/[-:]/g, '')
- .replace('T', '-')
- .split('.')[0];
-
- // 生成版本号(格式: v1.1.5-20250129-143005)
- const version = `v${appVersion}-${timestamp}`;
-
- const remoteVersionDir = `${androidRemotePath}/${version}`;
- const apkFileName = path.basename(apkPath);
- const remoteApkPath = `${remoteVersionDir}/${apkFileName}`;
-
- console.log(`📦 部署版本: ${version}`);
- console.log(` 本地 APK: ${apkPath}`);
- console.log(` 远程路径: ${remoteApkPath}`);
-
- // 创建远程 android-releases 根目录
- console.log('📁 创建远程目录结构...');
- const mkdirResult = await ssh.execCommand(`mkdir -p ${remoteVersionDir}`);
- if (mkdirResult.code !== 0) {
- throw new Error(`创建远程目录失败: ${mkdirResult.stderr}`);
- }
- console.log('✅ 远程目录已创建');
-
- // 上传 APK 文件
- console.log('📤 开始上传 APK 文件...');
- const fileStats = fs.statSync(apkPath);
- const fileSizeMB = (fileStats.size / 1024 / 1024).toFixed(2);
- console.log(` 文件大小: ${fileSizeMB} MB`);
-
- await ssh.putFile(apkPath, remoteApkPath);
- console.log('✅ APK 文件上传完成!');
-
- // 验证上传的文件
- console.log('🔍 验证上传的文件...');
- const lsResult = await ssh.execCommand(`ls -lh ${remoteApkPath}`);
- if (lsResult.code !== 0) {
- throw new Error(`文件验证失败: ${lsResult.stderr}`);
- }
- console.log(` ${lsResult.stdout}`);
-
- // 更新 latest.apk 符号链接
- console.log('🔗 更新 latest.apk 符号链接...');
- const latestLinkPath = `${androidRemotePath}/latest.apk`;
- const symlinkCmd = `cd ${androidRemotePath} && ln -sfn ${version}/${apkFileName} latest.apk`;
- const symlinkResult = await ssh.execCommand(symlinkCmd);
-
- if (symlinkResult.code !== 0) {
- throw new Error(`更新符号链接失败: ${symlinkResult.stderr}`);
- }
-
- // 验证符号链接
- console.log('🔍 验证符号链接...');
- const readlinkResult = await ssh.execCommand(`readlink ${latestLinkPath}`);
- const currentTarget = readlinkResult.stdout.trim();
- const expectedTarget = `${version}/${apkFileName}`;
-
- if (currentTarget === expectedTarget) {
- console.log(`✅ latest.apk 符号链接已正确更新 -> ${expectedTarget}`);
- } else {
- console.warn(`⚠️ 符号链接验证异常:`);
- console.warn(` 期望: ${expectedTarget}`);
- console.warn(` 实际: ${currentTarget}`);
- }
-
- // 生成/更新版本清单文件
- console.log('📋 更新版本清单文件...');
- await updateVersionsManifest(ssh, androidRemotePath, {
- version,
- appVersion,
- timestamp: new Date().toISOString(),
- filename: apkFileName,
- size: fileStats.size,
- downloadUrl: `/android-releases/${version}/${apkFileName}`
- });
-
- // 列出所有版本
- console.log('📊 版本列表:');
- const versionsResult = await ssh.execCommand(`ls -1 ${androidRemotePath} | grep "^v"`);
- const versions = versionsResult.stdout.split('\n').filter(v => v.trim());
- console.log(` 总计 ${versions.length} 个版本:`);
- versions.forEach((v, i) => {
- const marker = v === version ? '← 最新' : '';
- console.log(` ${i + 1}. ${v} ${marker}`);
- });
-
- console.log('\n🎉 Android APK 部署成功!');
- console.log(` 版本: ${version}`);
- console.log(` 最新版本下载: ${androidRemotePath}/latest.apk`);
- console.log(` 版本清单: ${androidRemotePath}/versions.json`);
-
- ssh.dispose();
- return version;
-
- } catch (error) {
- console.error('\n❌ 部署失败:', error.message);
-
- // 提供更详细的错误信息
- if (error.level === 'client-authentication') {
- console.error(' → 认证失败,请检查 SSH 私钥是否正确');
- } else if (error.level === 'client-timeout') {
- console.error(' → 连接超时,请检查网络和服务器地址');
- } else if (error.code === 'ENOENT') {
- console.error(' → 文件或目录不存在');
- }
-
- ssh.dispose();
- throw error;
- }
- }
- /**
- * 更新版本清单文件
- * @param {NodeSSH} ssh SSH 连接实例
- * @param {string} remotePath 远程 android-releases 路径
- * @param {Object} versionInfo 版本信息
- */
- async function updateVersionsManifest(ssh, remotePath, versionInfo) {
- const manifestPath = `${remotePath}/versions.json`;
- const tempLocalPath = path.join(__dirname, '.versions-temp.json');
-
- let manifest = {
- latest: versionInfo.version,
- versions: []
- };
-
- try {
- // 尝试下载现有的版本清单文件
- console.log(' 下载现有版本清单...');
- await ssh.getFile(tempLocalPath, manifestPath);
-
- // 读取并解析
- const content = fs.readFileSync(tempLocalPath, 'utf8');
- manifest = JSON.parse(content);
- console.log(` 已加载现有清单,包含 ${manifest.versions.length} 个版本`);
- } catch (error) {
- // 文件不存在,使用默认清单
- console.log(' 版本清单文件不存在,创建新清单');
- }
-
- // 更新清单
- manifest.latest = versionInfo.version;
-
- // 检查版本是否已存在
- const existingIndex = manifest.versions.findIndex(v => v.version === versionInfo.version);
- if (existingIndex !== -1) {
- // 更新现有版本信息
- manifest.versions[existingIndex] = versionInfo;
- console.log(' 更新现有版本信息');
- } else {
- // 添加新版本到数组开头
- manifest.versions.unshift(versionInfo);
- console.log(` 添加新版本,当前总计 ${manifest.versions.length} 个版本`);
- }
-
- // 写入本地临时文件
- fs.writeFileSync(tempLocalPath, JSON.stringify(manifest, null, 2), 'utf8');
-
- // 上传到服务器
- console.log(' 上传版本清单到服务器...');
- await ssh.putFile(tempLocalPath, manifestPath);
-
- // 清理临时文件
- fs.unlinkSync(tempLocalPath);
-
- console.log('✅ 版本清单已更新');
- }
- // CLI 入口点
- if (require.main === module) {
- const args = process.argv.slice(2);
- const testOnly = args.includes('--test');
-
- // 从环境变量读取配置
- const privateKey = process.env.DEPLOY_KEY;
- const host = process.env.DEPLOY_HOST;
- const username = process.env.DEPLOY_USER;
- const remotePath = process.env.DEPLOY_PATH;
-
- // APK 文件路径(需要从命令行参数或环境变量获取)
- const apkPath = process.env.APK_PATH || args.find(arg => arg.endsWith('.apk'));
- const appVersion = process.env.APP_VERSION || '0.0.0';
-
- // 验证必需的环境变量
- const missingVars = [];
- if (!privateKey) missingVars.push('DEPLOY_KEY');
- if (!host) missingVars.push('DEPLOY_HOST');
- if (!username) missingVars.push('DEPLOY_USER');
- if (!remotePath) missingVars.push('DEPLOY_PATH');
- if (!testOnly && !apkPath) missingVars.push('APK_PATH 或命令行参数');
-
- if (missingVars.length > 0) {
- console.error('❌ 缺少必需的环境变量/参数:');
- missingVars.forEach(varName => {
- console.error(` - ${varName}`);
- });
- console.error('\n请设置这些环境变量后重试。');
- console.error('\n使用示例:');
- console.error(' node deploy-android-to-server.js /path/to/app.apk');
- console.error(' 或');
- console.error(' APK_PATH=/path/to/app.apk APP_VERSION=1.0.0 node deploy-android-to-server.js');
- process.exit(1);
- }
-
- // 执行部署
- console.log('🚀 Android APK 部署系统启动\n');
- if (testOnly) {
- console.log('🧪 模式: 连接测试');
- } else {
- console.log('📦 模式: 完整部署');
- }
- console.log('='.repeat(50) + '\n');
-
- deployAndroidToServer({
- privateKey,
- host,
- username,
- remotePath,
- apkPath,
- appVersion,
- testOnly
- })
- .then(() => {
- console.log('\n' + '='.repeat(50));
- process.exit(0);
- })
- .catch(error => {
- console.error('\n' + '='.repeat(50));
- process.exit(1);
- });
- }
- module.exports = { deployAndroidToServer };
|