feat: 新增 disk.inode/disk.check 磁盘健康检查命令
This commit is contained in:
+88
-4
@@ -2,16 +2,26 @@
|
|||||||
|
|
||||||
> 返回 [README.md](README.md)
|
> 返回 [README.md](README.md)
|
||||||
|
|
||||||
看哪个目录、哪个文件最占空间。由 kopsc 内置实现,Linux、macOS、Windows 输出完全一致,
|
看哪个目录、哪个文件最占空间,查看 inode 使用,一键磁盘健康检查。
|
||||||
不依赖 `du`、`sort` 或 PowerShell。
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
kopsc disk usage # 当前目录的占用排行(路径可省略)
|
kopsc disk usage # 当前目录的占用排行(路径可省略)
|
||||||
kopsc disk usage /var # 指定目录
|
kopsc disk usage /var # 指定目录
|
||||||
kopsc disk usage /var --depth 2 # 下钻一层
|
kopsc disk usage /var --depth 2 # 下钻一层
|
||||||
kopsc disk usage --path /var # 用命名参数指定路径
|
kopsc disk inode # 查看 inode 使用情况
|
||||||
|
kopsc disk check # 一键磁盘健康检查
|
||||||
|
kopsc disk check /var # 检查指定路径
|
||||||
|
kopsc disk check --all # 检查所有挂载点
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 命令一览
|
||||||
|
|
||||||
|
| 命令 | 作用 |
|
||||||
|
| --- | --- |
|
||||||
|
| `kopsc disk usage [路径]` | 查看目录占用排行 |
|
||||||
|
| `kopsc disk inode [路径]` | 查看 inode 使用情况 |
|
||||||
|
| `kopsc disk check [路径]` | 一键磁盘健康检查 |
|
||||||
|
|
||||||
## 磁盘占用排行(disk.usage)
|
## 磁盘占用排行(disk.usage)
|
||||||
|
|
||||||
`kopsc disk usage [路径]` 等价于 `du -x --max-depth=1 -h <路径> | sort -hr`:
|
`kopsc disk usage [路径]` 等价于 `du -x --max-depth=1 -h <路径> | sort -hr`:
|
||||||
@@ -35,7 +45,7 @@ $ kopsc disk usage /var
|
|||||||
- 输出按大小降序,大小采用与 `du -h` 相同的二进制单位(1.0K、1.5M、2.0G)。
|
- 输出按大小降序,大小采用与 `du -h` 相同的二进制单位(1.0K、1.5M、2.0G)。
|
||||||
- 末尾输出该路径的合计行。
|
- 末尾输出该路径的合计行。
|
||||||
|
|
||||||
## 选项
|
### 选项
|
||||||
|
|
||||||
| 选项 | 说明 |
|
| 选项 | 说明 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
@@ -43,3 +53,77 @@ $ kopsc disk usage /var
|
|||||||
| `--depth N` / `-d N` / `--depth=N` | 列到的层数,默认 1;必须大于 0 |
|
| `--depth N` / `-d N` / `--depth=N` | 列到的层数,默认 1;必须大于 0 |
|
||||||
|
|
||||||
> `--path`、`--depth` 是 kopsc 自己的选项,不是 `du` 的参数,因此可以写在任意位置。
|
> `--path`、`--depth` 是 kopsc 自己的选项,不是 `du` 的参数,因此可以写在任意位置。
|
||||||
|
|
||||||
|
## inode 使用查看(disk.inode)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kopsc disk inode # 当前文件系统
|
||||||
|
kopsc disk inode /var # 指定路径所在文件系统
|
||||||
|
```
|
||||||
|
|
||||||
|
显示文件系统的 inode 使用统计。inode 耗尽会导致无法创建新文件,即使磁盘空间充足。
|
||||||
|
|
||||||
|
输出示例:
|
||||||
|
|
||||||
|
```
|
||||||
|
Filesystem Inodes IUsed IFree IUse% Mounted on
|
||||||
|
/dev/vda1 3276800 180432 3096368 6% /
|
||||||
|
```
|
||||||
|
|
||||||
|
| 平台 | 实际执行 |
|
||||||
|
| --- | --- |
|
||||||
|
| Linux / macOS | `df -i` |
|
||||||
|
| Windows | 不支持(NTFS 使用 MFT,无 inode 概念) |
|
||||||
|
|
||||||
|
## 一键磁盘健康检查(disk.check)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kopsc disk check # 检查根目录
|
||||||
|
kopsc disk check /var # 检查指定路径
|
||||||
|
kopsc disk check --all # 检查所有挂载点
|
||||||
|
```
|
||||||
|
|
||||||
|
一条命令完成磁盘排障,综合检查 5 项内容:
|
||||||
|
|
||||||
|
输出示例:
|
||||||
|
|
||||||
|
```
|
||||||
|
=== 磁盘健康检查 ===
|
||||||
|
|
||||||
|
[1/5] 空间使用
|
||||||
|
/dev/vda1 50G 总计 22G 已用 28G 可用 44% 使用率 ✓ 正常
|
||||||
|
|
||||||
|
[2/5] inode 使用
|
||||||
|
/dev/vda1 3.2M 总计 180K 已用 3.0M 可用 5% 使用率 ✓ 正常
|
||||||
|
|
||||||
|
[3/5] 大文件 TOP 5(>100M)
|
||||||
|
2.1G /var/log/syslog
|
||||||
|
1.5G /var/lib/docker/overlay2/...
|
||||||
|
800M /opt/app/data.db
|
||||||
|
|
||||||
|
[4/5] 大目录 TOP 5
|
||||||
|
15G /var/lib/docker
|
||||||
|
2.3G /var/log
|
||||||
|
1.2G /opt/app
|
||||||
|
|
||||||
|
[5/5] 建议
|
||||||
|
✓ 磁盘空间充足
|
||||||
|
✓ inode 使用正常
|
||||||
|
```
|
||||||
|
|
||||||
|
### 检查项目
|
||||||
|
|
||||||
|
| 项目 | 说明 |
|
||||||
|
| --- | --- |
|
||||||
|
| 空间使用 | 磁盘使用率,>80% 警告,>90% 危险 |
|
||||||
|
| inode 使用 | inode 使用率(Unix only) |
|
||||||
|
| 大文件 | 列出 >100M 的文件 TOP 5 |
|
||||||
|
| 大目录 | 列出直接子目录占用 TOP 5 |
|
||||||
|
| 建议 | 根据检查结果给出清理建议 |
|
||||||
|
|
||||||
|
### 选项
|
||||||
|
|
||||||
|
| 选项 | 说明 |
|
||||||
|
| --- | --- |
|
||||||
|
| `[路径]` | 要检查的路径,默认 `/` |
|
||||||
|
| `--all` / `-a` | 检查所有挂载点 |
|
||||||
|
|||||||
@@ -0,0 +1,423 @@
|
|||||||
|
package executor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// runDiskCheck implements `kopsc disk check`: comprehensive disk health check.
|
||||||
|
func runDiskCheck(ctx context.Context, req BuiltinRequest) error {
|
||||||
|
opts, err := parseDiskCheckArgs(req.Args)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
w := req.Stdout
|
||||||
|
|
||||||
|
fmt.Fprintln(w, "=== 磁盘健康检查 ===")
|
||||||
|
fmt.Fprintln(w)
|
||||||
|
|
||||||
|
if opts.all {
|
||||||
|
// Check all mount points
|
||||||
|
return diskCheckAll(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
path := opts.path
|
||||||
|
if path == "" {
|
||||||
|
path = "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
return diskCheckPath(ctx, req, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
type diskCheckOptions struct {
|
||||||
|
path string
|
||||||
|
all bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseDiskCheckArgs(args []string) (diskCheckOptions, error) {
|
||||||
|
var opts diskCheckOptions
|
||||||
|
var positional []string
|
||||||
|
|
||||||
|
for i := 0; i < len(args); i++ {
|
||||||
|
arg := args[i]
|
||||||
|
switch {
|
||||||
|
case arg == "--":
|
||||||
|
positional = append(positional, args[i+1:]...)
|
||||||
|
i = len(args)
|
||||||
|
case arg == "--all" || arg == "-a":
|
||||||
|
opts.all = true
|
||||||
|
case strings.HasPrefix(arg, "-") && arg != "-":
|
||||||
|
return opts, fmt.Errorf("未知选项 %q", arg)
|
||||||
|
default:
|
||||||
|
positional = append(positional, arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(positional) > 0 {
|
||||||
|
opts.path = positional[0]
|
||||||
|
}
|
||||||
|
return opts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func diskCheckAll(ctx context.Context, req BuiltinRequest) error {
|
||||||
|
// Get all mount points from df
|
||||||
|
var cmd *exec.Cmd
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
cmd = exec.CommandContext(ctx, "wmic", "logicaldisk", "get", "caption,size,freespace", "/format:list")
|
||||||
|
} else {
|
||||||
|
cmd = exec.CommandContext(ctx, "df", "-h")
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("获取磁盘信息失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse df output to get mount points
|
||||||
|
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
|
||||||
|
if len(lines) < 2 {
|
||||||
|
return fmt.Errorf("无法解析磁盘信息")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip header line
|
||||||
|
for _, line := range lines[1:] {
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) < 6 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
mountPoint := fields[len(fields)-1]
|
||||||
|
if runtime.GOOS != "windows" {
|
||||||
|
fmt.Fprintf(req.Stdout, "\n--- %s ---\n\n", mountPoint)
|
||||||
|
if err := diskCheckPath(ctx, req, mountPoint); err != nil {
|
||||||
|
fmt.Fprintf(req.Stderr, "检查 %s 失败: %v\n", mountPoint, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func diskCheckPath(ctx context.Context, req BuiltinRequest, path string) error {
|
||||||
|
w := req.Stdout
|
||||||
|
|
||||||
|
// 1. Space usage
|
||||||
|
fmt.Fprintln(w, "[1/5] 空间使用")
|
||||||
|
diskCheckSpace(ctx, w, path)
|
||||||
|
fmt.Fprintln(w)
|
||||||
|
|
||||||
|
// 2. Inode usage (Unix only)
|
||||||
|
if runtime.GOOS != "windows" {
|
||||||
|
fmt.Fprintln(w, "[2/5] inode 使用")
|
||||||
|
diskCheckInode(ctx, w, path)
|
||||||
|
fmt.Fprintln(w)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintln(w, "[2/5] inode 使用")
|
||||||
|
fmt.Fprintln(w, " Windows 不支持 inode")
|
||||||
|
fmt.Fprintln(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Large files
|
||||||
|
fmt.Fprintln(w, "[3/5] 大文件 TOP 5(>100M)")
|
||||||
|
diskCheckLargeFiles(ctx, w, path)
|
||||||
|
fmt.Fprintln(w)
|
||||||
|
|
||||||
|
// 4. Large directories
|
||||||
|
fmt.Fprintln(w, "[4/5] 大目录 TOP 5")
|
||||||
|
diskCheckLargeDirs(ctx, w, path)
|
||||||
|
fmt.Fprintln(w)
|
||||||
|
|
||||||
|
// 5. Recommendations
|
||||||
|
fmt.Fprintln(w, "[5/5] 建议")
|
||||||
|
diskCheckRecommendations(ctx, w, path)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func diskCheckSpace(ctx context.Context, w io.Writer, path string) {
|
||||||
|
var cmd *exec.Cmd
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
cmd = exec.CommandContext(ctx, "wmic", "logicaldisk", "get", "size,freespace,caption", "/format:list")
|
||||||
|
} else {
|
||||||
|
cmd = exec.CommandContext(ctx, "df", "-h", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(w, " 无法获取磁盘空间信息")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
|
||||||
|
if len(lines) < 2 {
|
||||||
|
fmt.Fprintln(w, " 无法解析磁盘信息")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse and display
|
||||||
|
for _, line := range lines[1:] {
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) < 5 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
// Parse WMIC output
|
||||||
|
fmt.Fprintf(w, " %s\n", line)
|
||||||
|
} else {
|
||||||
|
// Parse df output: Filesystem Size Used Avail Use% Mounted
|
||||||
|
filesystem := fields[0]
|
||||||
|
size := fields[1]
|
||||||
|
used := fields[2]
|
||||||
|
avail := fields[3]
|
||||||
|
usePct := fields[4]
|
||||||
|
mount := fields[5]
|
||||||
|
|
||||||
|
status := "✓ 正常"
|
||||||
|
if pct, err := strconv.Atoi(strings.TrimSuffix(usePct, "%")); err == nil {
|
||||||
|
if pct >= 90 {
|
||||||
|
status = "✗ 危险!空间即将耗尽"
|
||||||
|
} else if pct >= 80 {
|
||||||
|
status = "! 警告:空间使用率较高"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(w, " %s %s 总计 %s 已用 %s 可用 %s 使用率 %s\n",
|
||||||
|
filesystem, size, used, avail, usePct, status)
|
||||||
|
if mount != path {
|
||||||
|
fmt.Fprintf(w, " 挂载点: %s\n", mount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func diskCheckInode(ctx context.Context, w io.Writer, path string) {
|
||||||
|
cmd := exec.CommandContext(ctx, "df", "-i", path)
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(w, " 无法获取 inode 信息")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
|
||||||
|
if len(lines) < 2 {
|
||||||
|
fmt.Fprintln(w, " 无法解析 inode 信息")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, line := range lines[1:] {
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) < 5 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
filesystem := fields[0]
|
||||||
|
inodes := fields[1]
|
||||||
|
used := fields[2]
|
||||||
|
free := fields[3]
|
||||||
|
usePct := fields[4]
|
||||||
|
|
||||||
|
status := "✓ 正常"
|
||||||
|
if pct, err := strconv.Atoi(strings.TrimSuffix(usePct, "%")); err == nil {
|
||||||
|
if pct >= 90 {
|
||||||
|
status = "✗ 危险!inode 即将耗尽"
|
||||||
|
} else if pct >= 80 {
|
||||||
|
status = "! 警告:inode 使用率较高"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(w, " %s %s 总计 %s 已用 %s 可用 %s 使用率 %s\n",
|
||||||
|
filesystem, inodes, used, free, usePct, status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func diskCheckLargeFiles(ctx context.Context, w io.Writer, path string) {
|
||||||
|
type fileEntry struct {
|
||||||
|
path string
|
||||||
|
size int64
|
||||||
|
}
|
||||||
|
|
||||||
|
var files []fileEntry
|
||||||
|
threshold := int64(100 * 1024 * 1024) // 100MB
|
||||||
|
|
||||||
|
err := filepath.WalkDir(path, func(p string, d fs.DirEntry, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return nil // Skip errors
|
||||||
|
}
|
||||||
|
if d.IsDir() {
|
||||||
|
// Skip common non-user directories
|
||||||
|
base := filepath.Base(p)
|
||||||
|
if base == ".git" || base == "node_modules" || base == ".cache" {
|
||||||
|
return filepath.SkipDir
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if info, err := d.Info(); err == nil && info.Mode().IsRegular() {
|
||||||
|
if info.Size() >= threshold {
|
||||||
|
files = append(files, fileEntry{path: p, size: info.Size()})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(w, " 扫描失败: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(files) == 0 {
|
||||||
|
fmt.Fprintln(w, " 没有发现大于 100M 的文件 ✓")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by size descending
|
||||||
|
sort.Slice(files, func(i, j int) bool {
|
||||||
|
return files[i].size > files[j].size
|
||||||
|
})
|
||||||
|
|
||||||
|
// Show top 5
|
||||||
|
limit := 5
|
||||||
|
if len(files) < limit {
|
||||||
|
limit = len(files)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < limit; i++ {
|
||||||
|
fmt.Fprintf(w, " %s %s\n", humanSize(files[i].size), files[i].path)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(files) > 5 {
|
||||||
|
fmt.Fprintf(w, " ... 还有 %d 个大文件\n", len(files)-5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func diskCheckLargeDirs(ctx context.Context, w io.Writer, path string) {
|
||||||
|
type dirEntry struct {
|
||||||
|
path string
|
||||||
|
size int64
|
||||||
|
}
|
||||||
|
|
||||||
|
var dirs []dirEntry
|
||||||
|
|
||||||
|
// Only check direct children
|
||||||
|
entries, err := os.ReadDir(path)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(w, " 无法读取目录: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
if !entry.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
dirPath := filepath.Join(path, entry.Name())
|
||||||
|
size, err := dirSize(ctx, dirPath, 0, false)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if size > 0 {
|
||||||
|
dirs = append(dirs, dirEntry{path: dirPath, size: size})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(dirs) == 0 {
|
||||||
|
fmt.Fprintln(w, " 没有子目录")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by size descending
|
||||||
|
sort.Slice(dirs, func(i, j int) bool {
|
||||||
|
return dirs[i].size > dirs[j].size
|
||||||
|
})
|
||||||
|
|
||||||
|
// Show top 5
|
||||||
|
limit := 5
|
||||||
|
if len(dirs) < limit {
|
||||||
|
limit = len(dirs)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < limit; i++ {
|
||||||
|
fmt.Fprintf(w, " %s %s\n", humanSize(dirs[i].size), dirs[i].path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func diskCheckRecommendations(ctx context.Context, w io.Writer, path string) {
|
||||||
|
var recommendations []string
|
||||||
|
|
||||||
|
// Check disk usage percentage
|
||||||
|
cmd := exec.CommandContext(ctx, "df", path)
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err == nil {
|
||||||
|
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
|
||||||
|
if len(lines) >= 2 {
|
||||||
|
fields := strings.Fields(lines[1])
|
||||||
|
if len(fields) >= 5 {
|
||||||
|
usePct := fields[4]
|
||||||
|
if pct, err := strconv.Atoi(strings.TrimSuffix(usePct, "%")); err == nil {
|
||||||
|
if pct >= 90 {
|
||||||
|
recommendations = append(recommendations, "✗ 磁盘空间严重不足!建议立即清理或扩容")
|
||||||
|
} else if pct >= 80 {
|
||||||
|
recommendations = append(recommendations, "! 磁盘空间使用率较高,建议清理不必要的文件")
|
||||||
|
} else {
|
||||||
|
recommendations = append(recommendations, "✓ 磁盘空间充足")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check inode usage
|
||||||
|
if runtime.GOOS != "windows" {
|
||||||
|
cmd = exec.CommandContext(ctx, "df", "-i", path)
|
||||||
|
out, err = cmd.Output()
|
||||||
|
if err == nil {
|
||||||
|
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
|
||||||
|
if len(lines) >= 2 {
|
||||||
|
fields := strings.Fields(lines[1])
|
||||||
|
if len(fields) >= 5 {
|
||||||
|
usePct := fields[4]
|
||||||
|
if pct, err := strconv.Atoi(strings.TrimSuffix(usePct, "%")); err == nil {
|
||||||
|
if pct >= 90 {
|
||||||
|
recommendations = append(recommendations, "✗ inode 严重不足!大量小文件可能耗尽 inode")
|
||||||
|
} else if pct >= 80 {
|
||||||
|
recommendations = append(recommendations, "! inode 使用率较高")
|
||||||
|
} else {
|
||||||
|
recommendations = append(recommendations, "✓ inode 使用正常")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for common cleanup targets
|
||||||
|
commonTargets := []string{
|
||||||
|
"/var/log",
|
||||||
|
"/tmp",
|
||||||
|
"/var/cache",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, target := range commonTargets {
|
||||||
|
if info, err := os.Stat(target); err == nil && info.IsDir() {
|
||||||
|
size, _ := dirSize(ctx, target, 0, false)
|
||||||
|
if size > 1024*1024*1024 { // > 1GB
|
||||||
|
recommendations = append(recommendations, fmt.Sprintf("! %s 占用 %s,可考虑清理", target, humanSize(size)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(recommendations) == 0 {
|
||||||
|
fmt.Fprintln(w, " ✓ 一切正常")
|
||||||
|
} else {
|
||||||
|
for _, rec := range recommendations {
|
||||||
|
fmt.Fprintf(w, " %s\n", rec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package executor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// runDiskInode implements `kopsc disk inode`: show inode usage.
|
||||||
|
func runDiskInode(ctx context.Context, req BuiltinRequest) error {
|
||||||
|
opts, err := parseDiskInodeArgs(req.Args)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
path := opts.path
|
||||||
|
if path == "" {
|
||||||
|
path = "."
|
||||||
|
}
|
||||||
|
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "linux", "darwin":
|
||||||
|
return diskInodeUnix(ctx, req, path)
|
||||||
|
case "windows":
|
||||||
|
return fmt.Errorf("Windows 不支持 inode(NTFS 使用 MFT,无 inode 概念)")
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("不支持的操作系统: %s", runtime.GOOS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type diskInodeOptions struct {
|
||||||
|
path string
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseDiskInodeArgs(args []string) (diskInodeOptions, error) {
|
||||||
|
var opts diskInodeOptions
|
||||||
|
var positional []string
|
||||||
|
|
||||||
|
for i := 0; i < len(args); i++ {
|
||||||
|
arg := args[i]
|
||||||
|
switch {
|
||||||
|
case arg == "--":
|
||||||
|
positional = append(positional, args[i+1:]...)
|
||||||
|
i = len(args)
|
||||||
|
case strings.HasPrefix(arg, "-") && arg != "-":
|
||||||
|
return opts, fmt.Errorf("未知选项 %q", arg)
|
||||||
|
default:
|
||||||
|
positional = append(positional, arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(positional) > 0 {
|
||||||
|
opts.path = positional[0]
|
||||||
|
}
|
||||||
|
return opts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func diskInodeUnix(ctx context.Context, req BuiltinRequest, path string) error {
|
||||||
|
// Use df -i to get inode info
|
||||||
|
cmd := exec.CommandContext(ctx, "df", "-i", path)
|
||||||
|
cmd.Stdout = req.Stdout
|
||||||
|
cmd.Stderr = req.Stderr
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user