mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-09-11 02:44:57 +08:00
673 lines
20 KiB
Lua
673 lines
20 KiB
Lua
--[[
|
|
LuCI - Filesystem tools
|
|
|
|
Description:
|
|
A module offering often needed filesystem manipulation functions
|
|
|
|
FileId:
|
|
$Id$
|
|
|
|
License:
|
|
Copyright 2008 Steven Barth <steven@midlink.org>
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
|
|
]]--
|
|
|
|
local io = require "io"
|
|
local os = require "os"
|
|
local ltn12 = require "luci.ltn12"
|
|
local fs = require "nixio.fs"
|
|
local nutil = require "nixio.util"
|
|
local uci = require "luci.model.uci".cursor()
|
|
local SYS = require "luci.sys"
|
|
local HTTP = require "luci.http"
|
|
|
|
local type = type
|
|
local string = string
|
|
local tostring = tostring
|
|
local table = table
|
|
local math = math
|
|
local b64chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
|
|
|
--- LuCI filesystem library.
|
|
module "luci.openclash"
|
|
|
|
--- Test for file access permission on given path.
|
|
-- @class function
|
|
-- @name access
|
|
-- @param str String value containing the path
|
|
-- @return Number containing the return code, 0 on sucess or nil on error
|
|
-- @return String containing the error description (if any)
|
|
-- @return Number containing the os specific errno (if any)
|
|
access = fs.access
|
|
|
|
--- Evaluate given shell glob pattern and return a table containing all matching
|
|
-- file and directory entries.
|
|
-- @class function
|
|
-- @name glob
|
|
-- @param filename String containing the path of the file to read
|
|
-- @return Table containing file and directory entries or nil if no matches
|
|
-- @return String containing the error description (if no matches)
|
|
-- @return Number containing the os specific errno (if no matches)
|
|
function glob(...)
|
|
local iter, code, msg = fs.glob(...)
|
|
if iter then
|
|
return nutil.consume(iter)
|
|
else
|
|
return nil, code, msg
|
|
end
|
|
end
|
|
|
|
--- Checks wheather the given path exists and points to a regular file.
|
|
-- @param filename String containing the path of the file to test
|
|
-- @return Boolean indicating wheather given path points to regular file
|
|
function isfile(filename)
|
|
return fs.stat(filename, "type") == "reg"
|
|
end
|
|
|
|
--- Checks wheather the given path exists and points to a directory.
|
|
-- @param dirname String containing the path of the directory to test
|
|
-- @return Boolean indicating wheather given path points to directory
|
|
function isdirectory(dirname)
|
|
return fs.stat(dirname, "type") == "dir"
|
|
end
|
|
|
|
--- Read the whole content of the given file into memory.
|
|
-- @param filename String containing the path of the file to read
|
|
-- @return String containing the file contents or nil on error
|
|
-- @return String containing the error message on error
|
|
-- Wrapped readfile: if file is age-encrypted, try to decrypt with matching UCI secret first
|
|
function readfile(filename)
|
|
local content, err = fs.readfile(filename)
|
|
if not content then return nil, err end
|
|
if content:find("BEGIN AGE ENCRYPTED FILE") then
|
|
local keys = get_age_keys(filename)
|
|
if keys and keys.secret and keys.secret ~= "" then
|
|
return age_decrypt(keys.secret, content) or content
|
|
else
|
|
return content
|
|
end
|
|
end
|
|
return content
|
|
end
|
|
|
|
--- Write the contents of given string to given file.
|
|
-- @param filename String containing the path of the file to read
|
|
-- @param data String containing the data to write
|
|
-- @return Boolean containing true on success or nil on error
|
|
-- @return String containing the error message on error
|
|
-- Wrapped writefile: if public key exists for filename, encrypt before writing
|
|
function writefile(filename, data)
|
|
local keys = get_age_keys(filename)
|
|
if keys and keys.public and keys.public ~= "" then
|
|
return fs.writefile(filename, age_encrypt(keys.public, data) or data)
|
|
end
|
|
return fs.writefile(filename, data)
|
|
end
|
|
|
|
--- Copies a file.
|
|
-- @param source Source file
|
|
-- @param dest Destination
|
|
-- @return Boolean containing true on success or nil on error
|
|
copy = fs.datacopy
|
|
|
|
--- Renames a file.
|
|
-- @param source Source file
|
|
-- @param dest Destination
|
|
-- @return Boolean containing true on success or nil on error
|
|
rename = fs.move
|
|
|
|
--- Get the last modification time of given file path in Unix epoch format.
|
|
-- @param path String containing the path of the file or directory to read
|
|
-- @return Number containing the epoch time or nil on error
|
|
-- @return String containing the error description (if any)
|
|
-- @return Number containing the os specific errno (if any)
|
|
function mtime(path)
|
|
return fs.stat(path, "mtime")
|
|
end
|
|
|
|
--- Set the last modification time of given file path in Unix epoch format.
|
|
-- @param path String containing the path of the file or directory to read
|
|
-- @param mtime Last modification timestamp
|
|
-- @param atime Last accessed timestamp
|
|
-- @return 0 in case of success nil on error
|
|
-- @return String containing the error description (if any)
|
|
-- @return Number containing the os specific errno (if any)
|
|
function utime(path, mtime, atime)
|
|
return fs.utimes(path, atime, mtime)
|
|
end
|
|
|
|
--- Return the last element - usually the filename - from the given path with
|
|
-- the directory component stripped.
|
|
-- @class function
|
|
-- @name basename
|
|
-- @param path String containing the path to strip
|
|
-- @return String containing the base name of given path
|
|
-- @see dirname
|
|
basename = fs.basename
|
|
|
|
--- Return the directory component of the given path with the last element
|
|
-- stripped of.
|
|
-- @class function
|
|
-- @name dirname
|
|
-- @param path String containing the path to strip
|
|
-- @return String containing the directory component of given path
|
|
-- @see basename
|
|
dirname = fs.dirname
|
|
|
|
--- Return a table containing all entries of the specified directory.
|
|
-- @class function
|
|
-- @name dir
|
|
-- @param path String containing the path of the directory to scan
|
|
-- @return Table containing file and directory entries or nil on error
|
|
-- @return String containing the error description on error
|
|
-- @return Number containing the os specific errno on error
|
|
function dir(...)
|
|
local iter, code, msg = fs.dir(...)
|
|
if iter then
|
|
local t = nutil.consume(iter)
|
|
t[#t+1] = "."
|
|
t[#t+1] = ".."
|
|
return t
|
|
else
|
|
return nil, code, msg
|
|
end
|
|
end
|
|
|
|
--- Create a new directory, recursively on demand.
|
|
-- @param path String with the name or path of the directory to create
|
|
-- @param recursive Create multiple directory levels (optional, default is true)
|
|
-- @return Number with the return code, 0 on sucess or nil on error
|
|
-- @return String containing the error description on error
|
|
-- @return Number containing the os specific errno on error
|
|
function mkdir(path, recursive)
|
|
return recursive and fs.mkdirr(path) or fs.mkdir(path)
|
|
end
|
|
|
|
--- Remove the given empty directory.
|
|
-- @class function
|
|
-- @name rmdir
|
|
-- @param path String containing the path of the directory to remove
|
|
-- @return Number with the return code, 0 on sucess or nil on error
|
|
-- @return String containing the error description on error
|
|
-- @return Number containing the os specific errno on error
|
|
rmdir = fs.rmdir
|
|
|
|
local stat_tr = {
|
|
reg = "regular",
|
|
dir = "directory",
|
|
lnk = "link",
|
|
chr = "character device",
|
|
blk = "block device",
|
|
fifo = "fifo",
|
|
sock = "socket"
|
|
}
|
|
--- Get information about given file or directory.
|
|
-- @class function
|
|
-- @name stat
|
|
-- @param path String containing the path of the directory to query
|
|
-- @return Table containing file or directory properties or nil on error
|
|
-- @return String containing the error description on error
|
|
-- @return Number containing the os specific errno on error
|
|
function stat(path, key)
|
|
local data, code, msg = fs.stat(path)
|
|
if data then
|
|
data.mode = data.modestr
|
|
data.type = stat_tr[data.type] or "?"
|
|
end
|
|
return key and data and data[key] or data, code, msg
|
|
end
|
|
|
|
--- Set permissions on given file or directory.
|
|
-- @class function
|
|
-- @name chmod
|
|
-- @param path String containing the path of the directory
|
|
-- @param perm String containing the permissions to set ([ugoa][+-][rwx])
|
|
-- @return Number with the return code, 0 on sucess or nil on error
|
|
-- @return String containing the error description on error
|
|
-- @return Number containing the os specific errno on error
|
|
chmod = fs.chmod
|
|
|
|
--- Create a hard- or symlink from given file (or directory) to specified target
|
|
-- file (or directory) path.
|
|
-- @class function
|
|
-- @name link
|
|
-- @param path1 String containing the source path to link
|
|
-- @param path2 String containing the destination path for the link
|
|
-- @param symlink Boolean indicating wheather to create a symlink (optional)
|
|
-- @return Number with the return code, 0 on sucess or nil on error
|
|
-- @return String containing the error description on error
|
|
-- @return Number containing the os specific errno on error
|
|
function link(src, dest, sym)
|
|
return sym and fs.symlink(src, dest) or fs.link(src, dest)
|
|
end
|
|
|
|
--- Remove the given file.
|
|
-- @class function
|
|
-- @name unlink
|
|
-- @param path String containing the path of the file to remove
|
|
-- @return Number with the return code, 0 on sucess or nil on error
|
|
-- @return String containing the error description on error
|
|
-- @return Number containing the os specific errno on error
|
|
unlink = fs.unlink
|
|
|
|
--- Retrieve target of given symlink.
|
|
-- @class function
|
|
-- @name readlink
|
|
-- @param path String containing the path of the symlink to read
|
|
-- @return String containing the link target or nil on error
|
|
-- @return String containing the error description on error
|
|
-- @return Number containing the os specific errno on error
|
|
readlink = fs.readlink
|
|
|
|
function filename(str)
|
|
if not str then
|
|
return nil
|
|
end
|
|
local idx = str:match(".+()%.%w+$")
|
|
if idx then
|
|
return str:sub(1, idx-1)
|
|
else
|
|
return str
|
|
end
|
|
end
|
|
|
|
function filesize(e)
|
|
local t=0
|
|
local a={' KB',' MB',' GB',' TB',' PB'}
|
|
if e < 0 then
|
|
e = -e
|
|
end
|
|
repeat
|
|
e=e/1024
|
|
t=t+1
|
|
until(e<=1024)
|
|
return string.format("%.1f",e)..a[t] or "0.0 KB"
|
|
end
|
|
|
|
function lanip()
|
|
local lan_int_name = uci:get("openclash", "@overwrite[0]", "lan_interface_name") or uci:get("openclash", "config", "lan_interface_name") or "0"
|
|
local lan_ip
|
|
if lan_int_name == "0" then
|
|
lan_ip = SYS.exec("uci -q get network.lan.ipaddr 2>/dev/null |awk -F '/' '{print $1}' 2>/dev/null |tr -d '\n'")
|
|
else
|
|
lan_ip = SYS.exec(string.format("ip address show %s 2>/dev/null | grep -w 'inet' 2>/dev/null | grep -Eo 'inet [0-9\.]+' | awk '{print $2}' | head -1 | tr -d '\n'", lan_int_name))
|
|
end
|
|
if not lan_ip or lan_ip == "" then
|
|
lan_ip = SYS.exec("ip address show $(uci -q -p /tmp/state get network.lan.device || uci -q -p /tmp/state get network.lan.device) | grep -w 'inet' 2>/dev/null | grep -Eo 'inet [0-9\.]+' | awk '{print $2}' | head -1 | tr -d '\n'")
|
|
end
|
|
if not lan_ip or lan_ip == "" then
|
|
lan_ip = SYS.exec("ip addr show 2>/dev/null | grep -w 'inet' | grep 'global' | grep 'brd' | grep -Eo 'inet [0-9\.]+' | awk '{print $2}' | head -n 1 | tr -d '\n'")
|
|
end
|
|
return lan_ip
|
|
end
|
|
|
|
function find_case_insensitive_path(path)
|
|
local dir = dirname(path)
|
|
local base = basename(path)
|
|
local files = dir and fs.dir(dir)
|
|
if not files then
|
|
return nil
|
|
end
|
|
|
|
for f in files do
|
|
if f:lower() == base:lower() then
|
|
return dir .. "/" .. f
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
function get_resourse_mtime(path)
|
|
local real_path = path
|
|
if not fs.access(path) then
|
|
local found = find_case_insensitive_path(path)
|
|
if found then
|
|
real_path = found
|
|
elseif uci_get_config("config", "small_flash_memory") == "1" then
|
|
local fallback_path = path:gsub("^/etc/openclash/", "/tmp/etc/openclash/")
|
|
local fallback_found = find_case_insensitive_path(fallback_path)
|
|
if fallback_found then
|
|
real_path = fallback_found
|
|
else
|
|
return "File Not Exist"
|
|
end
|
|
else
|
|
return "File Not Exist"
|
|
end
|
|
end
|
|
local file = fs.readlink(real_path) or real_path
|
|
local resourse_etag_version = SYS.exec(string.format("source /usr/share/openclash/openclash_etag.sh && GET_ETAG_TIMESTAMP_BY_PATH '%s'", file))
|
|
if resourse_etag_version and resourse_etag_version ~= "" then
|
|
return resourse_etag_version
|
|
end
|
|
local resourse_version = os.date("%Y-%m-%d %H:%M:%S", mtime(file))
|
|
if resourse_version and resourse_version ~= "" then
|
|
return resourse_version
|
|
end
|
|
return "Unknown"
|
|
end
|
|
|
|
function uci_get_config(section, key)
|
|
local val
|
|
if section == "config" then
|
|
val = uci:get("openclash", "@overwrite[0]", key)
|
|
end
|
|
if val == nil then
|
|
val = uci:get("openclash", section, key)
|
|
end
|
|
return val
|
|
end
|
|
|
|
function get_file_path_from_request()
|
|
local file_path
|
|
local referer = HTTP.getenv("HTTP_REFERER")
|
|
if referer then
|
|
local _, _, file_value = referer:find("file=([^&]*)$")
|
|
if file_value and file_value ~= "" then
|
|
file_path = HTTP.urldecode(file_value)
|
|
end
|
|
end
|
|
|
|
if not file_path or file_path == "/" then
|
|
file_path = HTTP.formvalue("file")
|
|
if not file_path then
|
|
file_path = HTTP.urldecode(file_path)
|
|
end
|
|
end
|
|
|
|
return file_path
|
|
end
|
|
|
|
function get_age_keys(file)
|
|
local name = filename(basename(file))
|
|
local pub, sec
|
|
uci:foreach("openclash", "config_age_secret", function(s)
|
|
if s and s.name then
|
|
if s.name == name and (not s.hidden or (s.hidden ~= "true")) then
|
|
if not pub and s.public then pub = s.public end
|
|
if not sec and s.secret then sec = s.secret end
|
|
end
|
|
return false
|
|
end
|
|
end)
|
|
return { public = pub, secret = sec }
|
|
end
|
|
|
|
function age_encrypt(public, content)
|
|
if not public or public == "" or not content then return nil end
|
|
|
|
local tmp_in = os.tmpname()
|
|
local tmp_out = os.tmpname()
|
|
local f = io.open(tmp_in, "w")
|
|
if not f then return nil end
|
|
f:write(content)
|
|
f:close()
|
|
|
|
local cmd = string.format(
|
|
"/etc/openclash/core/clash_meta age encrypt %s %s %s 2>/dev/null",
|
|
public, tmp_in, tmp_out
|
|
)
|
|
os.execute(cmd)
|
|
|
|
local out = nil
|
|
local f_out = io.open(tmp_out, "r")
|
|
if f_out then
|
|
out = f_out:read("*a") or ""
|
|
f_out:close()
|
|
end
|
|
|
|
os.remove(tmp_in)
|
|
os.remove(tmp_out)
|
|
|
|
return out
|
|
end
|
|
|
|
function age_decrypt(secret, content)
|
|
if not secret or secret == "" or not content then return nil end
|
|
|
|
local tmp_in = os.tmpname()
|
|
local tmp_out = os.tmpname()
|
|
local f = io.open(tmp_in, "w")
|
|
if not f then return nil end
|
|
f:write(content)
|
|
f:close()
|
|
|
|
local cmd = string.format(
|
|
"/etc/openclash/core/clash_meta age decrypt %s %s %s 2>/dev/null",
|
|
secret, tmp_in, tmp_out
|
|
)
|
|
os.execute(cmd)
|
|
|
|
local out = nil
|
|
local f_out = io.open(tmp_out, "r")
|
|
if f_out then
|
|
out = f_out:read("*a") or ""
|
|
f_out:close()
|
|
end
|
|
|
|
os.remove(tmp_in)
|
|
os.remove(tmp_out)
|
|
|
|
return out
|
|
end
|
|
|
|
function decode64(data)
|
|
if not data then return nil end
|
|
data = data:gsub('[%s]', '')
|
|
if #data % 4 ~= 0 then return nil end
|
|
|
|
local out = {}
|
|
for i = 1, #data, 4 do
|
|
local c1, c2, c3, c4 = data:byte(i, i+3)
|
|
local i1 = b64chars:find(string.char(c1), 1, true) - 1
|
|
local i2 = b64chars:find(string.char(c2), 1, true) - 1
|
|
local i3 = (c3 == 61) and -1 or (b64chars:find(string.char(c3), 1, true) - 1)
|
|
local i4 = (c4 == 61) and -1 or (b64chars:find(string.char(c4), 1, true) - 1)
|
|
|
|
if not i1 or not i2 or (c3 ~= 61 and not i3) or (c4 ~= 61 and not i4) then
|
|
return nil
|
|
end
|
|
|
|
local x = i1 * 0x40000 + i2 * 0x1000
|
|
if i3 >= 0 then x = x + i3 * 0x40 end
|
|
if i4 >= 0 then x = x + i4 end
|
|
|
|
table.insert(out, string.char(math.floor(x / 0x10000) % 0x100))
|
|
if i3 >= 0 then
|
|
table.insert(out, string.char(math.floor(x / 0x100) % 0x100))
|
|
end
|
|
if i4 >= 0 then
|
|
table.insert(out, string.char(x % 0x100))
|
|
end
|
|
end
|
|
|
|
return table.concat(out)
|
|
end
|
|
|
|
function config_refs(old_file_name, new_file_name)
|
|
if not old_file_name or old_file_name == "" then return end
|
|
local old_name_no_ext = filename(basename(old_file_name))
|
|
local old_file_full = basename(old_file_name)
|
|
local is_rename = new_file_name and new_file_name ~= ""
|
|
local new_name_no_ext, new_file_full
|
|
|
|
if is_rename then
|
|
new_name_no_ext = filename(basename(new_file_name))
|
|
new_file_full = basename(new_file_name)
|
|
end
|
|
|
|
uci:foreach("openclash", "config_subscribe",
|
|
function(s)
|
|
if s.name == old_name_no_ext then
|
|
if is_rename and new_name_no_ext ~= new_file_name then
|
|
uci:set("openclash", s[".name"], "name", new_name_no_ext)
|
|
else
|
|
uci:delete("openclash", s[".name"])
|
|
end
|
|
return false
|
|
end
|
|
end
|
|
)
|
|
|
|
uci:foreach("openclash", "subscribe_info",
|
|
function(s)
|
|
if s.name == old_name_no_ext then
|
|
if is_rename and new_name_no_ext ~= new_file_name then
|
|
uci:set("openclash", s[".name"], "name", new_name_no_ext)
|
|
else
|
|
uci:delete("openclash", s[".name"])
|
|
end
|
|
return false
|
|
end
|
|
end
|
|
)
|
|
|
|
uci:foreach("openclash", "groups",
|
|
function(s)
|
|
if s.config == old_file_full then
|
|
if is_rename and new_name_no_ext ~= new_file_name then
|
|
uci:set("openclash", s[".name"], "config", new_file_full)
|
|
else
|
|
uci:delete("openclash", s[".name"])
|
|
end
|
|
end
|
|
end
|
|
)
|
|
|
|
uci:foreach("openclash", "proxy-provider",
|
|
function(s)
|
|
if s.config == old_file_full then
|
|
if is_rename and new_name_no_ext ~= new_file_name then
|
|
uci:set("openclash", s[".name"], "config", new_file_full)
|
|
else
|
|
uci:delete("openclash", s[".name"])
|
|
end
|
|
end
|
|
end
|
|
)
|
|
|
|
uci:foreach("openclash", "servers",
|
|
function(s)
|
|
if s.config == old_file_full then
|
|
if is_rename and new_name_no_ext ~= new_file_name then
|
|
uci:set("openclash", s[".name"], "config", new_file_full)
|
|
else
|
|
uci:delete("openclash", s[".name"])
|
|
end
|
|
end
|
|
end
|
|
)
|
|
|
|
uci:foreach("openclash", "config_age_secret",
|
|
function(s)
|
|
if s.name == old_name_no_ext then
|
|
if is_rename and new_name_no_ext ~= new_file_name then
|
|
uci:set("openclash", s[".name"], "name", new_name_no_ext)
|
|
else
|
|
uci:delete("openclash", s[".name"])
|
|
end
|
|
return false
|
|
end
|
|
end
|
|
)
|
|
|
|
uci:commit("openclash")
|
|
end
|
|
|
|
--- Returns the appropriate ps command string for the system's ps implementation.
|
|
-- Detects procps-ng (ps -efw) vs busybox (ps -w).
|
|
-- @return String containing the ps command prefix
|
|
function ps_cmd()
|
|
local ps_version = SYS.exec("ps --version 2>&1 |grep -c procps-ng |tr -d '\n'")
|
|
if ps_version == "1" then
|
|
return "ps -efw"
|
|
else
|
|
return "ps -w"
|
|
end
|
|
end
|
|
|
|
--- Returns the package manager type (opkg or apk).
|
|
-- @return String "opkg" or "apk"
|
|
function pkg_type()
|
|
if fs.access("/usr/bin/apk") then
|
|
return "apk"
|
|
else
|
|
return "opkg"
|
|
end
|
|
end
|
|
|
|
--- Reads the CDN proxy address list from /usr/share/openclash/res/cdn.list.
|
|
-- Lines starting with "#" are treated as comments and skipped.
|
|
-- @return Table of CDN address strings (deduplicated, in file order)
|
|
function cdn_list()
|
|
local list = {}
|
|
local seen = {}
|
|
local raw = fs.readfile("/usr/share/openclash/res/cdn.list")
|
|
if raw then
|
|
for line in raw:gmatch("[^\r\n]+") do
|
|
line = line:gsub("^%s+", ""):gsub("%s+$", "")
|
|
if line ~= "" and line:sub(1, 1) ~= "#" and not seen[line] then
|
|
seen[line] = true
|
|
list[#list + 1] = line
|
|
end
|
|
end
|
|
end
|
|
return list
|
|
end
|
|
|
|
--- Read a field of an installed package directly from the package database
|
|
-- (/usr/lib/opkg/status or /lib/apk/db/installed), avoiding the opkg/apk
|
|
-- binaries and their lock files (/var/lock/opkg.lock, /lib/apk/db/lock).
|
|
-- Safe to call repeatedly/concurrently and never blocks on a lock.
|
|
-- @param pkg Package name, e.g. "luci-app-openclash" or "libc"
|
|
-- @param field Field name; opkg: "Version"/"Architecture", apk: "V"/"A"
|
|
-- @return String field value, or "" when not found
|
|
function read_pkg_field(pkg, field)
|
|
local text
|
|
if pkg_type() == "opkg" then
|
|
text = fs.readfile("/usr/lib/opkg/status")
|
|
else
|
|
text = fs.readfile("/lib/apk/db/installed")
|
|
end
|
|
if not text then return "" end
|
|
text = text:gsub("\r\n", "\n")
|
|
|
|
local prefix = (pkg_type() == "opkg") and ("Package: " .. pkg .. "\n") or ("P:" .. pkg .. "\n")
|
|
local start = text:find(prefix, 1, true)
|
|
if not start then return "" end
|
|
local block_end = text:find("\n\n", start + #prefix, true) or #text
|
|
local block = text:sub(start + #prefix, block_end - 1)
|
|
local colon = (pkg_type() == "opkg") and ": " or ":"
|
|
local val = block:match("\n" .. field .. colon .. "([^\r\n]+)")
|
|
if not val then
|
|
val = block:match("^" .. field .. colon .. "([^\r\n]+)")
|
|
end
|
|
return val or ""
|
|
end
|
|
|
|
function oc_version()
|
|
local v = read_pkg_field("luci-app-openclash", pkg_type() == "opkg" and "Version" or "V")
|
|
if v == "" then
|
|
v = "0"
|
|
end
|
|
return v
|
|
end
|
|
|
|
function IsYamlExt(e)
|
|
e = e or ""
|
|
local lower = string.lower(e)
|
|
return lower:sub(-5) == ".yaml" or lower:sub(-4) == ".yml"
|
|
end
|