Files
larksuite__cli/shortcuts/doc/doc_errors.go
wangweiming-01 0f82591303 feat: preflight export permission for downloads (#2218)
Check Drive export permission before file and document media downloads, while preserving whiteboard behavior and adding actionable preview and rate-limit recovery hints.

Spec source: active@ddbecbafcf0d68cc115faf3fcf0214fb6edc30b284147d33e5c9934e7bad82b4
2026-08-07 18:41:34 +08:00

89 lines
3.1 KiB
Go

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package doc
import (
"errors"
"fmt"
"net/http"
"strings"
"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/shortcuts/common"
)
// wrapDocNetworkErr returns err unchanged when it is already a typed errs.*
// error (preserving its subtype / code / log_id from the runtime boundary),
// and only wraps a raw, unclassified error as a transport-level network error.
func wrapDocNetworkErr(err error, format string, args ...any) error {
if _, ok := errs.ProblemOf(err); ok {
return err
}
return errs.NewNetworkError(errs.SubtypeNetworkTransport, format, args...).WithCause(err)
}
// withDocMediaDownloadRecoveryHint keeps the final download error intact while
// adding recovery guidance for media permission and throttling failures.
// Whiteboard downloads use a different API and must not be redirected to the
// media preview shortcut.
func withDocMediaDownloadRecoveryHint(err error, mediaType string) error {
problem, ok := errs.ProblemOf(err)
if !ok || problem == nil {
return err
}
if mediaType != "whiteboard" &&
problem.Category == errs.CategoryNetwork &&
problem.Code == http.StatusForbidden &&
!strings.Contains(problem.Hint, "docs +media-preview") {
const tokenArg = "<MEDIA_TOKEN>"
hint := fmt.Sprintf("Direct document media download returned HTTP 403. To preview the image or file content, try `lark-cli docs +media-preview --token %s --output <path>`.", tokenArg)
appendDocRecoveryHint(problem, hint)
}
if docMediaDownloadIsRateLimit(problem) && !strings.Contains(problem.Hint, "exponential backoff") {
const hint = "Document media download was rate limited; stop immediate retries and retry later with exponential backoff."
appendDocRecoveryHint(problem, hint)
}
return err
}
func docMediaDownloadIsRateLimit(problem *errs.Problem) bool {
return problem.Subtype == errs.SubtypeRateLimit ||
problem.Code == 99991400 ||
problem.Code == http.StatusTooManyRequests
}
func appendDocRecoveryHint(problem *errs.Problem, hint string) {
if strings.TrimSpace(problem.Hint) == "" {
problem.Hint = hint
return
}
problem.Hint = strings.TrimSpace(problem.Hint) + "\n" + hint
}
func docMediaDownloadPermissionDeniedError() error {
const tokenArg = "<MEDIA_TOKEN>"
return errs.NewPermissionError(
errs.SubtypePermissionDenied,
"current identity does not have export permission for this document media",
).WithHint(
"Direct document media download is unavailable. To preview the image or file content, try `lark-cli docs +media-preview --token %s --output <path>`.",
tokenArg,
)
}
// wrapDocInputFileErr wraps a --file Stat/read failure via the shared typed
// helper (which sets the cause) and tags it with the --file param so agents
// learn which flag to fix. The common helper is flag-agnostic, so the param is
// attached here at the Doc call site rather than mutating shared behavior.
func wrapDocInputFileErr(err error, readMsg string) error {
wrapped := common.WrapInputStatErrorTyped(err, readMsg)
var ve *errs.ValidationError
if errors.As(wrapped, &ve) {
ve.Param = "--file"
}
return wrapped
}