const fs = require('fs') const path = require('path') /** ******************* 读取命令行以及配置文件里的参数 ******************** **/ const commander = require('commander') const deepmerge = require('deepmerge') // 默认参数 const wo = (global.wo = { envi: { deploy: { fromPath: './webroot', gotoTarget: 'github', vultr: { targetType: 'ssh', host: undefined, port: 22, targetPath: undefined, targetDir: 'webroot', user: undefined, password: undefined, key: `${process.env.HOME}/.ssh/id_rsa`, }, github: { targetType: 'git', repo: undefined, branch: 'main', gitname: undefined, gitemail: undefined, user: undefined, password: undefined, key: `${process.env.HOME}/.ssh/id_rsa`, } } } }) // 读取配置文件 try { let configFile if (fs.existsSync(configFile=path.join(process.cwd(), 'ConfigDeploy.js'))) { wo.envi = deepmerge(wo.envi, require(configFile)) console.info(`${configFile} loaded`) } if (fs.existsSync(configFile=path.join(process.cwd(), 'ConfigSecret.js'))) { wo.envi = deepmerge(wo.envi, require(configFile)) console.info(`${configFile} loaded`) } } catch (err) { console.error('Loading config files failed: ' + err.message) } wo.envi.deploy.connection = wo.envi.deploy[wo.envi.deploy.gotoTarget] // 读取命令行参数 commander .version('1.0', '-v, --version') // 默认是 -V。如果要 -v,就要加 '-v --version' .option('-f, --fromPath ', `local distribution path to copy from. Default to ${wo.envi.deploy.fromPath}`) .option('-g, --gotoTarget ', `connection section in config. Default to ${wo.envi.deploy.gotoTarget}`) .option('-t, --targetType ', `target type, git or ssh. Default to ${wo.envi.deploy.connection.targetType}`) .option('-H, --host ', `Host IP or domain name of the target server. Default to ${wo.envi.deploy.connection.host}`) .option('-P, --port ', `Ssh port number of the target server. Default to ${wo.envi.deploy.connection.port}`) .option('-b, --targetPath ', `Destination path to deploy on the target. Default to ${wo.envi.deploy.connection.targetPath}`) .option('-d, --targetDir ', `Destination folder to deploy on the target. Default to ${wo.envi.deploy.connection.targetDir}`) .option('-r, --repo ', `git repo address. Default to ${wo.envi.deploy.connection.repo}`) .option('-b, --branch ', `git repo branch. Default to ${wo.envi.deploy.connection.branch}`) .option('-n, --gitname ', `git user name. Default to ${wo.envi.deploy.connection.gitname}`) .option('-m, --gitemail ', `git user email. Default to ${wo.envi.deploy.connection.gitemail}`) .option('-u, --user ', `User id to login the target server. Default to ${wo.envi.deploy.connection.user}`) .option('-k, --key ', `User private key file to login the target server. Default to ${wo.envi.deploy.connection.key}`) .option('-p, --password ', `User password to login the target server. You may have to enclose it in "". Default to "${wo.envi.deploy.connection.password}"`) .parse(process.argv) wo.envi.deploy.fromPath = commander.fromPath || wo.envi.deploy.fromPath wo.envi.deploy.connection = wo.envi.deploy[commander.gotoTarget || wo.envi.deploy.gotoTarget] // 使用用户指定的连接 // 可以用命令行参数覆盖掉配置文件 const connection = { targetType: commander.targetType || wo.envi.deploy.connection.targetType, // for ssh host: commander.host || wo.envi.deploy.connection.host, port: commander.port || wo.envi.deploy.connection.port, targetPath: commander.targetPath || wo.envi.deploy.connection.targetPath, // 目标服务器上的目录。似乎该目录必须已经存在于服务器上 targetDir: commander.targetDir || wo.envi.deploy.connection.targetDir, // 新系统将发布在这个文件夹里。建议为dist,和npm run build产生的目录一致,这样既可以远程自动部署,也可以直接登录服务器手动部署。 // for git repo: commander.repo || wo.envi.deploy.connection.repo, branch: commander.branch || wo.envi.deploy.connection.branch, gitname: commander.gitname || wo.envi.deploy.connection.gitname, gitemail: commander.gitemail || wo.envi.deploy.connection.gitemail, // common username: commander.user || wo.envi.deploy.connection.user, privateKey: fs.existsSync(commander.key || wo.envi.deploy.key) ? (commander.key || wo.envi.deploy.key) : undefined, password: commander.password || wo.envi.deploy.connection.password, tryKeyboard: true, onKeyboardInteractive: (name, instructions, lang, prompts, finish) => { // 不起作用 if (prompts.length > 0 && prompts[0].prompt.toLowerCase().includes('password')) { finish([password]) } }, } console.log(` deploy from ${wo.envi.deploy.fromPath} to ${JSON.stringify(connection)}`) if (connection.targetType==='ssh') { deployToSsh(connection) }else if (connection.targetType==='git'){ deployToGit(connection) } console.info(`*** Successfully deployed ${connection.fromPath} to ${connection.targetPath}/${connection.targetDir} ***`) if (connection.url){ console.info(`*** Visiting ${connection.url} ***`) } /** ********************** 连接到 Ssh主机,拷贝文件到指定路径 ************* **/ function deployToSsh(connection){ const ssh = new (require('node-ssh'))() function subDirs (path) { const dirs = [path] if (fs.statSync(path).isFile()) { return dirs } fs.readdirSync(path).forEach(item => { const stat = fs.statSync(`${path}/${item}`) if (stat.isDirectory()) { dirs.push(...subDirs(`${path}/${item}`)) } }) return dirs } const necessaryPath = (path) => { return subDirs(path) .map(it => it.replace(path, '')) .filter(it => it) .map(it => it.split('/').filter(it => it)) } ssh.connect(connection).then(async () => { console.log(`[ mv ${connection.targetDir} ${connection.targetDir}-backup-${new Date().toISOString()} ... ]`) await ssh.execCommand(`mv ${connection.targetDir} ${connection.targetDir}-backup-${new Date().toISOString()}`, { cwd: connection.targetPath }) console.log(`[ mkdir ${connection.targetDir} ... ]`) await ssh.execCommand(`mkdir ${connection.targetDir}`, { cwd: connection.targetPath }) const toCreate = necessaryPath(path.join('./', wo.envi.deploy.fromPath)) for (const name of toCreate) { console.log(`[ mkdir ${connection.targetDir}/${name.join('/')} ... ]`) await ssh.execCommand(`mkdir ${connection.targetDir}/${name.join('/')}`, { cwd: connection.targetPath }) } let err console.log(`[ Upload to ${connection.targetPath}/${connection.targetDir} ... ]`) await ssh.putDirectory(path.join('./', wo.envi.deploy.fromPath), `${connection.targetPath}/${connection.targetDir}`, { concurrency: 10, recursive: true, validate: itemPath => { const baseName = path.basename(itemPath) return !baseName.endsWith('.map') }, tick: (fromPath, remotePath, error) => { console.log(`Uploading "${fromPath}" ===> "${remotePath}" ${error || 'succeeded!'}`) err = error }, }) ssh.dispose() if (err) { console.log('[ Uploaded with error! ]') process.exit(1) } else { console.log('[ Uploaded successfully! ]') process.exit() } }).catch(err => { console.error(err) ssh.dispose() process.exit(1) }) } /** ********************** 连接到 Git主机,拷贝文件到指定路径 ************* **/ function deployToGit(connection){ const pathFn = require('path') const fs = require('hexo-fs') const chalk = require('chalk') const swig = require('swig-templates') const moment = require('moment') const Promise = require('bluebird') const spawn = require('hexo-util/lib/spawn') const swigHelpers = { now: function(format) { return moment().format(format) } } const rRepoURL = /^(?:(?:git|https?|git\+https|git\+ssh):\/\/)?(?:[^@]+@)?([^\/]+?)[\/:](.+?)\.git$/ // eslint-disable-line no-useless-escape const rGithubPage = /\.github\.(io|com)$/ function parseRepo(repo) { const split = repo.split(',') const url = split.shift() let branch = split[0] if (!branch && rRepoURL.test(url)) { const match = url.match(rRepoURL) const host = match[1] const path = match[2] if (host === 'github.com') { branch = rGithubPage.test(path) ? (connection.branch || 'main') : 'gh-pages' } else if (host === 'coding.net') { branch = 'coding-pages' } } return { url: url, branch: branch || 'master' } } function parseConnection(args) { const repo = args.repo || args.repository if (!repo) throw new TypeError('repo is required!') if (typeof repo === 'string') { const data = parseRepo(repo) data.branch = args.branch || data.branch return [data] } const result = Object.keys(repo).map(key => { return parseRepo(repo[key]) }) return result } function exec() { const targetDir = '' const deployDir = pathFn.join(targetDir, '.deploy_git') const fromDir = wo.envi.deploy.fromPath let extendDirs = connection.extend_dirs const ignoreHidden = connection.ignore_hidden const ignorePattern = connection.ignore_pattern const message = commitMessage(connection) const verbose = !connection.silent if (!connection.repo && process.env.HEXO_DEPLOYER_REPO) { connection.repo = process.env.HEXO_DEPLOYER_REPO } if (!connection.repo && !connection.repository) { let help = '' help += 'You have to configure the deployment settings in config files or command line first!\n\n' help += 'Example:\n' help += ' node deploy.js -t git -r https://github.com/OWNER/OWNER.github.io -b master -f ./dist' console.log(help) return } function git(...connection) { return spawn('git', connection, { cwd: deployDir, verbose: verbose, stdio: 'inherit' }) } function setup() { const userName = wo.envi.deploy.gitname || '' const userEmail = wo.envi.deploy.gitemail || '' // Create a placeholder for the first commit return fs.writeFile(pathFn.join(deployDir, 'placeholder'), '').then(() => { return git('init') }).then(() => { return userName && git('config', 'user.name', userName) }).then(() => { return userEmail && git('config', 'user.email', userEmail) }).then(() => { return git('add', '-A') }).then(() => { return git('commit', '-m', 'First commit') }) } function push(repo) { return git('add', '-A').then(() => { return git('commit', '-m', message).catch(() => { // Do nothing. It's OK if nothing to commit. }) }).then(() => { return git('push', '-u', repo.url, 'HEAD:' + repo.branch, '--force') }) } return fs.exists(deployDir).then(function(exist) { if (exist) return // log.info('Setting up Git deployment...') return setup() }).then(() => { // log.info('Clearing .deploy_git folder...') return fs.emptyDir(deployDir) }).then(() => { const opts = {} // log.info('Copying files from public folder...') if (typeof ignoreHidden === 'object') { opts.ignoreHidden = ignoreHidden.public } else { opts.ignoreHidden = ignoreHidden } if (typeof ignorePattern === 'string') { opts.ignorePattern = new RegExp(ignorePattern) } else if (typeof ignorePattern === 'object' && Reflect.apply(Object.prototype.hasOwnProperty, ignorePattern, ['public'])) { opts.ignorePattern = new RegExp(ignorePattern.public) } return fs.copyDir(fromDir, deployDir, opts) }).then(() => { // log.info('Copying files from extend dirs...') if (!extendDirs) { return } if (typeof extendDirs === 'string') { extendDirs = [extendDirs] } const mapFn = function(dir) { const opts = {} const extendPath = pathFn.join(targetDir, dir) const extendDist = pathFn.join(deployDir, dir) if (typeof ignoreHidden === 'object') { opts.ignoreHidden = ignoreHidden[dir] } else { opts.ignoreHidden = ignoreHidden } if (typeof ignorePattern === 'string') { opts.ignorePattern = new RegExp(ignorePattern) } else if (typeof ignorePattern === 'object' && Reflect.apply(Object.prototype.hasOwnProperty, ignorePattern, [dir])) { opts.ignorePattern = new RegExp(ignorePattern[dir]) } return fs.copyDir(extendPath, extendDist, opts) } return Promise.map(extendDirs, mapFn, { concurrency: 2 }) }).then(() => { return parseConnection(connection) }).each(function(repo) { console.log('########## repo ###########') console.log(repo) return push(repo) }) } // end of function exec function commitMessage(connection) { const message = connection.m || connection.msg || connection.message || 'Site updated: {{ now(\'YYYY-MM-DD HH:mm:ss\') }}' return swig.compile(message)(swigHelpers) } exec() }