mirror of
https://github.com/larksuite/cli.git
synced 2026-07-03 14:02:43 +08:00
fix(drive): preserve parent token on nested overwrite (#908)
* fix(drive): preserve parent token on nested overwrite Ensure drive +push overwrite requests for nested files keep parent_node aligned with the actual remote parent folder and report parent resolution failures explicitly. * test(drive): cover nested overwrite push workflow Add a live drive +push workflow case for overwriting a nested remote file so the PR parent-token fix is exercised against the real backend and verified to converge via +status.
This commit is contained in:
@@ -275,7 +275,14 @@ var DrivePush = common.Shortcut{
|
||||
skipped++
|
||||
continue
|
||||
}
|
||||
token, version, upErr := drivePushUploadFile(ctx, runtime, localFile, entry.FileToken, folderToken)
|
||||
parentToken, parentErr := drivePushEnsureParentToken(ctx, runtime, folderToken, rel, folderCache)
|
||||
if parentErr != nil {
|
||||
items = append(items, drivePushItem{RelPath: rel, FileToken: entry.FileToken, Action: "failed", SizeBytes: localFile.Size, Error: parentErr.Error()})
|
||||
failed++
|
||||
uploadFailed = true
|
||||
continue
|
||||
}
|
||||
token, version, upErr := drivePushUploadFile(ctx, runtime, localFile, entry.FileToken, parentToken)
|
||||
if upErr != nil {
|
||||
// Token contract on overwrite failure: an in-place
|
||||
// overwrite preserves the file's token, so the
|
||||
@@ -580,6 +587,10 @@ func drivePushEnsureFolder(ctx context.Context, runtime *common.RuntimeContext,
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func drivePushEnsureParentToken(ctx context.Context, runtime *common.RuntimeContext, rootFolderToken, relPath string, folderCache map[string]string) (string, error) {
|
||||
return drivePushEnsureFolder(ctx, runtime, rootFolderToken, drivePushParentRel(relPath), folderCache)
|
||||
}
|
||||
|
||||
// drivePushUploadFile uploads (or overwrites) a single local file. When
|
||||
// existingToken is non-empty, the request adds the file_token form field to
|
||||
// trigger overwrite-with-version semantics on the backend; the response is
|
||||
|
||||
@@ -1296,6 +1296,130 @@ func TestDrivePushReusesExistingRemoteFolder(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestDrivePushOverwriteNestedFileUsesParentFolderToken verifies that
|
||||
// overwriting an existing nested remote file keeps parent_node aligned with
|
||||
// the file's actual parent folder instead of the root folder token.
|
||||
func TestDrivePushOverwriteNestedFileUsesParentFolderToken(t *testing.T) {
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, driveTestConfig())
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
withDriveWorkingDir(t, tmpDir)
|
||||
if err := os.MkdirAll(filepath.Join("local", "sub"), 0o755); err != nil {
|
||||
t.Fatalf("MkdirAll: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join("local", "sub", "keep.txt"), []byte("local"), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile: %v", err)
|
||||
}
|
||||
|
||||
reg.Register(&httpmock.Stub{
|
||||
Method: "GET",
|
||||
URL: "folder_token=folder_root",
|
||||
Body: map[string]interface{}{
|
||||
"code": 0, "msg": "ok",
|
||||
"data": map[string]interface{}{
|
||||
"files": []interface{}{
|
||||
map[string]interface{}{"token": "fld_existing_sub", "name": "sub", "type": "folder"},
|
||||
},
|
||||
"has_more": false,
|
||||
},
|
||||
},
|
||||
})
|
||||
reg.Register(&httpmock.Stub{
|
||||
Method: "GET",
|
||||
URL: "folder_token=fld_existing_sub",
|
||||
Body: map[string]interface{}{
|
||||
"code": 0, "msg": "ok",
|
||||
"data": map[string]interface{}{
|
||||
"files": []interface{}{
|
||||
map[string]interface{}{"token": "tok_keep_nested", "name": "keep.txt", "type": "file"},
|
||||
},
|
||||
"has_more": false,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
uploadStub := &httpmock.Stub{
|
||||
Method: "POST",
|
||||
URL: "/open-apis/drive/v1/files/upload_all",
|
||||
Body: map[string]interface{}{
|
||||
"code": 0, "msg": "ok",
|
||||
"data": map[string]interface{}{
|
||||
"file_token": "tok_keep_nested",
|
||||
"version": "v2",
|
||||
},
|
||||
},
|
||||
}
|
||||
reg.Register(uploadStub)
|
||||
|
||||
err := mountAndRunDrive(t, DrivePush, []string{
|
||||
"+push",
|
||||
"--local-dir", "local",
|
||||
"--folder-token", "folder_root",
|
||||
"--if-exists", "overwrite",
|
||||
"--as", "bot",
|
||||
}, f, stdout)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v\nstdout: %s", err, stdout.String())
|
||||
}
|
||||
|
||||
body := decodeDriveMultipartBody(t, uploadStub)
|
||||
if got := body.Fields["file_token"]; got != "tok_keep_nested" {
|
||||
t.Fatalf("upload_all file_token = %q, want tok_keep_nested", got)
|
||||
}
|
||||
if got := body.Fields["parent_node"]; got != "fld_existing_sub" {
|
||||
t.Fatalf("upload_all parent_node = %q, want fld_existing_sub", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDrivePushOverwriteNestedFileReportsParentEnsureFailure(t *testing.T) {
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, driveTestConfig())
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
withDriveWorkingDir(t, tmpDir)
|
||||
if err := os.MkdirAll(filepath.Join("local", "sub"), 0o755); err != nil {
|
||||
t.Fatalf("MkdirAll: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join("local", "sub", "keep.txt"), []byte("local"), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile: %v", err)
|
||||
}
|
||||
|
||||
reg.Register(&httpmock.Stub{
|
||||
Method: "GET",
|
||||
URL: "folder_token=folder_root",
|
||||
Body: map[string]interface{}{
|
||||
"code": 0, "msg": "ok",
|
||||
"data": map[string]interface{}{
|
||||
"files": []interface{}{
|
||||
map[string]interface{}{"token": "tok_keep_nested", "name": "sub/keep.txt", "type": "file"},
|
||||
},
|
||||
"has_more": false,
|
||||
},
|
||||
},
|
||||
})
|
||||
reg.Register(&httpmock.Stub{
|
||||
Method: "POST",
|
||||
URL: "/open-apis/drive/v1/files/create_folder",
|
||||
Body: map[string]interface{}{
|
||||
"code": 9999,
|
||||
"msg": "create parent failed",
|
||||
},
|
||||
})
|
||||
|
||||
err := mountAndRunDrive(t, DrivePush, []string{
|
||||
"+push",
|
||||
"--local-dir", "local",
|
||||
"--folder-token", "folder_root",
|
||||
"--if-exists", "overwrite",
|
||||
"--as", "bot",
|
||||
}, f, stdout)
|
||||
if err == nil {
|
||||
t.Fatalf("expected parent ensure failure\nstdout: %s", stdout.String())
|
||||
}
|
||||
if !strings.Contains(stdout.String(), `"action": "failed"`) || !strings.Contains(stdout.String(), "create parent failed") {
|
||||
t.Fatalf("expected failed item with create_folder error, got: %s", stdout.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestDrivePushMirrorsEmptyDirectories confirms the gap codex review
|
||||
// flagged: a local directory with no files inside must still surface on
|
||||
// Drive as a created sub-folder, not be silently dropped because the
|
||||
|
||||
@@ -207,4 +207,69 @@ func TestDrive_DuplicateRemoteWorkflow(t *testing.T) {
|
||||
t.Fatalf("+status should converge to a clean unchanged mirror\nstdout:\n%s", statusResult.Stdout)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("push overwrites nested remote file under its real parent", func(t *testing.T) {
|
||||
suffix := clie2e.GenerateSuffix()
|
||||
folderToken := createDriveFolder(t, parentT, ctx, "lark-cli-e2e-drive-nested-push-"+suffix, "")
|
||||
subFolderToken := createDriveFolder(t, parentT, ctx, "sub", folderToken)
|
||||
|
||||
workDir := t.TempDir()
|
||||
if err := os.MkdirAll(filepath.Join(workDir, "local", "sub"), 0o755); err != nil {
|
||||
t.Fatalf("mkdir local/sub: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(workDir, "local", "sub", "keep.txt"), []byte("local-nested-overwrite"), 0o644); err != nil {
|
||||
t.Fatalf("write local/sub/keep.txt: %v", err)
|
||||
}
|
||||
|
||||
existingToken := uploadNamedFile(t, workDir, subFolderToken, "_nested_keep.txt", "keep.txt", "remote-before")
|
||||
|
||||
pushResult, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"drive", "+push",
|
||||
"--local-dir", "local",
|
||||
"--folder-token", folderToken,
|
||||
"--if-exists", "overwrite",
|
||||
},
|
||||
WorkDir: workDir,
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
pushResult.AssertExitCode(t, 0)
|
||||
pushResult.AssertStdoutStatus(t, true)
|
||||
|
||||
if got := gjson.Get(pushResult.Stdout, "data.summary.uploaded").Int(); got != 1 {
|
||||
t.Fatalf("nested +push uploaded=%d, want 1\nstdout:\n%s", got, pushResult.Stdout)
|
||||
}
|
||||
if got := gjson.Get(pushResult.Stdout, `data.items.#(rel_path="sub/keep.txt").action`).String(); got != "overwritten" {
|
||||
t.Fatalf("nested +push action=%q, want overwritten\nstdout:\n%s", got, pushResult.Stdout)
|
||||
}
|
||||
if got := gjson.Get(pushResult.Stdout, `data.items.#(rel_path="sub/keep.txt").file_token`).String(); got != existingToken {
|
||||
t.Fatalf("nested +push file_token=%q, want existing token %q\nstdout:\n%s", got, existingToken, pushResult.Stdout)
|
||||
}
|
||||
|
||||
statusResult, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"drive", "+status",
|
||||
"--local-dir", "local",
|
||||
"--folder-token", folderToken,
|
||||
},
|
||||
WorkDir: workDir,
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
skipDriveStatusExactIfMissingDownloadScope(t, statusResult)
|
||||
statusResult.AssertExitCode(t, 0)
|
||||
statusResult.AssertStdoutStatus(t, true)
|
||||
if got := gjson.Get(statusResult.Stdout, "data.unchanged.#").Int(); got != 1 {
|
||||
t.Fatalf("nested +status unchanged count=%d, want 1\nstdout:\n%s", got, statusResult.Stdout)
|
||||
}
|
||||
if got := gjson.Get(statusResult.Stdout, "data.unchanged.0.rel_path").String(); got != "sub/keep.txt" {
|
||||
t.Fatalf("nested +status unchanged rel_path=%q, want sub/keep.txt\nstdout:\n%s", got, statusResult.Stdout)
|
||||
}
|
||||
if got := gjson.Get(statusResult.Stdout, "data.modified.#").Int(); got != 0 ||
|
||||
gjson.Get(statusResult.Stdout, "data.new_local.#").Int() != 0 ||
|
||||
gjson.Get(statusResult.Stdout, "data.new_remote.#").Int() != 0 {
|
||||
t.Fatalf("nested overwrite should converge to a clean unchanged mirror\nstdout:\n%s", statusResult.Stdout)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user