mirror of
https://github.com/larksuite/cli.git
synced 2026-07-08 18:13:01 +08:00
* feat(cmdutil): add shared file upload helpers Add ParseFileFlag, ValidateFileFlag, and BuildFormdata to support multipart file upload via --file flag across raw API and meta API commands. Change-Id: Ib724cf8b055b0b314af11d8d830f38559dac60eb * feat(api): add --file flag for multipart/form-data file uploads Add --file flag to `lark-cli api` command enabling file upload via multipart/form-data. The flag accepts [field=]path format and supports stdin (-). Includes mutual exclusion validation with --output, --page-all, and GET method. Dry-run mode shows file metadata instead of building actual formdata. Change-Id: Icf34aba5da3a558219a97a583e8f6aa951ded199 * feat(service): add --file flag with auto-detection from metadata Add file upload support to meta API service method commands. The --file flag is conditionally registered only for methods whose metadata declares file-type fields (POST/PUT/PATCH/DELETE). The default field name is auto-detected from metadata when exactly one file field exists. Change-Id: Ibbf04eb42341ba11bb1fd9750e63bc1d0eacd08d * feat(schema): show file upload indicators in method detail display Add hasFileFields helper to detect file-type fields in requestBody metadata. Modify printMethodDetail to display [file upload] tag on --data line, --file flag description with default field name, and --file <path> in CLI example for methods that accept file uploads. Change-Id: Iae3bc14fe07e16a8b5f6a50a2b3592d6d8490ed9 * fix: address code review findings for file upload feature - ParseFileFlag: change idx >= 0 to idx > 0 to prevent empty field name when input like "=photo.jpg" is passed - BuildFormdata: read file into bytes.Reader with defer Close to prevent file handle leak on later errors - BuildFormdata: remove unused ctx parameter from signature and callers - Eliminate duplicated dry-run logic by having buildAPIRequest and buildServiceRequest return FileUploadMeta when in dry-run mode, removing ~60 lines of copy-pasted URL building and validation code Change-Id: I27b9534fd0eaefce40390f6e723dd0c04a2cdf80 * fix: address PR review findings - Remove opts.File=="" guard on dual-stdin check so --file photo.jpg --params - --data - correctly reports an error instead of silently dropping --data content (P1 bug in both api.go and service.go) - Extract shared DetectFileFields into cmdutil, deduplicate detectFileFields (service.go) and hasFileFields (schema.go) - Show "<stdin>" instead of empty path in dry-run output for --file - Change-Id: Iccc5d879165ea6a3d04f0425ec6a5018a10e72e1 * fix: reject non-object --data with --file and improve multi-file schema - --data with --file now requires a JSON object; arrays/strings/numbers are rejected with a clear error instead of being silently dropped - Schema display for multi-file methods shows explicit field=path syntax and lists valid field names instead of advertising a false default Change-Id: I0facdb3ad86f68cb125c7ea109a33714fd91dba0
131 lines
3.8 KiB
Go
131 lines
3.8 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package cmdutil
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
|
|
"github.com/larksuite/cli/extension/fileio"
|
|
"github.com/larksuite/cli/internal/output"
|
|
"github.com/larksuite/cli/internal/registry"
|
|
larkcore "github.com/larksuite/oapi-sdk-go/v3/core"
|
|
)
|
|
|
|
// DetectFileFields returns field names with type "file" in the method's requestBody.
|
|
func DetectFileFields(method map[string]interface{}) []string {
|
|
rb, _ := method["requestBody"].(map[string]interface{})
|
|
var fields []string
|
|
for name, field := range rb {
|
|
f, _ := field.(map[string]interface{})
|
|
if registry.GetStrFromMap(f, "type") == "file" {
|
|
fields = append(fields, name)
|
|
}
|
|
}
|
|
return fields
|
|
}
|
|
|
|
// ParseFileFlag parses a --file flag value into its components.
|
|
// The format is either "path" or "field=path". When no explicit "field="
|
|
// prefix is present, defaultField is used as the field name.
|
|
// A path of "-" indicates stdin; in that case filePath is empty and isStdin is true.
|
|
func ParseFileFlag(raw, defaultField string) (fieldName, filePath string, isStdin bool) {
|
|
if idx := strings.IndexByte(raw, '='); idx > 0 {
|
|
fieldName = raw[:idx]
|
|
filePath = raw[idx+1:]
|
|
} else {
|
|
fieldName = defaultField
|
|
filePath = raw
|
|
}
|
|
if filePath == "-" {
|
|
return fieldName, "", true
|
|
}
|
|
return fieldName, filePath, false
|
|
}
|
|
|
|
// ValidateFileFlag checks mutual exclusion rules for the --file flag.
|
|
// Returns nil if file is empty (flag not provided).
|
|
func ValidateFileFlag(file, params, data, outputPath string, pageAll bool, httpMethod string) error {
|
|
if file == "" {
|
|
return nil
|
|
}
|
|
|
|
_, filePath, isStdin := ParseFileFlag(file, "file")
|
|
if !isStdin && filePath == "" {
|
|
return output.ErrValidation("--file: empty file path")
|
|
}
|
|
|
|
if outputPath != "" {
|
|
return output.ErrValidation("--file and --output are mutually exclusive")
|
|
}
|
|
if pageAll {
|
|
return output.ErrValidation("--file and --page-all are mutually exclusive")
|
|
}
|
|
if isStdin && data == "-" {
|
|
return output.ErrValidation("--file and --data cannot both read from stdin")
|
|
}
|
|
if isStdin && params == "-" {
|
|
return output.ErrValidation("--file and --params cannot both read from stdin")
|
|
}
|
|
|
|
switch httpMethod {
|
|
case "POST", "PUT", "PATCH", "DELETE":
|
|
default:
|
|
return output.ErrValidation("--file requires POST, PUT, PATCH, or DELETE method")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// FileUploadMeta holds file upload metadata for dry-run display.
|
|
// Returned by request builders when dry-run mode skips actual file reading.
|
|
type FileUploadMeta struct {
|
|
FieldName string
|
|
FilePath string
|
|
FormFields any
|
|
}
|
|
|
|
// BuildFormdata constructs a multipart form data payload for file upload.
|
|
// If isStdin is true, the file content is read from stdin.
|
|
// Top-level keys from dataJSON are added as text form fields.
|
|
func BuildFormdata(fileIO fileio.FileIO, fieldName, filePath string, isStdin bool, stdin io.Reader, dataJSON any) (*larkcore.Formdata, error) {
|
|
fd := larkcore.NewFormdata()
|
|
|
|
if isStdin {
|
|
if stdin == nil {
|
|
return nil, output.ErrValidation("--file: stdin is not available")
|
|
}
|
|
data, err := io.ReadAll(stdin)
|
|
if err != nil {
|
|
return nil, output.ErrValidation("--file: failed to read stdin: %v", err)
|
|
}
|
|
if len(data) == 0 {
|
|
return nil, output.ErrValidation("--file: stdin is empty")
|
|
}
|
|
fd.AddFile(fieldName, bytes.NewReader(data))
|
|
} else {
|
|
f, err := fileIO.Open(filePath)
|
|
if err != nil {
|
|
return nil, output.ErrValidation("cannot open file: %s", filePath)
|
|
}
|
|
defer f.Close()
|
|
data, err := io.ReadAll(f)
|
|
if err != nil {
|
|
return nil, output.ErrValidation("--file: failed to read %s: %v", filePath, err)
|
|
}
|
|
fd.AddFile(fieldName, bytes.NewReader(data))
|
|
}
|
|
|
|
// Add top-level JSON keys as text form fields.
|
|
if m, ok := dataJSON.(map[string]any); ok {
|
|
for k, v := range m {
|
|
fd.AddField(k, fmt.Sprintf("%v", v))
|
|
}
|
|
}
|
|
|
|
return fd, nil
|
|
}
|