feat(k8s): M3 Pod文件上传 + ExecInPod WebSocket协议重写

M3: Pod 文件上传
- 后端: POST .../pods/:pod/files/upload (multipart, base64分块exec写入)
- 前端: PodFileBrowser 增加上传按钮 + FormData API
- 路由: 新增 Kuboard (:clusterCode) + 旧 (:id) 两套路由

重大修复: ExecInPod 从 raw HTTP SPDY 改为 WebSocket channel.k8s.io
- 旧方案: HTTP POST + SPDY upgrade → 无法解析响应(协议头缺失 + 帧解析复杂)
- 新方案: gorilla/websocket Dialer + channel.k8s.io 子协议 → 和 ExecTerminal 一致
- 影响: 文件浏览/下载/上传全部恢复正常

修复: BusyBox ls 解析
- BusyBox 不支持 --time-style 参数,默认输出 Mon DD HH:MM 格式
- 解析器改为自动检测 GNU vs BusyBox 日期格式
This commit is contained in:
attacker committed 2026-06-23 02:21:29 +08:00
1 parent 69df073d42
commit 9ab7572b8e
11 files changed
+389 -126

No files matched your search

@@ -15,7 +15,7 @@ import {
} from '@/components/ui/select';
import {
Folder, FolderOpen, FileText, FileCode, FileArchive, FileImage,
File, Download, Eye, ChevronRight, ArrowLeft, ArrowRight, ArrowUp,
File, Download, Upload, Eye, ChevronRight, ArrowLeft, ArrowRight, ArrowUp,
RotateCw, Loader2, Maximize2, Minimize2, X, Box, Home, HardDrive,
AlertCircle, FileType, Binary,
} from 'lucide-react';
@@ -156,6 +156,10 @@ export function PodFileBrowser({
// 预览
const [preview, setPreview] = useState<{ path: string; content: string; loading: boolean } | null>(null);
// 上传
const [uploading, setUploading] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
// 选中的条目(用于高亮)
const [selected, setSelected] = useState<string>('');
@@ -287,6 +291,26 @@ export function PodFileBrowser({
toast({ title: '已开始下载', description: path });
}, [clusterCode, namespace, podName, container, toast]);
// 上传文件
const handleUpload = useCallback(async (files: FileList | null) => {
if (!files || files.length === 0) return;
setUploading(true);
try {
for (const file of Array.from(files)) {
await kubernetesApi.uploadPodFile(clusterCode, namespace, podName, currentPath, file, container || undefined);
toast({ title: '上传成功', description: `${file.name} → ${currentPath}/${file.name}` });
}
// 刷新文件列表
loadPath(currentPath);
} catch (e: any) {
toast({ title: '上传失败', description: e?.message || String(e), variant: 'destructive' });
} finally {
setUploading(false);
// 清空 input 允许重复上传同一文件
if (fileInputRef.current) fileInputRef.current.value = '';
}
}, [clusterCode, namespace, podName, container, currentPath, loadPath, toast]);
const breadcrumbs = useMemo(() => pathSegments(currentPath), [currentPath]);
return (
@@ -331,6 +355,23 @@ export function PodFileBrowser({
<Button variant="ghost" size="sm" className="h-8 px-2" onClick={() => loadPath(currentPath)} title="刷新">
<RotateCw className={cn('h-3.5 w-3.5', loading && 'animate-spin')} />
</Button>
<Button
variant="ghost"
size="sm"
className="h-8 px-2"
disabled={uploading}
onClick={() => fileInputRef.current?.click()}
title="上传文件"
>
{uploading ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Upload className="h-3.5 w-3.5" />}
</Button>
<input
ref={fileInputRef}
type="file"
className="hidden"
multiple
onChange={(e) => handleUpload(e.target.files)}
/>
<button
onClick={() => setFullscreen(!fullscreen)}
className="p-1.5 rounded hover:bg-[var(--dg-bg-hover)] text-[var(--dg-text-secondary)]"