blob: c2e0c4629e61744f0070a8e51253b75276c7ea64 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package filesystem
import (
"archive/zip"
"embed"
"errors"
"fmt"
"io"
"io/fs"
"net/url"
"os"
"path"
"path/filepath"
"strings"
)
import (
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
}
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)
return os.Readlink(filepath.Join(o.root, link))
}
// 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)
}
const (
EmbedSchema = "embed"
)
// UnionFS is an os.FS and embed.FS union fs.
// Files in embed.FS has the header "embed://", and files in os.FS don't have this header.
type UnionFS struct {
embedFS embed.FS
}
func NewUnionFS(embedFS embed.FS) UnionFS {
return UnionFS{
embedFS: embedFS,
}
}
func (u UnionFS) ReadFile(name string) ([]byte, error) {
uri, _ := url.Parse(name)
if uri != nil && uri.Scheme == EmbedSchema {
name = strings.TrimLeft(uri.Opaque, "/")
return u.embedFS.ReadFile(name)
}
return os.ReadFile(name)
}
func (u UnionFS) Open(name string) (fs.File, error) {
uri, _ := url.Parse(name)
if uri != nil && uri.Scheme == EmbedSchema {
name = strings.TrimLeft(uri.Opaque, "/")
return u.embedFS.Open(name)
}
return os.Open(name)
}
func (u UnionFS) ReadDir(name string) ([]fs.DirEntry, error) {
uri, _ := url.Parse(name)
if uri != nil && uri.Scheme == EmbedSchema {
name = strings.TrimLeft(uri.Opaque, "/")
return u.embedFS.ReadDir(name)
}
return os.ReadDir(name)
}
func (u UnionFS) Stat(name string) (fs.FileInfo, error) {
f, err := u.Open(name)
if err != nil {
return nil, err
}
return f.Stat()
}
func (u UnionFS) Readlink(link string) (string, error) {
uri, _ := url.Parse(link)
if uri != nil && uri.Scheme == EmbedSchema {
return "", errors.New("embed FS not support read link")
}
return os.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, 0o755)
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())
}
})
}