🎨 Sync 2026-10-12 21:43:19

This commit is contained in:
github-actions[bot] committed 2026-10-12 21:43:19 +08:00
1 parent 3a82c347ab
commit bfe30ffbef
51 files changed
+8523 -149

No files matched your search

@@ -43,7 +43,7 @@ d:option(DummyValue, "temp", translate("Temp"))
d:option(DummyValue, "p_table", translate("Partition Table"))
d:option(DummyValue, "sata_ver", translate("SATA Version"))
-- d:option(DummyValue, "rota_rate", translate("Rotation Rate"))
d:option(DummyValue, "health_status", translate("Health") .. "<br/>" .. translate("Status"))
d:option(DummyValue, "health_status", translate("Health") .. " / " .. translate("Status"))
-- d:option(DummyValue, "status", translate("Status"))
-- local btn_eject = d:option(Button, "_eject")
@@ -25,6 +25,25 @@ m.reset = false
local disk_info = dm.get_disk_info(dev, true)
local format_cmd = dm.get_format_cmd()
local function is_firmware_partition(part)
local name = part.partition_name or ""
return name:match("^0:") or name:find("rootfs", 1, true)
end
local first_partition_start
for _, part in ipairs(disk_info.partitions) do
local start = tonumber(part.sec_start)
if part.number > 0 and start then
first_partition_start = math.min(first_partition_start or start, start)
end
end
local function is_leading_free_space(part)
-- Bootloader data may live in the gap before the first real partition.
return part.number == -1 and first_partition_start and
tonumber(part.sec_end) < first_partition_start
end
s = m:section(Table, {disk_info}, translate("Device Info"))
-- s:option(DummyValue, "key")
-- s:option(DummyValue, "value")
@@ -118,14 +137,15 @@ end
-- partitions info
if not disk_info.p_table:match("Raid") then
s_partition_table = m:section(Table, disk_info.partitions, translate("Partitions Info"), translate("Default 2048 sector alignment, support +size{b,k,m,g,t} in End Sector"))
s_partition_table = m:section(Table, disk_info.partitions, translate("Partitions Info"), translate("Default 1 MiB alignment, support +size{b,k,m,g,t} in End Sector"))
-- s_partition_table:option(DummyValue, "number", translate("Number"))
s_partition_table:option(DummyValue, "name", translate("Name"))
s_partition_table:option(DummyValue, "partition_name", translate("Partition Name"))
local val_sec_start = s_partition_table:option(Value, "sec_start", translate("Start Sector"))
val_sec_start.render = function(self, section, scope)
-- could create new partition
if disk_info.partitions[section].number == -1 and disk_info.partitions[section].size > 1 * 1024 * 1024 then
if disk_info.partitions[section].number == -1 and not is_leading_free_space(disk_info.partitions[section]) and disk_info.partitions[section].size > 1 * 1024 * 1024 then
self.template = "cbi/value"
Value.render(self, section, scope)
else
@@ -136,7 +156,7 @@ if not disk_info.p_table:match("Raid") then
local val_sec_end = s_partition_table:option(Value, "sec_end", translate("End Sector"))
val_sec_end.render = function(self, section, scope)
-- could create new partition
if disk_info.partitions[section].number == -1 and disk_info.partitions[section].size > 1 * 1024 * 1024 then
if disk_info.partitions[section].number == -1 and not is_leading_free_space(disk_info.partitions[section]) and disk_info.partitions[section].size > 1 * 1024 * 1024 then
self.template = "cbi/value"
Value.render(self, section, scope)
else
@@ -192,8 +212,9 @@ if not disk_info.p_table:match("Raid") then
disk_info.partitions[section]._fs = value
end
val_fs.render = function(self, section, scope)
-- use listvalue when partition not mounted
if disk_info.partitions[section].mount_point == "-" and disk_info.partitions[section].number ~= -1 and disk_info.partitions[section].type ~= "extended" then
local part = disk_info.partitions[section]
-- Keep firmware partitions read-only in the filesystem column.
if not is_firmware_partition(part) and part.mount_point == "-" and part.number ~= -1 and part.type ~= "extended" then
self.template = "diskman/cbi/format_button"
self.inputstyle = "reset"
self.inputtitle = disk_info.partitions[section].fs == "raw" and translate("Format") or disk_info.partitions[section].fs
@@ -261,6 +282,12 @@ if not disk_info.p_table:match("Raid") then
btn_action.forcewrite = true
btn_action.template = "diskman/cbi/disabled_button"
btn_action.render = function(self, section, scope)
if is_firmware_partition(disk_info.partitions[section]) or is_leading_free_space(disk_info.partitions[section]) then
self.template = "cbi/dvalue"
DummyValue.render(self, section, scope)
return
end
self.template = "diskman/cbi/disabled_button"
-- if partition is mounted or the size < 1mb, then disable the add action
if disk_info.partitions[section].mount_point ~= "-" or (disk_info.partitions[section].type ~= "extended" and disk_info.partitions[section].number == -1 and disk_info.partitions[section].size <= 1 * 1024 * 1024) then
self.view_disabled = true
@@ -282,16 +309,17 @@ if not disk_info.p_table:match("Raid") then
Button.render(self, section, scope)
end
btn_action.write = function(self, section, value)
if is_firmware_partition(disk_info.partitions[section]) or is_leading_free_space(disk_info.partitions[section]) then return end
if value == translate("New") then
local start_sec = disk_info.partitions[section]._sec_start and tonumber(disk_info.partitions[section]._sec_start) or tonumber(disk_info.partitions[section].sec_start)
local end_sec = disk_info.partitions[section]._sec_end
local logic_sec = tonumber(disk_info.logic_sec)
local align = math.max(math.ceil(tonumber(disk_info.phy_sec) / logic_sec), math.ceil(1048576 / logic_sec))
if start_sec then
-- for sector alignment
local align = tonumber(disk_info.phy_sec) / tonumber(disk_info.logic_sec)
align = (align < 2048) and 2048
if start_sec < 2048 then
start_sec = "2048" .. "s"
if start_sec < align then
start_sec = tostring(align) .. "s"
elseif math.fmod( start_sec, align ) ~= 0 then
start_sec = tostring(start_sec + align - math.fmod( start_sec, align )) .. "s"
else
@@ -306,14 +334,14 @@ if not disk_info.p_table:match("Raid") then
if tonumber(end_size) and end_unit then
local unit ={
B=1,
S=512,
S=tonumber(disk_info.logic_sec),
K=1024,
M=1048576,
G=1073741824,
T=1099511627776
}
end_unit = end_unit:upper()
end_sec = tostring(tonumber(end_size) * unit[end_unit] / unit["S"] + tonumber(start_sec:sub(1,-2)) - 1 ) .. "s"
end_sec = tostring(math.ceil(tonumber(end_size) * unit[end_unit] / logic_sec) + tonumber(start_sec:sub(1,-2)) - 1) .. "s"
elseif tonumber(end_sec) then
end_sec = end_sec .. "s"
else
@@ -325,8 +353,8 @@ if not disk_info.p_table:match("Raid") then
if disk_info.p_table == "MBR" and disk_info["extended_partition_index"] then
if tonumber(disk_info.partitions[disk_info["extended_partition_index"]].sec_start) <= tonumber(start_sec:sub(1,-2)) and tonumber(disk_info.partitions[disk_info["extended_partition_index"]].sec_end) >= tonumber(end_sec:sub(1,-2)) then
part_type = "logical"
if tonumber(start_sec:sub(1,-2)) - tonumber(disk_info.partitions[section].sec_start) < 2048 then
start_sec = tonumber(start_sec:sub(1,-2)) + 2048
if tonumber(start_sec:sub(1,-2)) - tonumber(disk_info.partitions[section].sec_start) < align then
start_sec = tonumber(start_sec:sub(1,-2)) + align
start_sec = start_sec .."s"
end
end
@@ -363,4 +391,4 @@ if not disk_info.p_table:match("Raid") then
end
end
return m
return m
+46 -9
View File
@@ -9,6 +9,7 @@ local ver = require "luci.version"
local CMD = {"parted", "mdadm", "blkid", "smartctl", "df", "btrfs", "lsblk"}
local d = {command ={}}
local SYSFS_SECTOR_SIZE = 512
for _, cmd in ipairs(CMD) do
local command = luci.sys.exec("/usr/bin/which " .. cmd)
d.command[cmd] = command:match("^.+"..cmd) or nil
@@ -33,7 +34,42 @@ function byte_format(byte)
end
end
local function get_emmc_health(device)
if not device:match("^mmcblk%d+$") then return end
local path = "/sys/class/block/" .. device .. "/device/"
if not (nixio.fs.readfile(path .. "type") or ""):match("^MMC%s*$") then return end
local translate = require("luci.i18n").translate
local info = {}
local lifetime
-- Use the more worn of the supported A/B estimates; zero means undefined.
for value in (nixio.fs.readfile(path .. "life_time") or ""):gmatch("%S+") do
local estimate = tonumber(value)
if estimate and estimate >= 1 and estimate <= 11 then
lifetime = math.max(lifetime or 0, estimate)
end
end
if lifetime then
if lifetime == 11 then
info.health = translate("Estimated lifetime exceeded")
else
-- Report estimated remaining life as a range, not an exact percentage.
info.health = string.format("%d-%d%%", 100 - lifetime * 10, 110 - lifetime * 10)
end
end
local eol = tonumber(nixio.fs.readfile(path .. "pre_eol_info") or "")
if eol == 1 then
info.status = translate("Normal")
elseif eol == 2 then
info.status = translate("Warning")
elseif eol == 3 then
info.status = translate("Urgent")
end
return info
end
local get_smart_info = function(device)
local emmc_info = get_emmc_health(device)
if emmc_info then return emmc_info end
local section
local smart_info = {}
for _, line in ipairs(luci.util.execl(d.command.smartctl .. " -H -A -i -n standby -f brief /dev/" .. device)) do
@@ -188,6 +224,10 @@ local get_parted_info = function(device)
elseif device:match("mmcblk") or device:match("md") or device:match("nvme") then
partition_temp["name"] = device.."p"..partition_temp["number"]
end
-- Read the kernel partition name: older parted output does not escape colons.
local uevent = partition_temp["name"] ~= "-" and
nixio.fs.readfile("/sys/class/block/" .. partition_temp["name"] .. "/uevent") or ""
partition_temp["partition_name"] = (uevent or ""):match("\nPARTNAME=([^\n]+)") or "-"
if partition_temp["number"] > 0 and partition_temp["fs"] == "" and d.command.lsblk then
partition_temp["fs"] = (luci.util.exec(d.command.lsblk .. " /dev/" .. partition_temp["name"] .. " -no fstype") or ""):match("([^%s]+)") or ""
end
@@ -230,7 +270,7 @@ local get_parted_info = function(device)
elseif (p["number"] <= 4) and (p["number"] > 0) then
local s = nixio.fs.readfile("/sys/block/"..device.."/"..p["name"].."/size")
if s then
local real_size_sec = tonumber(s) * tonumber(disk_temp.logic_sec)
local real_size_sec = tonumber(s) * SYSFS_SECTOR_SIZE
-- if size not equal, it's an extended
if real_size_sec ~= p["size"] then
disk_temp["extended_partition_index"] = i
@@ -313,7 +353,7 @@ d.get_disk_info = function(device, wakeup)
-- if only weakup == true, weakup the disk and check parted_info.
if smart_info.status ~= "STANDBY" or wakeup or (smart_info["p_table"] and not smart_info["p_table"]:match("Raid")) or device:match("^md") then
disk_info = get_parted_info(device)
disk_info["sec_size"] = disk_info["logic_sec"] .. "/" .. disk_info["phy_sec"]
disk_info["sec_size"] = tostring(disk_info["logic_sec"] or "-") .. "/" .. tostring(disk_info["phy_sec"] or "-")
disk_info["size_formated"] = byte_format(tonumber(disk_info["size"]))
-- if status is standby, after get part info, the disk is weakuped, then get smart_info again for more informations
if smart_info.status ~= "ACTIVE" then smart_info = get_smart_info(device) end
@@ -362,11 +402,9 @@ d.list_raid_devices = function()
end
local size = tonumber(fs.readfile(string.format("/sys/class/block/%s/size", mdpath)))
local ss = tonumber(fs.readfile(string.format("/sys/class/block/%s/queue/logical_block_size", mdpath)))
device_info["path"] = "/dev/"..mdpath
device_info["size"] = size*ss
device_info["size_formated"] = byte_format(size*ss)
device_info["size"] = size * SYSFS_SECTOR_SIZE
device_info["size_formated"] = byte_format(device_info["size"])
device_info["active"] = active:upper()
device_info["level"] = level
device_info["members"] = members
@@ -418,19 +456,18 @@ d.list_devices = function()
local device_info = {}
local device = "/dev/" .. bname
local size = tonumber(fs.readfile(string.format("/sys/class/block/%s/size", bname)) or "0")
local ss = tonumber(fs.readfile(string.format("/sys/class/block/%s/queue/logical_block_size", bname)) or "0")
local model = fs.readfile(string.format("/sys/class/block/%s/device/model", bname))
local partitions = {}
for part in nixio.fs.glob("/sys/block/" .. bname .."/" .. bname .. "*") do
local pname = nixio.fs.basename(part)
local psize = byte_format(tonumber(nixio.fs.readfile(part .. "/size"))*ss)
local psize = byte_format(tonumber(nixio.fs.readfile(part .. "/size")) * SYSFS_SECTOR_SIZE)
local mount_point = get_mount_point(pname)
if mount_point then device_info["inuse"] = true end
table.insert(partitions, {name = pname, size_formated = psize, inuse = mount_point})
end
device_info["path"] = device
device_info["size_formated"] = byte_format(size*ss)
device_info["size_formated"] = byte_format(size * SYSFS_SECTOR_SIZE)
device_info["model"] = model
device_info["partitions"] = partitions
-- true or false
@@ -1,7 +1,66 @@
<style>
.diskman-table { table-layout: auto; width: 100%; }
.diskman-table > tbody > tr > th,
.diskman-table > tbody > tr > td {
padding-left: 6px;
padding-right: 6px;
white-space: nowrap;
overflow-wrap: normal;
width: auto;
flex: 0 0 auto;
}
.diskman-table > tbody > tr > [data-name="path"],
.diskman-table > tbody > tr > [data-name="model"] {
width: auto;
flex: 1 1 0;
min-width: 0;
white-space: normal;
overflow-wrap: anywhere;
}
.diskman-table > tbody > tr > .cbi-section-actions {
width: 6em;
min-width: 6em;
flex: 0 0 6em;
}
</style>
<script type="text/javascript">
window.onload = function () {
//disk partition info
let p_colors = ["#c0c0ff", "#fbbd00", "#e97c30", "#a0e0a0", "#e0c0ff"]
function renderPartitions(item, disk_info) {
if (!disk_info.partitions || !disk_info.partitions.length) return
let isTable = item.nodeName == "TR"
if (isTable) item.closest("table").classList.add("diskman-table")
let row = document.createElement(isTable ? "tr" : "div")
let cell = document.createElement(isTable ? "td" : "div")
if (isTable) cell.colSpan = item.children.length
else {
row.className = "tr cbi-section-table-row cbi-rowstyle-1"
cell.className = "td"
}
cell.style.cssText = "padding:0; border:0;"
let container = document.createElement("div")
container.style.cssText = "position:relative; height:1.8em; overflow:hidden;"
let bar = document.createElement("div")
// Keep partition labels out of the table's intrinsic width calculation.
bar.style.cssText = "position:absolute; inset:0; display:flex;"
disk_info.partitions.forEach((part, index) => {
let segment = document.createElement("div")
let label = (part.name != '-' && part.name || '') + ' ' + (part.fs != 'Free Space' && part.fs || '') + ' ' + part.size_formated + ' ' + (part.useage != '-' && part.useage || '')
let share = disk_info.size > 0 ? part.size / disk_info.size * 100 : 0
// Flex normalizes the weights even when there are many small partitions.
segment.style.cssText = "min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; text-align:center; color:#525F7F;"
segment.style.flex = Math.max(share || 0, 8) + " 1 0%"
segment.style.backgroundColor = p_colors[index % p_colors.length]
segment.title = label
segment.textContent = label
bar.appendChild(segment)
})
container.appendChild(bar)
cell.appendChild(container)
row.appendChild(cell)
item.parentNode.insertBefore(row, item.nextSibling)
}
let lines = document.querySelectorAll('[id^=cbi-disk-]')
lines.forEach((item) => {
let dev = item.id.match(/cbi-disk-(.*)/)[1]
@@ -19,7 +78,7 @@
} else if (cell.getAttribute("data-name") == "sata_ver" || cell.childNodes[1] && cell.childNodes[1].id.match(/sata_ver/)) {
cell.innerText = disk_info.sata_ver || "-"
} else if (cell.getAttribute("data-name") == "health_status" || cell.childNodes[1] && cell.childNodes[1].id.match(/health_status/)) {
cell.innerText = (disk_info.health || "-") + "\n" + (disk_info.status || "-")
cell.innerText = (disk_info.health || "-") + " / " + (disk_info.status || "-")
} else if (cell.getAttribute("data-name") == "health" || cell.childNodes[1] && cell.childNodes[1].id.match(/health/)) {
cell.innerText = disk_info.health || "-"
} else if (cell.getAttribute("data-name") == "status" || cell.childNodes[1] && cell.childNodes[1].id.match(/status/)) {
@@ -27,44 +86,7 @@
}
}
})
// handle partitons info
if (disk_info.partitions && disk_info.partitions.length > 0) {
let partitons_div
if (item.nodeName == "TR") {
partitons_div = '<tr width="100%" style="white-space:nowrap;"><td style="margin:0px; padding:0px; border:0px; white-space:nowrap;" colspan="15">'
} else if (item.nodeName == "DIV") {
partitons_div = '<div class="tr cbi-section-table-row cbi-rowstyle-1"><div style="white-space:nowrap; position:absolute; width:100%">'
}
let expand = 0
let need_expand = 0
disk_info.partitions.forEach((part) => {
let p = part.size / disk_info.size * 100
if (p <= 8) {
expand += 8
need_expand += p
part.part_percent = 8
}
})
let n = 0
disk_info.partitions.forEach((part) => {
let p = part.size / disk_info.size * 100
if (p > 8) {
part.part_percent = p * (100 - expand) / (100 - need_expand)
}
let part_percent = part.part_percent + '%'
let p_color = p_colors[n++]
if (n > 4) { n = 0 }
let inline_txt = (part.name != '-' && part.name || '') + ' ' + (part.fs != 'Free Space' && part.fs || '') + ' ' + part.size_formated + ' ' + (part.useage != '-' && part.useage || '')
let partiton_div = '<div title="' + inline_txt + '" style="color: #525F7F; display:inline-block; text-align:center;background-color:' + p_color + '; width:' + part_percent + '">' + inline_txt + '</div>'
partitons_div += partiton_div
})
if (item.nodeName == "TR") {
partitons_div += '</td></tr>'
} else if (item.nodeName == "DIV") {
partitons_div += '</div><div>&nbsp</div></div>'
}
item.insertAdjacentHTML('afterend', partitons_div);
}
renderPartitions(item, disk_info)
})
})
//raid table
@@ -82,28 +104,7 @@
}
}
})
// handle partitons info
let partitons_div
if (item.nodeName == "TR") {
partitons_div = '<tr width="100%" style="white-space:nowrap;"><td style="margin:0px; padding:0px; border:0px; white-space:nowrap;" colspan="15">'
} else if (item.nodeName == "DIV") {
partitons_div = '<div class="tr cbi-section-table-row cbi-rowstyle-1"><div style="white-space:nowrap; position:absolute; width:100%">'
}
let n = 0
disk_info.partitions.forEach((part) => {
let part_percent = part.size / disk_info.size * 100 + '%'
let p_color = p_colors[n++]
if (n > 4) { n = 0 }
let inline_txt = (part.name != '-' && part.name || '') + ' ' + (part.fs != 'Free Space' && part.fs || '') + ' ' + part.size_formated + ' ' + (part.useage != '-' && part.useage || '')
let partiton_div = '<div title="' + inline_txt + '" style="display:inline-block; text-align:center;background-color:' + p_color + '; width:' + part_percent + '">' + inline_txt + '</div>'
partitons_div += partiton_div
})
if (item.nodeName == "TR") {
partitons_div += '</td></tr>'
} else if (item.nodeName == "DIV") {
partitons_div += '</div><div>&nbsp</div></div>'
}
item.insertAdjacentHTML('afterend', partitons_div);
renderPartitions(item, disk_info)
})
})
}