feat(v0.3): add CopySeedDir with task.yaml/.ctask skip
CopySeedDir is the v0.3 user seed overlay primitive: recursive copy from a seed directory to a workspace, overwriting destination files. task.yaml and the .ctask metadata directory at the seed root are intentionally skipped so a stale or hostile seed cannot overwrite ctask-owned state. Missing src is a no-op (the seed directory is optional).
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
package seed
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// CopySeedDir recursively copies the contents of srcDir into dstDir.
|
||||
// Files at the destination are overwritten. Subdirectories are preserved.
|
||||
//
|
||||
// The following entries at the top level of srcDir are intentionally skipped:
|
||||
// - task.yaml (ctask owns this file)
|
||||
// - .ctask (directory; reserved for ctask metadata)
|
||||
//
|
||||
// If srcDir does not exist, this function returns nil (no-op). Other I/O errors
|
||||
// are returned to the caller.
|
||||
func CopySeedDir(srcDir, dstDir string) error {
|
||||
info, err := os.Stat(srcDir)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(srcDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
name := entry.Name()
|
||||
if name == "task.yaml" || name == ".ctask" {
|
||||
continue
|
||||
}
|
||||
srcPath := filepath.Join(srcDir, name)
|
||||
dstPath := filepath.Join(dstDir, name)
|
||||
if err := copyEntry(srcPath, dstPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyEntry(src, dst string) error {
|
||||
info, err := os.Stat(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.IsDir() {
|
||||
if err := os.MkdirAll(dst, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
entries, err := os.ReadDir(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, e := range entries {
|
||||
if err := copyEntry(filepath.Join(src, e.Name()), filepath.Join(dst, e.Name())); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return copyFile(src, dst, info.Mode())
|
||||
}
|
||||
|
||||
func copyFile(src, dst string, mode os.FileMode) error {
|
||||
in, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer in.Close()
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
out, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode.Perm())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
if _, err := io.Copy(out, in); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user