背景:公司开发项目发布系统
需求:
1.实现本地文件或者git/svn上的文件上传(ftp)到服务器
2.错误实时返回到前台
3.文件备份回滚
简单分析场景:
1.下载到本地时,如果本地存在,则会报错,故需要在下载之前删除本地文件夹
判断存在?执行删除
或者直接删除,捕获错误忽略(try)
2.只有在上一步成功过后才可以读取文件夹,所以需不需要判断文件夹存在情况呢?
还是判断一下吧!可能有直接输入文件夹上传,没有 下载 一步
3.读取文件列表 (此为同步进行) 成功失败后执行响应操作
4.以上成功。连接ftp。
5.以上成功。首先创建一个备份'_back'的文件夹,作为项目名称 也就是 name_back
6.以上成功。上传文件。适时返回进度,在上传完成(未出错)
7.以上成功。将项目名称改为 name_getTime() 。此处可能出错,因为文件可能不存在,捕获错误,忽略。
8.以上成功。讲上传的项目 name_back 重命名为 name
9.关闭连接,完成。
10. 回滚。需要传入 服务器端的 项目地址 以及 版本识别符
说明:
1.所有的失败(错误)均需要处理,防止服务崩溃
2.所有输入的文件夹路径前后不能有 '/'
3.对于有ftp连接的需要在失败或者错误或者结束业务的时候关闭 避免超时报错,服务器崩溃
4.针对回滚,考虑往数据库中存入数据,结合 uuid 识别项目
技术:nodejs + node-ftp
实现:
1.下载远程代码
通过命令node调取命令行实现下载(svn命令行需要安装subversion)
依赖 child_process 实现命令行调取 exec = require('child_process').exec
异步执行,需要结合 promise 返回状态
参数说明:
cmd 执行的命令行 (我执行多条命令的时候出错,然后没有去找解决的办法,妈的没有时间)
{} 配置项。设置下载的超时时间,以及下载的buffer(小了可能报错)
cb 回调处理
exec(cmd, {
encoding: 'utf8',
timeout: 0,
maxBuffer: 10000 * 1024, // 默认 200 * 1024
killSignal: 'SIGTERM'
}, function(err, stdout, stderr) {
if (err) {
console.log('下载文件%s出错%s',cmd,err)
} else {
cb()
}
});
2.读取本地文件(如果本地文件夹上传直接读取)
//递归读取文件
//参数 path 读取的文件夹 相对路径是相对当前的服务(server index)路径,绝对路径带盘符
function readFile(path) {
files = fs.readdirSync(path); //需要用到同步读取
files.forEach(walk);
function walk(file) {
states = fs.statSync(path + '/' + file);
if (states.isDirectory()) {
readFile(path + '/' + file);
} else {
_fileList.push(path + '/' + file);
}
}
}
try {
readFile(path);
resolve(fileList) //读取文件夹是同步api
} catch (err) {
reject(err)
}
3.实现ftp的远程连接
需要依赖 node-ftp (npm install)
Client = require('ftp')
var ftp_client = new Client()
ftp_client.on('ready', function() {
console.log('ftp连接建立成功');
//成功的执行
cb(file_client)
});
ftp_client.on('error', function(err) {
console.log('ftp连接建立失败',error);
//失败的回调
cb(err)
return;
});
4.根据文件列表,远程创建(调用nodejs的api好像不能远程创建,他只能创建在该服务运行的电脑)
使用ftp-node 的api
参数: ftp_client ftp的实例
fileList 文件列表
serverPath 服务器端的相对目录
还需要参数就是文件列表
准备操作:通过文件列表,获得所有的文件夹路径;路径数组进行去重.
hash快速去重:
function uniqArray(arr) {
var hash = {},
len = arr.length,
result = [];
for (var i = 0; i < len; i++) {
if (!hash[arr[i]]) {
hash[arr[i]] = true;
result.push(arr[i]);
}
}
return result;
}
创建文件夹:
var pre = new Promise(function(resolve, reject) {
// 建立相应的文件夹
async.each(fileList, function(item, cb) {
var _path = serverPath + "/" + item;
ftp_client.mkdir(_path, true, function(err) { //参数 true 表示已有文件夹不报错
if (err) {
console.log('创建文件夹%s出错%s', _path, err)
reject(err)
} else {
++file_num;
if (file_len == file_num) { //这一步很重要!!! 分别是文件的总数量,和当前数量
resolve()
}
}
});
})
})
5.上传文件
参数: ftp_client ftp的实例
fileList 文件列表
serverPath 服务器端的相对目录
上传:
var pre = new Promise(function(resolve, reject) {
async.each(fileList, function(item, cb) {
ftp_client.put(item, serverPath + "/" + item, function(err) {
if (err) {
console.log('上传文件%s出错:%s', item, err)
reject(err)
} else {
++file_num;
if (file_len == file_num) { //同上,很重要
resolve(ftp_client)
}
}
})
})
})
6.回滚实现
很抱歉,暂时没有找到copy到指定文件夹的快捷方法
暂时使用的是重命名,感觉挺快的。
参数:
/**
* file_client
* from 备份文件夹名称
* to 项目文件夹名称
*/
1.调用方法建立连接
2.参照文件夹操作
配合的文件夹操作:
依赖 fs 系统
1.删除本地文件夹
function deleteFloderLocal(path) {
var files = [];
if (fs.existsSync(path)) {
files = fs.readdirSync(path)
files.forEach(function(file, index) {
var curPath = path + "/" + file;
if (fs.statSync(curPath).isDirectory()) {
deleteFloderLocal(curPath);
} else {
fs.unlinkSync(curPath);
}
});
fs.rmdirSync(path);
}
}
//判断本地文件夹是否存在
function fsExistsSync(path) {
try {
fs.accessSync(path, fs.F_OK);
} catch (e) {
console.log(e)
return false;
}
return true;
}
2.删除服务器端文件夹
function deleteFloderServer(path, ftp_client) {
var pre = new Promise(function(resolve, reject) {
ftp_client.rmdir(path, true, function(err) {
if (err) {
console.log('删除目录失败:', err);
};
resolve();
console.log('删除目录' + path + "成功!")
})
})
return pre
}
说明: 1.rmdir true 参数说明文件夹非空时仍可以移除
2.所有的不要含有中文,我再操作的时候含有中文报错了
3.备份文件夹
说白了就是重命名,很显然只能保留一个,此处的备份是考虑到上传文件可能破坏,以后回滚
远程的,不要跟我讲node api
function copyFloderServer(path,lab, ftp_client) {
console.log('备份文件开始...')
ftp_client.rename(path, path + lab, function(err) {
if (err) {
console.log('备份目录失败:', err);
return
};
console.log('文件备份/恢复完成!')
})
}
说明:这个是临时接手node的一个需求,从0到0.1的新手还是有很多事情要做,很多地方考虑不周,敬请指出,感谢。