mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
Feat/update migrate (#910)
* * update migrate * * rename handlers to sources * * delete dead code * * fix go test error
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func ResolveTargetHome(override string) (string, error) {
|
||||
if override != "" {
|
||||
return ExpandHome(override), nil
|
||||
}
|
||||
if envHome := os.Getenv("PICOCLAW_HOME"); envHome != "" {
|
||||
return ExpandHome(envHome), nil
|
||||
}
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("resolving home directory: %w", err)
|
||||
}
|
||||
return filepath.Join(home, ".picoclaw"), nil
|
||||
}
|
||||
|
||||
func ExpandHome(path string) string {
|
||||
if path == "" {
|
||||
return path
|
||||
}
|
||||
if path[0] == '~' {
|
||||
home, _ := os.UserHomeDir()
|
||||
if len(path) > 1 && path[1] == '/' {
|
||||
return home + path[1:]
|
||||
}
|
||||
return home
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func ResolveWorkspace(homeDir string) string {
|
||||
return filepath.Join(homeDir, "workspace")
|
||||
}
|
||||
|
||||
func PlanWorkspaceMigration(
|
||||
srcWorkspace, dstWorkspace string,
|
||||
migrateableFiles []string,
|
||||
migrateableDirs []string,
|
||||
force bool,
|
||||
) ([]Action, error) {
|
||||
var actions []Action
|
||||
|
||||
for _, filename := range migrateableFiles {
|
||||
src := filepath.Join(srcWorkspace, filename)
|
||||
dst := filepath.Join(dstWorkspace, filename)
|
||||
action := planFileCopy(src, dst, force)
|
||||
if action.Type != ActionSkip || action.Description != "" {
|
||||
actions = append(actions, action)
|
||||
}
|
||||
}
|
||||
|
||||
for _, dirname := range migrateableDirs {
|
||||
srcDir := filepath.Join(srcWorkspace, dirname)
|
||||
if _, err := os.Stat(srcDir); os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
dirActions, err := planDirCopy(srcDir, filepath.Join(dstWorkspace, dirname), force)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
actions = append(actions, dirActions...)
|
||||
}
|
||||
|
||||
return actions, nil
|
||||
}
|
||||
|
||||
func planFileCopy(src, dst string, force bool) Action {
|
||||
if _, err := os.Stat(src); os.IsNotExist(err) {
|
||||
return Action{
|
||||
Type: ActionSkip,
|
||||
Source: src,
|
||||
Target: dst,
|
||||
Description: "source file not found",
|
||||
}
|
||||
}
|
||||
|
||||
_, dstExists := os.Stat(dst)
|
||||
if dstExists == nil && !force {
|
||||
return Action{
|
||||
Type: ActionBackup,
|
||||
Source: src,
|
||||
Target: dst,
|
||||
Description: "destination exists, will backup and overwrite",
|
||||
}
|
||||
}
|
||||
|
||||
return Action{
|
||||
Type: ActionCopy,
|
||||
Source: src,
|
||||
Target: dst,
|
||||
Description: "copy file",
|
||||
}
|
||||
}
|
||||
|
||||
func planDirCopy(srcDir, dstDir string, force bool) ([]Action, error) {
|
||||
var actions []Action
|
||||
|
||||
err := filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
relPath, err := filepath.Rel(srcDir, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dst := filepath.Join(dstDir, relPath)
|
||||
|
||||
if info.IsDir() {
|
||||
actions = append(actions, Action{
|
||||
Type: ActionCreateDir,
|
||||
Target: dst,
|
||||
Description: "create directory",
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
action := planFileCopy(path, dst, force)
|
||||
actions = append(actions, action)
|
||||
return nil
|
||||
})
|
||||
|
||||
return actions, err
|
||||
}
|
||||
|
||||
func RelPath(path, base string) string {
|
||||
rel, err := filepath.Rel(base, path)
|
||||
if err != nil {
|
||||
return filepath.Base(path)
|
||||
}
|
||||
return rel
|
||||
}
|
||||
|
||||
func CopyFile(src, dst string) error {
|
||||
srcFile, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer srcFile.Close()
|
||||
|
||||
info, err := srcFile.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dstFile, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, info.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dstFile.Close()
|
||||
|
||||
_, err = io.Copy(dstFile, srcFile)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestExpandHome(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"", ""},
|
||||
{"/absolute/path", "/absolute/path"},
|
||||
{"relative/path", "relative/path"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result := ExpandHome(tt.input)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpandHomeWithTilde(t *testing.T) {
|
||||
home, err := os.UserHomeDir()
|
||||
require.NoError(t, err)
|
||||
|
||||
result := ExpandHome("~/path")
|
||||
assert.Equal(t, home+"/path", result)
|
||||
|
||||
result = ExpandHome("~")
|
||||
assert.Equal(t, home, result)
|
||||
}
|
||||
|
||||
func TestResolveWorkspace(t *testing.T) {
|
||||
result := ResolveWorkspace("/home/user/.picoclaw")
|
||||
assert.Equal(t, "/home/user/.picoclaw/workspace", result)
|
||||
}
|
||||
|
||||
func TestRelPath(t *testing.T) {
|
||||
result := RelPath("/home/user/.picoclaw/workspace/file.txt", "/home/user/.picoclaw")
|
||||
assert.Equal(t, "workspace/file.txt", result)
|
||||
}
|
||||
|
||||
func TestRelPathError(t *testing.T) {
|
||||
result := RelPath("relative/path", "/different/base")
|
||||
assert.Equal(t, "path", result)
|
||||
}
|
||||
|
||||
func TestResolveTargetHome(t *testing.T) {
|
||||
home, err := os.UserHomeDir()
|
||||
require.NoError(t, err)
|
||||
|
||||
result, err := ResolveTargetHome("")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, filepath.Join(home, ".picoclaw"), result)
|
||||
}
|
||||
|
||||
func TestResolveTargetHomeWithOverride(t *testing.T) {
|
||||
result, err := ResolveTargetHome("/custom/path")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "/custom/path", result)
|
||||
}
|
||||
|
||||
func TestCopyFile(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
sourceFile := filepath.Join(tmpDir, "source.txt")
|
||||
err := os.WriteFile(sourceFile, []byte("test content"), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
dstFile := filepath.Join(tmpDir, "dest.txt")
|
||||
err = CopyFile(sourceFile, dstFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
content, err := os.ReadFile(dstFile)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "test content", string(content))
|
||||
}
|
||||
|
||||
func TestCopyFileSourceNotFound(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
err := CopyFile(filepath.Join(tmpDir, "nonexistent.txt"), filepath.Join(tmpDir, "dest.txt"))
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestPlanWorkspaceMigration(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
srcWorkspace := filepath.Join(tmpDir, "src", "workspace")
|
||||
dstWorkspace := filepath.Join(tmpDir, "dst", "workspace")
|
||||
|
||||
err := os.MkdirAll(srcWorkspace, 0o755)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = os.WriteFile(filepath.Join(srcWorkspace, "file1.txt"), []byte("content"), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = os.MkdirAll(filepath.Join(srcWorkspace, "subdir"), 0o755)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = os.WriteFile(filepath.Join(srcWorkspace, "subdir", "file2.txt"), []byte("content"), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
actions, err := PlanWorkspaceMigration(
|
||||
srcWorkspace,
|
||||
dstWorkspace,
|
||||
[]string{"file1.txt"},
|
||||
[]string{"subdir"},
|
||||
false,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.GreaterOrEqual(t, len(actions), 1)
|
||||
}
|
||||
|
||||
func TestPlanWorkspaceMigrationWithExistingDestination(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
srcWorkspace := filepath.Join(tmpDir, "src", "workspace")
|
||||
dstWorkspace := filepath.Join(tmpDir, "dst", "workspace")
|
||||
|
||||
err := os.MkdirAll(srcWorkspace, 0o755)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = os.MkdirAll(dstWorkspace, 0o755)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = os.WriteFile(filepath.Join(srcWorkspace, "file1.txt"), []byte("source"), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = os.WriteFile(filepath.Join(dstWorkspace, "file1.txt"), []byte("existing"), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
actions, err := PlanWorkspaceMigration(
|
||||
srcWorkspace,
|
||||
dstWorkspace,
|
||||
[]string{"file1.txt"},
|
||||
[]string{},
|
||||
false,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.GreaterOrEqual(t, len(actions), 1)
|
||||
assert.Equal(t, ActionBackup, actions[0].Type)
|
||||
}
|
||||
|
||||
func TestPlanWorkspaceMigrationForce(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
srcWorkspace := filepath.Join(tmpDir, "src", "workspace")
|
||||
dstWorkspace := filepath.Join(tmpDir, "dst", "workspace")
|
||||
|
||||
err := os.MkdirAll(srcWorkspace, 0o755)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = os.MkdirAll(dstWorkspace, 0o755)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = os.WriteFile(filepath.Join(srcWorkspace, "file1.txt"), []byte("source"), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = os.WriteFile(filepath.Join(dstWorkspace, "file1.txt"), []byte("existing"), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
actions, err := PlanWorkspaceMigration(
|
||||
srcWorkspace,
|
||||
dstWorkspace,
|
||||
[]string{"file1.txt"},
|
||||
[]string{},
|
||||
true,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.GreaterOrEqual(t, len(actions), 1)
|
||||
assert.Equal(t, ActionCopy, actions[0].Type)
|
||||
}
|
||||
|
||||
func TestPlanWorkspaceMigrationNonExistentSource(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
actions, err := PlanWorkspaceMigration(
|
||||
filepath.Join(tmpDir, "nonexistent"),
|
||||
filepath.Join(tmpDir, "dst", "workspace"),
|
||||
[]string{"file1.txt"},
|
||||
[]string{},
|
||||
false,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, actions, 1)
|
||||
assert.Equal(t, ActionSkip, actions[0].Type)
|
||||
assert.Contains(t, actions[0].Description, "source file not found")
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package internal
|
||||
|
||||
type Options struct {
|
||||
DryRun bool
|
||||
ConfigOnly bool
|
||||
WorkspaceOnly bool
|
||||
Force bool
|
||||
Refresh bool
|
||||
Source string
|
||||
SourceHome string
|
||||
TargetHome string
|
||||
}
|
||||
|
||||
type Operation interface {
|
||||
GetSourceName() string
|
||||
GetSourceHome() (string, error)
|
||||
GetSourceWorkspace() (string, error)
|
||||
GetSourceConfigFile() (string, error)
|
||||
ExecuteConfigMigration(srcConfigPath, dstConfigPath string) error
|
||||
GetMigrateableFiles() []string
|
||||
GetMigrateableDirs() []string
|
||||
}
|
||||
|
||||
type HandlerFactory func(opts Options) Operation
|
||||
|
||||
type ActionType int
|
||||
|
||||
const (
|
||||
ActionCopy ActionType = iota
|
||||
ActionSkip
|
||||
ActionBackup
|
||||
ActionConvertConfig
|
||||
ActionCreateDir
|
||||
ActionMergeConfig
|
||||
)
|
||||
|
||||
type Action struct {
|
||||
Type ActionType
|
||||
Source string
|
||||
Target string
|
||||
Description string
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
FilesCopied int
|
||||
FilesSkipped int
|
||||
BackupsCreated int
|
||||
ConfigMigrated bool
|
||||
DirsCreated int
|
||||
Warnings []string
|
||||
Errors []error
|
||||
}
|
||||
Reference in New Issue
Block a user