gogcli/internal/cmd/drive_copy.go
salmonumbrella 07ffcb5d84
fix(paths): expand ~ in user-provided file paths (#56)
* fix(paths): expand ~ in user-provided file paths

When users specify paths with ~ (e.g., --out ~/Downloads/file.pdf) and
the path is quoted in the shell command, the tilde is not expanded by
the shell. This caused files to be written to a literal ~/Downloads
directory instead of the user's home directory.

Add config.ExpandPath() function that expands ~ at the beginning of
paths to the user's home directory. Apply this fix to all user-provided
file paths across:

- gmail attachment download (--out)
- drive download/export (--out)
- drive upload (localPath argument)
- auth token export (--out)
- auth credentials/import/keep (input paths)
- gmail thread attachments (--out-dir)
- gmail send/drafts (--attach)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(lint): address wrapcheck and wsl issues

* fix(calendar): support ISO 8601 time format and add 'list' alias

- Add parsing for ISO 8601 datetime with numeric timezone without colon
  (e.g., 2026-01-09T16:38:41-0800), which is the format produced by
  macOS `date +%Y-%m-%dT%H:%M:%S%z`
- Add 'list' as an alias for 'events' subcommand for more intuitive CLI
  usage (gog calendar list instead of gog calendar events)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore(changelog): note PR #56

* chore(lint): dedupe file string

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-01-10 02:19:30 +00:00

95 lines
1.9 KiB
Go

package cmd
import (
"context"
"errors"
"fmt"
"os"
"strings"
"google.golang.org/api/drive/v3"
"github.com/steipete/gogcli/internal/outfmt"
"github.com/steipete/gogcli/internal/ui"
)
type copyViaDriveOptions struct {
ArgName string
ExpectedMime string
KindLabel string
}
func copyViaDrive(ctx context.Context, flags *RootFlags, opts copyViaDriveOptions, id string, name string, parent string) error {
u := ui.FromContext(ctx)
account, err := requireAccount(flags)
if err != nil {
return err
}
argName := strings.TrimSpace(opts.ArgName)
if argName == "" {
argName = "id"
}
id = strings.TrimSpace(id)
if id == "" {
return usage(fmt.Sprintf("empty %s", argName))
}
name = strings.TrimSpace(name)
if name == "" {
return usage("empty name")
}
svc, err := newDriveService(ctx, account)
if err != nil {
return err
}
meta, err := svc.Files.Get(id).
SupportsAllDrives(true).
Fields("id, name, mimeType").
Context(ctx).
Do()
if err != nil {
return err
}
if meta == nil {
return errors.New("file not found")
}
if opts.ExpectedMime != "" && meta.MimeType != opts.ExpectedMime {
label := strings.TrimSpace(opts.KindLabel)
if label == "" {
label = "expected type"
}
return fmt.Errorf("file is not a %s (mimeType=%q)", label, meta.MimeType)
}
parent = strings.TrimSpace(parent)
req := &drive.File{Name: name}
if parent != "" {
req.Parents = []string{parent}
}
created, err := svc.Files.Copy(id, req).
SupportsAllDrives(true).
Fields("id, name, mimeType, webViewLink").
Context(ctx).
Do()
if err != nil {
return err
}
if created == nil {
return errors.New("copy failed")
}
if outfmt.IsJSON(ctx) {
return outfmt.WriteJSON(os.Stdout, map[string]any{strFile: created})
}
u.Out().Printf("id\t%s", created.Id)
u.Out().Printf("name\t%s", created.Name)
u.Out().Printf("mime\t%s", created.MimeType)
if created.WebViewLink != "" {
u.Out().Printf("link\t%s", created.WebViewLink)
}
return nil
}