mirror of
https://github.com/larksuite/cli.git
synced 2026-07-03 14:02:43 +08:00
Drive-domain errors now leave the CLI as typed, machine-branchable envelopes — a stable `type` plus `subtype` and named fields (param, params, retryable, log_id, hint) — so scripts and AI agents can branch on structure and act on a recovery hint instead of parsing prose. Changes: - Every error produced in the drive domain — validation, file I/O, and the failures returned from its Lark API calls — is emitted as a typed errs.* error; the exit code is derived from the error category. Drive's API calls now go through a shared typed classifier, so failures carry subtype, troubleshooter, a recovery hint, and the request's log_id whether the server returns it in the response body or the x-tt-logid header; an already-typed network/auth error is never downgraded into a generic API error. - Known API conditions (resource conflict, cross-tenant, cross-brand, ...) carry a recovery hint keyed by their error class; a command can refine that hint with command-specific guidance. - Batch partial failures (+push / +pull / +sync, where some items succeed and some fail) now report an honest ok:false multi-status result on stdout — the summary and every per-item outcome stay machine-readable — and exit non-zero, instead of a misleading ok:true success envelope. - Duplicate rel_path conflicts report each colliding path as a structured params entry (RFC 7807 invalid-params style). - Static guards lock the drive path so legacy error construction — direct envelopes or the auto-classifying API helpers — cannot be reintroduced, making drive the template for the remaining domains. Output changes worth noting for consumers: - Error envelopes now carry typed type/subtype and named fields; exit codes follow the error category (malformed or incomplete API responses are reported as internal errors rather than generic API errors). - Batch partial failures (+push / +pull / +sync) emit an ok:false result envelope on stdout (summary + per-item items[]) and exit non-zero; the per-item results stay on stdout rather than in a stderr error envelope. Errors surfaced through shared cross-domain helpers (scope precheck, media import upload, metadata lookup, save-path resolution) are not yet typed; they migrate with the shared layer in a follow-up change.
123 lines
3.9 KiB
Go
123 lines
3.9 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package drive
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/larksuite/cli/errs"
|
|
"github.com/larksuite/cli/internal/validate"
|
|
"github.com/larksuite/cli/shortcuts/common"
|
|
)
|
|
|
|
type driveCreateFolderSpec struct {
|
|
Name string
|
|
FolderToken string
|
|
}
|
|
|
|
func newDriveCreateFolderSpec(runtime *common.RuntimeContext) driveCreateFolderSpec {
|
|
return driveCreateFolderSpec{
|
|
Name: strings.TrimSpace(runtime.Str("name")),
|
|
FolderToken: strings.TrimSpace(runtime.Str("folder-token")),
|
|
}
|
|
}
|
|
|
|
func (s driveCreateFolderSpec) RequestBody() map[string]interface{} {
|
|
return map[string]interface{}{
|
|
"name": s.Name,
|
|
"folder_token": s.FolderToken,
|
|
}
|
|
}
|
|
|
|
// DriveCreateFolder creates a new Drive folder under the specified parent
|
|
// folder, or under the caller's root folder when --folder-token is omitted.
|
|
var DriveCreateFolder = common.Shortcut{
|
|
Service: "drive",
|
|
Command: "+create-folder",
|
|
Description: "Create a folder in Drive",
|
|
Risk: "write",
|
|
Scopes: []string{"space:folder:create"},
|
|
AuthTypes: []string{"user", "bot"},
|
|
Flags: []common.Flag{
|
|
{Name: "name", Desc: "folder name", Required: true},
|
|
{Name: "folder-token", Desc: "parent folder token (default: root folder)"},
|
|
},
|
|
Tips: []string{
|
|
"Omit --folder-token to create the folder in the caller's root folder.",
|
|
},
|
|
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
|
return validateDriveCreateFolderSpec(newDriveCreateFolderSpec(runtime))
|
|
},
|
|
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
|
spec := newDriveCreateFolderSpec(runtime)
|
|
dry := common.NewDryRunAPI().
|
|
Desc("Create a folder in Drive").
|
|
POST("/open-apis/drive/v1/files/create_folder").
|
|
Desc("[1] Create folder").
|
|
Body(spec.RequestBody())
|
|
if runtime.IsBot() {
|
|
dry.Desc("After folder creation succeeds in bot mode, the CLI will also try to grant the current CLI user full_access (可管理权限) on the new folder.")
|
|
}
|
|
return dry
|
|
},
|
|
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
|
spec := newDriveCreateFolderSpec(runtime)
|
|
|
|
target := "root folder"
|
|
if spec.FolderToken != "" {
|
|
target = "folder " + common.MaskToken(spec.FolderToken)
|
|
}
|
|
fmt.Fprintf(runtime.IO().ErrOut, "Creating folder %q in %s...\n", spec.Name, target)
|
|
|
|
data, err := runtime.CallAPITyped(
|
|
"POST",
|
|
"/open-apis/drive/v1/files/create_folder",
|
|
nil,
|
|
spec.RequestBody(),
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
folderToken := common.GetString(data, "token")
|
|
if folderToken == "" {
|
|
return errs.NewInternalError(errs.SubtypeInvalidResponse, "drive create_folder succeeded but returned no folder token (data.token)")
|
|
}
|
|
out := map[string]interface{}{
|
|
"created": true,
|
|
"name": spec.Name,
|
|
"folder_token": folderToken,
|
|
"parent_folder_token": spec.FolderToken,
|
|
}
|
|
if url := strings.TrimSpace(common.GetString(data, "url")); url != "" {
|
|
out["url"] = url
|
|
} else if u := common.BuildResourceURL(runtime.Config.Brand, "folder", folderToken); u != "" {
|
|
out["url"] = u
|
|
}
|
|
if grant := common.AutoGrantCurrentUserDrivePermission(runtime, folderToken, "folder"); grant != nil {
|
|
out["permission_grant"] = grant
|
|
}
|
|
|
|
runtime.Out(out, nil)
|
|
return nil
|
|
},
|
|
}
|
|
|
|
func validateDriveCreateFolderSpec(spec driveCreateFolderSpec) error {
|
|
if spec.Name == "" {
|
|
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--name must not be empty").WithParam("--name")
|
|
}
|
|
if nameBytes := len([]byte(spec.Name)); nameBytes > 256 {
|
|
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--name exceeds the maximum of 256 bytes (got %d)", nameBytes).WithParam("--name")
|
|
}
|
|
if spec.FolderToken != "" {
|
|
if err := validate.ResourceName(spec.FolderToken, "--folder-token"); err != nil {
|
|
return errs.NewValidationError(errs.SubtypeInvalidArgument, "%s", err).WithParam("--folder-token")
|
|
}
|
|
}
|
|
return nil
|
|
}
|