diff --git a/main.go b/main.go index 14b28d0..7b1be0a 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,7 @@ import ( "bufio" "errors" "fmt" + "io" "io/fs" "os" "path/filepath" @@ -36,6 +37,10 @@ func main() { err = applyCmd(args[1:]) case "add": err = addCmd(args[1:]) + case "unlink": + err = unlinkCmd(args[1:]) + case "remove": + err = removeCmd(args[1:]) case "help", "-h", "--help": usage() return @@ -54,6 +59,8 @@ func usage() { fmt.Println("usage:") fmt.Println(" sigil apply [--prune]") fmt.Println(" sigil add ") + fmt.Println(" sigil unlink ") + fmt.Println(" sigil remove ") } func applyCmd(args []string) error { @@ -238,6 +245,72 @@ func addCmd(args []string) error { return applyPackage(filesDir, targetRoot) } +func unlinkCmd(args []string) error { + if len(args) < 1 { + return errors.New("missing package name") + } + + pkgName := args[0] + repo, err := repoPath() + if err != nil { + return err + } + + pkgDir := filepath.Join(repo, pkgName) + configPath := filepath.Join(pkgDir, configFileName) + cfg, err := loadConfig(configPath) + if err != nil { + return err + } + + targetRoot, err := selectTarget(cfg.targets) + if err != nil { + return err + } + + filesDir := filepath.Join(pkgDir, filesDirName) + if _, err := os.Stat(filesDir); err != nil { + return err + } + + return restorePackage(filesDir, targetRoot) +} + +func removeCmd(args []string) error { + if len(args) < 1 { + return errors.New("missing package name") + } + + pkgName := args[0] + repo, err := repoPath() + if err != nil { + return err + } + + pkgDir := filepath.Join(repo, pkgName) + configPath := filepath.Join(pkgDir, configFileName) + cfg, err := loadConfig(configPath) + if err != nil { + return err + } + + targetRoot, err := selectTarget(cfg.targets) + if err != nil { + return err + } + + filesDir := filepath.Join(pkgDir, filesDirName) + if _, err := os.Stat(filesDir); err != nil { + return err + } + + if err := restorePackage(filesDir, targetRoot); err != nil { + return err + } + + return os.RemoveAll(pkgDir) +} + func applyPackage(filesDir, targetRoot string) error { if err := ensureDir(targetRoot); err != nil { return err @@ -528,3 +601,87 @@ func removeLinks(paths []string) error { } return nil } + +func restorePackage(filesDir, targetRoot string) error { + filesAbs, err := filepath.Abs(filesDir) + if err != nil { + return err + } + + return filepath.WalkDir(filesDir, func(path string, entry fs.DirEntry, err error) error { + if err != nil { + return err + } + if path == filesDir { + return nil + } + if entry.IsDir() { + return nil + } + + rel, err := filepath.Rel(filesDir, path) + if err != nil { + return err + } + targetPath := filepath.Join(targetRoot, rel) + + info, err := os.Lstat(targetPath) + if errors.Is(err, os.ErrNotExist) { + return nil + } + if err != nil { + return err + } + if info.Mode()&os.ModeSymlink == 0 { + return nil + } + + src, err := os.Readlink(targetPath) + if err != nil { + return err + } + if !filepath.IsAbs(src) { + src = filepath.Join(filepath.Dir(targetPath), src) + } + src = filepath.Clean(src) + + if !strings.HasPrefix(src, filesAbs+string(os.PathSeparator)) && src != filesAbs { + return nil + } + + if err := os.Remove(targetPath); err != nil { + return err + } + + return copyFile(path, targetPath) + }) +} + +func copyFile(src, dst string) error { + if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil { + return err + } + + 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_CREATE|os.O_WRONLY|os.O_TRUNC, info.Mode()) + if err != nil { + return err + } + defer dstFile.Close() + + if _, err := io.Copy(dstFile, srcFile); err != nil { + return err + } + + return nil +}