Skip to content

Commit c67f267

Browse files
committed
feat: server-side move
1 parent c791507 commit c67f267

6 files changed

Lines changed: 80 additions & 30 deletions

File tree

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,15 @@ You can now manage your Google Drive mounts directly from the terminal using the
100100
```
101101
This command allows you to copy files or folders from one Google Drive to another, or within the same drive, using rclone's server-side copy. This avoids any file format conversion and is the recommended way to copy Google Docs, Sheets, and Slides natively.
102102

103-
> **Why not use normal copy?**
103+
- **Move files or folders between Google Drives (server-side, no conversion):**
104+
```bash
105+
adfinis-rclone-mgr mv <source>... <destination>
106+
```
107+
This command allows you to move files or folders from one Google Drive to another, or within the same drive, using rclone's server-side move. This avoids any file format conversion and is the recommended way to move Google Docs, Sheets, and Slides natively.
108+
109+
> **Why not use normal copy or move?**
104110
>
105-
> If you use the standard copy methods (Ctrl+C, right-click + Copy, or drag & drop) in your file manager, rclone mounts will convert Google Docs, Sheets, and Slides to Microsoft Office formats (e.g., gdocs to .docx) during the copy. This can cause formatting issues. The `cp` command and the "Copy on Google Drive" Nautilus context menu entry ensure that all copy actions are performed server-side, preserving the native Google format and avoiding unwanted conversions.
111+
> If you use the standard copy or move methods (ctrl+c/ctrl+x, right-click + Copy/Move, or drag & drop) in your file manager or `cp`, `rsync`, `mv`, etc. in the terminal, rclone mountes will convert Google Docs, Sheets, and Slides to Microsoft Office formats (e.g., gdocs to .docx) during the operation. This can cause formatting issues. The `cp` and `mv` commands and the "Copy on Google Drive"/"Move on Google Drive" Nautilus context menu entries ensure that all copy and move actions are performed server-side, preserving the native Google format and avoiding unwanted conversions.
106112
107113
### Daemon Mode
108114

assets/google_drive_opener.py

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import subprocess
44
import webbrowser
55
import json
6-
import threading
76

87
"""
98
This extension adds a context menu item to Nautilus for opening files in Google Drive.
@@ -45,11 +44,19 @@ def get_file_items(self, *args):
4544
copy_file_item = Nautilus.MenuItem(
4645
name="GoogleDriveOpener::CopyFile",
4746
label="Copy on Google Drive",
48-
tip="Copy Files or Folders on Google Drive",
47+
tip="Copy on Google Drive",
4948
)
50-
copy_file_item.connect("activate", self._copy_file, file_paths)
49+
copy_file_item.connect("activate", self.copy_file, file_paths)
5150
items.append(copy_file_item)
5251

52+
move_file_item = Nautilus.MenuItem(
53+
name="GoogleDriveOpener::MoveFile",
54+
label="Move on Google Drive",
55+
tip="Move on Google Drive",
56+
)
57+
move_file_item.connect("activate", self.move_file, file_paths)
58+
items.append(move_file_item)
59+
5360
# add copy button if only one file is selected
5461
if len(file_paths) == 1:
5562
copy_file_link_item = Nautilus.MenuItem(
@@ -139,10 +146,9 @@ def copy_file_link(self, menu, file_paths):
139146
["zenity", "--error", "--text", f"Unexpected error:\n{str(e)}"]
140147
)
141148

142-
def _copy_file(self, menu, file_paths):
149+
def _send_file_op(self, file_paths, op):
143150
try:
144151
import httpx
145-
# Determine the drive name from the first file path
146152
if not file_paths:
147153
return
148154
relative_path = os.path.relpath(file_paths[0], self.RCLONE_MOUNT_PATH)
@@ -152,19 +158,19 @@ def _copy_file(self, menu, file_paths):
152158
"adfinis-rclone-mgr",
153159
f"{drive_name}.sock",
154160
)
155-
# httpx expects the socket path as a str, and the URL as http://localhost/...
156-
url = "http://localhost/gdrive/copy"
161+
url = f"http://localhost/gdrive/{op}"
157162
transport = httpx.HTTPTransport(uds=sock_path)
158163
with httpx.Client(transport=transport) as client:
159164
resp = client.post(url, json={"sources": file_paths}, timeout=600)
160165
if resp.status_code != 200:
161166
raise Exception(f"Server error: {resp.text}")
162167
except Exception as e:
163168
subprocess.Popen([
164-
"zenity", "--error", "--text", f"Failed to copy file(s) via daemon: {str(e)}"
169+
"zenity", "--error", "--text", f"Failed to {op} file(s) via daemon: {str(e)}"
165170
])
166171

167172
def copy_file(self, menu, file_paths):
168-
threading.Thread(
169-
target=self._copy_file, args=(menu, file_paths), daemon=True
170-
).start()
173+
self._send_file_op(file_paths, "copy")
174+
175+
def move_file(self, menu, file_paths):
176+
self._send_file_op(file_paths, "move")

daemon.go

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -71,34 +71,38 @@ func ipcServer(driveName string) {
7171
}
7272
}
7373

74-
type copyRequest struct {
74+
type gdriveOPRequest struct {
7575
Sources []string `json:"sources"`
7676
}
7777

78-
func newHTTPHandler() http.Handler {
79-
mux := http.NewServeMux()
80-
mux.HandleFunc("/gdrive/copy", func(w http.ResponseWriter, r *http.Request) {
78+
func handleGDriveOp(op string) http.HandlerFunc {
79+
return func(w http.ResponseWriter, r *http.Request) {
8180
if r.Method != http.MethodPost {
8281
w.WriteHeader(http.StatusMethodNotAllowed)
8382
w.Write([]byte("Method not allowed")) // nolint:errcheck
8483
return
8584
}
86-
var req copyRequest
85+
var req gdriveOPRequest
8786
err := json.NewDecoder(r.Body).Decode(&req)
8887
if err != nil || len(req.Sources) == 0 {
8988
w.WriteHeader(http.StatusBadRequest)
9089
w.Write([]byte("Invalid request: must provide sources")) // nolint:errcheck
9190
return
9291
}
9392

94-
log.Println("Received copy request for sources:", req.Sources)
93+
log.Printf("Received %s request for sources: %v", op, req.Sources)
9594

96-
// copy files in background
97-
go selectDestAndCopy(req.Sources)
95+
// run files in background
96+
go selectDestAndRunOP(req.Sources, op)
9897

9998
w.WriteHeader(http.StatusOK)
10099
w.Write([]byte("OK")) // nolint:errcheck
101-
})
100+
}
101+
}
102102

103+
func newHTTPHandler() http.Handler {
104+
mux := http.NewServeMux()
105+
mux.HandleFunc("/gdrive/copy", handleGDriveOp("copy"))
106+
mux.HandleFunc("/gdrive/move", handleGDriveOp("move"))
103107
return mux
104108
}

filehandler.go

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,13 @@ import (
1515
func copy(cmd *cobra.Command, args []string) {
1616
// last arg is the destination directory
1717
dest := args[len(args)-1]
18-
copyFile(args[:len(args)-1], dest)
18+
runRcloneOp("copy", args[:len(args)-1], dest)
19+
}
20+
21+
func move(cmd *cobra.Command, args []string) {
22+
// last arg is the destination directory
23+
dest := args[len(args)-1]
24+
runRcloneOp("move", args[:len(args)-1], dest)
1925
}
2026

2127
// isSubdir checks if sub is a subdirectory (or the same) as root.
@@ -85,7 +91,7 @@ func selectDestination() (string, error) {
8591
return destDir, nil
8692
}
8793

88-
func selectDestAndCopy(srcPaths []string) {
94+
func selectDestAndRunOP(srcPaths []string, op string) {
8995
destDir, err := selectDestination()
9096
if err != nil {
9197
log.Println("Failed to select destination:", err)
@@ -97,10 +103,11 @@ func selectDestAndCopy(srcPaths []string) {
97103
return
98104
}
99105

100-
copyFile(srcPaths, destDir)
106+
runRcloneOp(op, srcPaths, destDir)
101107
}
102108

103-
func copyFile(srcPaths []string, destDir string) {
109+
// runRcloneOp runs rclone with the given operation ("copy" or "move")
110+
func runRcloneOp(op string, srcPaths []string, destDir string) {
104111
absGoogleRoot, err := filepath.Abs(getGooglePath())
105112
if err != nil {
106113
showZenityError("Failed to resolve Google Drive root")
@@ -140,18 +147,33 @@ func copyFile(srcPaths []string, destDir string) {
140147
if isDir(src) {
141148
destRclone = patchDestPath(srcPath, destRclone)
142149
}
143-
log.Printf("Copying from %s to %s", srcRclone, destRclone)
150+
// ahhh yes
151+
verb := op
152+
switch op {
153+
case "copy":
154+
verb = "copy"
155+
case "move":
156+
verb = "mov"
157+
}
158+
log.Printf("%sing from %s to %s", verb, srcRclone, destRclone)
144159

145-
cmd := exec.Command("rclone", "copy", "--drive-server-side-across-configs", srcRclone, destRclone, "-v")
160+
cmd := exec.Command("rclone", op, "--drive-server-side-across-configs", srcRclone, destRclone, "-v")
146161
cmd.Stdout = os.Stdout
147162
cmd.Stderr = os.Stderr
148163
err = cmd.Run()
149164
if err != nil {
150-
showZenityError("rclone copy failed: " + err.Error())
165+
showZenityError(fmt.Sprintf("rclone %s failed: %v", op, err))
151166
return
152167
}
153168
}
154-
if err := exec.Command("zenity", "--info", "--text", "File(s) copied successfully").Run(); err != nil {
169+
var msg string
170+
switch op {
171+
case "copy":
172+
msg = "File(s) copied successfully"
173+
case "move":
174+
msg = "File(s) moved successfully"
175+
}
176+
if err := exec.Command("zenity", "--info", "--text", msg).Run(); err != nil {
155177
log.Println("Failed to show success dialog:", err)
156178
}
157179
}

main.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ func init() {
4646
listCmd,
4747
daemonCmd,
4848
copyCmd,
49+
moveCmd,
4950
versionCmd,
5051
manCmd,
5152
)
@@ -144,6 +145,16 @@ var copyCmd = &cobra.Command{
144145
Run: copy,
145146
}
146147

148+
var moveCmd = &cobra.Command{
149+
Use: "mv",
150+
Short: "Move files or folders from one drive to another",
151+
Long: "The mv command allows you to move files or folders from one Google Drive to another.\n" +
152+
"It supports moving single files, multiple files, or entire folders.\n" +
153+
"You can use it as a drop-in replacement for the linux mv command, but with the added benefit of working across Google Drives.\n",
154+
Args: cobra.MinimumNArgs(2),
155+
Run: move,
156+
}
157+
147158
var versionCmd = &cobra.Command{
148159
Use: "version",
149160
Short: "Print the version details",

main_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ func TestRootCmd_HasSubcommands(t *testing.T) {
2626
"ls",
2727
"daemon",
2828
"cp",
29+
"mv",
2930
"version",
3031
"man",
3132
}

0 commit comments

Comments
 (0)