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 聚合所有集群的资源数据进行对比