package filesystem import ( "archive/zip" "fmt" "io" "io/fs" "os" "path" "path/filepath" "strings" billy "github.com/go-git/go-billy/v5" ) // Filesystems // Wrap the implementations of FS with their subtle differences into the // common interface for accessing template files defined herein. // os: standard for on-disk extensible template repositories. // zip: embedded filesystem backed by the byte array representing zipfile. // billy: go-git library's filesystem used for remote git template repos. type Filesystem interface { fs.ReadDirFS fs.StatFS Readlink(link string) (string, error) } type zipFS struct { archive *zip.Reader } func NewZipFS(archive *zip.Reader) zipFS { return zipFS{archive: archive} } func (z zipFS) Open(name string) (fs.File, error) { return z.archive.Open(name) } func (z zipFS) ReadDir(name string) ([]fs.DirEntry, error) { var dirEntries []fs.DirEntry for _, file := range z.archive.File { cleanName := strings.TrimRight(file.Name, "/") if path.Dir(cleanName) == name { f, err := z.archive.Open(cleanName) if err != nil { return nil, err } fi, err := f.Stat() if err != nil { return nil, err } dirEntries = append(dirEntries, dirEntry{FileInfo: fi}) } } return dirEntries, nil } func (z zipFS) Readlink(link string) (string, error) { f, err := z.archive.Open(link) if err != nil { return "", err } defer f.Close() fi, err := f.Stat() if err != nil { return "", err } if fi.Mode()&fs.ModeSymlink == 0 { return "", &fs.PathError{Op: "readlink", Path: link, Err: fs.ErrInvalid} } bs, err := io.ReadAll(f) if err != nil { return "", err } return string(bs), nil } func (z zipFS) Stat(name string) (fs.FileInfo, error) { f, err := z.archive.Open(name) if err != nil { return nil, err } return f.Stat() } // BillyFilesystem is a template file accessor backed by a billy FS type BillyFilesystem struct{ fs billy.Filesystem } func NewBillyFilesystem(fs billy.Filesystem) BillyFilesystem { return BillyFilesystem{fs: fs} } func (b BillyFilesystem) Readlink(link string) (string, error) { return b.fs.Readlink(link) } type bfsFile struct { billy.File stats func() (fs.FileInfo, error) } func (b bfsFile) Stat() (fs.FileInfo, error) { return b.stats() } func (b BillyFilesystem) Open(name string) (fs.File, error) { f, err := b.fs.Open(name) if err != nil { return nil, err } return bfsFile{ File: f, stats: func() (fs.FileInfo, error) { return b.fs.Stat(name) }}, nil } type dirEntry struct { fs.FileInfo } func (d dirEntry) Type() fs.FileMode { return d.Mode().Type() } func (d dirEntry) Info() (fs.FileInfo, error) { return d, nil } func (b BillyFilesystem) ReadDir(name string) ([]fs.DirEntry, error) { fis, err := b.fs.ReadDir(name) if err != nil { return nil, err } var des = make([]fs.DirEntry, len(fis)) for i, fi := range fis { des[i] = dirEntry{fi} } return des, nil } func (b BillyFilesystem) Stat(name string) (fs.FileInfo, error) { return b.fs.Lstat(name) } // osFilesystem is a template file accessor backed by the os. type osFilesystem struct{ root string } func NewOsFilesystem(root string) osFilesystem { return osFilesystem{root: root} } func (o osFilesystem) Open(name string) (fs.File, error) { name = filepath.FromSlash(name) return os.Open(filepath.Join(o.root, name)) } func (o osFilesystem) ReadDir(name string) ([]fs.DirEntry, error) { name = filepath.FromSlash(name) return os.ReadDir(filepath.Join(o.root, name)) } func (o osFilesystem) Stat(name string) (fs.FileInfo, error) { name = filepath.FromSlash(name) return os.Lstat(filepath.Join(o.root, name)) } func (o osFilesystem) Readlink(link string) (string, error) { link = filepath.FromSlash(link) t, err := os.Readlink(filepath.Join(o.root, link)) if err != nil { return "", err } return filepath.ToSlash(t), nil } // subFS exposes subdirectory of underlying FS, this is similar to `chroot`. type subFS struct { root string fs Filesystem } func NewSubFS(root string, fs Filesystem) subFS { return subFS{root: root, fs: fs} } func (o subFS) Open(name string) (fs.File, error) { return o.fs.Open(path.Join(o.root, name)) } func (o subFS) ReadDir(name string) ([]fs.DirEntry, error) { return o.fs.ReadDir(path.Join(o.root, name)) } func (o subFS) Stat(name string) (fs.FileInfo, error) { return o.fs.Stat(path.Join(o.root, name)) } func (o subFS) Readlink(link string) (string, error) { return o.fs.Readlink(path.Join(o.root, link)) } type maskingFS struct { masked func(path string) bool fs Filesystem } func NewMaskingFS(masked func(path string) bool, fs Filesystem) maskingFS { return maskingFS{masked: masked, fs: fs} } func (m maskingFS) Open(name string) (fs.File, error) { if m.masked(name) { return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist} } return m.fs.Open(name) } func (m maskingFS) ReadDir(name string) ([]fs.DirEntry, error) { if m.masked(name) { return nil, &fs.PathError{Op: "readdir", Path: name, Err: fs.ErrNotExist} } des, err := m.fs.ReadDir(name) if err != nil { return nil, err } result := make([]fs.DirEntry, 0, len(des)) for _, de := range des { if !m.masked(path.Join(name, de.Name())) { result = append(result, de) } } return result, nil } func (m maskingFS) Stat(name string) (fs.FileInfo, error) { if m.masked(name) { return nil, &fs.PathError{Op: "stat", Path: name, Err: fs.ErrNotExist} } return m.fs.Stat(name) } func (m maskingFS) Readlink(link string) (string, error) { if m.masked(link) { return "", &fs.PathError{Op: "readlink", Path: link, Err: fs.ErrNotExist} } return m.fs.Readlink(link) } // CopyFromFS copies files from the `src` dir on the accessor Filesystem to local filesystem into `dest` dir. // The src path uses slashes as their separator. // The dest path uses OS specific separator. func CopyFromFS(root, dest string, fsys Filesystem) (err error) { // Walks the filesystem rooted at root. return fs.WalkDir(fsys, root, func(path string, de fs.DirEntry, err error) error { if err != nil { return err } p, err := filepath.Rel(filepath.FromSlash(root), filepath.FromSlash(path)) if err != nil { return err } dest := filepath.Join(dest, p) switch { case de.IsDir(): // Ideally we should use the file mode of the src node // but it seems the git module is reporting directories // as 0644 instead of 0755. For now, just do it this way. // See https://github.com/go-git/go-git/issues/364 // Upon resolution, return accessor.Stat(src).Mode() return os.MkdirAll(dest, 0755) case de.Type()&fs.ModeSymlink != 0: var symlinkTarget string symlinkTarget, err = fsys.Readlink(path) if err != nil { return err } return os.Symlink(symlinkTarget, dest) case de.Type().IsRegular(): fi, err := de.Info() if err != nil { return err } destFile, err := os.OpenFile(dest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fi.Mode()) if err != nil { return err } defer destFile.Close() srcFile, err := fsys.Open(path) if err != nil { return err } defer srcFile.Close() _, err = io.Copy(destFile, srcFile) return err default: return fmt.Errorf("unsuported file type: %s", de.Type().String()) } }) }