2026-05-28 17:04:29 +08:00
|
|
|
|
package testweb
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"fmt"
|
|
|
|
|
|
"os"
|
|
|
|
|
|
"path/filepath"
|
|
|
|
|
|
"regexp"
|
|
|
|
|
|
"sort"
|
|
|
|
|
|
"strings"
|
|
|
|
|
|
"time"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-07-21 16:35:46 +08:00
|
|
|
|
// logRelRE 允许 file 与 supervise 通道 api-log 相对路径。
|
|
|
|
|
|
var logRelRE = regexp.MustCompile(`^\d{4}-\d{2}-\d{2} \d{2}/\d{8,14}_(file|supervise)_[a-f0-9]+\.log$`)
|
2026-05-28 17:04:29 +08:00
|
|
|
|
|
2026-07-21 16:35:46 +08:00
|
|
|
|
// APILogEntry api-logs 中的日志条目。
|
|
|
|
|
|
type APILogEntry struct {
|
2026-05-28 17:04:29 +08:00
|
|
|
|
Rel string `json:"rel"`
|
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
|
HourDir string `json:"hourDir"`
|
2026-07-21 16:35:46 +08:00
|
|
|
|
Kind string `json:"kind"`
|
2026-05-28 17:04:29 +08:00
|
|
|
|
Modified string `json:"modified"`
|
|
|
|
|
|
Size int64 `json:"size"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-21 16:35:46 +08:00
|
|
|
|
// FileLogEntry 兼容旧 API(file 通道)。
|
|
|
|
|
|
type FileLogEntry = APILogEntry
|
|
|
|
|
|
|
|
|
|
|
|
// ListAPILogs 列出 api-logs;kind 为空则 file+supervise,否则 file 或 supervise。
|
|
|
|
|
|
func ListAPILogs(root, kind string) ([]APILogEntry, error) {
|
2026-05-28 17:04:29 +08:00
|
|
|
|
root, err := filepath.Abs(root)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
2026-07-21 16:35:46 +08:00
|
|
|
|
kind = strings.TrimSpace(strings.ToLower(kind))
|
|
|
|
|
|
var entries []APILogEntry
|
2026-05-28 17:04:29 +08:00
|
|
|
|
err = filepath.WalkDir(root, func(path string, d os.DirEntry, walkErr error) error {
|
|
|
|
|
|
if walkErr != nil {
|
|
|
|
|
|
return walkErr
|
|
|
|
|
|
}
|
|
|
|
|
|
if d.IsDir() {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
name := d.Name()
|
2026-07-21 16:35:46 +08:00
|
|
|
|
if !strings.HasSuffix(name, ".log") {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
entryKind := ""
|
|
|
|
|
|
if strings.Contains(name, "_file_") {
|
|
|
|
|
|
entryKind = "file"
|
|
|
|
|
|
} else if strings.Contains(name, "_supervise_") {
|
|
|
|
|
|
entryKind = "supervise"
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
if kind != "" && entryKind != kind {
|
2026-05-28 17:04:29 +08:00
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
rel, err := filepath.Rel(root, path)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
rel = filepath.ToSlash(rel)
|
|
|
|
|
|
if !logRelRE.MatchString(rel) {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
info, err := d.Info()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
2026-07-21 16:35:46 +08:00
|
|
|
|
entries = append(entries, APILogEntry{
|
2026-05-28 17:04:29 +08:00
|
|
|
|
Rel: rel,
|
|
|
|
|
|
Name: name,
|
2026-07-21 16:35:46 +08:00
|
|
|
|
HourDir: filepath.ToSlash(filepath.Dir(rel)),
|
|
|
|
|
|
Kind: entryKind,
|
2026-05-28 17:04:29 +08:00
|
|
|
|
Modified: info.ModTime().Format(time.RFC3339),
|
|
|
|
|
|
Size: info.Size(),
|
|
|
|
|
|
})
|
|
|
|
|
|
return nil
|
|
|
|
|
|
})
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
if os.IsNotExist(err) {
|
|
|
|
|
|
return nil, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
sort.Slice(entries, func(i, j int) bool {
|
|
|
|
|
|
return entries[i].Modified > entries[j].Modified
|
|
|
|
|
|
})
|
|
|
|
|
|
return entries, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-21 16:35:46 +08:00
|
|
|
|
// ListFileLogs 列出 api-logs 下所有 *_file_*.log(按修改时间倒序)。
|
|
|
|
|
|
func ListFileLogs(root string) ([]FileLogEntry, error) {
|
|
|
|
|
|
return ListAPILogs(root, "file")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ListSuperviseLogs 列出 supervise 通道日志。
|
|
|
|
|
|
func ListSuperviseLogs(root string) ([]APILogEntry, error) {
|
|
|
|
|
|
return ListAPILogs(root, "supervise")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-28 17:04:29 +08:00
|
|
|
|
// resolveLogRelPath 校验 rel 并返回绝对路径。
|
|
|
|
|
|
func resolveLogRelPath(root, rel string) (string, error) {
|
|
|
|
|
|
rel = filepath.ToSlash(strings.TrimSpace(rel))
|
|
|
|
|
|
if rel == "" || strings.Contains(rel, "..") {
|
|
|
|
|
|
return "", fmt.Errorf("invalid rel path")
|
|
|
|
|
|
}
|
|
|
|
|
|
if !logRelRE.MatchString(rel) {
|
|
|
|
|
|
return "", fmt.Errorf("rel path not allowed")
|
|
|
|
|
|
}
|
|
|
|
|
|
root, err := filepath.Abs(root)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return "", err
|
|
|
|
|
|
}
|
|
|
|
|
|
full := filepath.Join(root, filepath.FromSlash(rel))
|
|
|
|
|
|
full, err = filepath.Abs(full)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return "", err
|
|
|
|
|
|
}
|
|
|
|
|
|
if !strings.HasPrefix(full, root+string(os.PathSeparator)) && full != root {
|
|
|
|
|
|
return "", fmt.Errorf("path escape")
|
|
|
|
|
|
}
|
|
|
|
|
|
return full, nil
|
|
|
|
|
|
}
|