Skip to content

Commit ac116e7

Browse files
feat(drive): add drive preview and cover shortcuts and document quota details (#1259)
* feat: support get quota detail * feat: add drive preview and cover shortcuts - add `drive +preview` and `drive +cover` shortcuts - wrap `preview_result` output with stable preview item fields - support cover download via `preview_download` with validated preset mappings - update lark-drive skill references for preview and cover usage * fix(drive): classify cover 404 as failed precondition * fix(drive): show preview download step in dry-run * docs(drive): clarify quota details user-only usage * fix(drive): soften cover 404 guidance
1 parent 5e6a3eb commit ac116e7

11 files changed

Lines changed: 2591 additions & 25 deletions

File tree

shortcuts/drive/drive_cover.go

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
2+
// SPDX-License-Identifier: MIT
3+
4+
package drive
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"net/http"
10+
"strings"
11+
12+
"github.com/larksuite/cli/errs"
13+
"github.com/larksuite/cli/internal/validate"
14+
"github.com/larksuite/cli/shortcuts/common"
15+
)
16+
17+
var DriveCover = common.Shortcut{
18+
Service: "drive",
19+
Command: "+cover",
20+
Description: "List or download stable cover presets for a Drive file",
21+
Risk: "read",
22+
Scopes: []string{"drive:file:download"},
23+
AuthTypes: []string{"user", "bot"},
24+
Flags: []common.Flag{
25+
{Name: "file-token", Desc: "Drive file token", Required: true},
26+
{Name: "spec", Desc: "cover preset: default | icon | grid | small | middle | big | square"},
27+
{Name: "version", Desc: "optional file version"},
28+
{Name: "list-only", Type: "bool", Desc: "list built-in cover specs without downloading"},
29+
{Name: "output", Desc: "local output path for downloaded cover"},
30+
{Name: "if-exists", Desc: "output conflict policy: error | overwrite | rename", Default: drivePreviewIfExistsError, Enum: []string{drivePreviewIfExistsError, drivePreviewIfExistsOverwrite, drivePreviewIfExistsRename}},
31+
},
32+
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
33+
if err := validate.ResourceName(runtime.Str("file-token"), "--file-token"); err != nil {
34+
return errs.NewValidationError(errs.SubtypeInvalidArgument, "%s", err).WithParam("--file-token")
35+
}
36+
if err := validateDrivePreviewMode(runtime.Str("spec"), runtime.Bool("list-only"), runtime.Str("output"), "spec"); err != nil {
37+
return err
38+
}
39+
if err := validateDrivePreviewIfExists(runtime.Str("if-exists")); err != nil {
40+
return err
41+
}
42+
if spec := strings.TrimSpace(runtime.Str("spec")); spec != "" {
43+
if _, ok := findDriveCoverSpec(spec); !ok {
44+
return wrapDriveCoverUnavailable(spec)
45+
}
46+
}
47+
return nil
48+
},
49+
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
50+
fileToken := runtime.Str("file-token")
51+
if runtime.Bool("list-only") {
52+
return common.NewDryRunAPI().
53+
Desc("List built-in cover specs (no API call)").
54+
Set("mode", "list").
55+
Set("file_token", fileToken).
56+
Set("candidates", buildDriveCoverListOutput(fileToken)["candidates"])
57+
}
58+
59+
spec, _ := findDriveCoverSpec(runtime.Str("spec"))
60+
params := buildDriveCoverDownloadParams(strings.TrimSpace(runtime.Str("version")), spec)
61+
dry := common.NewDryRunAPI().
62+
GET("/open-apis/drive/v1/medias/:file_token/preview_download").
63+
Desc("Download selected cover preset directly via preview_download").
64+
Params(params).
65+
Set("file_token", fileToken).
66+
Set("selected_spec", spec.Name).
67+
Set("output", runtime.Str("output"))
68+
return dry
69+
},
70+
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
71+
fileToken := runtime.Str("file-token")
72+
version := strings.TrimSpace(runtime.Str("version"))
73+
requestedSpec := strings.TrimSpace(runtime.Str("spec"))
74+
outputPath := runtime.Str("output")
75+
ifExists := runtime.Str("if-exists")
76+
77+
if runtime.Bool("list-only") {
78+
runtime.Out(buildDriveCoverListOutput(fileToken), nil)
79+
return nil
80+
}
81+
82+
spec, ok := findDriveCoverSpec(requestedSpec)
83+
if !ok {
84+
return wrapDriveCoverUnavailable(requestedSpec)
85+
}
86+
87+
fmt.Fprintf(runtime.IO().ErrOut, "Downloading cover %s for file %s\n", spec.Name, common.MaskToken(fileToken))
88+
result, err := downloadDrivePreviewArtifactWithParams(ctx, runtime, fileToken, buildDriveCoverDownloadParams(version, spec), outputPath, ifExists, spec.FallbackExt)
89+
if err != nil {
90+
return wrapDriveCoverDownloadError(err, spec.Name)
91+
}
92+
result["mode"] = "download"
93+
result["file_token"] = fileToken
94+
result["selected_spec"] = spec.Name
95+
runtime.Out(result, nil)
96+
return nil
97+
},
98+
}
99+
100+
// wrapDriveCoverDownloadError reclassifies preview_download HTTP 404 responses
101+
// on the +cover path as a failed precondition on --spec, because the Drive
102+
// shortcut contract documents 404 as "this file has no artifact for that cover
103+
// preset" rather than a transient transport failure.
104+
func wrapDriveCoverDownloadError(err error, requestedSpec string) error {
105+
if err == nil {
106+
return nil
107+
}
108+
problem, ok := errs.ProblemOf(err)
109+
if !ok || problem.Code != http.StatusNotFound {
110+
return err
111+
}
112+
hint := fmt.Sprintf(
113+
"This may mean no artifact exists for --spec %q, or that the file token/version is invalid. Verify the inputs, or rerun with `lark-cli drive +cover --file-token <file-token> --list-only`. Available cover specs: %s",
114+
requestedSpec,
115+
strings.Join(availableDriveCoverSpecs(), ", "),
116+
)
117+
return errs.NewValidationError(
118+
errs.SubtypeFailedPrecondition,
119+
"preview_download returned HTTP 404 for --spec %q",
120+
requestedSpec,
121+
).WithParam("--spec").WithCode(problem.Code).WithLogID(problem.LogID).WithHint(hint).WithCause(err)
122+
}

shortcuts/drive/drive_preview.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
2+
// SPDX-License-Identifier: MIT
3+
4+
package drive
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"strings"
10+
11+
"github.com/larksuite/cli/errs"
12+
"github.com/larksuite/cli/internal/validate"
13+
"github.com/larksuite/cli/shortcuts/common"
14+
)
15+
16+
var DrivePreview = common.Shortcut{
17+
Service: "drive",
18+
Command: "+preview",
19+
Description: "List or download available preview artifacts for a Drive file",
20+
Risk: "read",
21+
Scopes: []string{"drive:file:download"},
22+
AuthTypes: []string{"user", "bot"},
23+
Flags: []common.Flag{
24+
{Name: "file-token", Desc: "Drive file token", Required: true},
25+
{Name: "type", Desc: "preview type to download: pdf | html | text | image | source"},
26+
{Name: "version", Desc: "optional file version"},
27+
{Name: "list-only", Type: "bool", Desc: "list preview candidates without downloading"},
28+
{Name: "output", Desc: "local output path for downloaded preview"},
29+
{Name: "if-exists", Desc: "output conflict policy: error | overwrite | rename", Default: drivePreviewIfExistsError, Enum: []string{drivePreviewIfExistsError, drivePreviewIfExistsOverwrite, drivePreviewIfExistsRename}},
30+
},
31+
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
32+
if err := validate.ResourceName(runtime.Str("file-token"), "--file-token"); err != nil {
33+
return errs.NewValidationError(errs.SubtypeInvalidArgument, "%s", err).WithParam("--file-token")
34+
}
35+
if err := validateDrivePreviewMode(runtime.Str("type"), runtime.Bool("list-only"), runtime.Str("output"), "type"); err != nil {
36+
return err
37+
}
38+
return validateDrivePreviewIfExists(runtime.Str("if-exists"))
39+
},
40+
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
41+
fileToken := runtime.Str("file-token")
42+
version := strings.TrimSpace(runtime.Str("version"))
43+
body := map[string]interface{}{}
44+
if version != "" {
45+
body["version"] = version
46+
}
47+
dry := common.NewDryRunAPI().
48+
POST("/open-apis/drive/v1/medias/:file_token/preview_result").
49+
Desc("[1] Fetch preview candidates for a Drive file").
50+
Set("file_token", fileToken)
51+
if len(body) > 0 {
52+
dry.Body(body)
53+
}
54+
if runtime.Bool("list-only") {
55+
return dry.Set("mode", "list")
56+
}
57+
downloadParams := map[string]interface{}{
58+
"preview_type": "<selected type_code from preview_result>",
59+
}
60+
if version != "" {
61+
downloadParams["version"] = version
62+
} else {
63+
downloadParams["version"] = "<resolved version from preview_result>"
64+
}
65+
return dry.
66+
GET("/open-apis/drive/v1/medias/:file_token/preview_download").
67+
Desc("[2] Download the requested preview after selecting a matching candidate from preview_result").
68+
Params(downloadParams).
69+
Set("mode", "download").
70+
Set("requested_type", runtime.Str("type")).
71+
Set("output", runtime.Str("output"))
72+
},
73+
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
74+
fileToken := runtime.Str("file-token")
75+
version := strings.TrimSpace(runtime.Str("version"))
76+
requestedType := strings.TrimSpace(runtime.Str("type"))
77+
outputPath := runtime.Str("output")
78+
ifExists := runtime.Str("if-exists")
79+
80+
body := map[string]interface{}{}
81+
if version != "" {
82+
body["version"] = version
83+
}
84+
85+
fmt.Fprintf(runtime.IO().ErrOut, "Fetching preview candidates: %s\n", common.MaskToken(fileToken))
86+
data, candidates, err := fetchDrivePreviewCandidates(runtime, fileToken, body)
87+
if err != nil {
88+
return err
89+
}
90+
if runtime.Bool("list-only") {
91+
runtime.Out(buildDrivePreviewListOutput(fileToken, candidates), nil)
92+
return nil
93+
}
94+
95+
candidate, ok := selectDrivePreviewCandidate(candidates, requestedType)
96+
if !ok {
97+
return wrapDrivePreviewUnavailable(fileToken, requestedType, candidates, "")
98+
}
99+
if !candidate.Downloadable {
100+
return wrapDrivePreviewNotReady(fileToken, requestedType, candidate)
101+
}
102+
103+
downloadVersion := version
104+
if downloadVersion == "" {
105+
downloadVersion = versionString(data["version"])
106+
}
107+
fmt.Fprintf(runtime.IO().ErrOut, "Downloading preview %s for file %s\n", candidate.Type, common.MaskToken(fileToken))
108+
result, err := downloadDrivePreviewArtifact(ctx, runtime, fileToken, candidate.TypeCode, downloadVersion, outputPath, ifExists, drivePreviewFallbackExt(candidate.Type))
109+
if err != nil {
110+
return err
111+
}
112+
result["mode"] = "download"
113+
result["file_token"] = fileToken
114+
result["selected_type"] = candidate.Type
115+
runtime.Out(result, nil)
116+
return nil
117+
},
118+
}

0 commit comments

Comments
 (0)