mirror of
https://github.com/larksuite/cli.git
synced 2026-07-03 14:02:43 +08:00
refactor: migrate vc/minutes shortcuts to FileIO (#336)
* refactor: migrate vc/minutes shortcuts to FileIO - vc_notes: replace vfs.Stat + validate.SafeOutputPath + validate.AtomicWrite with FileIO.Stat/Save for transcript download - minutes_download: replace validate.SafeOutputPath + validate.AtomicWriteFromReader with FileIO.Save, use FileIO.Stat for overwrite checks - Use WrapSaveError to preserve original error messages
This commit is contained in:
@@ -363,6 +363,24 @@ func WrapOpenError(err error, pathMsg, readMsg string) error {
|
||||
return fmt.Errorf("%s: %w", readMsg, err)
|
||||
}
|
||||
|
||||
// WrapSaveErrorByCategory maps a FileIO.Save error to structured output errors,
|
||||
// using standardized messages and the given error category (e.g. "api_error", "io").
|
||||
// Path validation errors always use ErrValidation (exit code 2).
|
||||
func WrapSaveErrorByCategory(err error, category string) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
var me *fileio.MkdirError
|
||||
switch {
|
||||
case errors.Is(err, fileio.ErrPathValidation):
|
||||
return output.ErrValidation("unsafe output path: %s", err)
|
||||
case errors.As(err, &me):
|
||||
return output.Errorf(output.ExitInternal, category, "cannot create parent directory: %s", err)
|
||||
default:
|
||||
return output.Errorf(output.ExitInternal, category, "cannot create file: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ValidatePath checks that path is a valid relative input path within the
|
||||
// working directory by delegating to FileIO.Stat. Returns nil if the path is
|
||||
// valid or does not exist yet; returns an error only for illegal paths
|
||||
|
||||
@@ -14,8 +14,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/larksuite/cli/internal/vfs"
|
||||
|
||||
"github.com/larksuite/cli/extension/fileio"
|
||||
"github.com/larksuite/cli/internal/output"
|
||||
"github.com/larksuite/cli/internal/validate"
|
||||
"github.com/larksuite/cli/shortcuts/common"
|
||||
@@ -79,7 +78,7 @@ var MinutesDownload = common.Shortcut{
|
||||
|
||||
// Batch mode: --output must be a directory, not an existing file.
|
||||
if !single && outputPath != "" {
|
||||
if fi, err := vfs.Stat(outputPath); err == nil && !fi.IsDir() {
|
||||
if fi, err := runtime.FileIO().Stat(outputPath); err == nil && !fi.IsDir() {
|
||||
return output.ErrValidation("--output %q is a file; batch mode expects a directory path", outputPath)
|
||||
}
|
||||
}
|
||||
@@ -162,7 +161,7 @@ var MinutesDownload = common.Shortcut{
|
||||
fmt.Fprintf(errOut, "Downloading media: %s\n", common.MaskToken(token))
|
||||
|
||||
// single token: --output is a file path; batch: --output is a directory
|
||||
opts := downloadOpts{overwrite: overwrite, usedNames: usedNames}
|
||||
opts := downloadOpts{fio: runtime.FileIO(), overwrite: overwrite, usedNames: usedNames}
|
||||
if single {
|
||||
opts.outputPath = outputPath
|
||||
} else {
|
||||
@@ -229,8 +228,9 @@ type downloadResult struct {
|
||||
}
|
||||
|
||||
type downloadOpts struct {
|
||||
outputPath string // explicit output file path (single mode only)
|
||||
outputDir string // output directory (batch mode)
|
||||
fio fileio.FileIO // file I/O abstraction
|
||||
outputPath string // explicit output file path (single mode only)
|
||||
outputDir string // output directory (batch mode)
|
||||
overwrite bool
|
||||
usedNames map[string]bool // tracks used filenames to deduplicate in batch mode
|
||||
}
|
||||
@@ -275,22 +275,24 @@ func downloadMediaFile(ctx context.Context, client *http.Client, downloadURL, mi
|
||||
outputPath = filepath.Join(opts.outputDir, filename)
|
||||
}
|
||||
|
||||
safePath, err := validate.SafeOutputPath(outputPath)
|
||||
if err != nil {
|
||||
return nil, output.ErrValidation("unsafe output path: %s", err)
|
||||
}
|
||||
if err := common.EnsureWritableFile(safePath, opts.overwrite); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := vfs.MkdirAll(filepath.Dir(safePath), 0700); err != nil {
|
||||
return nil, output.Errorf(output.ExitInternal, "api_error", "cannot create parent directory: %s", err)
|
||||
if !opts.overwrite {
|
||||
if _, statErr := opts.fio.Stat(outputPath); statErr == nil {
|
||||
return nil, output.ErrValidation("output file already exists: %s (use --overwrite to replace)", outputPath)
|
||||
}
|
||||
}
|
||||
|
||||
sizeBytes, err := validate.AtomicWriteFromReader(safePath, resp.Body, 0600)
|
||||
result, err := opts.fio.Save(outputPath, fileio.SaveOptions{
|
||||
ContentType: resp.Header.Get("Content-Type"),
|
||||
ContentLength: resp.ContentLength,
|
||||
}, resp.Body)
|
||||
if err != nil {
|
||||
return nil, output.Errorf(output.ExitInternal, "api_error", "cannot create file: %s", err)
|
||||
return nil, common.WrapSaveErrorByCategory(err, "io")
|
||||
}
|
||||
return &downloadResult{savedPath: safePath, sizeBytes: sizeBytes}, nil
|
||||
resolvedPath, err := opts.fio.ResolvePath(outputPath)
|
||||
if err != nil || resolvedPath == "" {
|
||||
resolvedPath = outputPath
|
||||
}
|
||||
return &downloadResult{savedPath: resolvedPath, sizeBytes: result.Size()}, nil
|
||||
}
|
||||
|
||||
// resolveFilenameFromResponse derives the filename from HTTP response headers.
|
||||
|
||||
@@ -11,8 +11,10 @@
|
||||
package vc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -22,11 +24,11 @@ import (
|
||||
|
||||
larkcore "github.com/larksuite/oapi-sdk-go/v3/core"
|
||||
|
||||
"github.com/larksuite/cli/extension/fileio"
|
||||
"github.com/larksuite/cli/internal/auth"
|
||||
"github.com/larksuite/cli/internal/credential"
|
||||
"github.com/larksuite/cli/internal/output"
|
||||
"github.com/larksuite/cli/internal/validate"
|
||||
"github.com/larksuite/cli/internal/vfs"
|
||||
"github.com/larksuite/cli/shortcuts/common"
|
||||
)
|
||||
|
||||
@@ -262,25 +264,16 @@ func downloadTranscriptFile(runtime *common.RuntimeContext, minuteToken string,
|
||||
base = outDir
|
||||
}
|
||||
dirName := filepath.Join(base, sanitizeDirName(title, minuteToken))
|
||||
transcriptPath := filepath.Join(dirName, "transcript.txt")
|
||||
|
||||
// Overwrite check via FileIO.Stat
|
||||
if !runtime.Bool("overwrite") {
|
||||
transcriptPath := filepath.Join(dirName, "transcript.txt")
|
||||
if _, statErr := vfs.Stat(transcriptPath); statErr == nil {
|
||||
if _, statErr := runtime.FileIO().Stat(transcriptPath); statErr == nil {
|
||||
fmt.Fprintf(errOut, "%s transcript already exists: %s (use --overwrite to replace)\n", logPrefix, transcriptPath)
|
||||
return transcriptPath
|
||||
}
|
||||
}
|
||||
|
||||
transcriptPath := filepath.Join(dirName, "transcript.txt")
|
||||
safePath, err := validate.SafeOutputPath(transcriptPath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(errOut, "%s invalid transcript path: %v\n", logPrefix, err)
|
||||
return ""
|
||||
}
|
||||
if err := vfs.MkdirAll(filepath.Dir(safePath), 0755); err != nil {
|
||||
fmt.Fprintf(errOut, "%s failed to create directory: %v\n", logPrefix, err)
|
||||
return ""
|
||||
}
|
||||
|
||||
fmt.Fprintf(errOut, "%s downloading transcript: %s\n", logPrefix, transcriptPath)
|
||||
apiResp, err := runtime.DoAPI(&larkcore.ApiReq{
|
||||
HttpMethod: http.MethodGet,
|
||||
@@ -303,8 +296,16 @@ func downloadTranscriptFile(runtime *common.RuntimeContext, minuteToken string,
|
||||
fmt.Fprintf(errOut, "%s transcript is empty (not available for this minute)\n", logPrefix)
|
||||
return ""
|
||||
}
|
||||
if err := validate.AtomicWrite(safePath, apiResp.RawBody, 0644); err != nil {
|
||||
fmt.Fprintf(errOut, "%s failed to write transcript: %v\n", logPrefix, err)
|
||||
if _, err := runtime.FileIO().Save(transcriptPath, fileio.SaveOptions{}, bytes.NewReader(apiResp.RawBody)); err != nil {
|
||||
var me *fileio.MkdirError
|
||||
switch {
|
||||
case errors.Is(err, fileio.ErrPathValidation):
|
||||
fmt.Fprintf(errOut, "%s invalid transcript path: %v\n", logPrefix, err)
|
||||
case errors.As(err, &me):
|
||||
fmt.Fprintf(errOut, "%s failed to create directory: %v\n", logPrefix, err)
|
||||
default:
|
||||
fmt.Fprintf(errOut, "%s failed to write transcript: %v\n", logPrefix, err)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
return transcriptPath
|
||||
|
||||
Reference in New Issue
Block a user