Compare commits
2 Commits
main
...
fix/drive-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7fe91482f | ||
|
|
9811df2891 |
@ -16,6 +16,7 @@
|
||||
- Gmail: include primary display name in `gmail send` From header when using service account impersonation (domain-wide delegation). (#184) — thanks @salmonumbrella.
|
||||
- Drive: include shared drives in `drive ls` and `drive search`; reject `drive download --format` for non-Google Workspace files. (#256) — thanks @salmonumbrella.
|
||||
- Drive: validate `drive download --format` values and error early for unknown formats. (#259)
|
||||
- Drive: make `drive delete` move files to trash by default; add `--permanent` for irreversible deletion. (#262) — thanks @laihenyi.
|
||||
|
||||
## 0.10.0 - 2026-02-14
|
||||
|
||||
|
||||
@ -825,10 +825,11 @@ gog drive mkdir "New Folder" --parent <parentFolderId>
|
||||
gog drive rename <fileId> "New Name"
|
||||
gog drive move <fileId> --parent <destinationFolderId>
|
||||
gog drive delete <fileId> # Move to trash
|
||||
gog drive delete <fileId> --permanent # Permanently delete
|
||||
|
||||
# Permissions
|
||||
gog drive permissions <fileId>
|
||||
gog drive share <fileId> --to user --email user@example.com --role reader
|
||||
# Permissions
|
||||
gog drive permissions <fileId>
|
||||
gog drive share <fileId> --to user --email user@example.com --role reader
|
||||
gog drive share <fileId> --to user --email user@example.com --role writer
|
||||
gog drive unshare <fileId> --permission-id <permissionId>
|
||||
|
||||
|
||||
@ -175,7 +175,7 @@ Flag aliases:
|
||||
- `gog drive download <fileId> [--out PATH] [--format F]` (`--format` only applies to Google Workspace files)
|
||||
- `gog drive upload <localPath> [--name N] [--parent ID] [--convert] [--convert-to doc|sheet|slides]`
|
||||
- `gog drive mkdir <name> [--parent ID]`
|
||||
- `gog drive delete <fileId>`
|
||||
- `gog drive delete <fileId> [--permanent]`
|
||||
- `gog drive move <fileId> --parent ID`
|
||||
- `gog drive rename <fileId> <newName>`
|
||||
- `gog drive share <fileId> --to anyone|user|domain [--email addr] [--domain example.com] [--role reader|writer] [--discoverable]`
|
||||
|
||||
@ -57,7 +57,7 @@ type DriveCmd struct {
|
||||
Copy DriveCopyCmd `cmd:"" name:"copy" help:"Copy a file"`
|
||||
Upload DriveUploadCmd `cmd:"" name:"upload" help:"Upload a file"`
|
||||
Mkdir DriveMkdirCmd `cmd:"" name:"mkdir" help:"Create a folder"`
|
||||
Delete DriveDeleteCmd `cmd:"" name:"delete" help:"Delete a file (moves to trash)" aliases:"rm,del"`
|
||||
Delete DriveDeleteCmd `cmd:"" name:"delete" help:"Move a file to trash (use --permanent to delete forever)" aliases:"rm,del"`
|
||||
Move DriveMoveCmd `cmd:"" name:"move" help:"Move a file to a different folder"`
|
||||
Rename DriveRenameCmd `cmd:"" name:"rename" help:"Rename a file or folder"`
|
||||
Share DriveShareCmd `cmd:"" name:"share" help:"Share a file or folder"`
|
||||
@ -541,7 +541,8 @@ func (c *DriveMkdirCmd) Run(ctx context.Context, flags *RootFlags) error {
|
||||
}
|
||||
|
||||
type DriveDeleteCmd struct {
|
||||
FileID string `arg:"" name:"fileId" help:"File ID"`
|
||||
FileID string `arg:"" name:"fileId" help:"File ID"`
|
||||
Permanent bool `name:"permanent" help:"Permanently delete instead of moving to trash" default:"false"`
|
||||
}
|
||||
|
||||
func (c *DriveDeleteCmd) Run(ctx context.Context, flags *RootFlags) error {
|
||||
@ -555,7 +556,11 @@ func (c *DriveDeleteCmd) Run(ctx context.Context, flags *RootFlags) error {
|
||||
return usage("empty fileId")
|
||||
}
|
||||
|
||||
if confirmErr := confirmDestructive(ctx, flags, fmt.Sprintf("delete drive file %s", fileID)); confirmErr != nil {
|
||||
action := "trash drive file"
|
||||
if c.Permanent {
|
||||
action = "permanently delete drive file"
|
||||
}
|
||||
if confirmErr := confirmDestructive(ctx, flags, fmt.Sprintf("%s %s", action, fileID)); confirmErr != nil {
|
||||
return confirmErr
|
||||
}
|
||||
|
||||
@ -564,16 +569,32 @@ func (c *DriveDeleteCmd) Run(ctx context.Context, flags *RootFlags) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := svc.Files.Delete(fileID).SupportsAllDrives(true).Context(ctx).Do(); err != nil {
|
||||
return err
|
||||
trashed := !c.Permanent
|
||||
deleted := c.Permanent
|
||||
|
||||
if c.Permanent {
|
||||
if err := svc.Files.Delete(fileID).SupportsAllDrives(true).Context(ctx).Do(); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
_, err := svc.Files.Update(fileID, &drive.File{Trashed: true}).
|
||||
SupportsAllDrives(true).
|
||||
Fields("id, trashed").
|
||||
Context(ctx).
|
||||
Do()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if outfmt.IsJSON(ctx) {
|
||||
return outfmt.WriteJSON(ctx, os.Stdout, map[string]any{
|
||||
"deleted": true,
|
||||
"trashed": trashed,
|
||||
"deleted": deleted,
|
||||
"id": fileID,
|
||||
})
|
||||
}
|
||||
u.Out().Printf("deleted\ttrue")
|
||||
u.Out().Printf("trashed\t%t", trashed)
|
||||
u.Out().Printf("deleted\t%t", deleted)
|
||||
u.Out().Printf("id\t%s", fileID)
|
||||
return nil
|
||||
}
|
||||
|
||||
151
internal/cmd/execute_drive_delete_test.go
Normal file
151
internal/cmd/execute_drive_delete_test.go
Normal file
@ -0,0 +1,151 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"google.golang.org/api/drive/v3"
|
||||
"google.golang.org/api/option"
|
||||
)
|
||||
|
||||
func TestExecute_DriveDelete_DefaultAndPermanent(t *testing.T) {
|
||||
t.Run("default_trash", func(t *testing.T) {
|
||||
origNew := newDriveService
|
||||
t.Cleanup(func() { newDriveService = origNew })
|
||||
|
||||
var patchCount int
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !strings.Contains(r.URL.Path, "/files/id1") || (r.Method != http.MethodPatch && r.Method != http.MethodPut) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
patchCount++
|
||||
if got := r.URL.Query().Get("supportsAllDrives"); got != "true" {
|
||||
t.Fatalf("expected supportsAllDrives=true, got: %q (raw=%q)", got, r.URL.RawQuery)
|
||||
}
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
if !strings.Contains(string(body), "\"trashed\":true") {
|
||||
t.Fatalf("expected trashed=true body, got: %q", string(body))
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"id": "id1",
|
||||
"trashed": true,
|
||||
"kind": "drive#file",
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
svc, err := drive.NewService(context.Background(),
|
||||
option.WithoutAuthentication(),
|
||||
option.WithHTTPClient(srv.Client()),
|
||||
option.WithEndpoint(srv.URL+"/"),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("NewService: %v", err)
|
||||
}
|
||||
newDriveService = func(context.Context, string) (*drive.Service, error) { return svc, nil }
|
||||
|
||||
out := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if execErr := Execute([]string{"--force", "--account", "a@b.com", "drive", "delete", "id1"}); execErr != nil {
|
||||
t.Fatalf("Execute: %v", execErr)
|
||||
}
|
||||
})
|
||||
})
|
||||
if !strings.Contains(out, "trashed\ttrue") || !strings.Contains(out, "deleted\tfalse") {
|
||||
t.Fatalf("unexpected text output: %q", out)
|
||||
}
|
||||
|
||||
jsonOut := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if execErr := Execute([]string{"--json", "--force", "--account", "a@b.com", "drive", "delete", "id1"}); execErr != nil {
|
||||
t.Fatalf("Execute: %v", execErr)
|
||||
}
|
||||
})
|
||||
})
|
||||
var parsed struct {
|
||||
Trashed bool `json:"trashed"`
|
||||
Deleted bool `json:"deleted"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(jsonOut), &parsed); err != nil {
|
||||
t.Fatalf("json parse: %v\nout=%q", err, jsonOut)
|
||||
}
|
||||
if !parsed.Trashed || parsed.Deleted || parsed.ID != "id1" {
|
||||
t.Fatalf("unexpected json output: %#v", parsed)
|
||||
}
|
||||
|
||||
if patchCount != 2 {
|
||||
t.Fatalf("expected 2 PATCH calls, got %d", patchCount)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("permanent_delete", func(t *testing.T) {
|
||||
origNew := newDriveService
|
||||
t.Cleanup(func() { newDriveService = origNew })
|
||||
|
||||
var deleteCount int
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !strings.Contains(r.URL.Path, "/files/id1") || r.Method != http.MethodDelete {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
deleteCount++
|
||||
if got := r.URL.Query().Get("supportsAllDrives"); got != "true" {
|
||||
t.Fatalf("expected supportsAllDrives=true, got: %q (raw=%q)", got, r.URL.RawQuery)
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
svc, err := drive.NewService(context.Background(),
|
||||
option.WithoutAuthentication(),
|
||||
option.WithHTTPClient(srv.Client()),
|
||||
option.WithEndpoint(srv.URL+"/"),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("NewService: %v", err)
|
||||
}
|
||||
newDriveService = func(context.Context, string) (*drive.Service, error) { return svc, nil }
|
||||
|
||||
out := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if execErr := Execute([]string{"--force", "--account", "a@b.com", "drive", "delete", "id1", "--permanent"}); execErr != nil {
|
||||
t.Fatalf("Execute: %v", execErr)
|
||||
}
|
||||
})
|
||||
})
|
||||
if !strings.Contains(out, "trashed\tfalse") || !strings.Contains(out, "deleted\ttrue") {
|
||||
t.Fatalf("unexpected text output: %q", out)
|
||||
}
|
||||
|
||||
jsonOut := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if execErr := Execute([]string{"--json", "--force", "--account", "a@b.com", "drive", "delete", "id1", "--permanent"}); execErr != nil {
|
||||
t.Fatalf("Execute: %v", execErr)
|
||||
}
|
||||
})
|
||||
})
|
||||
var parsed struct {
|
||||
Trashed bool `json:"trashed"`
|
||||
Deleted bool `json:"deleted"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(jsonOut), &parsed); err != nil {
|
||||
t.Fatalf("json parse: %v\nout=%q", err, jsonOut)
|
||||
}
|
||||
if parsed.Trashed || !parsed.Deleted || parsed.ID != "id1" {
|
||||
t.Fatalf("unexpected json output: %#v", parsed)
|
||||
}
|
||||
|
||||
if deleteCount != 2 {
|
||||
t.Fatalf("expected 2 DELETE calls, got %d", deleteCount)
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -3,6 +3,7 @@ package cmd
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
@ -66,6 +67,9 @@ func TestExecute_DriveMoreCommands_JSON(t *testing.T) {
|
||||
return
|
||||
case strings.Contains(path, "/files/id1") && (r.Method == http.MethodPatch || r.Method == http.MethodPut):
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if got := r.URL.Query().Get("supportsAllDrives"); got != "true" {
|
||||
t.Fatalf("expected supportsAllDrives=true, got: %q (raw=%q)", got, r.URL.RawQuery)
|
||||
}
|
||||
if addParents := r.URL.Query().Get("addParents"); addParents != "" {
|
||||
if addParents != "np" {
|
||||
t.Fatalf("expected addParents=np, got: %q", r.URL.RawQuery)
|
||||
@ -81,6 +85,14 @@ func TestExecute_DriveMoreCommands_JSON(t *testing.T) {
|
||||
})
|
||||
return
|
||||
}
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
if strings.Contains(string(body), "\"trashed\":true") {
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"id": "id1",
|
||||
"trashed": true,
|
||||
})
|
||||
return
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"id": "id1",
|
||||
"name": "New",
|
||||
@ -234,6 +246,17 @@ func TestExecute_DriveMoreCommands_Text(t *testing.T) {
|
||||
return
|
||||
case strings.Contains(path, "/files/id1") && (r.Method == http.MethodPatch || r.Method == http.MethodPut):
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if got := r.URL.Query().Get("supportsAllDrives"); got != "true" {
|
||||
t.Fatalf("expected supportsAllDrives=true, got: %q (raw=%q)", got, r.URL.RawQuery)
|
||||
}
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
if strings.Contains(string(body), "\"trashed\":true") {
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"id": "id1",
|
||||
"trashed": true,
|
||||
})
|
||||
return
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"id": "id1",
|
||||
"name": "New",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user