2
0
Эх сурвалжийг харах

luci-app-ssr-plus: Add backup and restore and reset functions

*** 增加备份、恢复和重置功能后,在最新的数据库和较多节点情况下,不需再重新获取订阅,并对软件重新设置以及重新进行数据库的更新。
zxlhhyccc 9 сар өмнө
parent
commit
0c3c269f0b

+ 32 - 3
luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua

@@ -21,11 +21,15 @@ function index()
 	entry({"admin", "services", "shadowsocksr", "subscribe"}, call("subscribe"))
 	entry({"admin", "services", "shadowsocksr", "checkport"}, call("check_port"))
 	entry({"admin", "services", "shadowsocksr", "log"}, form("shadowsocksr/log"), _("Log"), 80).leaf = true
+	entry({"admin", "services", "shadowsocksr", "get_log"}, call("get_log")).leaf = true
+	entry({"admin", "services", "shadowsocksr", "clear_log"}, call("clear_log")).leaf = true
 	entry({"admin", "services", "shadowsocksr", "run"}, call("act_status"))
 	entry({"admin", "services", "shadowsocksr", "ping"}, call("act_ping"))
 	entry({"admin", "services", "shadowsocksr", "reset"}, call("act_reset"))
 	entry({"admin", "services", "shadowsocksr", "restart"}, call("act_restart"))
 	entry({"admin", "services", "shadowsocksr", "delete"}, call("act_delete"))
+	--[[Backup]]
+	entry({"admin", "services", "shadowsocksr", "backup"}, call("create_backup")).leaf = true
 end
 
 function subscribe()
@@ -107,9 +111,9 @@ function check_port()
 		ret = socket:connect(s.server, s.server_port)
 		if tostring(ret) == "true" then
 			socket:close()
-			retstring = retstring .. "<font color = 'green'>[" .. server_name .. "] OK.</font><br />"
+			retstring .. "<font><b style='color:green'>[" .. server_name .. "] OK.</b></font><br />"
 		else
-			retstring = retstring .. "<font color = 'red'>[" .. server_name .. "] Error.</font><br />"
+			retstring = retstring .. "<font><b style='color:red'>[" .. server_name .. "] Error.</b></font><br />"
 		end
 		if iret == 0 then
 			luci.sys.call("ipset del ss_spec_wan_ac " .. s.server)
@@ -120,7 +124,7 @@ function check_port()
 end
 
 function act_reset()
-	luci.sys.call("/etc/init.d/shadowsocksr reset &")
+	luci.sys.call("/etc/init.d/shadowsocksr reset >/dev/null 2>&1")
 	luci.http.redirect(luci.dispatcher.build_url("admin", "services", "shadowsocksr"))
 end
 
@@ -133,3 +137,28 @@ function act_delete()
 	luci.sys.call("/etc/init.d/shadowsocksr restart &")
 	luci.http.redirect(luci.dispatcher.build_url("admin", "services", "shadowsocksr", "servers"))
 end
+
+function get_log()
+	luci.http.write(luci.sys.exec("[ -f '/var/log/ssrplus.log' ] && cat /var/log/ssrplus.log"))
+end
+	
+function clear_log()
+	luci.sys.call("echo '' > /var/log/ssrplus.log")
+end
+
+function create_backup()
+	local backup_files = {
+		"/etc/config/shadowsocksr",
+		"/etc/ssrplus/*"
+	}
+	local date = os.date("%Y%m%d")
+	local tar_file = "/tmp/shadowsocksr-" .. date .. "-backup.tar.gz"
+	nixio.fs.remove(tar_file)
+	local cmd = "tar -czf " .. tar_file .. " " .. table.concat(backup_files, " ")
+	luci.sys.call(cmd)
+	luci.http.header("Content-Disposition", "attachment; filename=shadowsocksr-" .. date .. "-backup.tar.gz")
+	luci.http.header("X-Backup-Filename", "shadowsocksr-" .. date .. "-backup.tar.gz")
+	luci.http.prepare_content("application/octet-stream")
+	luci.http.write(nixio.fs.readfile(tar_file))
+	nixio.fs.remove(tar_file)
+end

+ 96 - 14
luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/log.lua

@@ -1,20 +1,102 @@
 require "luci.util"
 require "nixio.fs"
+require "luci.sys"
+require "luci.http"
+
 f = SimpleForm("logview")
 f.reset = false
 f.submit = false
-t = f:field(TextValue, "conf")
-t.rmempty = true
-t.rows = 20
-function t.cfgvalue()
-	if nixio.fs.access("/var/log/ssrplus.log") then
-		local logs = luci.util.execi("cat /var/log/ssrplus.log")
-		local s = ""
-		for line in logs do
-			s = line .. "\n" .. s
-		end
-		return s
-	end
+f:append(Template("shadowsocksr/log"))
+
+-- 自定义 log 函数
+function log(...)
+    local result = os.date("%Y-%m-%d %H:%M:%S: ") .. table.concat({...}, " ")
+    local f, err = io.open("/var/log/ssrplus.log", "a")
+    if f and err == nil then
+        f:write(result .. "\n")
+        f:close()
+    end
 end
-t.readonly = "readonly"
-return f
+
+-- 创建备份与恢复表单
+fb = SimpleForm('backup-restore')
+fb.reset = false
+fb.submit = false
+s = fb:section(SimpleSection, translate("Backup and Restore"), translate("Backup or Restore Client and Server Configurations.") ..
+                            "<br><font style='color:red'><b>" ..
+                            translate("Note: Restoring configurations across different versions may cause compatibility issues.") ..
+                            "</b></font>")
+o = s:option(DummyValue, '', nil)
+o.template = "shadowsocksr/backup_restore"
+
+-- 定义备份目标文件和目录
+local backup_targets = {
+    files = {
+        "/etc/config/shadowsocksr"
+    },
+    dirs = {
+        "/etc/ssrplus"
+    }
+}
+
+local file_path = '/tmp/shadowsocksr_upload.tar.gz'
+local temp_dir = '/tmp/shadowsocksr_bak'
+local fd
+
+-- 处理文件上传
+luci.http.setfilehandler(function(meta, chunk, eof)
+    if not fd and meta and meta.name == "ulfile" and chunk then
+        -- 初始化上传处理
+        luci.sys.call("rm -rf " .. temp_dir)
+        nixio.fs.remove(file_path)
+        fd = nixio.open(file_path, "w")
+        luci.sys.call("echo '' > /var/log/ssrplus.log")
+    end
+
+    if fd and chunk then
+        fd:write(chunk)
+    end
+
+    if eof and fd then
+        fd:close()
+        fd = nil
+        if nixio.fs.access(file_path) then
+            log(" * shadowsocksr 配置文件上传成功…")  -- 使用自定义的 log 函数
+            luci.sys.call("mkdir -p " .. temp_dir)
+
+            if luci.sys.call("tar -xzf " .. file_path .. " -C " .. temp_dir) == 0 then
+                -- 处理文件还原
+                for _, target in ipairs(backup_targets.files) do
+                    local temp_file = temp_dir .. target
+                    if nixio.fs.access(temp_file) then
+                        luci.sys.call(string.format("cp -f '%s' '%s'", temp_file, target))
+                        log(" * 文件 " .. target .. " 还原成功…")  -- 使用自定义的 log 函数
+                    end
+                end
+
+                -- 处理目录还原
+                for _, target in ipairs(backup_targets.dirs) do
+                    local temp_dir_path = temp_dir .. target
+                    if nixio.fs.access(temp_dir_path) then
+                        luci.sys.call(string.format("cp -rf '%s'/* '%s/'", temp_dir_path, target))
+                        log(" * 目录 " .. target .. " 还原成功…")  -- 使用自定义的 log 函数
+                    end
+                end
+
+                log(" * shadowsocksr 配置还原成功…")  -- 使用自定义的 log 函数
+                log(" * 重启 shadowsocksr 服务中…\n")  -- 使用自定义的 log 函数
+                luci.sys.call('/etc/init.d/shadowsocksr restart > /dev/null 2>&1 &')
+            else
+                log(" * shadowsocksr 配置文件解压失败,请重试!")  -- 使用自定义的 log 函数
+            end
+        else
+            log(" * shadowsocksr 配置文件上传失败,请重试!")  -- 使用自定义的 log 函数
+        end
+
+        -- 清理临时文件
+        luci.sys.call("rm -rf " .. temp_dir)
+        nixio.fs.remove(file_path)
+    end
+end)
+
+return f, fb

+ 154 - 0
luci-app-ssr-plus/luasrc/view/shadowsocksr/backup_restore.htm

@@ -0,0 +1,154 @@
+<%+cbi/valueheader%>
+<div class="cbi-value" id="_backup_div">
+    <label class="cbi-value-title"><%:Create Backup File%></label>
+    <div class="cbi-value-field">
+        <input type="button" class="btn cbi-button cbi-input-apply" onclick="dl_backup()" value="<%:DL Backup%>" />
+    </div>
+</div>
+
+<div class="cbi-value" id="_upload_div">
+	<label class="cbi-value-title"><%:Restore Backup File%></label>
+	<div class="cbi-value-field">
+		<input type="button" class="btn cbi-button cbi-input-apply" id="upload-btn" value="<%:RST Backup%>" />
+	</div>
+</div>
+
+<div class="cbi-value" id="_reset_div">
+    <label class="cbi-value-title"><%:Restore to default configuration%></label>
+    <div class="cbi-value-field">
+        <input type="button" class="btn cbi-button cbi-button-remove" onclick="do_reset()" value="<%:Do Reset%>" />
+    </div>
+</div>
+
+<div id="upload-modal" class="up-modal" style="display:none;">
+	<div class="up-modal-content">
+		<h3><%:Restore Backup File%></h3>
+		<div class="cbi-value" id="_upload_div">
+			<div class="up-cbi-value-field">
+				<input class="cbi-input-file" type="file" id="ulfile" name="ulfile" accept=".tar.gz" required />
+				<br />
+				<div class="up-button-container">
+					<input type="submit" class="btn cbi-button cbi-input-apply" value="<%:UL Restore%>" />
+					<button class="btn cbi-button cbi-button-remove" id="upload-close"><%:CLOSE WIN%></button>
+				</div>
+			</div>
+		</div>
+	</div>
+</div>
+
+<style>
+.up-modal {
+	position: fixed;
+	left: 50%;
+	top: 50%;
+	transform: translate(-50%, -50%);
+	background: white;
+	padding: 20px;
+	border: 2px solid #ccc;
+	box-shadow: 0 0 10px rgba(0,0,0,0.5);
+	z-index: 1000;
+}
+
+.up-modal-content {
+	width: 100%;
+	max-width: 400px;
+	text-align: center;
+	display: flex;
+	flex-direction: column;
+	justify-content: center;
+	align-items: center;
+}
+
+.up-button-container {
+	display: flex;
+	justify-content: space-between;
+	width: 100%;
+	max-width: 250px;
+}
+
+.up-cbi-value-field {
+	display: flex;
+	flex-direction: column;
+	align-items: center;
+	width: 100%;
+}
+</style>
+
+<script>
+    // JavaScript 版本的 url 函数
+    function url(...args) {
+        let url = "/cgi-bin/luci/admin/services/shadowsocksr";
+        for (let i = 0; i < args.length; i++) {
+            if (args[i] !== "") {
+                url += "/" + args[i];
+            }
+        }
+        return url;
+    }
+
+    // 上传按钮点击事件
+    document.getElementById("upload-btn").addEventListener("click", function() {
+        document.getElementById("upload-modal").style.display = "block";
+    });
+
+    // 关闭上传模态框
+    document.getElementById("upload-close").addEventListener("click", function() {
+        document.getElementById("upload-modal").style.display = "none";
+    });
+
+    // 备份下载函数
+    function dl_backup(btn) {
+        fetch(url("backup"), {  // 使用 JavaScript 版本的 url 函数
+            method: 'POST',
+            credentials: 'same-origin'
+        })
+        .then(response => {
+            if (!response.ok) {
+                throw new Error("备份失败!");
+            }
+            const filename = response.headers.get("X-Backup-Filename");
+            if (!filename) {
+                return;
+            }
+            return response.blob().then(blob => ({ blob, filename }));
+        })
+        .then(result => {
+            if (!result) return;
+            const { blob, filename } = result;
+            const url = window.URL.createObjectURL(blob);
+            const a = document.createElement("a");
+            a.href = url;
+            a.download = filename;
+            document.body.appendChild(a);
+            a.click();
+            a.remove();
+            window.URL.revokeObjectURL(url);
+        })
+        .catch(error => alert(error.message));
+    }
+
+    // 恢复出厂设置
+    function do_reset(btn) {
+        if (confirm("<%: Do you want to restore the client to default settings?%>")) {
+            setTimeout(function () {
+                if (confirm("<%: Are you sure you want to restore the client to default settings?%>")) {
+                    // 清理日志
+                    var xhr1 = new XMLHttpRequest();
+                    xhr1.open("GET", url("clear_log"), true);  // 使用 JavaScript 版本的 url 函数
+                    xhr1.send();
+                    // 恢复出厂
+                    var xhr2 = new XMLHttpRequest();
+                    xhr2.open("GET", url("reset"), true);  // 使用 JavaScript 版本的 url 函数
+                    xhr2.send();
+                    // 处理响应
+                    xhr2.onload = function() {
+                        if (xhr2.status === 200) {
+                            window.location.href = url("reset");
+                        }
+                    };
+                }
+            }, 1000);
+        }
+    }
+</script>
+<%+cbi/valuefooter%>

+ 37 - 0
luci-app-ssr-plus/luasrc/view/shadowsocksr/log.htm

@@ -0,0 +1,37 @@
+<%
+local dsp = require "luci.dispatcher"
+-%>
+<script type="text/javascript">
+	//<![CDATA[
+	function clearlog(btn) {
+		XHR.get('<%=dsp.build_url("admin/services/shadowsocksr/clear_log")%>', null,
+			function(x, data) {
+				if (x && x.status == 200) {
+					var log_textarea = document.getElementById('log_textarea');
+					log_textarea.innerHTML = "";
+					log_textarea.scrollTop = log_textarea.scrollHeight;
+				}
+			}
+		);
+	}
+
+	XHR.poll(5, '<%=dsp.build_url("admin/services/shadowsocksr/get_log")%>', null,
+		function(x, data) {
+			if (x && x.status == 200) {
+				var log_textarea = document.getElementById('log_textarea');
+				// 将日志分行处理,移除最后一行空行但保留中间空行
+				var logs = x.responseText.split("\n");
+				if (logs[logs.length - 1].trim() === "") {
+					logs.pop(); // 删除最后的空行
+				}
+				logs = logs.reverse().join("\n"); // 倒序排列
+				log_textarea.innerHTML = logs;
+			}
+		}
+	);
+	//]]>
+</script>
+<fieldset class="cbi-section" id="_log_fieldset">
+	<input class="btn cbi-button cbi-button-remove" type="button" onclick="clearlog()" value="<%:Clear logs%>" />
+	<textarea id="log_textarea" class="cbi-input-textarea" style="width: 100%;margin-top: 10px;" data-update="change" rows="20" wrap="off" readonly="readonly"></textarea>
+</fieldset>

+ 42 - 0
luci-app-ssr-plus/po/zh_Hans/ssr-plus.po

@@ -1168,3 +1168,45 @@ msgstr "socks5 服务器可以从外部接收的最大数据包大小(单位
 
 msgid "Disable ChinaDNS-NG"
 msgstr "直通模式(禁用 ChinaDNS-NG)"
+
+msgid "Clear logs"
+msgstr "清空日志"
+
+msgid "Backup and Restore"
+msgstr "备份还原"
+
+msgid "Backup or Restore Client and Server Configurations."
+msgstr "备份或还原客户端及服务端配置。"
+
+msgid "Note: Restoring configurations across different versions may cause compatibility issues."
+msgstr "注意:不同版本间的配置恢复可能会导致兼容性问题。"
+
+msgid "Create Backup File"
+msgstr "创建备份文件"
+
+msgid "Restore Backup File"
+msgstr "恢复备份文件"
+
+msgid "DL Backup"
+msgstr "下载备份"
+
+msgid "RST Backup"
+msgstr "恢复备份"
+
+msgid "UL Restore"
+msgstr "上传恢复"
+
+msgid "CLOSE WIN"
+msgstr "关闭窗口"
+
+msgid "Restore to default configuration"
+msgstr "恢复默认配置"
+
+msgid "Do Reset"
+msgstr "执行重置"
+
+msgid "Do you want to restore the client to default settings?"
+msgstr "是否要恢复客户端默认配置?"
+
+msgid "Are you sure you want to restore the client to default settings?"
+msgstr "是否真的要恢复客户端默认配置?"