浏览代码

luci-app-ssr-plus: Add subscribe link detection. Without update do not restart service.

zxlhhyccc 4 月之前
父节点
当前提交
ec6df9604a
共有 1 个文件被更改,包括 171 次插入74 次删除
  1. 171 74
      luci-app-ssr-plus/root/usr/share/shadowsocksr/subscribe.lua

+ 171 - 74
luci-app-ssr-plus/root/usr/share/shadowsocksr/subscribe.lua

@@ -683,20 +683,54 @@ local function processData(szType, content)
 	result.switch_enable = switch_enable
 	return result
 end
+
+-- 计算、储存和读取 md5 值
+-- 计算 md5 值
+local function md5_string(data)
+	-- 生成临时文件名
+	local tmp = "/tmp/md5_tmp_" .. os.time() .. "_" .. math.random(1000,9999) -- os.time 保证每秒唯一,但不足以避免全部冲突;math.random(1000,9999) 增加文件名唯一性,避免并发时冲突
+	nixio.fs.writefile(tmp, data) -- 写入临时文件
+	-- 执行 md5sum 命令
+	local md5 = luci.sys.exec(string.format('md5sum "%s" 2>/dev/null | cut -d " " -f1', tmp)):gsub("%s+", "")
+	nixio.fs.remove(tmp) -- 删除临时文件
+	return md5
+end
+
+-- 返回临时文件路径,用来存储订阅的 MD5 值,以便判断订阅内容是否发生变化。
+local function get_md5_path(groupHash)
+	return "/tmp/sub_md5_" .. groupHash
+end
+
+-- 读取上次订阅时记录的 MD5 值,以便和当前内容的 MD5 进行对比,从而判断是否需要更新节点列表。
+local function read_old_md5(groupHash)
+	local path = get_md5_path(groupHash)
+	if nixio.fs.access(path) then
+		return trim(nixio.fs.readfile(path) or "")
+	end
+	return ""
+end
+
+-- 将订阅分组最新内容的 MD5 值保存到对应的临时文件中,以便下次更新时进行对比。
+local function write_new_md5(groupHash, md5)
+	nixio.fs.writefile(get_md5_path(groupHash), md5)
+end
+
 -- curl
 local function curl(url)
-    -- 清理 URL 中的隐藏字符
-    url = url:gsub("%s+$", ""):gsub("^%s+", ""):gsub("%z", "")
-
-    -- 构建curl命令(确保 user_agent 为空时不添加 -A 参数)
-    local cmd = string.format(
-        'curl -sSL --connect-timeout 20 --max-time 30 --retry 3 %s --insecure --location "%s"',
-        user_agent ~= "" and ('-A "' .. user_agent .. '"') or "",  -- 添加 or "" 处理 nil 情况
-        url:gsub('["$`\\]', '\\%0')  -- 安全转义
-    )
-    
-    local stdout = luci.sys.exec(cmd)
-    return trim(stdout)
+	-- 清理 URL 中的隐藏字符
+	url = url:gsub("%s+$", ""):gsub("^%s+", ""):gsub("%z", "")
+
+	-- 构建curl命令(确保 user_agent 为空时不添加 -A 参数)
+	local cmd = string.format(
+		'curl -sSL --connect-timeout 20 --max-time 30 --retry 3 %s --insecure --location "%s"',
+		user_agent ~= "" and ('-A "' .. user_agent .. '"') or "",  -- 添加 or "" 处理 nil 情况
+		url:gsub('["$`\\]', '\\%0')  -- 安全转义
+	)
+
+	local stdout = luci.sys.exec(cmd)
+	stdout = trim(stdout)
+	local md5 = md5_string(stdout)
+	return stdout, md5
 end
 
 local function check_filer(result)
@@ -741,86 +775,149 @@ local function check_filer(result)
 	end
 end
 
+-- 加载订阅未变化的节点用于防止被误删
+local function loadOldNodes(groupHash)
+	local nodes = {}
+	cache[groupHash] = {}
+	nodeResult[#nodeResult + 1] = nodes
+	local index = #nodeResult
+
+	ucic:foreach(name, uciType, function(s)
+		if s.grouphashkey == groupHash and s.hashkey then
+			local section = setmetatable({}, {__index = s})
+			nodes[s.hashkey] = section
+			cache[groupHash][s.hashkey] = section
+		end
+	end)
+end
+
 local execute = function()
 	-- exec
 	do
-		if proxy == '0' then -- 不使用代理更新的话先暂停
-			log('服务正在暂停')
-			luci.sys.init.stop(name)
-		end
+		--local updated = false 
+		local service_stopped = false
 		for k, url in ipairs(subscribe_url) do
-			local raw = curl(url)
+			local raw, new_md5 = curl(url)
+			--log("raw 长度: "..#raw)
+			local groupHash = md5(url)
+			local old_md5 = read_old_md5(groupHash)
+
+			log("处理订阅: " .. url)
+			log("groupHash: " .. groupHash)
+			log("old_md5: " .. tostring(old_md5))
+			log("new_md5: " .. tostring(new_md5))
+
 			if #raw > 0 then
-				local nodes, szType
-				local groupHash = md5(url)
-				cache[groupHash] = {}
-				tinsert(nodeResult, {})
-				local index = #nodeResult
-				-- SSD 似乎是这种格式 ssd:// 开头的
-				if raw:find('ssd://') then
-					szType = 'ssd'
-					local nEnd = select(2, raw:find('ssd://'))
-					nodes = base64Decode(raw:sub(nEnd + 1, #raw))
-					nodes = jsonParse(nodes)
-					local extra = {airport = nodes.airport, port = nodes.port, encryption = nodes.encryption, password = nodes.password}
-					local servers = {}
-					-- SS里面包着 干脆直接这样
-					for _, server in ipairs(nodes.servers) do
-						tinsert(servers, setmetatable(server, {__index = extra}))
+				if old_md5 and new_md5 == old_md5 then
+					log("订阅未变化, 跳过无需更新的订阅: " .. url)
+					-- 防止 diff 阶段误删未更新订阅节点
+					loadOldNodes(groupHash)
+					--ucic:foreach(name, uciType, function(s)
+					--	if s.grouphashkey == groupHash and s.hashkey then
+					--		cache[groupHash][s.hashkey] = s
+					--		tinsert(nodeResult[index], s)
+					--	end
+					--end)
+				else
+					updated = true
+					-- 保存更新后的 MD5 值到以 groupHash 为标识的临时文件中,用于下次订阅更新时进行对比
+					write_new_md5(groupHash, new_md5)
+
+					-- 暂停服务(仅当 MD5 有变化时才执行)
+					if proxy == '0' and not service_stopped then
+						log('服务正在暂停')
+						luci.sys.init.stop(name)
+						service_stopped = true
 					end
-					nodes = servers
-				-- SS SIP008 直接使用 Json 格式
-				elseif jsonParse(raw) then
-					nodes = jsonParse(raw).servers or jsonParse(raw)
-					if nodes[1].server and nodes[1].method then
-						szType = 'sip008'
+
+					cache[groupHash] = {}
+					tinsert(nodeResult, {})
+					local index = #nodeResult
+					local nodes, szType
+
+					-- SSD 似乎是这种格式 ssd:// 开头的
+					if raw:find('ssd://') then
+						szType = 'ssd'
+						local nEnd = select(2, raw:find('ssd://'))
+						nodes = base64Decode(raw:sub(nEnd + 1, #raw))
+						nodes = jsonParse(nodes)
+						local extra = {
+							airport = nodes.airport,
+							port = nodes.port,
+							encryption = nodes.encryption,
+							password = nodes.password
+						}
+						local servers = {}
+						-- SS里面包着 干脆直接这样
+						for _, server in ipairs(nodes.servers or {}) do
+							tinsert(servers, setmetatable(server, {__index = extra}))
+						end
+						nodes = servers
+					-- SS SIP008 直接使用 Json 格式
+					elseif jsonParse(raw) then
+						nodes = jsonParse(raw).servers or jsonParse(raw)
+						if nodes[1] and nodes[1].server and nodes[1].method then
+							szType = 'sip008'
+						end
+					-- 其他 base64 格式
+					else
+						-- ssd 外的格式
+						nodes = split(base64Decode(raw):gsub(" ", "_"), "\n")
 					end
-				else
-					-- ssd 外的格式
-					nodes = split(base64Decode(raw):gsub(" ", "_"), "\n")
-				end
-				for _, v in ipairs(nodes) do
-					if v then
-						local result
-						if szType then
-							result = processData(szType, v)
-						elseif not szType then
-							local node = trim(v)
-							local dat = split(node, "://")
-							if dat and dat[1] and dat[2] then
-								local dat3 = ""
-								if dat[3] then
-									dat3 = "://" .. dat[3]
+					for _, v in ipairs(nodes) do
+						if v then
+							local result
+							if szType then
+								result = processData(szType, v)
+							elseif not szType then
+								local node = trim(v)
+								local dat = split(node, "://")
+								if dat and dat[1] and dat[2] then
+									local dat3 = ""
+									if dat[3] then
+										dat3 = "://" .. dat[3]
+									end
+									if dat[1] == 'ss' or dat[1] == 'trojan' then
+										result = processData(dat[1], dat[2] .. dat3)
+									else
+										result = processData(dat[1], base64Decode(dat[2]))
+									end
 								end
-								if dat[1] == 'ss' or dat[1] == 'trojan' then
-									result = processData(dat[1], dat[2] .. dat3)
+							else
+								log('跳过未知类型: ' .. szType)
+							end
+							-- log(result)
+							if result then
+								-- 中文做地址的 也没有人拿中文域名搞,就算中文域也有Puny Code SB 机场
+								if not result.server or not result.server_port
+									or result.alias == "NULL"
+									or check_filer(result)
+									or result.server:match("[^0-9a-zA-Z%-_%.%s]")
+									or cache[groupHash][result.hashkey]
+								then
+									log('丢弃无效节点: ' .. result.alias)
 								else
-									result = processData(dat[1], base64Decode(dat[2]))
+									-- log('成功解析: ' .. result.type ..' 节点, ' .. result.alias)
+									result.grouphashkey = groupHash
+									tinsert(nodeResult[index], result)
+									cache[groupHash][result.hashkey] = nodeResult[index][#nodeResult[index]]
 								end
 							end
-						else
-							log('跳过未知类型: ' .. szType)
-						end
-						-- log(result)
-						if result then
-							-- 中文做地址的 也没有人拿中文域名搞,就算中文域也有Puny Code SB 机场
-							if not result.server or not result.server_port or result.alias == "NULL" or check_filer(result) or result.server:match("[^0-9a-zA-Z%-_%.%s]") or cache[groupHash][result.hashkey] then
-								log('丢弃无效节点: ' .. result.alias)
-							else
-								-- log('成功解析: ' .. result.type ..' 节点, ' .. result.alias)
-								result.grouphashkey = groupHash
-								tinsert(nodeResult[index], result)
-								cache[groupHash][result.hashkey] = nodeResult[index][#nodeResult[index]]
-							end
 						end
 					end
+					log('成功解析节点数量: ' .. #nodes)
 				end
-				log('成功解析节点数量: ' .. #nodes)
 			else
 				log(url .. ': 获取内容为空')
 			end
 		end
 	end
+	-- 输出日志并判断是否需要进行 diff
+	if not updated then
+		log("订阅未变化,无需更新节点信息。")
+		log('保留手动添加的节点。')
+		return
+	end
 	-- diff
 	do
 		if next(nodeResult) == nil then
@@ -875,7 +972,7 @@ local execute = function()
 				if not ucic:get(name, globalServer) then
 					luci.sys.call("/etc/init.d/" .. name .. " stop > /dev/null 2>&1 &")
 					ucic:commit(name)
-					ucic:set(name, ucic:get_first(name, 'global'), 'global_server', ucic:get_first(name, uciType))
+					ucic:set(name, ucic:get_first(name, 'global'), 'global_server', firstServer)
 					ucic:commit(name)
 					log('当前主服务器节点已被删除,正在自动更换为第一个节点。')
 					luci.sys.call("/etc/init.d/" .. name .. " start > /dev/null 2>&1 &")