const fs = require('fs') const path = require('path') /** ******************* 读取命令行以及配置文件里的参数 ******************** **/ const wo = (global.wo = { envar: require('wo-base-envar').merge_envar({ rawEnvar: { commanderOptions: [ // 命令行里可以接受的参数。将传给 commander。每个参数的定义格式是 [参数名,参数键,描述],后两者用于传给 commander,取值后覆盖掉同名变量。 [ 'fromPath', '-f, --fromPath ', 'local distribution path to copy from.' ], [ 'gotoTarget', '-g, --gotoTarget ', 'connection section name.' ], [ 'targetType', '-t, --targetType ', 'target type, git or ssh.' ], [ 'host', '-H, --host ', 'Host IP or domain name of the target server.' ], ['port', '-P, --port ', 'Ssh port number of the target server.'], [ 'targetPath', '-d, --targetPath ', 'Destination path to deploy on the target.' ], [ 'targetFolder', '-D, --targetFolder ', 'Destination folder to deploy on the target.' ], ['repo', '-r, --repo ', 'git repo address.'], ['branch', '-b, --branch ', 'git repo branch.'], ['gitname', '-n, --gitname ', 'git user name.'], ['gitemail', '-m, --gitemail ', 'git user email.'], ['user', '-u, --user ', 'User id to login the target server.'], [ 'privateKey', '-k, --privateKey ', 'User private key file to login the target server.' ], [ 'password', '-p, --password ', 'User password to login the target server. You may have to enclose it in "".' ] ], // 最基础的必须的默认配置,如果用户什么也没有提供 deploy: { fromPath: './_webroot', gotoTarget: 'github', server: { targetType: 'ssh', host: undefined, port: 22, targetPath: undefined, // 目标服务器上的目录。似乎该目录必须已经存在于服务器上 targetFolder: '_webroot', // 新系统将发布在这个文件夹里。建议为dist,和npm run build产生的目录一致,这样既可以远程自动部署,也可以直接登录服务器手动部署。 user: undefined, password: undefined, privateKey: `${process.env.HOME}/.ssh/id_rsa` }, github: { targetType: 'git', repo: undefined, branch: 'main', gitname: undefined, gitemail: undefined, user: undefined, password: undefined, privateKey: `${process.env.HOME}/.ssh/id_rsa` } } }, envarFiles: ['./envar-deploy.js', './envar-deploy.gitignore.js'], withCmd: true }) }) const envarDeploy = wo.envar.deploy delete wo.envar.deploy // 用 commander 采集到的配置 替换 文件中采集到的配置 envarDeploy.fromPath = wo.envar.fromPath || envarDeploy.fromPath envarDeploy.gotoTarget = wo.envar.gotoTarget || envarDeploy.gotoTarget // 使用用户指定的连接 const connection = envarDeploy[envarDeploy.gotoTarget] Object.assign(connection, wo.envar) // 用 commander 采集到的配置 替换 文件中采集到的配置 connection.tryKeyboard = true connection.onKeyboardInteractive = ( name, instructions, lang, prompts, finish ) => { // 不起作用 if ( prompts.length > 0 && prompts[0].prompt.toLowerCase().includes('password') ) { finish([password]) } } console.log( `*** Deploy from ${envarDeploy.fromPath} to ${JSON.stringify(connection)} ***` ) if (connection.targetType === 'ssh') { deployToSsh(connection) } else if (connection.targetType === 'git') { deployToGit(connection) } /** ********************** 连接到 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.targetFolder} ${ connection.targetFolder }-backup-${new Date().toISOString()} ... ]` ) await ssh.execCommand( `mv ${connection.targetFolder} ${ connection.targetFolder }-backup-${new Date().toISOString()}`, { cwd: connection.targetPath } ) console.log(`[ mkdir ${connection.targetFolder} ... ]`) await ssh.execCommand(`mkdir ${connection.targetFolder}`, { cwd: connection.targetPath }) const toCreate = necessaryPath(path.join('./', envarDeploy.fromPath)) for (const name of toCreate) { console.log( `[ mkdir ${connection.targetFolder}/${name.join('/')} ... ]` ) await ssh.execCommand( `mkdir ${connection.targetFolder}/${name.join('/')}`, { cwd: connection.targetPath } ) } let err console.log( `[ Upload to ${connection.targetPath}/${connection.targetFolder} ... ]` ) await ssh.putDirectory( path.join('./', envarDeploy.fromPath), `${connection.targetPath}/${connection.targetFolder}`, { 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.error( new Date(), `❌❌❌ Failed deploy ${envarDeploy.fromPath} to ${connection.targetPath}/${connection.targetFolder} ❌❌❌` ) process.exit(1) } else { console.info( new Date(), `✅✅✅ Successfully deployed [${envarDeploy.fromPath}] to [${connection.targetPath}/${connection.targetFolder}] ✅✅✅` ) if (connection.url) { console.info(`✅✅✅ ${connection.url} ✅✅✅`) } process.exit() } }) .catch(err => { console.error(err) ssh.dispose() console.error( new Date(), `❌❌❌ Failed deploy [${envarDeploy.fromPath}] to [${connection.targetPath}/${connection.targetFolder}] ❌` ) 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 || 'main' } } 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 = envarDeploy.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 main -f ./dist' console.log(help) return } 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) } function git (...connection) { return spawn('git', connection, { cwd: deployDir, verbose: verbose, stdio: 'inherit' }) } function setup () { const userName = connection.gitname || '' const userEmail = connection.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') }) .then(() => { console.info( new Date(), `✅✅✅ Deployed [${envarDeploy.fromPath}] to [${connection.repo}#${connection.branch}] ✅✅✅` ) if (connection.url) { console.info(new Date(), `✅✅✅ ${connection.url} ✅✅✅`) } }) .catch(err => { console.error( new Date(), `❌❌❌ Failed deploy [${envarDeploy.fromPath}] to [${connection.repo}#${connection.branch}] ❌❌❌` ) process.exit(1) }) } return fs .exists(deployDir) .then(function (exist) { if (exist) return console.info('Setting up Git deployment...') return setup() }) .then(() => { console.info('Clearing .deploy_git folder...') return fs.emptyDir(deployDir) }) .then(() => { const opts = {} console.info('Copying files from local 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(() => { console.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(() => { if (connection.cname) { console.info(`Adding CNAME [${connection.cname}]`) return fs.writeFile( path.join(deployDir, 'CNAME'), connection.cname, { flag: 'w' }, console.error ) } return }) .then(() => { return parseConnection(connection) }) .each(function (repo) { console.info('##########', repo, '##########') return push(repo) }) } // end of function exec exec() }