mirror of
https://github.com/larksuite/cli.git
synced 2026-07-06 00:06:28 +08:00
672 lines
20 KiB
Go
672 lines
20 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package wiki
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/larksuite/cli/internal/core"
|
|
"github.com/larksuite/cli/internal/output"
|
|
"github.com/larksuite/cli/internal/validate"
|
|
"github.com/larksuite/cli/shortcuts/common"
|
|
)
|
|
|
|
var (
|
|
wikiMovePollAttempts = 30
|
|
wikiMovePollInterval = 2 * time.Second
|
|
)
|
|
|
|
const (
|
|
wikiMoveModeNode = "node"
|
|
wikiMoveModeDocsToWiki = "docs_to_wiki"
|
|
)
|
|
|
|
var wikiMoveObjectTypes = []string{
|
|
"doc",
|
|
"sheet",
|
|
"bitable",
|
|
"mindnote",
|
|
"docx",
|
|
"file",
|
|
"slides",
|
|
}
|
|
|
|
// WikiMove moves an existing wiki node inside Wiki or migrates a Drive
|
|
// document into Wiki with bounded polling for async task completion.
|
|
var WikiMove = common.Shortcut{
|
|
Service: "wiki",
|
|
Command: "+move",
|
|
Description: "Move a wiki node, or move a Drive document into Wiki",
|
|
Risk: "write",
|
|
Scopes: []string{"wiki:node:move", "wiki:node:read", "wiki:space:read"},
|
|
AuthTypes: []string{"user", "bot"},
|
|
Flags: []common.Flag{
|
|
{Name: "node-token", Desc: "wiki node token to move inside Wiki"},
|
|
{Name: "source-space-id", Desc: "source wiki space ID for --node-token; if omitted, it is resolved from the node token"},
|
|
{Name: "target-space-id", Desc: "target wiki space ID; required for docs-to-wiki, optional for node move when --target-parent-token is set"},
|
|
{Name: "target-parent-token", Desc: "target parent wiki node token; if omitted for docs-to-wiki, the document is moved to the target space root"},
|
|
{Name: "obj-type", Desc: "Drive document type for docs-to-wiki mode", Enum: wikiMoveObjectTypes},
|
|
{Name: "obj-token", Desc: "Drive document token for docs-to-wiki mode"},
|
|
{Name: "apply", Type: "bool", Desc: "submit a move request when the caller lacks permission to move the document immediately"},
|
|
},
|
|
Tips: []string{
|
|
"Use --node-token to move an existing wiki node inside or across wiki spaces.",
|
|
"Use --obj-type and --obj-token to move a Drive document into Wiki.",
|
|
"If docs-to-wiki returns a long-running task, this command polls for a bounded window and then prints a follow-up drive +task_result command.",
|
|
},
|
|
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
|
spec := readWikiMoveSpec(runtime)
|
|
// `my_library` is a per-user personal-library alias; it has no meaning
|
|
// for a tenant_access_token (--as bot), so reject early with a clear
|
|
// hint instead of letting the API return a confusing error.
|
|
if runtime.As().IsBot() && spec.TargetSpaceID == wikiMyLibrarySpaceID {
|
|
return output.ErrValidation("--target-space-id my_library is a per-user personal library alias and cannot be used with --as bot; resolve it to a real space_id first via `lark-cli wiki spaces get --params '{\"space_id\":\"my_library\"}' --as user`")
|
|
}
|
|
return validateWikiMoveSpec(spec)
|
|
},
|
|
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
|
return buildWikiMoveDryRun(readWikiMoveSpec(runtime))
|
|
},
|
|
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
|
spec := readWikiMoveSpec(runtime)
|
|
fmt.Fprintf(runtime.IO().ErrOut, "Running wiki move (%s)...\n", spec.Mode())
|
|
|
|
out, err := runWikiMove(ctx, wikiMoveAPI{runtime: runtime}, runtime, spec)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
runtime.Out(out, nil)
|
|
return nil
|
|
},
|
|
}
|
|
|
|
type wikiMoveSpec struct {
|
|
NodeToken string
|
|
SourceSpaceID string
|
|
TargetSpaceID string
|
|
TargetParentToken string
|
|
ObjType string
|
|
ObjToken string
|
|
Apply bool
|
|
}
|
|
|
|
func (spec wikiMoveSpec) Mode() string {
|
|
if spec.NodeToken != "" {
|
|
return wikiMoveModeNode
|
|
}
|
|
return wikiMoveModeDocsToWiki
|
|
}
|
|
|
|
func (spec wikiMoveSpec) NodeMoveBody() map[string]interface{} {
|
|
body := map[string]interface{}{}
|
|
if spec.TargetParentToken != "" {
|
|
body["target_parent_token"] = spec.TargetParentToken
|
|
}
|
|
if spec.TargetSpaceID != "" {
|
|
body["target_space_id"] = spec.TargetSpaceID
|
|
}
|
|
return body
|
|
}
|
|
|
|
func (spec wikiMoveSpec) DocsToWikiBody() map[string]interface{} {
|
|
body := map[string]interface{}{
|
|
"obj_type": spec.ObjType,
|
|
"obj_token": spec.ObjToken,
|
|
}
|
|
if spec.TargetParentToken != "" {
|
|
body["parent_wiki_token"] = spec.TargetParentToken
|
|
}
|
|
if spec.Apply {
|
|
body["apply"] = true
|
|
}
|
|
return body
|
|
}
|
|
|
|
type wikiMoveTaskResult struct {
|
|
Node *wikiNodeRecord
|
|
Status int
|
|
StatusMsg string
|
|
}
|
|
|
|
type wikiMoveTaskStatus struct {
|
|
TaskID string
|
|
MoveResults []wikiMoveTaskResult
|
|
}
|
|
|
|
func (s wikiMoveTaskStatus) Ready() bool {
|
|
if len(s.MoveResults) == 0 {
|
|
return false
|
|
}
|
|
for _, result := range s.MoveResults {
|
|
if result.Status != 0 {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (s wikiMoveTaskStatus) Failed() bool {
|
|
for _, result := range s.MoveResults {
|
|
if result.Status < 0 {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (s wikiMoveTaskStatus) Pending() bool {
|
|
return !s.Ready() && !s.Failed()
|
|
}
|
|
|
|
func (s wikiMoveTaskStatus) FirstResult() *wikiMoveTaskResult {
|
|
if len(s.MoveResults) == 0 {
|
|
return nil
|
|
}
|
|
return &s.MoveResults[0]
|
|
}
|
|
|
|
// primaryResult picks the most informative move_result for top-level status
|
|
// surfacing: prefer a failing entry so multi-doc tasks don't mask failures
|
|
// behind an earlier success, then a still-processing entry, and finally fall
|
|
// back to the first entry.
|
|
func (s wikiMoveTaskStatus) primaryResult() *wikiMoveTaskResult {
|
|
for i := range s.MoveResults {
|
|
if s.MoveResults[i].Status < 0 {
|
|
return &s.MoveResults[i]
|
|
}
|
|
}
|
|
for i := range s.MoveResults {
|
|
if s.MoveResults[i].Status > 0 {
|
|
return &s.MoveResults[i]
|
|
}
|
|
}
|
|
return s.FirstResult()
|
|
}
|
|
|
|
func (s wikiMoveTaskStatus) PrimaryStatusCode() int {
|
|
if r := s.primaryResult(); r != nil {
|
|
return r.Status
|
|
}
|
|
return 1
|
|
}
|
|
|
|
func (s wikiMoveTaskStatus) PrimaryStatusLabel() string {
|
|
if r := s.primaryResult(); r != nil {
|
|
if msg := strings.TrimSpace(r.StatusMsg); msg != "" {
|
|
return msg
|
|
}
|
|
}
|
|
switch {
|
|
case s.Ready():
|
|
return "success"
|
|
case s.Failed():
|
|
return "failure"
|
|
default:
|
|
return "processing"
|
|
}
|
|
}
|
|
|
|
type wikiMoveDocsResponse struct {
|
|
WikiToken string
|
|
TaskID string
|
|
Applied bool
|
|
}
|
|
|
|
type wikiMoveClient interface {
|
|
GetNode(ctx context.Context, token string) (*wikiNodeRecord, error)
|
|
MoveNode(ctx context.Context, sourceSpaceID string, spec wikiMoveSpec) (*wikiNodeRecord, error)
|
|
MoveDocsToWiki(ctx context.Context, targetSpaceID string, spec wikiMoveSpec) (*wikiMoveDocsResponse, error)
|
|
GetMoveTask(ctx context.Context, taskID string) (wikiMoveTaskStatus, error)
|
|
}
|
|
|
|
type wikiMoveAPI struct {
|
|
runtime *common.RuntimeContext
|
|
}
|
|
|
|
func (api wikiMoveAPI) GetNode(ctx context.Context, token string) (*wikiNodeRecord, error) {
|
|
data, err := api.runtime.CallAPI(
|
|
"GET",
|
|
"/open-apis/wiki/v2/spaces/get_node",
|
|
map[string]interface{}{"token": token},
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return parseWikiNodeRecord(common.GetMap(data, "node"))
|
|
}
|
|
|
|
func (api wikiMoveAPI) MoveNode(ctx context.Context, sourceSpaceID string, spec wikiMoveSpec) (*wikiNodeRecord, error) {
|
|
data, err := api.runtime.CallAPI(
|
|
"POST",
|
|
fmt.Sprintf(
|
|
"/open-apis/wiki/v2/spaces/%s/nodes/%s/move",
|
|
validate.EncodePathSegment(sourceSpaceID),
|
|
validate.EncodePathSegment(spec.NodeToken),
|
|
),
|
|
nil,
|
|
spec.NodeMoveBody(),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return parseWikiNodeRecord(common.GetMap(data, "node"))
|
|
}
|
|
|
|
func (api wikiMoveAPI) MoveDocsToWiki(ctx context.Context, targetSpaceID string, spec wikiMoveSpec) (*wikiMoveDocsResponse, error) {
|
|
data, err := api.runtime.CallAPI(
|
|
"POST",
|
|
fmt.Sprintf(
|
|
"/open-apis/wiki/v2/spaces/%s/nodes/move_docs_to_wiki",
|
|
validate.EncodePathSegment(targetSpaceID),
|
|
),
|
|
nil,
|
|
spec.DocsToWikiBody(),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &wikiMoveDocsResponse{
|
|
WikiToken: common.GetString(data, "wiki_token"),
|
|
TaskID: common.GetString(data, "task_id"),
|
|
Applied: common.GetBool(data, "applied"),
|
|
}, nil
|
|
}
|
|
|
|
func (api wikiMoveAPI) GetMoveTask(ctx context.Context, taskID string) (wikiMoveTaskStatus, error) {
|
|
data, err := api.runtime.CallAPI(
|
|
"GET",
|
|
fmt.Sprintf("/open-apis/wiki/v2/tasks/%s", validate.EncodePathSegment(taskID)),
|
|
map[string]interface{}{"task_type": "move"},
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
return wikiMoveTaskStatus{}, err
|
|
}
|
|
return parseWikiMoveTaskStatus(taskID, common.GetMap(data, "task"))
|
|
}
|
|
|
|
func readWikiMoveSpec(runtime *common.RuntimeContext) wikiMoveSpec {
|
|
return wikiMoveSpec{
|
|
NodeToken: strings.TrimSpace(runtime.Str("node-token")),
|
|
SourceSpaceID: strings.TrimSpace(runtime.Str("source-space-id")),
|
|
TargetSpaceID: strings.TrimSpace(runtime.Str("target-space-id")),
|
|
TargetParentToken: strings.TrimSpace(runtime.Str("target-parent-token")),
|
|
ObjType: strings.ToLower(strings.TrimSpace(runtime.Str("obj-type"))),
|
|
ObjToken: strings.TrimSpace(runtime.Str("obj-token")),
|
|
Apply: runtime.Bool("apply"),
|
|
}
|
|
}
|
|
|
|
func validateWikiMoveSpec(spec wikiMoveSpec) error {
|
|
if err := validateOptionalResourceName(spec.NodeToken, "--node-token"); err != nil {
|
|
return err
|
|
}
|
|
if err := validateOptionalResourceName(spec.SourceSpaceID, "--source-space-id"); err != nil {
|
|
return err
|
|
}
|
|
if err := validateOptionalResourceName(spec.TargetSpaceID, "--target-space-id"); err != nil {
|
|
return err
|
|
}
|
|
if err := validateOptionalResourceName(spec.TargetParentToken, "--target-parent-token"); err != nil {
|
|
return err
|
|
}
|
|
if err := validateOptionalResourceName(spec.ObjToken, "--obj-token"); err != nil {
|
|
return err
|
|
}
|
|
|
|
if spec.NodeToken != "" {
|
|
if spec.ObjType != "" || spec.ObjToken != "" || spec.Apply {
|
|
return output.ErrValidation("--node-token cannot be combined with --obj-type, --obj-token, or --apply")
|
|
}
|
|
if spec.TargetParentToken == "" && spec.TargetSpaceID == "" {
|
|
return output.ErrValidation("--target-parent-token and --target-space-id cannot both be empty for wiki node move")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if spec.SourceSpaceID != "" {
|
|
return output.ErrValidation("--source-space-id can only be used with --node-token")
|
|
}
|
|
if spec.ObjType == "" && spec.ObjToken == "" && !spec.Apply {
|
|
return output.ErrValidation("provide --node-token for wiki node move, or provide --obj-type and --obj-token for docs-to-wiki move")
|
|
}
|
|
if spec.ObjType == "" {
|
|
return output.ErrValidation("--obj-type is required for docs-to-wiki move")
|
|
}
|
|
if spec.ObjToken == "" {
|
|
return output.ErrValidation("--obj-token is required for docs-to-wiki move")
|
|
}
|
|
if spec.TargetSpaceID == "" {
|
|
return output.ErrValidation("--target-space-id is required for docs-to-wiki move")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func buildWikiMoveDryRun(spec wikiMoveSpec) *common.DryRunAPI {
|
|
dry := common.NewDryRunAPI()
|
|
switch spec.Mode() {
|
|
case wikiMoveModeNode:
|
|
step := 1
|
|
switch {
|
|
case spec.SourceSpaceID == "" && spec.TargetParentToken != "":
|
|
dry.Desc("3-step orchestration: resolve source node -> resolve target parent -> move wiki node")
|
|
case spec.SourceSpaceID == "":
|
|
dry.Desc("2-step orchestration: resolve source node -> move wiki node")
|
|
case spec.TargetParentToken != "":
|
|
dry.Desc("2-step orchestration: resolve target parent -> move wiki node")
|
|
default:
|
|
dry.Desc("1-step request: move wiki node")
|
|
}
|
|
|
|
if spec.SourceSpaceID == "" {
|
|
dry.GET("/open-apis/wiki/v2/spaces/get_node").
|
|
Desc(fmt.Sprintf("[%d] Resolve source space from node token", step)).
|
|
Params(map[string]interface{}{"token": spec.NodeToken})
|
|
step++
|
|
}
|
|
if spec.TargetParentToken != "" {
|
|
dry.GET("/open-apis/wiki/v2/spaces/get_node").
|
|
Desc(fmt.Sprintf("[%d] Resolve target parent node", step)).
|
|
Params(map[string]interface{}{"token": spec.TargetParentToken})
|
|
step++
|
|
}
|
|
|
|
dry.POST(fmt.Sprintf(
|
|
"/open-apis/wiki/v2/spaces/%s/nodes/%s/move",
|
|
dryRunWikiMoveSourceSpaceID(spec),
|
|
validate.EncodePathSegment(spec.NodeToken),
|
|
)).
|
|
Desc(fmt.Sprintf("[%d] Move wiki node", step)).
|
|
Body(spec.NodeMoveBody())
|
|
case wikiMoveModeDocsToWiki:
|
|
dry.Desc("2-step orchestration: move Drive document into Wiki -> poll wiki task result when task_id is returned")
|
|
dry.POST(fmt.Sprintf(
|
|
"/open-apis/wiki/v2/spaces/%s/nodes/move_docs_to_wiki",
|
|
dryRunWikiMoveTargetSpaceID(spec),
|
|
)).
|
|
Desc("[1] Move Drive document into Wiki").
|
|
Body(spec.DocsToWikiBody())
|
|
dry.GET("/open-apis/wiki/v2/tasks/:task_id").
|
|
Desc("[2] Poll wiki move task result when async").
|
|
Set("task_id", "<task_id>").
|
|
Params(map[string]interface{}{"task_type": "move"})
|
|
default:
|
|
dry.Set("error", "unknown wiki move mode")
|
|
}
|
|
return dry
|
|
}
|
|
|
|
func dryRunWikiMoveSourceSpaceID(spec wikiMoveSpec) string {
|
|
if spec.SourceSpaceID != "" {
|
|
return validate.EncodePathSegment(spec.SourceSpaceID)
|
|
}
|
|
return "<resolved_source_space_id>"
|
|
}
|
|
|
|
func dryRunWikiMoveTargetSpaceID(spec wikiMoveSpec) string {
|
|
if spec.TargetSpaceID != "" {
|
|
return validate.EncodePathSegment(spec.TargetSpaceID)
|
|
}
|
|
return "<target_space_id>"
|
|
}
|
|
|
|
func runWikiMove(ctx context.Context, client wikiMoveClient, runtime *common.RuntimeContext, spec wikiMoveSpec) (map[string]interface{}, error) {
|
|
switch spec.Mode() {
|
|
case wikiMoveModeNode:
|
|
return runWikiNodeMove(ctx, client, spec)
|
|
case wikiMoveModeDocsToWiki:
|
|
return runWikiDocsToWikiMove(ctx, client, runtime, spec)
|
|
default:
|
|
return nil, output.ErrValidation("unknown wiki move mode")
|
|
}
|
|
}
|
|
|
|
func runWikiNodeMove(ctx context.Context, client wikiMoveClient, spec wikiMoveSpec) (map[string]interface{}, error) {
|
|
sourceSpaceID, targetSpaceID, err := resolveWikiNodeMoveSpaces(ctx, client, spec)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
node, err := client.MoveNode(ctx, sourceSpaceID, spec)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
out := map[string]interface{}{
|
|
"mode": wikiMoveModeNode,
|
|
"source_space_id": sourceSpaceID,
|
|
"target_space_id": targetSpaceID,
|
|
}
|
|
appendWikiNodeOutput(out, node)
|
|
return out, nil
|
|
}
|
|
|
|
func resolveWikiNodeMoveSpaces(ctx context.Context, client wikiMoveClient, spec wikiMoveSpec) (string, string, error) {
|
|
// Node move requests may start from just a node token and/or a target parent.
|
|
// Resolve both ends up front so we can fail on space mismatches before sending
|
|
// the mutation request.
|
|
sourceSpaceID := spec.SourceSpaceID
|
|
if sourceSpaceID == "" {
|
|
sourceNode, err := client.GetNode(ctx, spec.NodeToken)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
sourceSpaceID, err = requireWikiNodeSpaceID(sourceNode)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
}
|
|
|
|
targetSpaceID := spec.TargetSpaceID
|
|
if spec.TargetParentToken != "" {
|
|
targetParent, err := client.GetNode(ctx, spec.TargetParentToken)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
parentSpaceID, err := requireWikiNodeSpaceID(targetParent)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
if targetSpaceID == "" {
|
|
targetSpaceID = parentSpaceID
|
|
} else if targetSpaceID != parentSpaceID {
|
|
return "", "", output.ErrValidation(
|
|
"--target-space-id %q does not match target parent node space %q",
|
|
spec.TargetSpaceID,
|
|
parentSpaceID,
|
|
)
|
|
}
|
|
}
|
|
|
|
if targetSpaceID == "" {
|
|
targetSpaceID = sourceSpaceID
|
|
}
|
|
|
|
return sourceSpaceID, targetSpaceID, nil
|
|
}
|
|
|
|
func runWikiDocsToWikiMove(ctx context.Context, client wikiMoveClient, runtime *common.RuntimeContext, spec wikiMoveSpec) (map[string]interface{}, error) {
|
|
response, err := client.MoveDocsToWiki(ctx, spec.TargetSpaceID, spec)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
out := map[string]interface{}{
|
|
"mode": wikiMoveModeDocsToWiki,
|
|
"obj_type": spec.ObjType,
|
|
"obj_token": spec.ObjToken,
|
|
"target_space_id": spec.TargetSpaceID,
|
|
"target_parent_token": spec.TargetParentToken,
|
|
}
|
|
|
|
// move_docs_to_wiki has three success-shaped responses: immediate completion,
|
|
// approval-request submission, or an async task that must be polled.
|
|
switch {
|
|
case response.WikiToken != "":
|
|
out["ready"] = true
|
|
out["failed"] = false
|
|
out["wiki_token"] = response.WikiToken
|
|
out["node_token"] = response.WikiToken
|
|
return out, nil
|
|
case response.Applied:
|
|
out["ready"] = false
|
|
out["failed"] = false
|
|
out["applied"] = true
|
|
out["status_msg"] = "move request submitted for approval"
|
|
return out, nil
|
|
case response.TaskID != "":
|
|
fmt.Fprintf(runtime.IO().ErrOut, "Docs-to-wiki move is async, polling task %s...\n", response.TaskID)
|
|
status, ready, err := pollWikiMoveTask(ctx, client, runtime, response.TaskID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
out["task_id"] = response.TaskID
|
|
out["ready"] = ready
|
|
out["failed"] = status.Failed()
|
|
out["status"] = status.PrimaryStatusCode()
|
|
out["status_msg"] = status.PrimaryStatusLabel()
|
|
if first := status.FirstResult(); first != nil {
|
|
appendWikiNodeOutput(out, first.Node)
|
|
if first.Node != nil && first.Node.NodeToken != "" {
|
|
out["wiki_token"] = first.Node.NodeToken
|
|
}
|
|
}
|
|
if !ready {
|
|
nextCommand := wikiMoveTaskResultCommand(response.TaskID, runtime.As())
|
|
fmt.Fprintf(runtime.IO().ErrOut, "Wiki move task is still in progress. Continue with: %s\n", nextCommand)
|
|
out["timed_out"] = true
|
|
out["next_command"] = nextCommand
|
|
}
|
|
return out, nil
|
|
default:
|
|
return nil, output.Errorf(output.ExitAPI, "api_error", "move_docs_to_wiki returned neither wiki_token, task_id, nor applied result")
|
|
}
|
|
}
|
|
|
|
func wikiMoveTaskResultCommand(taskID string, identity core.Identity) string {
|
|
asFlag := string(identity)
|
|
if asFlag == "" {
|
|
asFlag = "user"
|
|
}
|
|
return fmt.Sprintf("lark-cli drive +task_result --scenario wiki_move --task-id %s --as %s", taskID, asFlag)
|
|
}
|
|
|
|
func pollWikiMoveTask(ctx context.Context, client wikiMoveClient, runtime *common.RuntimeContext, taskID string) (wikiMoveTaskStatus, bool, error) {
|
|
lastStatus := wikiMoveTaskStatus{TaskID: taskID}
|
|
var lastErr error
|
|
hadSuccessfulPoll := false
|
|
|
|
// The move request itself already succeeded. Treat poll failures as transient
|
|
// until every attempt fails, then return a resume hint instead of discarding
|
|
// the task identifier.
|
|
for attempt := 1; attempt <= wikiMovePollAttempts; attempt++ {
|
|
if attempt > 1 {
|
|
select {
|
|
case <-ctx.Done():
|
|
return lastStatus, false, ctx.Err()
|
|
case <-time.After(wikiMovePollInterval):
|
|
}
|
|
}
|
|
|
|
status, err := client.GetMoveTask(ctx, taskID)
|
|
if err != nil {
|
|
lastErr = err
|
|
fmt.Fprintf(runtime.IO().ErrOut, "Wiki move status attempt %d/%d failed: %v\n", attempt, wikiMovePollAttempts, err)
|
|
continue
|
|
}
|
|
lastStatus = status
|
|
hadSuccessfulPoll = true
|
|
|
|
if status.Ready() {
|
|
fmt.Fprintf(runtime.IO().ErrOut, "Wiki move task completed successfully.\n")
|
|
return status, true, nil
|
|
}
|
|
if status.Failed() {
|
|
return status, false, output.Errorf(output.ExitAPI, "api_error", "wiki move task failed: %s", status.PrimaryStatusLabel())
|
|
}
|
|
|
|
fmt.Fprintf(runtime.IO().ErrOut, "Wiki move status %d/%d: %s\n", attempt, wikiMovePollAttempts, status.PrimaryStatusLabel())
|
|
}
|
|
|
|
if !hadSuccessfulPoll && lastErr != nil {
|
|
nextCommand := wikiMoveTaskResultCommand(taskID, runtime.As())
|
|
hint := fmt.Sprintf(
|
|
"the wiki move task was created but every status poll failed (task_id=%s)\nretry status lookup with: %s",
|
|
taskID,
|
|
nextCommand,
|
|
)
|
|
var exitErr *output.ExitError
|
|
if errors.As(lastErr, &exitErr) && exitErr.Detail != nil {
|
|
if strings.TrimSpace(exitErr.Detail.Hint) != "" {
|
|
hint = exitErr.Detail.Hint + "\n" + hint
|
|
}
|
|
return lastStatus, false, output.ErrWithHint(exitErr.Code, exitErr.Detail.Type, exitErr.Detail.Message, hint)
|
|
}
|
|
return lastStatus, false, output.ErrWithHint(output.ExitAPI, "api_error", lastErr.Error(), hint)
|
|
}
|
|
|
|
return lastStatus, false, nil
|
|
}
|
|
|
|
func parseWikiMoveTaskStatus(taskID string, task map[string]interface{}) (wikiMoveTaskStatus, error) {
|
|
if task == nil {
|
|
return wikiMoveTaskStatus{}, output.Errorf(output.ExitAPI, "api_error", "wiki task response missing task")
|
|
}
|
|
|
|
status := wikiMoveTaskStatus{
|
|
TaskID: common.GetString(task, "task_id"),
|
|
}
|
|
if status.TaskID == "" {
|
|
status.TaskID = taskID
|
|
}
|
|
|
|
for _, item := range common.GetSlice(task, "move_result") {
|
|
resultMap, ok := item.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
var node *wikiNodeRecord
|
|
if nodeMap := common.GetMap(resultMap, "node"); nodeMap != nil {
|
|
parsedNode, err := parseWikiNodeRecord(nodeMap)
|
|
if err != nil {
|
|
return wikiMoveTaskStatus{}, err
|
|
}
|
|
node = parsedNode
|
|
}
|
|
|
|
status.MoveResults = append(status.MoveResults, wikiMoveTaskResult{
|
|
Node: node,
|
|
Status: int(common.GetFloat(resultMap, "status")),
|
|
StatusMsg: common.GetString(resultMap, "status_msg"),
|
|
})
|
|
}
|
|
|
|
return status, nil
|
|
}
|
|
|
|
func appendWikiNodeOutput(out map[string]interface{}, node *wikiNodeRecord) {
|
|
if out == nil || node == nil {
|
|
return
|
|
}
|
|
out["space_id"] = node.SpaceID
|
|
out["node_token"] = node.NodeToken
|
|
out["obj_token"] = node.ObjToken
|
|
out["obj_type"] = node.ObjType
|
|
out["parent_node_token"] = node.ParentNodeToken
|
|
out["node_type"] = node.NodeType
|
|
out["origin_node_token"] = node.OriginNodeToken
|
|
out["title"] = node.Title
|
|
out["has_child"] = node.HasChild
|
|
}
|