Files
2026-07-09 14:16:56 +08:00

239 lines
7.2 KiB
Go

package controller
import (
"encoding/base64"
"fmt"
"io"
"log"
"path"
"strconv"
"strings"
"ezops/model"
"github.com/gin-gonic/gin"
)
func (h *KubernetesHandler) ListPodFilesNS(c *gin.Context) {
h.listPodFilesWithNS(c, c.Param("namespace"))
}
func (h *KubernetesHandler) listPodFilesWithNS(c *gin.Context, ns string) {
clusterID, _ := strconv.Atoi(c.Param("id"))
if clusterID == 0 {
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
}
podName := c.Param("podName")
var req struct {
Path string `json:"path"`
Container string `json:"container"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, model.Response{Code: 400, Message: "参数错误: " + err.Error()})
return
}
if req.Path == "" {
req.Path = "/"
}
client, _, err := h.getK8sClient(clusterID)
if err != nil {
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
return
}
// ls -la — BusyBox ignores --time-style, use default "Mon DD HH:MM" format
cmd := []string{"ls", "-la", req.Path}
result, err := client.ExecInPod(ns, podName, req.Container, cmd)
if err != nil {
c.JSON(500, model.Response{Code: 500, Message: "执行 ls 失败: " + err.Error()})
return
}
type FileInfo struct {
Name string `json:"name"`
Size string `json:"size"`
Mode string `json:"mode"`
ModTime string `json:"mod_time"`
IsDir bool `json:"is_dir"`
}
files := []FileInfo{}
output := result.Stdout // WebSocket output is already clean text
for _, line := range strings.Split(output, "\n") {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "total ") {
continue
}
fields := strings.Fields(line)
if len(fields) < 7 {
continue
}
mode := fields[0]
if len(mode) == 0 || (mode[0] != 'd' && mode[0] != '-' && mode[0] != 'l') {
continue
}
size := fields[4]
// BusyBox ls date format: "Jun 23 02:17" (3 fields: month day time)
// GNU ls --time-style: "2026-06-23T02:17:00" (1 field)
// Detect format: if fields[5] contains ":" it's time-only (BusyBox style)
var name, modTime string
if strings.Contains(fields[5], ":") || strings.Contains(fields[5], "T") {
// GNU style: single timestamp field
modTime = fields[5]
name = strings.Join(fields[6:], " ")
} else {
// BusyBox style: "Mon DD HH:MM" = 3 fields starting at index 5
modTime = strings.Join(fields[5:8], " ")
name = strings.Join(fields[8:], " ")
}
if name == "" || name == "." || name == ".." {
continue
}
files = append(files, FileInfo{
Name: name,
Size: size,
Mode: mode,
ModTime: modTime,
IsDir: mode[0] == 'd',
})
}
c.JSON(200, model.Response{Code: 200, Message: "success", Data: gin.H{"path": req.Path, "files": files}})
}
// DownloadPodFileNS 下载 Pod 内指定路径的文件(通过 exec cat)
func (h *KubernetesHandler) DownloadPodFileNS(c *gin.Context) {
h.downloadPodFileWithNS(c, c.Param("namespace"))
}
func (h *KubernetesHandler) downloadPodFileWithNS(c *gin.Context, ns string) {
clusterID, _ := strconv.Atoi(c.Param("id"))
if clusterID == 0 {
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
}
podName := c.Param("podName")
filePath := c.Query("path")
container := c.Query("container")
if filePath == "" {
c.JSON(400, model.Response{Code: 400, Message: "path 参数不能为空"})
return
}
client, _, err := h.getK8sClient(clusterID)
if err != nil {
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
return
}
cmd := []string{"cat", filePath}
result, err := client.ExecInPod(ns, podName, container, cmd)
if err != nil {
c.JSON(500, model.Response{Code: 500, Message: "读取文件失败: " + err.Error()})
return
}
content := result.Stdout
fileName := path.Base(filePath)
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", fileName))
c.Header("Content-Type", "application/octet-stream")
c.String(200, content)
}
// UploadPodFileNS 上传文件到 Pod 容器内指定目录(通过 exec base64 分块写入)
func (h *KubernetesHandler) UploadPodFileNS(c *gin.Context) {
h.uploadPodFileWithNS(c, c.Param("namespace"))
}
func (h *KubernetesHandler) uploadPodFileWithNS(c *gin.Context, ns string) {
clusterID, _ := strconv.Atoi(c.Param("id"))
if clusterID == 0 {
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
}
podName := c.Param("podName")
container := c.PostForm("container")
targetDir := c.PostForm("path")
if targetDir == "" {
targetDir = "/"
}
// 读取上传的文件
file, header, err := c.Request.FormFile("file")
if err != nil {
c.JSON(400, model.Response{Code: 400, Message: "文件上传失败: " + err.Error()})
return
}
defer file.Close()
// 限制文件大小 10MB
const maxUploadSize = 10 << 20 // 10MB
if header.Size > maxUploadSize {
c.JSON(400, model.Response{Code: 400, Message: fmt.Sprintf("文件过大: %d bytes, 上限 %d bytes", header.Size, maxUploadSize)})
return
}
content, err := io.ReadAll(file)
if err != nil {
c.JSON(500, model.Response{Code: 500, Message: "读取文件失败: " + err.Error()})
return
}
client, _, err := h.getK8sClient(clusterID)
if err != nil {
c.JSON(500, model.Response{Code: 500, Message: err.Error()})
return
}
// 目标完整路径
targetFile := strings.TrimRight(targetDir, "/") + "/" + header.Filename
// 确保目标目录存在
_, _ = client.ExecInPod(ns, podName, container, []string{"mkdir", "-p", targetDir})
// base64 分块写入:每块 3000 字节 base64 (~2250 bytes 二进制),保证 URL 长度安全
encoded := base64.StdEncoding.EncodeToString(content)
const chunkSize = 3000
first := true
for i := 0; i < len(encoded); i += chunkSize {
end := i + chunkSize
if end > len(encoded) {
end = len(encoded)
}
chunk := encoded[i:end]
var cmd []string
if first {
// 首块:创建/覆盖文件
cmd = []string{"sh", "-c", fmt.Sprintf("echo -n '%s' | base64 -d > '%s'", chunk, targetFile)}
first = false
} else {
// 后续块:追加
cmd = []string{"sh", "-c", fmt.Sprintf("echo -n '%s' | base64 -d >> '%s'", chunk, targetFile)}
}
result, err := client.ExecInPod(ns, podName, container, cmd)
if err != nil {
stderr := ""
if result != nil {
stderr = result.Stderr
}
c.JSON(500, model.Response{Code: 500, Message: fmt.Sprintf("写入文件失败 (chunk %d): %s\n%s", i/chunkSize, err.Error(), stderr)})
return
}
}
// 验证文件大小
verifyResult, verifyErr := client.ExecInPod(ns, podName, container, []string{"stat", "-c", "%s", targetFile})
writtenSize := ""
if verifyErr == nil && verifyResult != nil {
writtenSize = strings.TrimSpace(verifyResult.Stdout)
}
log.Printf("[UploadPodFile] %s/%s → %s (%d bytes, %d chunks)", ns, podName, targetFile, len(content), (len(encoded)+chunkSize-1)/chunkSize)
c.JSON(200, model.Response{
Code: 200,
Message: "上传成功",
Data: gin.H{
"path": targetFile,
"name": header.Filename,
"size": len(content),
"verify": writtenSize,
},
})
}
// ============================================================================
// M13: 多集群资源对比看板 (Multi-Cluster Comparison Dashboard) // Logan.Li
// ============================================================================
// GetMultiClusterComparison 聚合所有集群的资源数据进行对比