feat(网络): 添加基于 MAC 地址的网卡命名绑定功能

实现通过 systemd .link 文件按 MAC 地址固定网卡命名,避免重启或硬件变更后网卡顺序变化导致网络配置失效。新增功能包括:
- 支持创建、删除和查看 MAC 绑定
- 自动检测并清理旧式 udev 规则
- 提供冲突检测与依赖检查
- 更新 README 和界面截图
This commit is contained in:
Maple
2026-05-07 22:00:13 +08:00
parent c20e9666a6
commit 2bd646c746
5 changed files with 562 additions and 5 deletions

View File

@@ -8635,6 +8635,536 @@ host_network_validate_mtu() {
fi
}
host_network_get_iface_mac() {
local iface="$1"
local mac
mac="$(cat "/sys/class/net/${iface}/address" 2>/dev/null || true)"
[[ "$mac" =~ ^([0-9a-f]{2}:){5}[0-9a-f]{2}$ ]] && echo "$mac" || echo ""
}
host_network_get_physical_ifaces_with_mac() {
local iface mac
for iface in $(ip -o link show 2>/dev/null | awk -F': ' '{print $2}' | cut -d'@' -f1 | sort -u); do
[[ "$iface" == "lo" ]] && continue
# 跳过 bridge、bond、vlan 等虚拟接口
[[ -f "/sys/class/net/${iface}/device/vendor" ]] || continue
mac="$(host_network_get_iface_mac "$iface")"
[[ -n "$mac" ]] || continue
printf '%s|%s\n' "$iface" "$mac"
done
}
host_network_validate_mac() {
local mac="$1"
[[ "$mac" =~ ^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$ ]]
}
host_network_mac_to_iface() {
local target_mac="$1"
target_mac="$(echo "$target_mac" | tr '[:upper:]' '[:lower:]')"
local iface mac
for iface in $(ip -o link show 2>/dev/null | awk -F': ' '{print $2}' | cut -d'@' -f1 | sort -u); do
[[ "$iface" == "lo" ]] && continue
mac="$(host_network_get_iface_mac "$iface")"
[[ "$(echo "$mac" | tr '[:upper:]' '[:lower:]')" == "$target_mac" ]] && { echo "$iface"; return 0; }
done
return 1
}
host_network_validate_systemd_link_name() {
local name="$1"
[[ -n "$name" && ${#name} -le 15 ]] || return 1
[[ "$name" != "." && "$name" != ".." && "$name" != "all" && "$name" != "default" ]] || return 1
[[ "$name" =~ ^[A-Za-z0-9_.-]+$ ]] || return 1
[[ "$name" =~ ^[0-9]+$ ]] && return 1
}
host_network_systemd_link_dir() {
echo "/etc/systemd/network"
}
host_network_systemd_link_file_for_mac() {
local mac="$1"
local lower_mac
lower_mac="$(echo "$mac" | tr '[:upper:]' '[:lower:]')"
echo "$(host_network_systemd_link_dir)/10-pve-tools-${lower_mac//:/-}.link"
}
host_network_systemd_link_get_value() {
local file="$1"
local section="$2"
local key="$3"
[[ -f "$file" ]] || return 1
awk -v section="$section" -v key="$key" '
BEGIN { in_section=0 }
{
line=$0
sub(/[[:space:]]*#.*/, "", line)
sub(/^[[:space:]]+/, "", line)
sub(/[[:space:]]+$/, "", line)
if (line == "") next
if (line ~ /^\[[^]]+\]$/) {
in_section = (line == "[" section "]")
next
}
if (in_section && line ~ ("^" key "[[:space:]]*=[[:space:]]*")) {
sub("^" key "[[:space:]]*=[[:space:]]*", "", line)
gsub(/^"/, "", line)
gsub(/"$/, "", line)
print line
exit
}
}
' "$file"
}
host_network_systemd_link_file_has_binding() {
local file="$1"
local mac="$2"
local name="$3"
local lower_mac file_mac file_name
file_mac="$(host_network_systemd_link_get_value "$file" "Match" "MACAddress" 2>/dev/null || true)"
file_name="$(host_network_systemd_link_get_value "$file" "Link" "Name" 2>/dev/null || true)"
lower_mac="$(echo "$mac" | tr '[:upper:]' '[:lower:]')"
file_mac="$(echo "$file_mac" | tr '[:upper:]' '[:lower:]')"
[[ "$file_mac" == "$lower_mac" && "$file_name" == "$name" ]]
}
host_network_systemd_link_list_managed_files() {
local dir
dir="$(host_network_systemd_link_dir)"
[[ -d "$dir" ]] || return 0
find "$dir" -maxdepth 1 -type f -name '10-pve-tools-*.link' 2>/dev/null | sort
}
host_network_systemd_link_find_conflicts() {
local mac="$1"
local name="$2"
local target_file="$3"
local dir file file_mac file_name search_dirs=()
for dir in /etc/systemd/network /run/systemd/network /usr/local/lib/systemd/network /usr/lib/systemd/network; do
[[ -d "$dir" ]] && search_dirs+=("$dir")
done
(( ${#search_dirs[@]} > 0 )) || return 0
while IFS= read -r -d '' file; do
[[ "$file" == "$target_file" ]] && continue
file_mac="$(host_network_systemd_link_get_value "$file" "Match" "MACAddress" 2>/dev/null || true)"
file_name="$(host_network_systemd_link_get_value "$file" "Link" "Name" 2>/dev/null || true)"
file_mac="$(echo "$file_mac" | tr '[:upper:]' '[:lower:]')"
if [[ "$file_mac" == "$mac" ]]; then
printf '%s|MACAddress=%s\n' "$file" "$file_mac"
elif [[ "$file_name" == "$name" ]]; then
printf '%s|Name=%s\n' "$file" "$file_name"
fi
done < <(find "${search_dirs[@]}" -maxdepth 1 -type f -name '*.link' -print0 2>/dev/null)
}
host_network_systemd_link_write_binding() {
local mac="$1"
local name="$2"
local lower_mac target_dir target_file tmp backup_path=""
lower_mac="$(echo "$mac" | tr '[:upper:]' '[:lower:]')"
target_dir="$(host_network_systemd_link_dir)"
target_file="$(host_network_systemd_link_file_for_mac "$lower_mac")"
mkdir -p "$target_dir" 2>/dev/null || {
log_error "无法创建 systemd .link 目录: $target_dir"
return 1
}
if [[ -f "$target_file" ]]; then
if ! backup_file "$target_file" backup_path >/dev/null 2>&1; then
log_error "无法备份现有 systemd .link 文件: $target_file"
return 1
fi
fi
tmp="$(mktemp "${target_dir}/.pve-tools-link.XXXXXX")" || {
log_error "无法创建临时文件: $target_dir"
return 1
}
cat > "$tmp" <<EOF
# Generated by PVE-Tools
[Match]
MACAddress=$lower_mac
[Link]
Name=$name
EOF
chmod 0644 "$tmp" >/dev/null 2>&1 || true
if ! mv -f "$tmp" "$target_file"; then
rm -f "$tmp" >/dev/null 2>&1 || true
if [[ -n "$backup_path" && -f "$backup_path" ]]; then
cp -a "$backup_path" "$target_file" >/dev/null 2>&1 || true
else
rm -f "$target_file" >/dev/null 2>&1 || true
fi
log_error "写入 systemd .link 文件失败: $target_file"
return 1
fi
local verify_mac verify_name
verify_mac="$(host_network_systemd_link_get_value "$target_file" "Match" "MACAddress" 2>/dev/null || true)"
verify_name="$(host_network_systemd_link_get_value "$target_file" "Link" "Name" 2>/dev/null || true)"
verify_mac="$(echo "$verify_mac" | tr '[:upper:]' '[:lower:]')"
if [[ "$verify_mac" != "$lower_mac" || "$verify_name" != "$name" ]]; then
if [[ -n "$backup_path" && -f "$backup_path" ]]; then
cp -a "$backup_path" "$target_file" >/dev/null 2>&1 || rm -f "$target_file" >/dev/null 2>&1 || true
else
rm -f "$target_file" >/dev/null 2>&1 || true
fi
log_error "写入后的 systemd .link 规则校验失败: $target_file"
return 1
fi
printf '%s\n' "$target_file"
}
host_network_systemd_link_remove_binding_by_mac() {
local mac="$1"
local target_file backup_path=""
target_file="$(host_network_systemd_link_file_for_mac "$mac")"
[[ -f "$target_file" ]] || return 0
if ! backup_file "$target_file" backup_path >/dev/null 2>&1; then
log_error "无法备份待删除的 systemd .link 文件: $target_file"
return 1
fi
if ! rm -f "$target_file"; then
log_error "删除 systemd .link 文件失败: $target_file"
return 1
fi
[[ -e "$target_file" ]] && {
log_error "systemd .link 文件仍然存在: $target_file"
return 1
}
}
host_network_legacy_udev_rules_file() {
echo "/etc/udev/rules.d/70-persistent-net.rules"
}
host_network_legacy_udev_list_bindings() {
local file line mac legacy_name
file="$(host_network_legacy_udev_rules_file)"
[[ -f "$file" ]] || return 0
while IFS= read -r line; do
[[ -n "$line" ]] || continue
mac="$(echo "$line" | sed -n 's/.*ATTR{address}=="\([^"]*\)".*/\1/p')"
legacy_name="$(echo "$line" | sed -n 's/.*NAME="\([^"]*\)".*/\1/p')"
[[ -n "$mac" && -n "$legacy_name" ]] || continue
printf '%s|%s\n' "$mac" "$legacy_name"
done < "$file"
}
host_network_legacy_udev_name_conflicts() {
local mac="$1"
local name="$2"
local file line legacy_mac legacy_name lower_mac
file="$(host_network_legacy_udev_rules_file)"
[[ -f "$file" ]] || return 1
lower_mac="$(echo "$mac" | tr '[:upper:]' '[:lower:]')"
while IFS= read -r line; do
[[ -n "$line" ]] || continue
legacy_mac="$(echo "$line" | sed -n 's/.*ATTR{address}=="\([^"]*\)".*/\1/p')"
legacy_name="$(echo "$line" | sed -n 's/.*NAME="\([^"]*\)".*/\1/p')"
[[ -n "$legacy_mac" && -n "$legacy_name" ]] || continue
legacy_mac="$(echo "$legacy_mac" | tr '[:upper:]' '[:lower:]')"
if [[ "$legacy_name" == "$name" && "$legacy_mac" != "$lower_mac" ]]; then
return 0
fi
done < "$file"
return 1
}
host_network_legacy_udev_prune_binding() {
local mac="$1"
local file tmp backup_path="" lower_mac
file="$(host_network_legacy_udev_rules_file)"
[[ -f "$file" ]] || return 0
if ! backup_file "$file" backup_path >/dev/null 2>&1; then
log_error "无法备份 legacy udev 规则文件: $file"
return 1
fi
lower_mac="$(echo "$mac" | tr '[:upper:]' '[:lower:]')"
tmp="$(mktemp)" || {
log_error "无法创建临时文件用于清理 legacy udev 规则"
return 1
}
awk -v mac="$lower_mac" '
{
line=$0
line_lc=tolower(line)
if (index(line_lc, "ATTR{address}==\"" mac "\"") > 0) next
print
}
' "$file" > "$tmp"
if ! mv -f "$tmp" "$file"; then
rm -f "$tmp" >/dev/null 2>&1 || true
if [[ -n "$backup_path" && -f "$backup_path" ]]; then
cp -a "$backup_path" "$file" >/dev/null 2>&1 || true
fi
log_error "清理 legacy udev 规则失败: $file"
return 1
fi
[[ -s "$file" ]] || rm -f "$file"
}
host_network_interface_has_config_reference() {
local iface_name="$1"
awk -v iface_name="$iface_name" '
{
line=$0
sub(/[[:space:]]*#.*/, "", line)
sub(/^[[:space:]]+/, "", line)
sub(/[[:space:]]+$/, "", line)
if (line == "") next
n=split(line, fields, /[[:space:]]+/)
if (fields[1] == "iface" && n >= 2 && fields[2] == iface_name) {
print line
exit
}
if (fields[1] == "auto" || fields[1] ~ /^allow-/ || fields[1] == "bridge-ports" || fields[1] == "bond-slaves" || fields[1] == "vlan-raw-device") {
for (i=2; i<=n; i++) {
if (fields[i] == iface_name) {
print line
exit
}
}
}
}
' "$HOST_NETWORK_INTERFACES_FILE" 2>/dev/null | grep -q .
}
host_network_mac_binding_has_dependency() {
local iface_name="$1"
host_network_interface_has_master_dependency "$iface_name" && return 0
host_network_interface_has_config_reference "$iface_name"
}
host_network_show_mac_bindings() {
local file iface mac link_name
echo -e "${CYAN}当前 MAC 地址绑定systemd .link 规则):${NC}"
if host_network_systemd_link_list_managed_files | grep -q .; then
while IFS= read -r file; do
[[ -n "$file" ]] || continue
mac="$(host_network_systemd_link_get_value "$file" "Match" "MACAddress" 2>/dev/null || true)"
link_name="$(host_network_systemd_link_get_value "$file" "Link" "Name" 2>/dev/null || true)"
[[ -n "$mac" && -n "$link_name" ]] || continue
iface="$(host_network_mac_to_iface "$mac" 2>/dev/null || true)"
if [[ -n "$iface" ]]; then
printf ' %s → %s (当前接口: %s)\n' "$mac" "$link_name" "$iface"
else
printf ' %s → %s (当前未识别)\n' "$mac" "$link_name"
fi
done < <(host_network_systemd_link_list_managed_files)
else
echo " (无)"
fi
echo "$UI_DIVIDER"
echo -e "${YELLOW}旧式 udev 绑定(兼容清理中):${NC}"
if host_network_legacy_udev_list_bindings | grep -q .; then
while IFS='|' read -r mac link_name; do
[[ -n "$mac" ]] || continue
printf ' %s → %s\n' "$mac" "$link_name"
done < <(host_network_legacy_udev_list_bindings)
else
echo " (无)"
fi
echo "$UI_DIVIDER"
echo -e "${CYAN}物理网卡 MAC 地址列表:${NC}"
local entry iface_name iface_mac
while IFS='|' read -r iface_name iface_mac; do
[[ -n "$iface_name" ]] || continue
link_name="$(host_network_systemd_link_get_value "$(host_network_systemd_link_file_for_mac "$iface_mac")" "Link" "Name" 2>/dev/null || true)"
if [[ -n "$link_name" ]]; then
printf ' [%s] %s (已绑定为 %s)\n' "$iface_name" "$iface_mac" "$link_name"
else
printf ' [%s] %s\n' "$iface_name" "$iface_mac"
fi
done < <(host_network_get_physical_ifaces_with_mac)
}
host_network_create_mac_binding() {
local entries=()
local entry iface_name iface_mac idx pick fixed_name target_file backup_path link_name
while IFS='|' read -r iface_name iface_mac; do
[[ -n "$iface_name" ]] || continue
entries+=("$iface_name|$iface_mac")
done < <(host_network_get_physical_ifaces_with_mac)
if (( ${#entries[@]} == 0 )); then
display_error "未发现物理网卡" "请确认系统存在带有 PCI 设备的网络接口。"
return 1
fi
echo -e "${CYAN}可用物理网卡(按 MAC 地址固定命名):${NC}"
idx=1
for entry in "${entries[@]}"; do
iface_name="${entry%%|*}"
iface_mac="${entry##*|}"
link_name="$(host_network_systemd_link_get_value "$(host_network_systemd_link_file_for_mac "$iface_mac")" "Link" "Name" 2>/dev/null || true)"
if [[ -n "$link_name" ]]; then
printf ' [%d] %s | %s (已绑定: %s)\n' "$idx" "$iface_name" "$iface_mac" "$link_name"
else
printf ' [%d] %s | %s\n' "$idx" "$iface_name" "$iface_mac"
fi
idx=$((idx + 1))
done
echo "$UI_DIVIDER"
read -p "请选择要固定命名的网卡序号 (0 返回): " pick
[[ "$pick" == "0" ]] && return 0
[[ "$pick" =~ ^[0-9]+$ ]] || { display_error "无效选择"; return 1; }
if (( pick < 1 || pick > ${#entries[@]} )); then
display_error "选择超出范围"
return 1
fi
entry="${entries[$((pick - 1))]}"
iface_name="${entry%%|*}"
iface_mac="${entry##*|}"
target_file="$(host_network_systemd_link_file_for_mac "$iface_mac")"
read -r -p "请输入固定接口名称(如 lan0、pve-eth0: " fixed_name
[[ -n "$fixed_name" ]] || { display_error "接口名称不能为空"; return 1; }
host_network_validate_systemd_link_name "$fixed_name" || {
display_error "接口名称不合法: $fixed_name" "名称需为 1-15 位只允许字母、数字、_、.、-,且不能是纯数字或保留名。"
return 1
}
if [[ "$fixed_name" != "$iface_name" ]] && host_network_iface_exists "$fixed_name"; then
display_error "目标接口名称已被占用: $fixed_name" "请改用未被现有接口使用的名称。"
return 1
fi
if host_network_mac_binding_has_dependency "$iface_name"; then
display_error "该网卡已被宿主机网络配置引用" "请先迁移或删除依赖该接口的 bridge / bond / VLAN / direct iface 配置,再修改命名。"
return 1
fi
if host_network_legacy_udev_name_conflicts "$iface_mac" "$fixed_name"; then
display_error "旧式 udev 规则中已存在同名绑定" "请先人工处理 /etc/udev/rules.d/70-persistent-net.rules 中的同名规则,再继续迁移。"
return 1
fi
mapfile -t conflicts < <(host_network_systemd_link_find_conflicts "$iface_mac" "$fixed_name" "$target_file")
if (( ${#conflicts[@]} > 0 )); then
local conflict_report="检测到其他 systemd .link 文件已经匹配相同 MAC 或同名接口:"
local conflict
for conflict in "${conflicts[@]}"; do
conflict_report+=$'\n - '"${conflict%%|*} (${conflict##*|})"
done
display_error "检测到 systemd .link 规则冲突" "$conflict_report"
return 1
fi
if [[ -f "$target_file" ]] && host_network_systemd_link_file_has_binding "$target_file" "$iface_mac" "$fixed_name"; then
display_success "固定命名规则已存在" "文件: $target_file;重启宿主机后仍会保持该命名。"
return 0
fi
if ! confirm_high_risk_action "为 MAC 地址 ${iface_mac} 写入固定命名 ${fixed_name}" "将生成 ${target_file},让 systemd 根据 MAC 在启动时重命名接口。" "固定名与现有接口或其他 .link 文件冲突时,接口命名可能变得不可预测。" "请确认名称唯一,并准备好控制台或带外管理手段以便回滚。" "BIND-LINK"; then
return 0
fi
if ! host_network_legacy_udev_prune_binding "$iface_mac"; then
display_error "清理旧式 udev 绑定失败" "请先处理 /etc/udev/rules.d/70-persistent-net.rules 后重试。"
return 1
fi
if ! target_file="$(host_network_systemd_link_write_binding "$iface_mac" "$fixed_name")"; then
display_error "MAC 地址固定命名写入失败" "请先处理冲突提示,或检查 /etc/systemd/network 目录权限。"
return 1
fi
display_success "MAC 地址固定命名规则已写入" "文件: $target_file;建议重启宿主机后生效。"
}
host_network_delete_mac_binding() {
local file mac link_name entries=() idx pick file_list
file_list="$(host_network_systemd_link_list_managed_files)" || file_list=""
while IFS= read -r file; do
[[ -n "$file" ]] || continue
mac="$(host_network_systemd_link_get_value "$file" "Match" "MACAddress" 2>/dev/null || true)"
link_name="$(host_network_systemd_link_get_value "$file" "Link" "Name" 2>/dev/null || true)"
[[ -n "$mac" && -n "$link_name" ]] || continue
entries+=("$file|$mac|$link_name")
done <<< "$file_list"
if (( ${#entries[@]} == 0 )); then
display_error "没有可删除的 MAC 绑定规则" "当前没有 PVE-Tools 管理的 systemd .link 文件。"
return 1
fi
echo -e "${CYAN}已存在的 MAC 地址绑定:${NC}"
idx=1
for entry in "${entries[@]}"; do
file="${entry%%|*}"
mac="${entry#*|}"
mac="${mac%%|*}"
link_name="${entry##*|}"
printf ' [%d] %s → %s (%s)\n' "$idx" "$mac" "$link_name" "$(basename "$file")"
idx=$((idx + 1))
done
echo "$UI_DIVIDER"
read -p "请选择要删除的绑定序号 (0 返回): " pick
[[ "$pick" == "0" ]] && return 0
[[ "$pick" =~ ^[0-9]+$ ]] || { display_error "无效选择"; return 1; }
if (( pick < 1 || pick > ${#entries[@]} )); then
display_error "选择超出范围"
return 1
fi
entry="${entries[$((pick - 1))]}"
file="${entry%%|*}"
mac="${entry#*|}"
mac="${mac%%|*}"
link_name="${entry##*|}"
if ! confirm_action "删除 MAC 地址固定命名 ${mac}${link_name}"; then
return 0
fi
if ! host_network_legacy_udev_prune_binding "$mac"; then
display_error "清理旧式 udev 绑定失败" "请检查 /etc/udev/rules.d/70-persistent-net.rules 的权限与内容。"
return 1
fi
if ! host_network_systemd_link_remove_binding_by_mac "$mac"; then
display_error "删除 MAC 地址固定命名失败" "请检查文件权限或磁盘状态。"
return 1
fi
display_success "MAC 地址固定命名已删除: ${mac}${link_name}" "如需恢复,请重新创建对应 .link 文件。"
}
host_network_validate_ipv4() {
local ip="$1"
[[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || return 1
@@ -9279,13 +9809,39 @@ host_network_bridge_menu() {
show_menu_option "1" "列出当前网卡与桥接"
show_menu_option "2" "创建桥接"
show_menu_option "3" "删除桥接"
show_menu_option "4" "MAC 地址绑定管理(防网卡顺序变化)"
show_menu_option "0" "返回"
show_menu_footer
read -p "请选择操作 [0-3]: " choice
read -p "请选择操作 [0-4]: " choice
case "$choice" in
1) host_network_show_current_overview ;;
2) host_network_create_bridge ;;
3) host_network_delete_bridge ;;
4) host_network_mac_binding_menu ;;
0) return ;;
*) log_error "无效选择" ;;
esac
pause_function
done
}
host_network_mac_binding_menu() {
while true; do
clear
show_menu_header "MAC 地址绑定管理"
echo -e "${YELLOW}说明:通过 MAC 地址而不是接口名来固定网卡命名,避免重启或硬件变更后网卡顺序变化导致网络配置失效。${NC}"
echo -e "${YELLOW}绑定后会在 /etc/systemd/network/ 中生成 .link 规则;若系统残留旧式 udev 规则,脚本会尝试同步清理。${NC}"
echo "$UI_DIVIDER"
host_network_show_mac_bindings
echo "$UI_DIVIDER"
show_menu_option "1" "创建 MAC 地址绑定"
show_menu_option "2" "删除 MAC 地址绑定"
show_menu_option "0" "返回"
show_menu_footer
read -p "请选择操作 [0-2]: " choice
case "$choice" in
1) host_network_create_mac_binding ;;
2) host_network_delete_mac_binding ;;
0) return ;;
*) log_error "无效选择" ;;
esac

View File

@@ -11,7 +11,7 @@
[![Proxmox VE](https://img.shields.io/badge/Proxmox-VE%209.x-E57000?logo=proxmox&logoColor=white)](https://www.proxmox.com/)
[![Debian](https://img.shields.io/badge/Debian-13%20(Trixie)-A81D33?logo=debian&logoColor=white)](https://www.debian.org/)
![产品截图](./images/main-cover.png)
![产品截图](./images/main.png)
</div>

View File

@@ -2,8 +2,8 @@
"upcoming_features": [
{
"title": "支持按 MAC 地址绑定网卡到 Bridge",
"status": "in-progress",
"description": "通过 MAC 地址而不是 PCIe 地址来识别网卡,避免重启或硬件变更后网卡顺序变化,导致网络配置失效甚至失联。"
"status": "completed",
"description": "通过 systemd .link 按 MAC 地址固定网卡命名,避免重启或硬件变更后网卡顺序变化,导致网络配置失效甚至失联。"
},
{
"title": "提供 Intel 核显 ROM 自动编译与下载服务",
@@ -88,4 +88,4 @@
"description": "PVE-Tools 1.0 发布,奠定了一键优化与维护脚本的基础。"
}
]
}
}

1
images/cover.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.1 KiB

BIN
images/main.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB