gogcli/internal/cmd/drive.go
2025-12-26 22:24:22 +01:00

1071 lines
26 KiB
Go

package cmd
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/spf13/cobra"
"github.com/steipete/gogcli/internal/googleapi"
"github.com/steipete/gogcli/internal/outfmt"
"github.com/steipete/gogcli/internal/ui"
"google.golang.org/api/drive/v3"
gapi "google.golang.org/api/googleapi"
)
var newDriveService = googleapi.NewDrive
func newDriveCmd(flags *rootFlags) *cobra.Command {
cmd := &cobra.Command{
Use: "drive",
Short: "Google Drive",
}
cmd.AddCommand(newDriveLsCmd(flags))
cmd.AddCommand(newDriveSearchCmd(flags))
cmd.AddCommand(newDriveGetCmd(flags))
cmd.AddCommand(newDriveDownloadCmd(flags))
cmd.AddCommand(newDriveCopyCmd(flags))
cmd.AddCommand(newDriveUploadCmd(flags))
cmd.AddCommand(newDriveMkdirCmd(flags))
cmd.AddCommand(newDriveDeleteCmd(flags))
cmd.AddCommand(newDriveMoveCmd(flags))
cmd.AddCommand(newDriveRenameCmd(flags))
cmd.AddCommand(newDriveShareCmd(flags))
cmd.AddCommand(newDriveUnshareCmd(flags))
cmd.AddCommand(newDrivePermissionsCmd(flags))
cmd.AddCommand(newDriveURLCmd(flags))
return cmd
}
func newDriveLsCmd(flags *rootFlags) *cobra.Command {
var max int64
var page string
var query string
var parent string
cmd := &cobra.Command{
Use: "ls",
Short: "List files in a folder (default: root)",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
u := ui.FromContext(cmd.Context())
account, err := requireAccount(flags)
if err != nil {
return err
}
folderID := strings.TrimSpace(parent)
if folderID == "" {
folderID = "root"
}
svc, err := newDriveService(cmd.Context(), account)
if err != nil {
return err
}
q := buildDriveListQuery(folderID, query)
resp, err := svc.Files.List().
Q(q).
PageSize(max).
PageToken(page).
OrderBy("modifiedTime desc").
SupportsAllDrives(true).
IncludeItemsFromAllDrives(true).
Fields("nextPageToken, files(id, name, mimeType, size, modifiedTime, parents, webViewLink)").
Context(cmd.Context()).
Do()
if err != nil {
return err
}
if outfmt.IsJSON(cmd.Context()) {
return outfmt.WriteJSON(os.Stdout, map[string]any{
"files": resp.Files,
"nextPageToken": resp.NextPageToken,
})
}
if len(resp.Files) == 0 {
u.Err().Println("No files")
return nil
}
w, flush := tableWriter(cmd.Context())
defer flush()
fmt.Fprintln(w, "ID\tNAME\tTYPE\tSIZE\tMODIFIED")
for _, f := range resp.Files {
fmt.Fprintf(
w,
"%s\t%s\t%s\t%s\t%s\n",
f.Id,
f.Name,
driveType(f.MimeType),
formatDriveSize(f.Size),
formatDateTime(f.ModifiedTime),
)
}
printNextPageHint(u, resp.NextPageToken)
return nil
},
}
cmd.Flags().Int64Var(&max, "max", 20, "Max results")
cmd.Flags().StringVar(&page, "page", "", "Page token")
cmd.Flags().StringVar(&query, "query", "", "Drive query filter")
cmd.Flags().StringVar(&parent, "parent", "", "Folder ID to list (default: root)")
return cmd
}
func newDriveSearchCmd(flags *rootFlags) *cobra.Command {
var max int64
var page string
cmd := &cobra.Command{
Use: "search <text>",
Short: "Full-text search across Drive",
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
u := ui.FromContext(cmd.Context())
account, err := requireAccount(flags)
if err != nil {
return err
}
text := strings.Join(args, " ")
svc, err := newDriveService(cmd.Context(), account)
if err != nil {
return err
}
resp, err := svc.Files.List().
Q(buildDriveSearchQuery(text)).
PageSize(max).
PageToken(page).
OrderBy("modifiedTime desc").
SupportsAllDrives(true).
IncludeItemsFromAllDrives(true).
Fields("nextPageToken, files(id, name, mimeType, size, modifiedTime, parents, webViewLink)").
Context(cmd.Context()).
Do()
if err != nil {
return err
}
if outfmt.IsJSON(cmd.Context()) {
return outfmt.WriteJSON(os.Stdout, map[string]any{
"files": resp.Files,
"nextPageToken": resp.NextPageToken,
})
}
if len(resp.Files) == 0 {
u.Err().Println("No results")
return nil
}
w, flush := tableWriter(cmd.Context())
defer flush()
fmt.Fprintln(w, "ID\tNAME\tTYPE\tSIZE\tMODIFIED")
for _, f := range resp.Files {
fmt.Fprintf(
w,
"%s\t%s\t%s\t%s\t%s\n",
f.Id,
f.Name,
driveType(f.MimeType),
formatDriveSize(f.Size),
formatDateTime(f.ModifiedTime),
)
}
printNextPageHint(u, resp.NextPageToken)
return nil
},
}
cmd.Flags().Int64Var(&max, "max", 20, "Max results")
cmd.Flags().StringVar(&page, "page", "", "Page token")
return cmd
}
func newDriveGetCmd(flags *rootFlags) *cobra.Command {
return &cobra.Command{
Use: "get <fileId>",
Short: "Get file metadata",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
u := ui.FromContext(cmd.Context())
account, err := requireAccount(flags)
if err != nil {
return err
}
fileID := args[0]
svc, err := newDriveService(cmd.Context(), account)
if err != nil {
return err
}
f, err := svc.Files.Get(fileID).
SupportsAllDrives(true).
Fields("id, name, mimeType, size, modifiedTime, createdTime, parents, webViewLink, description, starred").
Context(cmd.Context()).
Do()
if err != nil {
return err
}
if outfmt.IsJSON(cmd.Context()) {
return outfmt.WriteJSON(os.Stdout, map[string]any{"file": f})
}
u.Out().Printf("id\t%s", f.Id)
u.Out().Printf("name\t%s", f.Name)
u.Out().Printf("type\t%s", f.MimeType)
u.Out().Printf("size\t%s", formatDriveSize(f.Size))
u.Out().Printf("created\t%s", f.CreatedTime)
u.Out().Printf("modified\t%s", f.ModifiedTime)
if f.Description != "" {
u.Out().Printf("description\t%s", f.Description)
}
u.Out().Printf("starred\t%t", f.Starred)
if f.WebViewLink != "" {
u.Out().Printf("link\t%s", f.WebViewLink)
}
return nil
},
}
}
func newDriveDownloadCmd(flags *rootFlags) *cobra.Command {
var outPathFlag string
var format string
cmd := &cobra.Command{
Use: "download <fileId>",
Short: "Download a file (exports Google Docs formats)",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
u := ui.FromContext(cmd.Context())
account, err := requireAccount(flags)
if err != nil {
return err
}
fileID := args[0]
svc, err := newDriveService(cmd.Context(), account)
if err != nil {
return err
}
meta, err := svc.Files.Get(fileID).
SupportsAllDrives(true).
Fields("id, name, mimeType").
Context(cmd.Context()).
Do()
if err != nil {
return err
}
if meta.Name == "" {
return errors.New("file has no name")
}
destPath, err := resolveDriveDownloadDestPath(meta, outPathFlag)
if err != nil {
return err
}
downloadedPath, size, err := downloadDriveFile(cmd.Context(), svc, meta, destPath, format)
if err != nil {
return err
}
if outfmt.IsJSON(cmd.Context()) {
return outfmt.WriteJSON(os.Stdout, map[string]any{
"path": downloadedPath,
"size": size,
})
}
u.Out().Printf("path\t%s", downloadedPath)
u.Out().Printf("size\t%s", formatDriveSize(size))
return nil
},
}
cmd.Flags().StringVar(&outPathFlag, "out", "", "Output file path (default: gogcli config dir)")
cmd.Flags().StringVar(&format, "format", "", "Export format for Google Docs files: pdf|csv|xlsx|pptx|txt|png|docx (default: auto)")
return cmd
}
func newDriveUploadCmd(flags *rootFlags) *cobra.Command {
var name string
var parent string
cmd := &cobra.Command{
Use: "upload <localPath>",
Short: "Upload a file",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
u := ui.FromContext(cmd.Context())
account, err := requireAccount(flags)
if err != nil {
return err
}
localPath := args[0]
f, err := os.Open(localPath)
if err != nil {
return err
}
defer f.Close()
fileName := name
if fileName == "" {
fileName = filepath.Base(localPath)
}
svc, err := newDriveService(cmd.Context(), account)
if err != nil {
return err
}
meta := &drive.File{Name: fileName}
parent = strings.TrimSpace(parent)
if parent != "" {
meta.Parents = []string{parent}
}
mimeType := guessMimeType(localPath)
created, err := svc.Files.Create(meta).
SupportsAllDrives(true).
Media(f, gapi.ContentType(mimeType)).
Fields("id, name, mimeType, size, webViewLink").
Context(cmd.Context()).
Do()
if err != nil {
return err
}
if outfmt.IsJSON(cmd.Context()) {
return outfmt.WriteJSON(os.Stdout, map[string]any{"file": created})
}
u.Out().Printf("id\t%s", created.Id)
u.Out().Printf("name\t%s", created.Name)
if created.WebViewLink != "" {
u.Out().Printf("link\t%s", created.WebViewLink)
}
return nil
},
}
cmd.Flags().StringVar(&name, "name", "", "Override filename")
cmd.Flags().StringVar(&parent, "parent", "", "Destination folder ID")
return cmd
}
func newDriveMkdirCmd(flags *rootFlags) *cobra.Command {
var parent string
cmd := &cobra.Command{
Use: "mkdir <name>",
Short: "Create a folder",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
u := ui.FromContext(cmd.Context())
account, err := requireAccount(flags)
if err != nil {
return err
}
name := args[0]
svc, err := newDriveService(cmd.Context(), account)
if err != nil {
return err
}
f := &drive.File{
Name: name,
MimeType: "application/vnd.google-apps.folder",
}
if parent != "" {
f.Parents = []string{parent}
}
created, err := svc.Files.Create(f).
SupportsAllDrives(true).
Fields("id, name, webViewLink").
Context(cmd.Context()).
Do()
if err != nil {
return err
}
if outfmt.IsJSON(cmd.Context()) {
return outfmt.WriteJSON(os.Stdout, map[string]any{"folder": created})
}
u.Out().Printf("id\t%s", created.Id)
u.Out().Printf("name\t%s", created.Name)
if created.WebViewLink != "" {
u.Out().Printf("link\t%s", created.WebViewLink)
}
return nil
},
}
cmd.Flags().StringVar(&parent, "parent", "", "Parent folder ID")
return cmd
}
func newDriveDeleteCmd(flags *rootFlags) *cobra.Command {
return &cobra.Command{
Use: "delete <fileId>",
Short: "Delete a file (moves to trash)",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
u := ui.FromContext(cmd.Context())
account, err := requireAccount(flags)
if err != nil {
return err
}
fileID := args[0]
if confirmErr := confirmDestructive(cmd, flags, fmt.Sprintf("delete drive file %s", fileID)); confirmErr != nil {
return confirmErr
}
svc, err := newDriveService(cmd.Context(), account)
if err != nil {
return err
}
if err := svc.Files.Delete(fileID).SupportsAllDrives(true).Context(cmd.Context()).Do(); err != nil {
return err
}
if outfmt.IsJSON(cmd.Context()) {
return outfmt.WriteJSON(os.Stdout, map[string]any{
"deleted": true,
"id": fileID,
})
}
u.Out().Printf("deleted\ttrue")
u.Out().Printf("id\t%s", fileID)
return nil
},
}
}
func newDriveMoveCmd(flags *rootFlags) *cobra.Command {
var parent string
cmd := &cobra.Command{
Use: "move <fileId>",
Short: "Move a file to a different folder",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
u := ui.FromContext(cmd.Context())
account, err := requireAccount(flags)
if err != nil {
return err
}
fileID := args[0]
parent = strings.TrimSpace(parent)
if parent == "" {
return usage("missing --parent")
}
svc, err := newDriveService(cmd.Context(), account)
if err != nil {
return err
}
meta, err := svc.Files.Get(fileID).
SupportsAllDrives(true).
Fields("id, name, parents").
Context(cmd.Context()).
Do()
if err != nil {
return err
}
call := svc.Files.Update(fileID, &drive.File{}).
SupportsAllDrives(true).
AddParents(parent).
Fields("id, name, parents, webViewLink")
if len(meta.Parents) > 0 {
call = call.RemoveParents(strings.Join(meta.Parents, ","))
}
updated, err := call.Context(cmd.Context()).Do()
if err != nil {
return err
}
if outfmt.IsJSON(cmd.Context()) {
return outfmt.WriteJSON(os.Stdout, map[string]any{"file": updated})
}
u.Out().Printf("id\t%s", updated.Id)
u.Out().Printf("name\t%s", updated.Name)
return nil
},
}
cmd.Flags().StringVar(&parent, "parent", "", "New parent folder ID (required)")
return cmd
}
func newDriveRenameCmd(flags *rootFlags) *cobra.Command {
return &cobra.Command{
Use: "rename <fileId> <newName>",
Short: "Rename a file or folder",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
u := ui.FromContext(cmd.Context())
account, err := requireAccount(flags)
if err != nil {
return err
}
fileID := args[0]
newName := args[1]
svc, err := newDriveService(cmd.Context(), account)
if err != nil {
return err
}
updated, err := svc.Files.Update(fileID, &drive.File{Name: newName}).
SupportsAllDrives(true).
Fields("id, name").
Context(cmd.Context()).
Do()
if err != nil {
return err
}
if outfmt.IsJSON(cmd.Context()) {
return outfmt.WriteJSON(os.Stdout, map[string]any{"file": updated})
}
u.Out().Printf("id\t%s", updated.Id)
u.Out().Printf("name\t%s", updated.Name)
return nil
},
}
}
func newDriveShareCmd(flags *rootFlags) *cobra.Command {
var anyone bool
var email string
var role string
var discoverable bool
cmd := &cobra.Command{
Use: "share <fileId>",
Short: "Share a file or folder",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
u := ui.FromContext(cmd.Context())
account, err := requireAccount(flags)
if err != nil {
return err
}
fileID := args[0]
if !anyone && email == "" {
return usage("must specify --anyone or --email")
}
if role == "" {
role = "reader"
}
if role != "reader" && role != "writer" {
return usage("invalid --role (expected reader|writer)")
}
svc, err := newDriveService(cmd.Context(), account)
if err != nil {
return err
}
perm := &drive.Permission{Role: role}
if anyone {
perm.Type = "anyone"
perm.AllowFileDiscovery = discoverable
} else {
perm.Type = "user"
perm.EmailAddress = email
}
created, err := svc.Permissions.Create(fileID, perm).
SupportsAllDrives(true).
SendNotificationEmail(false).
Fields("id, type, role, emailAddress").
Context(cmd.Context()).
Do()
if err != nil {
return err
}
link, err := driveWebLink(cmd.Context(), svc, fileID)
if err != nil {
return err
}
if outfmt.IsJSON(cmd.Context()) {
return outfmt.WriteJSON(os.Stdout, map[string]any{
"link": link,
"permissionId": created.Id,
"permission": created,
})
}
u.Out().Printf("link\t%s", link)
u.Out().Printf("permission_id\t%s", created.Id)
return nil
},
}
cmd.Flags().BoolVar(&anyone, "anyone", false, "Make publicly accessible")
cmd.Flags().StringVar(&email, "email", "", "Share with specific user")
cmd.Flags().StringVar(&role, "role", "reader", "Permission: reader|writer")
cmd.Flags().BoolVar(&discoverable, "discoverable", false, "Allow file discovery in search (anyone/domain only)")
return cmd
}
func newDriveUnshareCmd(flags *rootFlags) *cobra.Command {
return &cobra.Command{
Use: "unshare <fileId> <permissionId>",
Short: "Remove a permission from a file",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
u := ui.FromContext(cmd.Context())
account, err := requireAccount(flags)
if err != nil {
return err
}
fileID := args[0]
permissionID := args[1]
if confirmErr := confirmDestructive(cmd, flags, fmt.Sprintf("remove permission %s from drive file %s", permissionID, fileID)); confirmErr != nil {
return confirmErr
}
svc, err := newDriveService(cmd.Context(), account)
if err != nil {
return err
}
if err := svc.Permissions.Delete(fileID, permissionID).SupportsAllDrives(true).Context(cmd.Context()).Do(); err != nil {
return err
}
if outfmt.IsJSON(cmd.Context()) {
return outfmt.WriteJSON(os.Stdout, map[string]any{
"removed": true,
"fileId": fileID,
"permissionId": permissionID,
})
}
u.Out().Printf("removed\ttrue")
u.Out().Printf("file_id\t%s", fileID)
u.Out().Printf("permission_id\t%s", permissionID)
return nil
},
}
}
func newDrivePermissionsCmd(flags *rootFlags) *cobra.Command {
var max int64
var page string
cmd := &cobra.Command{
Use: "permissions <fileId>",
Short: "List permissions on a file",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
u := ui.FromContext(cmd.Context())
account, err := requireAccount(flags)
if err != nil {
return err
}
fileID := args[0]
svc, err := newDriveService(cmd.Context(), account)
if err != nil {
return err
}
call := svc.Permissions.List(fileID).
SupportsAllDrives(true).
Fields("nextPageToken, permissions(id, type, role, emailAddress)").
Context(cmd.Context())
if max > 0 {
call = call.PageSize(max)
}
if strings.TrimSpace(page) != "" {
call = call.PageToken(page)
}
resp, err := call.Do()
if err != nil {
return err
}
if outfmt.IsJSON(cmd.Context()) {
return outfmt.WriteJSON(os.Stdout, map[string]any{
"fileId": fileID,
"permissions": resp.Permissions,
"permissionCount": len(resp.Permissions),
"nextPageToken": resp.NextPageToken,
})
}
if len(resp.Permissions) == 0 {
u.Err().Println("No permissions")
return nil
}
w, flush := tableWriter(cmd.Context())
defer flush()
fmt.Fprintln(w, "ID\tTYPE\tROLE\tEMAIL")
for _, p := range resp.Permissions {
email := p.EmailAddress
if email == "" {
email = "-"
}
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", p.Id, p.Type, p.Role, email)
}
printNextPageHint(u, resp.NextPageToken)
return nil
},
}
cmd.Flags().Int64Var(&max, "max", 100, "Max results")
cmd.Flags().StringVar(&page, "page", "", "Page token")
return cmd
}
func newDriveURLCmd(flags *rootFlags) *cobra.Command {
return &cobra.Command{
Use: "url <fileIds...>",
Short: "Print web URLs for files",
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
u := ui.FromContext(cmd.Context())
account, err := requireAccount(flags)
if err != nil {
return err
}
svc, err := newDriveService(cmd.Context(), account)
if err != nil {
return err
}
for _, id := range args {
link, err := driveWebLink(cmd.Context(), svc, id)
if err != nil {
return err
}
if outfmt.IsJSON(cmd.Context()) {
// collected below
} else {
u.Out().Printf("%s\t%s", id, link)
}
}
if outfmt.IsJSON(cmd.Context()) {
urls := make([]map[string]string, 0, len(args))
for _, id := range args {
link, err := driveWebLink(cmd.Context(), svc, id)
if err != nil {
return err
}
urls = append(urls, map[string]string{"id": id, "url": link})
}
return outfmt.WriteJSON(os.Stdout, map[string]any{"urls": urls})
}
return nil
},
}
}
func buildDriveListQuery(folderID string, userQuery string) string {
q := strings.TrimSpace(userQuery)
parent := fmt.Sprintf("'%s' in parents", folderID)
if q != "" {
q = q + " and " + parent
} else {
q = parent
}
if !strings.Contains(q, "trashed") {
q = q + " and trashed = false"
}
return q
}
func buildDriveSearchQuery(text string) string {
q := fmt.Sprintf("fullText contains '%s'", escapeDriveQueryString(text))
return q + " and trashed = false"
}
func escapeDriveQueryString(s string) string {
// Escape backslashes first, then single quotes
s = strings.ReplaceAll(s, "\\", "\\\\")
s = strings.ReplaceAll(s, "'", "\\'")
return s
}
func driveType(mimeType string) string {
if mimeType == "application/vnd.google-apps.folder" {
return "folder"
}
return "file"
}
func formatDateTime(iso string) string {
if iso == "" {
return "-"
}
if len(iso) >= 16 {
return strings.ReplaceAll(iso[:16], "T", " ")
}
return iso
}
func formatDriveSize(bytes int64) string {
if bytes <= 0 {
return "-"
}
const unit = 1024.0
b := float64(bytes)
units := []string{"B", "KB", "MB", "GB", "TB"}
i := 0
for b >= unit && i < len(units)-1 {
b /= unit
i++
}
if i == 0 {
return fmt.Sprintf("%d B", bytes)
}
return fmt.Sprintf("%.1f %s", b, units[i])
}
func guessMimeType(path string) string {
ext := strings.ToLower(filepath.Ext(path))
switch ext {
case ".pdf":
return "application/pdf"
case ".doc":
return "application/msword"
case ".docx":
return "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
case ".xls":
return "application/vnd.ms-excel"
case ".xlsx":
return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
case ".ppt":
return "application/vnd.ms-powerpoint"
case ".pptx":
return "application/vnd.openxmlformats-officedocument.presentationml.presentation"
case ".png":
return "image/png"
case ".jpg", ".jpeg":
return "image/jpeg"
case ".gif":
return "image/gif"
case ".txt":
return "text/plain"
case ".html":
return "text/html"
case ".css":
return "text/css"
case ".js":
return "application/javascript"
case ".json":
return "application/json"
case ".zip":
return "application/zip"
case ".csv":
return "text/csv"
case ".md":
return "text/markdown"
default:
return "application/octet-stream"
}
}
func downloadDriveFile(ctx context.Context, svc *drive.Service, meta *drive.File, destPath string, format string) (string, int64, error) {
isGoogleDoc := strings.HasPrefix(meta.MimeType, "application/vnd.google-apps.")
var (
resp *http.Response
outPath string
err error
)
if isGoogleDoc {
exportMimeType := ""
if strings.TrimSpace(format) == "" {
exportMimeType = driveExportMimeType(meta.MimeType)
} else {
var mimeErr error
exportMimeType, mimeErr = driveExportMimeTypeForFormat(meta.MimeType, format)
if mimeErr != nil {
return "", 0, mimeErr
}
}
outPath = replaceExt(destPath, driveExportExtension(exportMimeType))
resp, err = driveExportDownload(ctx, svc, meta.Id, exportMimeType)
} else {
outPath = destPath
resp, err = driveDownload(ctx, svc, meta.Id)
}
if err != nil {
return "", 0, err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
body, _ := io.ReadAll(resp.Body)
return "", 0, fmt.Errorf("download failed: %s: %s", resp.Status, strings.TrimSpace(string(body)))
}
f, err := os.Create(outPath)
if err != nil {
return "", 0, err
}
defer f.Close()
n, err := io.Copy(f, resp.Body)
if err != nil {
return "", 0, err
}
return outPath, n, nil
}
var driveDownload = func(ctx context.Context, svc *drive.Service, fileID string) (*http.Response, error) {
return svc.Files.Get(fileID).SupportsAllDrives(true).Context(ctx).Download()
}
var driveExportDownload = func(ctx context.Context, svc *drive.Service, fileID string, mimeType string) (*http.Response, error) {
return svc.Files.Export(fileID, mimeType).Context(ctx).Download()
}
func replaceExt(path string, ext string) string {
base := strings.TrimSuffix(path, filepath.Ext(path))
return base + ext
}
func driveExportMimeType(googleMimeType string) string {
switch googleMimeType {
case "application/vnd.google-apps.document":
return "application/pdf"
case "application/vnd.google-apps.spreadsheet":
return "text/csv"
case "application/vnd.google-apps.presentation":
return "application/pdf"
case "application/vnd.google-apps.drawing":
return "image/png"
default:
return "application/pdf"
}
}
func driveExportMimeTypeForFormat(googleMimeType string, format string) (string, error) {
format = strings.ToLower(strings.TrimSpace(format))
if format == "" {
return driveExportMimeType(googleMimeType), nil
}
switch googleMimeType {
case "application/vnd.google-apps.document":
switch format {
case "pdf":
return "application/pdf", nil
case "docx":
return "application/vnd.openxmlformats-officedocument.wordprocessingml.document", nil
case "txt":
return "text/plain", nil
default:
return "", fmt.Errorf("invalid --format %q for Google Doc (use pdf|docx|txt)", format)
}
case "application/vnd.google-apps.spreadsheet":
switch format {
case "pdf":
return "application/pdf", nil
case "csv":
return "text/csv", nil
case "xlsx":
return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", nil
default:
return "", fmt.Errorf("invalid --format %q for Google Sheet (use pdf|csv|xlsx)", format)
}
case "application/vnd.google-apps.presentation":
switch format {
case "pdf":
return "application/pdf", nil
case "pptx":
return "application/vnd.openxmlformats-officedocument.presentationml.presentation", nil
default:
return "", fmt.Errorf("invalid --format %q for Google Slides (use pdf|pptx)", format)
}
case "application/vnd.google-apps.drawing":
switch format {
case "png":
return "image/png", nil
case "pdf":
return "application/pdf", nil
default:
return "", fmt.Errorf("invalid --format %q for Google Drawing (use png|pdf)", format)
}
default:
if format == "pdf" {
return "application/pdf", nil
}
return "", fmt.Errorf("invalid --format %q for file type %q (use pdf)", format, googleMimeType)
}
}
func driveExportExtension(mimeType string) string {
switch mimeType {
case "application/pdf":
return ".pdf"
case "text/csv":
return ".csv"
case "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
return ".xlsx"
case "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
return ".docx"
case "application/vnd.openxmlformats-officedocument.presentationml.presentation":
return ".pptx"
case "image/png":
return ".png"
case "text/plain":
return ".txt"
default:
return ".pdf"
}
}
func driveWebLink(ctx context.Context, svc *drive.Service, fileID string) (string, error) {
f, err := svc.Files.Get(fileID).SupportsAllDrives(true).Fields("webViewLink").Context(ctx).Do()
if err != nil {
return "", err
}
if f.WebViewLink != "" {
return f.WebViewLink, nil
}
return fmt.Sprintf("https://drive.google.com/file/d/%s/view", fileID), nil
}