Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 45 additions & 13 deletions shortcuts/common/drive_meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,61 @@

package common

// FetchDriveMetaTitle looks up the document title via the drive metas batch_query API.
func FetchDriveMetaTitle(runtime *RuntimeContext, token, docType string) (string, error) {
// DriveMeta is the subset of drive metas/batch_query fields used by shortcuts.
type DriveMeta struct {
Title string
URL string
}

// FetchDriveMeta looks up document metadata via the drive metas batch_query API.
func FetchDriveMeta(runtime *RuntimeContext, token, docType string, withURL bool) (DriveMeta, error) {
body := map[string]interface{}{
"request_docs": []map[string]interface{}{
{
"doc_token": token,
"doc_type": docType,
},
},
}
if withURL {
body["with_url"] = true
}

data, err := runtime.CallAPI(
"POST",
"/open-apis/drive/v1/metas/batch_query",
nil,
map[string]interface{}{
"request_docs": []map[string]interface{}{
{
"doc_token": token,
"doc_type": docType,
},
},
},
body,
)
if err != nil {
return "", err
return DriveMeta{}, err
}

metas := GetSlice(data, "metas")
if len(metas) == 0 {
return "", nil
return DriveMeta{}, nil
}
meta, _ := metas[0].(map[string]interface{})
return GetString(meta, "title"), nil
return DriveMeta{
Title: GetString(meta, "title"),
URL: GetString(meta, "url"),
}, nil
}

// FetchDriveMetaTitle looks up the document title via the drive metas batch_query API.
func FetchDriveMetaTitle(runtime *RuntimeContext, token, docType string) (string, error) {
meta, err := FetchDriveMeta(runtime, token, docType, false)
if err != nil {
return "", err
}
return meta.Title, nil
}

// FetchDriveMetaURL looks up the document access URL via the drive metas batch_query API.
func FetchDriveMetaURL(runtime *RuntimeContext, token, docType string) (string, error) {
meta, err := FetchDriveMeta(runtime, token, docType, true)
if err != nil {
return "", err

Check warning on line 60 in shortcuts/common/drive_meta.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/common/drive_meta.go#L60

Added line #L60 was not covered by tests
}
return meta.URL, nil
}
39 changes: 39 additions & 0 deletions shortcuts/common/drive_meta_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package common

import (
"context"
"encoding/json"
"fmt"
"sync/atomic"
"testing"
Expand Down Expand Up @@ -105,6 +106,44 @@ func TestFetchDriveMetaTitle(t *testing.T) {
})
}

func TestFetchDriveMetaURL(t *testing.T) {
runtime, reg := newDriveMetaTestRuntime(t)
stub := &httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/metas/batch_query",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"metas": []map[string]interface{}{
{
"doc_token": "boxcnABC",
"doc_type": "file",
"title": "report.pdf",
"url": "https://tenant.example.com/file/boxcnABC",
},
},
},
},
}
reg.Register(stub)

got, err := FetchDriveMetaURL(runtime, "boxcnABC", "file")
if err != nil {
t.Fatalf("FetchDriveMetaURL() error: %v", err)
}
if got != "https://tenant.example.com/file/boxcnABC" {
t.Fatalf("url = %q, want tenant URL", got)
}

var body map[string]interface{}
if err := json.Unmarshal(stub.CapturedBody, &body); err != nil {
t.Fatalf("decode captured body: %v", err)
}
if body["with_url"] != true {
t.Fatalf("with_url = %#v, want true", body["with_url"])
}
}

func newDriveMetaTestRuntime(t *testing.T) (*RuntimeContext, *httpmock.Registry) {
t.Helper()
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
Expand Down
69 changes: 52 additions & 17 deletions shortcuts/drive/drive_io_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -604,9 +604,9 @@ func TestDriveUploadSmallFileToWiki(t *testing.T) {
}
}

func TestDriveUploadFallbackURLForExplorerParent(t *testing.T) {
func TestDriveUploadUsesMetaURLForExplorerParent(t *testing.T) {
uploadTestConfig := &core.CliConfig{
AppID: "drive-upload-explorer-fallback-url", AppSecret: "test-secret", Brand: core.BrandFeishu,
AppID: "drive-upload-explorer-meta-url", AppSecret: "test-secret", Brand: core.BrandFeishu,
}
f, stdout, _, reg := cmdutil.TestFactory(t, uploadTestConfig)

Expand All @@ -615,12 +615,21 @@ func TestDriveUploadFallbackURLForExplorerParent(t *testing.T) {
URL: "/open-apis/drive/v1/files/upload_all",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
// upload_all only ever returns file_token; url is never present —
// this exercises the fallback path unconditionally for explorer
// parents.
"data": map[string]interface{}{"file_token": "file_explorer_small"},
},
})
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/metas/batch_query",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"data": map[string]interface{}{
"metas": []map[string]interface{}{
{"doc_token": "file_explorer_small", "doc_type": "file", "url": "https://tenant.example.com/file/file_explorer_small"},
},
},
},
})

tmpDir := t.TempDir()
origDir, _ := os.Getwd()
Expand All @@ -641,14 +650,14 @@ func TestDriveUploadFallbackURLForExplorerParent(t *testing.T) {
}

data := decodeDriveEnvelope(t, stdout)
if got, want := data["url"], "https://www.feishu.cn/file/file_explorer_small"; got != want {
t.Fatalf("data.url = %#v, want %q (brand-standard fallback)", got, want)
if got, want := data["url"], "https://tenant.example.com/file/file_explorer_small"; got != want {
t.Fatalf("data.url = %#v, want %q (metadata URL)", got, want)
}
}

func TestDriveUploadOmitsURLForWikiParent(t *testing.T) {
func TestDriveUploadUsesMetaURLForWikiParent(t *testing.T) {
uploadTestConfig := &core.CliConfig{
AppID: "drive-upload-wiki-no-url", AppSecret: "test-secret", Brand: core.BrandFeishu,
AppID: "drive-upload-wiki-meta-url", AppSecret: "test-secret", Brand: core.BrandFeishu,
}
f, stdout, _, reg := cmdutil.TestFactory(t, uploadTestConfig)

Expand All @@ -660,6 +669,18 @@ func TestDriveUploadOmitsURLForWikiParent(t *testing.T) {
"data": map[string]interface{}{"file_token": "file_wiki_small"},
},
})
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/metas/batch_query",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"data": map[string]interface{}{
"metas": []map[string]interface{}{
{"doc_token": "file_wiki_small", "doc_type": "file", "url": "https://tenant.example.com/file/file_wiki_small"},
},
},
},
})

tmpDir := t.TempDir()
withDriveWorkingDir(t, tmpDir)
Expand All @@ -677,8 +698,8 @@ func TestDriveUploadOmitsURLForWikiParent(t *testing.T) {
}

data := decodeDriveEnvelope(t, stdout)
if _, ok := data["url"]; ok {
t.Fatalf("data.url should be omitted for wiki-hosted files (no standalone URL); got %#v", data["url"])
if got, want := data["url"], "https://tenant.example.com/file/file_wiki_small"; got != want {
t.Fatalf("data.url = %#v, want %q (metadata URL)", got, want)
}
}

Expand Down Expand Up @@ -1078,21 +1099,28 @@ func TestDriveUploadDryRunUsesWikiTarget(t *testing.T) {

var got struct {
API []struct {
URL string `json:"url"`
Body map[string]interface{} `json:"body"`
} `json:"api"`
}
if err := json.Unmarshal(data, &got); err != nil {
t.Fatalf("unmarshal dry run json: %v", err)
}
if len(got.API) != 1 {
t.Fatalf("expected 1 API call, got %d", len(got.API))
if len(got.API) != 2 {
t.Fatalf("expected 2 API calls, got %d", len(got.API))
}
if got.API[0].Body["parent_type"] != driveUploadParentTypeWiki {
t.Fatalf("parent_type = %#v, want %q", got.API[0].Body["parent_type"], driveUploadParentTypeWiki)
}
if got.API[0].Body["parent_node"] != "wikcn_dryrun_upload_target" {
t.Fatalf("parent_node = %#v, want %q", got.API[0].Body["parent_node"], "wikcn_dryrun_upload_target")
}
if got.API[1].URL != "/open-apis/drive/v1/metas/batch_query" {
t.Fatalf("metadata URL = %q, want metas/batch_query", got.API[1].URL)
}
if got.API[1].Body["with_url"] != true {
t.Fatalf("metadata with_url = %#v, want true", got.API[1].Body["with_url"])
}
}

func TestNewDriveUploadSpecPreservesPathAndName(t *testing.T) {
Expand Down Expand Up @@ -1168,18 +1196,25 @@ func TestDriveUploadDryRunIncludesFileToken(t *testing.T) {

var got struct {
API []struct {
URL string `json:"url"`
Body map[string]interface{} `json:"body"`
} `json:"api"`
}
if err := json.Unmarshal(data, &got); err != nil {
t.Fatalf("unmarshal dry run json: %v", err)
}
if len(got.API) != 1 {
t.Fatalf("expected 1 API call, got %d", len(got.API))
if len(got.API) != 2 {
t.Fatalf("expected 2 API calls, got %d", len(got.API))
}
if got.API[0].Body["file_token"] != "boxcn_dryrun_overwrite" {
t.Fatalf("file_token = %#v, want %q", got.API[0].Body["file_token"], "boxcn_dryrun_overwrite")
}
if got.API[1].URL != "/open-apis/drive/v1/metas/batch_query" {
t.Fatalf("metadata URL = %q, want metas/batch_query", got.API[1].URL)
}
if got.API[1].Body["with_url"] != true {
t.Fatalf("metadata with_url = %#v, want true", got.API[1].Body["with_url"])
}
}

func TestDriveUploadDryRunBotOverwriteSkipsPermissionGrantHint(t *testing.T) {
Expand Down Expand Up @@ -1222,8 +1257,8 @@ func TestDriveUploadDryRunBotOverwriteSkipsPermissionGrantHint(t *testing.T) {
if err := json.Unmarshal(data, &got); err != nil {
t.Fatalf("unmarshal dry run json: %v", err)
}
if len(got.API) != 1 {
t.Fatalf("expected 1 API call, got %d", len(got.API))
if len(got.API) != 2 {
t.Fatalf("expected 2 API calls, got %d", len(got.API))
}
if got.API[0].Body["file_token"] != "boxcn_dryrun_overwrite" {
t.Fatalf("file_token = %#v, want %q", got.API[0].Body["file_token"], "boxcn_dryrun_overwrite")
Expand Down
28 changes: 18 additions & 10 deletions shortcuts/drive/drive_upload.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@
Command: "+upload",
Description: "Upload a local file to Drive",
Risk: "write",
Scopes: []string{"drive:file:upload"},
Scopes: []string{"drive:file:upload", "drive:drive.metadata:readonly"},
AuthTypes: []string{"user", "bot"},
Flags: []common.Flag{
{Name: "file", Desc: "local file path (files > 20MB use multipart upload automatically)", Required: true},
Expand Down Expand Up @@ -124,11 +124,22 @@
body["file_token"] = spec.FileToken
}
d := common.NewDryRunAPI().
Desc("multipart/form-data upload (files > 20MB use chunked 3-step upload)").
Desc("multipart/form-data upload (files > 20MB use chunked 3-step upload), then fetch the real Drive URL via metadata").
POST("/open-apis/drive/v1/files/upload_all").
Body(body)
d.POST("/open-apis/drive/v1/metas/batch_query").
Desc("Fetch the uploaded file's real access URL").
Body(map[string]interface{}{
"request_docs": []map[string]interface{}{
{
"doc_token": "<file_token from upload response>",
"doc_type": "file",
},
},
"with_url": true,
})
if runtime.IsBot() && !isOverwrite {
d.Desc("After file upload succeeds in bot mode, the CLI will also try to grant the current CLI user full_access (可管理权限) on the new file.")
d.Set("post_upload_note", "After file upload succeeds in bot mode, the CLI will also try to grant the current CLI user full_access (可管理权限) on the new file.")

Check warning on line 142 in shortcuts/drive/drive_upload.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/drive/drive_upload.go#L142

Added line #L142 was not covered by tests
}
return d
},
Expand Down Expand Up @@ -165,13 +176,10 @@
if uploadResult.Version != "" {
out["version"] = uploadResult.Version
}
// wiki-hosted files have no standalone /file/<token> URL — only the
// wiki node URL, which the upload response doesn't carry. Skip the
// fallback for parent_type=wiki rather than emit a link that 404s.
if target.ParentType == driveUploadParentTypeExplorer {
if u := common.BuildResourceURL(runtime.Config.Brand, "file", uploadResult.FileToken); u != "" {
out["url"] = u
}
if u, metaErr := common.FetchDriveMetaURL(runtime, uploadResult.FileToken, "file"); metaErr == nil && strings.TrimSpace(u) != "" {
out["url"] = u
} else if metaErr != nil {
fmt.Fprintf(runtime.IO().ErrOut, "warning: uploaded file URL lookup failed: %v\n", metaErr)
}
if !isOverwrite {
if grant := common.AutoGrantCurrentUserDrivePermission(runtime, uploadResult.FileToken, "file"); grant != nil {
Expand Down
25 changes: 19 additions & 6 deletions shortcuts/markdown/markdown_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import (
"context"
"fmt"
"io"
"strings"

Expand All @@ -16,7 +17,7 @@
Command: "+create",
Description: "Create a Markdown file in Drive",
Risk: "write",
Scopes: []string{"drive:file:upload"},
Scopes: []string{"drive:file:upload", "drive:drive.metadata:readonly"},
AuthTypes: []string{"user", "bot"},
HasFormat: true,
Flags: []common.Flag{
Expand Down Expand Up @@ -55,7 +56,19 @@
if err != nil {
return common.NewDryRunAPI().Set("error", err.Error())
}
return markdownUploadDryRun(spec, fileSize, fileSize > markdownSinglePartSizeLimit)
dry := markdownUploadDryRun(spec, fileSize, fileSize > markdownSinglePartSizeLimit)
dry.POST("/open-apis/drive/v1/metas/batch_query").
Desc("Fetch the created Markdown file's real access URL").
Body(map[string]interface{}{
"request_docs": []map[string]interface{}{
{
"doc_token": "<file_token from upload response>",
"doc_type": "file",
},
},
"with_url": true,
})
return dry
},
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
spec := markdownUploadSpec{
Expand Down Expand Up @@ -87,10 +100,10 @@
"file_name": finalMarkdownFileName(spec),
"size_bytes": fileSize,
}
if target := spec.Target(); target.ParentType == markdownUploadParentTypeExplorer {
if u := common.BuildResourceURL(runtime.Config.Brand, "file", result.FileToken); u != "" {
out["url"] = u
}
if u, metaErr := common.FetchDriveMetaURL(runtime, result.FileToken, "file"); metaErr == nil && strings.TrimSpace(u) != "" {
out["url"] = u
} else if metaErr != nil {
fmt.Fprintf(runtime.IO().ErrOut, "warning: created Markdown file URL lookup failed: %v\n", metaErr)

Check warning on line 106 in shortcuts/markdown/markdown_create.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/markdown/markdown_create.go#L106

Added line #L106 was not covered by tests
}
if grant := common.AutoGrantCurrentUserDrivePermission(runtime, result.FileToken, "file"); grant != nil {
out["permission_grant"] = grant
Expand Down
Loading
Loading