uploads,tools,config,main: 将 upload_file 返回路径改为服务器端绝对路径并透传给 spark_submit
之前 upload_file 把文件写到 DataDir/uploads/<filename>,返回相对路径
uploads/hello.txt。MCP 服务器与 agent 不在同一台机器,agent 无法构造服务
器本地路径,因此 spark_submit 无法定位脚本。
改为由新的 internal/uploads 包 mint 一个 32 字符十六进制 file_id,文件落盘为
DataDir/uploads/<file_id>,元数据写入 <file_id>.meta.json(原始文件名只存在
sidecar 里)。upload_file 返回 {file_id, path, name, size, sha256},其中 path
是绝对路径。spark_submit 的 description 明确要求 LLM 直接把 upload_file 返回
的 path 放进 args,不要自己构造路径。
为什么只在描述里约束而不在代码里拒绝非 mint 的绝对路径:集群本地已有路径
(如 /opt/spark/examples/pi.py)是合法的 spark-submit 参数,代码不能替
LLM 拒绝。
测试锁定:
- internal/uploads: Save 往返、AbsPath/Validate 非法路径、Sweep 过期/未过期/
孤立 sidecar
- internal/mcp/tools: upload_file 新响应字段、spark_submit 透传 mint 路径与
集群本地路径
刻意未做:S3/HDFS 上传、给 spark_submit 新增 file_id 参数、在代码层面拒绝
集群本地绝对路径。
破坏性变更:upload_file 响应从相对 path 改为绝对 path,并新增 file_id/name/
sha256 字段。该工具尚无外部调用者。
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -18,6 +19,7 @@ import (
|
||||
"spark-mcp-go/internal/cluster"
|
||||
"spark-mcp-go/internal/httpclient"
|
||||
"spark-mcp-go/internal/storage"
|
||||
"spark-mcp-go/internal/uploads"
|
||||
)
|
||||
|
||||
// testDepsWithDataDir returns dependencies backed by an in-memory DB and a
|
||||
@@ -30,6 +32,12 @@ func testDepsWithDataDir(t *testing.T) (*Deps, *storage.ClusterRepo) {
|
||||
}
|
||||
t.Cleanup(func() { _ = db.Close() })
|
||||
|
||||
dataDir := t.TempDir()
|
||||
uploadStore, err := uploads.New(filepath.Join(dataDir, "uploads"))
|
||||
if err != nil {
|
||||
t.Fatalf("new upload store: %v", err)
|
||||
}
|
||||
|
||||
return &Deps{
|
||||
HTTPClient: httpclient.New(httpclient.Config{
|
||||
Timeout: 5 * time.Second,
|
||||
@@ -37,7 +45,8 @@ func testDepsWithDataDir(t *testing.T) (*Deps, *storage.ClusterRepo) {
|
||||
}),
|
||||
ClusterRepo: db.Clusters(),
|
||||
MaxResponseBytes: 1 << 20,
|
||||
DataDir: t.TempDir(),
|
||||
DataDir: dataDir,
|
||||
UploadStore: uploadStore,
|
||||
}, db.Clusters()
|
||||
}
|
||||
|
||||
@@ -324,15 +333,26 @@ func TestUploadFile(t *testing.T) {
|
||||
}
|
||||
|
||||
payload := resultJSON(t, res)
|
||||
if payload["path"] != "uploads/hello.txt" {
|
||||
t.Errorf("path=%v, want uploads/hello.txt", payload["path"])
|
||||
fileID, ok := payload["file_id"].(string)
|
||||
if !ok || !regexp.MustCompile(`^[0-9a-f]{32}$`).MatchString(fileID) {
|
||||
t.Errorf("file_id=%v, want 32 lowercase hex chars", payload["file_id"])
|
||||
}
|
||||
path, ok := payload["path"].(string)
|
||||
if !ok || !filepath.IsAbs(path) || !strings.HasSuffix(path, "/"+fileID) {
|
||||
t.Errorf("path=%v, want absolute path ending in /%s", payload["path"], fileID)
|
||||
}
|
||||
if payload["name"] != "hello.txt" {
|
||||
t.Errorf("name=%v, want hello.txt", payload["name"])
|
||||
}
|
||||
if payload["size"] != float64(len("hello world")) {
|
||||
t.Errorf("size=%v, want %d", payload["size"], len("hello world"))
|
||||
}
|
||||
sha, ok := payload["sha256"].(string)
|
||||
if !ok || !regexp.MustCompile(`^[0-9a-f]{64}$`).MatchString(sha) {
|
||||
t.Errorf("sha256=%v, want 64-char hex", payload["sha256"])
|
||||
}
|
||||
|
||||
final := filepath.Join(deps.DataDir, "uploads", "hello.txt")
|
||||
got, err := os.ReadFile(final)
|
||||
got, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("read uploaded file: %v", err)
|
||||
}
|
||||
@@ -340,7 +360,7 @@ func TestUploadFile(t *testing.T) {
|
||||
t.Errorf("content=%q, want %q", got, "hello world")
|
||||
}
|
||||
|
||||
info, err := os.Stat(final)
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
t.Fatalf("stat uploaded file: %v", err)
|
||||
}
|
||||
@@ -393,9 +413,19 @@ func TestUploadFile_Base64(t *testing.T) {
|
||||
t.Fatalf("handler error: %v", err)
|
||||
}
|
||||
|
||||
_ = resultJSON(t, res)
|
||||
final := filepath.Join(deps.DataDir, "uploads", "hello.bin")
|
||||
got, err := os.ReadFile(final)
|
||||
payload := resultJSON(t, res)
|
||||
path, ok := payload["path"].(string)
|
||||
if !ok || !filepath.IsAbs(path) {
|
||||
t.Errorf("path=%v, want absolute path", payload["path"])
|
||||
}
|
||||
if payload["name"] != "hello.bin" {
|
||||
t.Errorf("name=%v, want hello.bin", payload["name"])
|
||||
}
|
||||
if payload["size"] != float64(len("hello")) {
|
||||
t.Errorf("size=%v, want %d", payload["size"], len("hello"))
|
||||
}
|
||||
|
||||
got, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("read uploaded file: %v", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user