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 = '
    \ +

    \ + 使用SFTP:\ 是否使用SFTP进行数据传输 \ +

    \ +

    \ + Host:\ + *服务器地址,FTP默认端口21, SFTP默认端口22\ +

    \ +

    \ + 用户名:\ + *指定用户名\ +

    \ +

    \ + 密码:\ + *登录密码\ +

    \ +

    \ + 存储位置:\ + *相对于根目录的路径,默认是/backup\ +

    \ +
    '; + 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: "
    \ + content: "
    \
    安装版本:" + option + "
    \
    ", success:function(){