diff --git a/plugins/backup_ftp/class/ftp_client.py b/plugins/backup_ftp/class/ftp_client.py
new file mode 100644
index 000000000..896e13364
--- /dev/null
+++ b/plugins/backup_ftp/class/ftp_client.py
@@ -0,0 +1,208 @@
+# coding:utf-8
+
+'''
+doc: https://docs.python.org/zh-cn/3/library/ftplib.html
+'''
+
+import sys
+import io
+import os
+import time
+import re
+import json
+
+import paramiko
+import ftplib
+
+sys.path.append(os.getcwd() + "/class/core")
+import mw
+
+DEBUG = True
+
+"""
+=============自定义异常===================
+"""
+
+
+class OsError(Exception):
+ """OS端异常"""
+
+
+class ObjectNotFound(OsError):
+ """对象不存在时抛出的异常"""
+
+ def __init__(self, *args, **kwargs):
+ message = "文件对象不存在。"
+ super(ObjectNotFound, self).__init__(message, *args, **kwargs)
+
+
+class APIError(Exception):
+ """API参数错误异常"""
+
+ def __init__(self, *args, **kwargs):
+ _api_error_msg = 'API资料校验失败,请核实!'
+ super(APIError, self).__init__(_api_error_msg, *args, **kwargs)
+
+
+class FtpPSClient:
+ _title = "FTP"
+ _name = "ftp"
+ __host = None
+ __port = None
+ __user = None
+ __password = None
+ default_port = 21
+ default_backup_path = "/backup/"
+ config_file = "cfg.json"
+
+ def __init__(self, load_config=True, timeout=10):
+ self.timeout = timeout
+ if load_config:
+ data = self.get_config()
+ self.injection_config(data)
+
+ def get_config(self):
+ default_config = {
+ "ftp_host": '',
+ "ftp_user": '',
+ "ftp_pass": '',
+ "backup_path": self.default_backup_path
+ }
+
+ cfg = mw.getServerDir() + "/backup_ftp/" + self.config_file
+ if os.path.exists(cfg):
+ data = mw.readFile(cfg)
+ return json.loads(data)
+ else:
+ return default_config
+
+ def injection_config(self, data):
+ host = data["ftp_host"].strip()
+ if host.find(':') == -1:
+ self.__port = self.default_port
+
+ self.__host = data['ftp_host'].strip()
+ self.__user = data['ftp_user'].strip()
+ self.__password = data['ftp_pass'].strip()
+ bp = data['backup_path'].strip()
+ if bp:
+ self.backup_path = self.getPath(bp)
+ else:
+ self.backup_path = self.getPath(self.default_backup_path)
+
+ def authorize(self):
+ try:
+ if self.timeout is not None:
+ ftp = ftplib.FTP(timeout=self.timeout)
+ else:
+ ftp = ftplib.FTP()
+
+ debuglevel = 0
+ # if DEBUG:
+ # debuglevel = 3
+ ftp.set_debuglevel(debuglevel)
+ # ftp.set_pasv(True)
+ ftp.connect(self.__host, int(self.__port))
+ ftp.login(self.__user, self.__password)
+ return ftp
+ except Exception as e:
+ raise OsError("无法连接FTP客户端,请检查配置参数是否正确。")
+
+ # 取目录路径
+ def getPath(self, path):
+ if path[-1:] != '/':
+ path += '/'
+ if path[:1] != '/':
+ path = '/' + path
+ return path.replace('//', '/')
+
+ def generateDownloadUrl(self, object_name):
+
+ return 'ftp://' + \
+ self.__user + ':' + \
+ self.__password + '@' + \
+ self.__host + ':' + \
+ "/" + object_name
+
+ def createDir(self, path, name):
+ ftp = self.authorize()
+ path = self.getPath(path)
+ ftp.cwd(path)
+ try:
+ ftp.mkd(name)
+ ftp.close()
+ return True
+ except Exception as e:
+ ftp.close()
+ return False
+
+ def deleteDir(self, path, dir_name):
+ try:
+ ftp = self.authorize()
+ ftp.rmd(dir_name)
+ return True
+ except ftplib.error_perm as e:
+ print(str(e) + ":" + dir_name)
+ except Exception as e:
+ print(e)
+ return False
+
+ def deleteFile(self, filename):
+ try:
+ ftp = self.authorize()
+ ftp.delete(filename)
+ return True
+ except Exception as e:
+ return False
+
+ def getList(self, path="/"):
+ ftp = self.authorize()
+ path = self.getPath(path)
+ ftp.cwd(path)
+ mlsd = False
+ try:
+ files = list(ftp.mlsd())
+ files = files[1:]
+ mlsd = True
+ except:
+ try:
+ files = ftp.nlst(path)
+ mlsd = False
+ except:
+ raise RuntimeError("ftp服务器数据返回异常。")
+
+ ftp.close()
+ f_list = []
+ dirs = []
+ data = []
+ default_time = '1971/01/01 01:01:01'
+ for dt in files:
+ # print(dt)
+ if mlsd:
+ dt_name = dt[0]
+ dt_info = dt[1]
+ else:
+ if dt.find("/") >= 0:
+ dt = dt.split("/")[-1]
+ tmp = {}
+ tmp['name'] = dt_name
+ if dt_name == '.' or dt_name == '..':
+ continue
+
+ tmp['time'] = dt_info['modify']
+ try:
+ tmp['size'] = dt_info['size']
+ tmp['type'] = "File"
+ tmp['download'] = self.generateDownloadUrl(path + dt_name)
+ f_list.append(tmp)
+ except:
+ tmp['size'] = dt_info['sizd']
+ tmp['type'] = None
+ tmp['download'] = ''
+ dirs.append(tmp)
+ data = dirs + f_list
+
+ mlist = {}
+ mlist['path'] = path
+ mlist['list'] = data
+ return mlist
diff --git a/plugins/backup_ftp/ico.png b/plugins/backup_ftp/ico.png
new file mode 100644
index 000000000..2f7bc6873
Binary files /dev/null and b/plugins/backup_ftp/ico.png differ
diff --git a/plugins/backup_ftp/index.html b/plugins/backup_ftp/index.html
new file mode 100755
index 000000000..ef4ac28b4
--- /dev/null
+++ b/plugins/backup_ftp/index.html
@@ -0,0 +1,104 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/plugins/backup_ftp/index.py b/plugins/backup_ftp/index.py
new file mode 100755
index 000000000..44f6da948
--- /dev/null
+++ b/plugins/backup_ftp/index.py
@@ -0,0 +1,198 @@
+# coding:utf-8
+
+import sys
+import io
+import os
+import time
+import re
+import json
+
+sys.path.append(os.getcwd() + "/class/core")
+import mw
+
+_ver = sys.version_info
+is_py2 = (_ver[0] == 2)
+is_py3 = (_ver[0] == 3)
+
+DEBUG = False
+
+if is_py2:
+ reload(sys)
+ sys.setdefaultencoding('utf-8')
+
+app_debug = False
+if mw.isAppleSystem():
+ app_debug = True
+
+
+def getPluginName():
+ return 'backup_ftp'
+
+
+def getPluginDir():
+ return mw.getPluginDir() + '/' + getPluginName()
+
+
+sys.path.append(getPluginDir() + "/class")
+from ftp_client import FtpPSClient
+
+
+def getServerDir():
+ return mw.getServerDir() + '/' + getPluginName()
+
+
+def getArgs():
+ args = sys.argv[2:]
+ tmp = {}
+ args_len = len(args)
+
+ if args_len == 1:
+ t = args[0].strip('{').strip('}')
+ t = t.split(':')
+ tmp[t[0]] = t[1]
+ elif args_len > 1:
+ for i in range(len(args)):
+ t = args[i].split(':')
+ tmp[t[0]] = t[1]
+
+ return tmp
+
+
+def checkArgs(data, ck=[]):
+ for i in range(len(ck)):
+ if not ck[i] in data:
+ return (False, mw.returnJson(False, '参数:(' + ck[i] + ')没有!'))
+ return (True, mw.returnJson(True, 'ok'))
+
+
+def status():
+ return 'start'
+
+
+def getConf():
+ cfg = getServerDir() + "/cfg.json"
+ if not os.path.exists(cfg):
+ return mw.returnJson(False, "未配置", [])
+ data = mw.readFile(cfg)
+ data = json.loads(data)
+ return mw.returnJson(True, "OK", data)
+
+
+def setConf():
+ args = getArgs()
+ data = checkArgs(args, ['use_sftp', 'ftp_user',
+ 'ftp_pass', 'ftp_host', 'backup_path'])
+ if not data[0]:
+ return data[1]
+
+ cfg = getServerDir() + "/cfg.json"
+
+ values = ['ftp_user',
+ 'ftp_pass',
+ 'ftp_host']
+ for v in values:
+ if args[v] == '':
+ return mw.returnJson(False, '必填资料不能为空,请核实!', [])
+
+ if args['backup_path'] == '':
+ args['backup_path'] = "/backup"
+
+ try:
+ ftp = FtpPSClient(load_config=False)
+ ftp.injection_config(args)
+ data = ftp.getList()
+ if data:
+ mw.writeFile(cfg, mw.getJson(args))
+ return mw.returnJson(True, '设置成功', [])
+ except Exception as e:
+ # print(str(e))
+ pass
+
+ return mw.returnJson(False, 'FTP校验失败,请核实!', [])
+
+
+def getList():
+ cfg = getServerDir() + "/cfg.json"
+ if not os.path.exists(cfg):
+ return mw.returnJson(False, "未配置FTP,请点击`账户设置`", [])
+
+ args = getArgs()
+ data = checkArgs(args, ['path'])
+ if not data[0]:
+ return data[1]
+
+ ftp = FtpPSClient()
+ flist = ftp.getList(args['path'])
+ return mw.returnJson(True, "ok", flist)
+
+
+def createDir():
+ args = getArgs()
+ data = checkArgs(args, ['path', 'name'])
+ if not data[0]:
+ return data[1]
+
+ ftp = FtpPSClient()
+ isok = ftp.createDir(args['path'], args['name'])
+ if isok:
+ return mw.returnJson(True, "创建成功")
+ return mw.returnJson(False, "创建失败")
+
+
+def deleteDir():
+ args = getArgs()
+ data = checkArgs(args, ['dir_name', 'path'])
+ if not data[0]:
+ return data[1]
+
+ ftp = FtpPSClient()
+ isok = ftp.deleteDir(args['path'], args['dir_name'])
+ if isok:
+ return mw.returnJson(True, "删除成功")
+ return mw.returnJson(False, "删除失败")
+
+
+def deleteFile():
+ args = getArgs()
+ data = checkArgs(args, ['path', 'filename'])
+ if not data[0]:
+ return data[1]
+
+ ftp = FtpPSClient()
+ isok = ftp.deleteFile(args['filename'])
+ if isok:
+ return mw.returnJson(True, "删除成功")
+ return mw.returnJson(False, "删除失败")
+
+
+def installPreInspection():
+ return 'ok'
+
+if __name__ == "__main__":
+ func = sys.argv[1]
+ if func == 'status':
+ print(status())
+ elif func == 'start':
+ print(start())
+ elif func == 'stop':
+ print(stop())
+ elif func == 'restart':
+ print(restart())
+ elif func == 'reload':
+ print(reload())
+ elif func == 'install_pre_inspection':
+ print(installPreInspection())
+ elif func == 'conf':
+ print(getConf())
+ elif func == 'set_config':
+ print(setConf())
+ elif func == "get_list":
+ print(getList())
+ elif func == "create_dir":
+ print(createDir())
+ elif func == "delete_dir":
+ print(deleteDir())
+ elif func == 'delete_file':
+ print(deleteFile())
+ else:
+ print('error')
diff --git a/plugins/backup_ftp/info.json b/plugins/backup_ftp/info.json
new file mode 100755
index 000000000..a899fb0a7
--- /dev/null
+++ b/plugins/backup_ftp/info.json
@@ -0,0 +1,17 @@
+{
+ "title":"FTP存储空间",
+ "hook":["backup"],
+ "tip":"soft",
+ "name":"backup_ftp",
+ "type":"运行环境",
+ "ps":"将网站或数据库打包备份到FTP存储空间",
+ "versions":["1.0"],
+ "install_pre_inspection":false,
+ "shell":"install.sh",
+ "checks":"server/backup_ftp",
+ "path": "server/backup_ftp",
+ "author":"midoks",
+ "home":"",
+ "date":"2022-10-23",
+ "pid": "4"
+}
\ No newline at end of file
diff --git a/plugins/backup_ftp/install.sh b/plugins/backup_ftp/install.sh
new file mode 100755
index 000000000..97683fce1
--- /dev/null
+++ b/plugins/backup_ftp/install.sh
@@ -0,0 +1,32 @@
+#!/bin/bash
+PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
+export PATH
+
+curPath=`pwd`
+rootPath=$(dirname "$curPath")
+rootPath=$(dirname "$rootPath")
+serverPath=$(dirname "$rootPath")
+
+install_tmp=${rootPath}/tmp/mw_install.pl
+sys_os=`uname`
+
+
+Install_App()
+{
+ mkdir -p ${serverPath}/backup_ftp
+ echo "${1}" > ${serverPath}/backup_ftp/version.pl
+ echo '安装完成' > $install_tmp
+
+}
+
+Uninstall_App()
+{
+ rm -rf ${serverPath}/bk_demo
+}
+
+action=$1
+if [ "${1}" == 'install' ];then
+ Install_App $2
+else
+ Uninstall_App $2
+fi
diff --git a/plugins/backup_ftp/js/backup_ftp.js b/plugins/backup_ftp/js/backup_ftp.js
new file mode 100755
index 000000000..ca63a749d
--- /dev/null
+++ b/plugins/backup_ftp/js/backup_ftp.js
@@ -0,0 +1,250 @@
+
+
+function bkfPost(method,args,callback){
+ var _args = null;
+ if (typeof(args) == 'string'){
+ _args = JSON.stringify(toArrayObject(args));
+ } else {
+ _args = JSON.stringify(args);
+ }
+
+ var loadT = layer.msg('正在获取...', { icon: 16, time: 0, shade: 0.3 });
+ $.post('/plugins/run', {name:'backup_ftp', func:method, args:_args}, function(data) {
+ layer.close(loadT);
+ if (!data.status){
+ layer.msg(data.msg,{icon:0,time:2000,shade: [0.3, '#000']});
+ return;
+ }
+
+ if(typeof(callback) == 'function'){
+ callback(data);
+ }
+ },'json');
+}
+
+// 自定义部分
+var i = null;
+//设置API
+function upyunApi(){
+
+ bkfPost('conf', {}, function(rdata){
+ var rdata = $.parseJSON(rdata.data);
+ var token = rdata.data;
+ var check_status = token.use_sftp;
+ var sftp_checked = check_status === "true" ? " checked=\"checked\"" : "";
+
+ if (typeof(token.ftp_host) == 'undefined'){
+ token.ftp_host = '';
+ }
+
+ if (typeof(token.ftp_user) == 'undefined'){
+ token.ftp_user = '';
+ }
+
+ if (typeof(token.ftp_pass) == 'undefined'){
+ token.ftp_pass = '';
+ }
+
+ if (typeof(token.backup_path) == 'undefined'){
+ token.backup_path = '';
+ }
+
+ var apicon = '';
+ layer.open({
+ type: 1,
+ area: "600px",
+ title: "FTP/SFTP帐户设置",
+ closeBtn: 1,
+ shift: 5,
+ shadeClose: false,
+ btn: ['确定','取消'],
+ content:apicon,
+ yes:function(index,layero){
+ var data = {
+ use_sftp:$("input[name='use_sftp']").prop('checked'),
+ ftp_user:$("input[name='ftp_username']").val(),
+ ftp_pass:$("input[name='ftp_password']").val(),
+ ftp_host:$("input[name='upyun_service']").val(),
+ backup_path:$("input[name='backup_path']").val()
+ }
+ bkfPost('set_config', data, function(rdata){
+ var rdata = $.parseJSON(rdata.data);
+ if (rdata.status){
+ showMsg(rdata.msg,function(){
+ layer.close(index);
+ osList("/");
+ },{icon:1},2000);
+ } else{
+ layer.msg(rdata.msg,{icon:2});
+ }
+ })
+ },
+ });
+ });
+}
+
+function createDir(){
+ layer.open({
+ type: 1,
+ area: "400px",
+ title: "创建目录",
+ closeBtn: 1,
+ shift: 5,
+ shadeClose: false,
+ btn: ['确定','取消'],
+ content:'',
+ success:function(){
+ $("input[name='newPath']").focus().keyup(function(e){
+ if(e.keyCode == 13) $(".layui-layer-btn0").click();
+ });
+ },
+ yes:function(index,layero){
+ var name = $("input[name='newPath']").val();
+ if(name == ''){
+ layer.msg('目录名称不能为空!',{icon:2});
+ return;
+ }
+ var path = $("#myPath").val();
+ var dirname = name;
+ // var loadT = layer.msg('正在创建目录['+dirname+']...',{icon:16,time:0,shade: [0.3, '#000']});
+ bkfPost('create_dir', {path:path,name:dirname}, function(data){
+ var rdata = $.parseJSON(data.data);
+ if(rdata.status) {
+ showMsg(rdata.msg, function(){
+ layer.close(index);
+ osList(path);
+ } ,{icon:1}, 2000);
+ } else{
+ layer.msg(rdata.msg,{icon:2});
+ }
+ });
+ }
+ });
+}
+
+//删除文件
+function deleteFile(name, is_dir){
+ if (is_dir === false){
+ safeMessage('删除文件','删除后将无法恢复,真的要删除['+name+']吗?',function(){
+ var path = $("#myPath").val();
+ var filename = name;
+ bkfPost('delete_file', {filename:filename,path:path}, function(rdata){
+ var rdata = $.parseJSON(rdata.data);
+ showMsg(rdata.msg,function(){
+ osList(path);
+ },{icon:rdata.status?1:2},2000);
+ });
+ });
+ } else {
+ safeMessage('删除文件夹','删除后将无法恢复,真的要删除['+name+']吗?',function(){
+ var path = $("#myPath").val();
+ bkfPost('delete_dir', {dir_name:name,path:path}, function(rdata){
+ var rdata = $.parseJSON(rdata.data);
+ showMsg(rdata.msg,function(){
+ osList(path);
+ },{icon:rdata.status?1:2},2000);
+ });
+ });
+ }
+}
+
+function osList(path){
+ bkfPost('get_list', {path:path}, function(rdata){
+
+ var rdata = $.parseJSON(rdata.data);
+ if(rdata.status === false){
+ upyunApi();
+ return;
+ }
+
+ var mlist = rdata.data;
+ // console.log(mlist);
+ var listBody = ''
+ var listFiles = ''
+ for(var i=0;i\'+mlist.list[i].name+' | \
+ - | \
+ - | \
+ 删除 | '
+ }else{
+ listFiles += '\'+mlist.list[i].name+' | \
+ '+toSize(mlist.list[i].size)+' | \
+ '+getLocalTime(mlist.list[i].time)+' | \
+ 下载 | 删除 |
'
+ }
+ }
+ listBody += listFiles;
+
+ var pathLi='';
+ var tmp = path.split('/')
+ var pathname = '';
+ var n = 0;
+ for(var i=0;i 0 && tmp[i] == '') continue;
+ var dirname = tmp[i];
+ if(dirname == '') {
+ dirname = '根目录';
+ n++;
+ }
+ pathname += '/' + tmp[i];
+ pathname = pathname.replace('//','/');
+ pathLi += ''+dirname+'';
+ }
+ var um = 1;
+ if(tmp[tmp.length-1] == '') um = 2;
+ var backPath = tmp.slice(0,tmp.length-um).join('/') || '/';
+ $('#myPath').val(path);
+ $(".upyunCon .place-input ul").html(pathLi);
+ $(".upyunlist .list-list").html(listBody);
+
+ upPathLeft();
+
+ $('#backBtn').click(function() {
+ osList(backPath);
+ });
+
+ $('.upyunCon .refreshBtn').click(function(){
+ osList(path);
+ });
+ });
+}
+
+//计算当前目录偏移
+function upPathLeft(){
+ var UlWidth = $(".place-input ul").width();
+ var SpanPathWidth = $(".place-input").width() - 20;
+ var Ml = UlWidth - SpanPathWidth;
+ if(UlWidth > SpanPathWidth ){
+ $(".place-input ul").css("left",-Ml)
+ }
+ else{
+ $(".place-input ul").css("left",0)
+ }
+}
+// $('.layui-layer-page').css('height','670px');
\ No newline at end of file
diff --git a/route/static/app/soft.js b/route/static/app/soft.js
index 0ace1a662..4b9b308d7 100755
--- a/route/static/app/soft.js
+++ b/route/static/app/soft.js
@@ -208,7 +208,7 @@ function addVersion(name, ver, type, obj, title, install_pre_inspection) {
closeBtn: 1,
shadeClose: true,
btn: ['提交','关闭'],
- content: "