Skip to content

Commit 992d7e2

Browse files
Copilotaooohan
andcommitted
Add cross-drive compatible Rename function and replace os.Rename in plugin operations
Co-authored-by: aooohan <40265686+aooohan@users.noreply.github.com>
1 parent 27e8291 commit 992d7e2

File tree

4 files changed

+234
-5
lines changed

4 files changed

+234
-5
lines changed

internal/manager.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,7 @@ func (m *Manager) Update(pluginName string) error {
344344
success := false
345345
backupPath := sdkMetadata.PluginInstalledPath + "-bak"
346346
logger.Debugf("Backup %s plugin to %s \n", sdkMetadata.PluginInstalledPath, backupPath)
347-
if err = os.Rename(sdkMetadata.PluginInstalledPath, backupPath); err != nil {
347+
if err = util.Rename(sdkMetadata.PluginInstalledPath, backupPath); err != nil {
348348
return fmt.Errorf("backup %s plugin failed, err: %w", sdkMetadata.PluginInstalledPath, err)
349349
}
350350
defer func() {
@@ -353,11 +353,11 @@ func (m *Manager) Update(pluginName string) error {
353353
_ = os.RemoveAll(backupPath)
354354
} else {
355355
logger.Debugf("Restoring from backup: %s\n", backupPath)
356-
_ = os.Rename(backupPath, sdkMetadata.PluginInstalledPath)
356+
_ = util.Rename(backupPath, sdkMetadata.PluginInstalledPath)
357357
}
358358
}()
359359
logger.Debugf("Moving updated plugin from %s to %s\n", tempPlugin.InstalledPath, sdkMetadata.PluginInstalledPath)
360-
if err = os.Rename(tempPlugin.InstalledPath, sdkMetadata.PluginInstalledPath); err != nil {
360+
if err = util.Rename(tempPlugin.InstalledPath, sdkMetadata.PluginInstalledPath); err != nil {
361361
return fmt.Errorf("update %s plugin failed, err: %w", pluginName, err)
362362
}
363363

@@ -539,7 +539,7 @@ func (m *Manager) Add(pluginName, url, alias string) error {
539539
}
540540
}
541541
logger.Debugf("Moving plugin from %s to %s\n", tempPlugin.InstalledPath, installPath)
542-
if err = os.Rename(tempPlugin.InstalledPath, installPath); err != nil {
542+
if err = util.Rename(tempPlugin.InstalledPath, installPath); err != nil {
543543
logger.Debugf("Failed to move plugin: %v\n", err)
544544
return fmt.Errorf("install plugin error: %w", err)
545545
}
@@ -611,7 +611,7 @@ func (m *Manager) installPluginToTemp(path string) (*plugin.Wrapper, error) {
611611
// make a directory to store the wrapper and rename the wrapper file to main.lua
612612
if ext == ".lua" {
613613
logger.Debugf("Moving wrapper %s to %s \n", localPath, tempInstallPath)
614-
if err = os.Rename(localPath, filepath.Join(tempInstallPath, "main.lua")); err != nil {
614+
if err = util.Rename(localPath, filepath.Join(tempInstallPath, "main.lua")); err != nil {
615615
logger.Debugf("Failed to move lua plugin: %v\n", err)
616616
return nil, fmt.Errorf("install wrapper error: %w", err)
617617
}

internal/shared/util/file.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,80 @@ func CopyFile(src, dst string) error {
5757
return nil
5858
}
5959

60+
// copyDir recursively copies a directory tree
61+
func copyDir(src, dst string) error {
62+
srcInfo, err := os.Stat(src)
63+
if err != nil {
64+
return err
65+
}
66+
67+
// Create destination directory with same permissions
68+
if err := os.MkdirAll(dst, srcInfo.Mode()); err != nil {
69+
return err
70+
}
71+
72+
entries, err := os.ReadDir(src)
73+
if err != nil {
74+
return err
75+
}
76+
77+
for _, entry := range entries {
78+
srcPath := filepath.Join(src, entry.Name())
79+
dstPath := filepath.Join(dst, entry.Name())
80+
81+
if entry.IsDir() {
82+
if err := copyDir(srcPath, dstPath); err != nil {
83+
return err
84+
}
85+
} else {
86+
if err := CopyFile(srcPath, dstPath); err != nil {
87+
return err
88+
}
89+
// Preserve file permissions
90+
if info, err := os.Stat(srcPath); err == nil {
91+
os.Chmod(dstPath, info.Mode())
92+
}
93+
}
94+
}
95+
96+
return nil
97+
}
98+
99+
// Rename renames (moves) a file or directory, handling cross-drive operations on Windows.
100+
// It first attempts a direct rename using os.Rename. If that fails (e.g., cross-drive on Windows),
101+
// it falls back to copy-and-delete.
102+
func Rename(src, dst string) error {
103+
// First try a direct rename (fast, works on same filesystem)
104+
err := os.Rename(src, dst)
105+
if err == nil {
106+
return nil
107+
}
108+
109+
// If rename failed, check if source exists
110+
srcInfo, statErr := os.Stat(src)
111+
if statErr != nil {
112+
return statErr
113+
}
114+
115+
// Copy source to destination
116+
if srcInfo.IsDir() {
117+
if err := copyDir(src, dst); err != nil {
118+
return err
119+
}
120+
} else {
121+
if err := CopyFile(src, dst); err != nil {
122+
return err
123+
}
124+
// Preserve file permissions
125+
if err := os.Chmod(dst, srcInfo.Mode()); err != nil {
126+
return err
127+
}
128+
}
129+
130+
// Remove source after successful copy
131+
return os.RemoveAll(src)
132+
}
133+
60134
// MoveFiles Move a folder or file to a specified directory
61135
func MoveFiles(src, targetDir string) error {
62136
info, err := os.Stat(src)

internal/shared/util/file_test.go

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
/*
2+
* Copyright 2026 Han Li and contributors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package util
18+
19+
import (
20+
"os"
21+
"path/filepath"
22+
"testing"
23+
)
24+
25+
func TestRenameFile(t *testing.T) {
26+
// Create temp directory for testing
27+
tmpDir, err := os.MkdirTemp("", "vfox-rename-test-")
28+
if err != nil {
29+
t.Fatalf("Failed to create temp dir: %v", err)
30+
}
31+
defer os.RemoveAll(tmpDir)
32+
33+
// Create a test file
34+
srcFile := filepath.Join(tmpDir, "test-src.txt")
35+
content := "test content"
36+
if err := os.WriteFile(srcFile, []byte(content), 0644); err != nil {
37+
t.Fatalf("Failed to create test file: %v", err)
38+
}
39+
40+
// Test renaming file
41+
dstFile := filepath.Join(tmpDir, "test-dst.txt")
42+
if err := Rename(srcFile, dstFile); err != nil {
43+
t.Fatalf("Rename failed: %v", err)
44+
}
45+
46+
// Verify source no longer exists
47+
if FileExists(srcFile) {
48+
t.Error("Source file still exists after rename")
49+
}
50+
51+
// Verify destination exists with correct content
52+
if !FileExists(dstFile) {
53+
t.Error("Destination file does not exist")
54+
}
55+
readContent, err := os.ReadFile(dstFile)
56+
if err != nil {
57+
t.Fatalf("Failed to read destination file: %v", err)
58+
}
59+
if string(readContent) != content {
60+
t.Errorf("Content mismatch: expected %q, got %q", content, string(readContent))
61+
}
62+
}
63+
64+
func TestRenameDirectory(t *testing.T) {
65+
// Create temp directory for testing
66+
tmpDir, err := os.MkdirTemp("", "vfox-rename-test-")
67+
if err != nil {
68+
t.Fatalf("Failed to create temp dir: %v", err)
69+
}
70+
defer os.RemoveAll(tmpDir)
71+
72+
// Create a test directory structure
73+
srcDir := filepath.Join(tmpDir, "test-src-dir")
74+
if err := os.Mkdir(srcDir, 0755); err != nil {
75+
t.Fatalf("Failed to create test directory: %v", err)
76+
}
77+
78+
// Create nested structure
79+
subDir := filepath.Join(srcDir, "subdir")
80+
if err := os.Mkdir(subDir, 0755); err != nil {
81+
t.Fatalf("Failed to create subdirectory: %v", err)
82+
}
83+
84+
file1 := filepath.Join(srcDir, "file1.txt")
85+
file2 := filepath.Join(subDir, "file2.txt")
86+
content1 := "content1"
87+
content2 := "content2"
88+
89+
if err := os.WriteFile(file1, []byte(content1), 0644); err != nil {
90+
t.Fatalf("Failed to create file1: %v", err)
91+
}
92+
if err := os.WriteFile(file2, []byte(content2), 0644); err != nil {
93+
t.Fatalf("Failed to create file2: %v", err)
94+
}
95+
96+
// Test renaming directory
97+
dstDir := filepath.Join(tmpDir, "test-dst-dir")
98+
if err := Rename(srcDir, dstDir); err != nil {
99+
t.Fatalf("Rename directory failed: %v", err)
100+
}
101+
102+
// Verify source no longer exists
103+
if FileExists(srcDir) {
104+
t.Error("Source directory still exists after rename")
105+
}
106+
107+
// Verify destination exists with correct structure
108+
if !FileExists(dstDir) {
109+
t.Error("Destination directory does not exist")
110+
}
111+
112+
// Check file1
113+
dstFile1 := filepath.Join(dstDir, "file1.txt")
114+
if !FileExists(dstFile1) {
115+
t.Error("file1.txt does not exist in destination")
116+
}
117+
readContent1, err := os.ReadFile(dstFile1)
118+
if err != nil {
119+
t.Fatalf("Failed to read file1: %v", err)
120+
}
121+
if string(readContent1) != content1 {
122+
t.Errorf("Content mismatch in file1: expected %q, got %q", content1, string(readContent1))
123+
}
124+
125+
// Check file2 in subdirectory
126+
dstFile2 := filepath.Join(dstDir, "subdir", "file2.txt")
127+
if !FileExists(dstFile2) {
128+
t.Error("file2.txt does not exist in destination subdirectory")
129+
}
130+
readContent2, err := os.ReadFile(dstFile2)
131+
if err != nil {
132+
t.Fatalf("Failed to read file2: %v", err)
133+
}
134+
if string(readContent2) != content2 {
135+
t.Errorf("Content mismatch in file2: expected %q, got %q", content2, string(readContent2))
136+
}
137+
}
138+
139+
func TestRenameNonExistent(t *testing.T) {
140+
// Create temp directory for testing
141+
tmpDir, err := os.MkdirTemp("", "vfox-rename-test-")
142+
if err != nil {
143+
t.Fatalf("Failed to create temp dir: %v", err)
144+
}
145+
defer os.RemoveAll(tmpDir)
146+
147+
srcFile := filepath.Join(tmpDir, "nonexistent.txt")
148+
dstFile := filepath.Join(tmpDir, "destination.txt")
149+
150+
// Test renaming non-existent file should fail
151+
err = Rename(srcFile, dstFile)
152+
if err == nil {
153+
t.Error("Expected error when renaming non-existent file, got nil")
154+
}
155+
}

vfox

18.7 MB
Binary file not shown.

0 commit comments

Comments
 (0)