mirror of
https://github.com/larksuite/cli.git
synced 2026-07-03 14:02:43 +08:00
Emit structured validation, API, network, file, and internal error envelopes for Doc shortcuts so users and agents can recover from failed document workflows using stable type, subtype, param, and code fields. Add Doc domain errscontract and golangci guards to prevent legacy envelope and common helper regressions.
105 lines
3.6 KiB
Go
105 lines
3.6 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package doc
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
larkcore "github.com/larksuite/oapi-sdk-go/v3/core"
|
|
|
|
"github.com/larksuite/cli/errs"
|
|
"github.com/larksuite/cli/extension/fileio"
|
|
"github.com/larksuite/cli/internal/validate"
|
|
"github.com/larksuite/cli/shortcuts/common"
|
|
)
|
|
|
|
const PreviewType_SOURCE_FILE = "16"
|
|
|
|
var DocMediaPreview = common.Shortcut{
|
|
Service: "docs",
|
|
Command: "+media-preview",
|
|
Description: "Preview document media file (auto-detects extension)",
|
|
Risk: "read",
|
|
Scopes: []string{"docs:document.media:download"},
|
|
AuthTypes: []string{"user", "bot"},
|
|
Flags: []common.Flag{
|
|
{Name: "token", Desc: "media file token", Required: true},
|
|
{Name: "output", Desc: "local save path", Required: true},
|
|
{Name: "overwrite", Type: "bool", Desc: "overwrite existing output file"},
|
|
},
|
|
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
|
token := runtime.Str("token")
|
|
outputPath := runtime.Str("output")
|
|
return common.NewDryRunAPI().
|
|
GET("/open-apis/drive/v1/medias/:token/preview_download").
|
|
Desc("Preview document media file").
|
|
Params(map[string]interface{}{"preview_type": PreviewType_SOURCE_FILE}).
|
|
Set("token", token).Set("output", outputPath)
|
|
},
|
|
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
|
token := runtime.Str("token")
|
|
outputPath := runtime.Str("output")
|
|
overwrite := runtime.Bool("overwrite")
|
|
|
|
if err := validate.ResourceName(token, "--token"); err != nil {
|
|
return errs.NewValidationError(errs.SubtypeInvalidArgument, "%s", err).WithParam("--token")
|
|
}
|
|
// Early path validation before API call (final validation after auto-extension below)
|
|
if _, err := runtime.ResolveSavePath(outputPath); err != nil {
|
|
return errs.NewValidationError(errs.SubtypeInvalidArgument, "unsafe output path: %s", err).WithParam("--output").WithCause(err)
|
|
}
|
|
|
|
fmt.Fprintf(runtime.IO().ErrOut, "Previewing: media %s\n", common.MaskToken(token))
|
|
|
|
encodedToken := validate.EncodePathSegment(token)
|
|
apiPath := fmt.Sprintf("/open-apis/drive/v1/medias/%s/preview_download", encodedToken)
|
|
|
|
resp, err := runtime.DoAPIStream(ctx, &larkcore.ApiReq{
|
|
HttpMethod: http.MethodGet,
|
|
ApiPath: apiPath,
|
|
QueryParams: larkcore.QueryParams{
|
|
"preview_type": []string{PreviewType_SOURCE_FILE},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return wrapDocNetworkErr(err, "preview failed: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
finalPath, _ := autoAppendDocMediaExtension(outputPath, resp.Header, "")
|
|
|
|
// Validate final path after extension append
|
|
if finalPath != outputPath {
|
|
if _, err := runtime.ResolveSavePath(finalPath); err != nil {
|
|
return errs.NewValidationError(errs.SubtypeInvalidArgument, "unsafe output path: %s", err).WithParam("--output").WithCause(err)
|
|
}
|
|
}
|
|
|
|
// Overwrite check on final path (after extension detection)
|
|
if !overwrite {
|
|
if _, statErr := runtime.FileIO().Stat(finalPath); statErr == nil {
|
|
return errs.NewValidationError(errs.SubtypeFailedPrecondition, "output file already exists: %s (use --overwrite to replace)", finalPath).WithParam("--output")
|
|
}
|
|
}
|
|
|
|
result, err := runtime.FileIO().Save(finalPath, fileio.SaveOptions{
|
|
ContentType: resp.Header.Get("Content-Type"),
|
|
ContentLength: resp.ContentLength,
|
|
}, resp.Body)
|
|
if err != nil {
|
|
return common.WrapSaveErrorTyped(err)
|
|
}
|
|
|
|
savedPath, _ := runtime.ResolveSavePath(finalPath)
|
|
runtime.Out(map[string]interface{}{
|
|
"saved_path": savedPath,
|
|
"size_bytes": result.Size(),
|
|
"content_type": resp.Header.Get("Content-Type"),
|
|
}, nil)
|
|
return nil
|
|
},
|
|
}
|