Files
releases/bundle/file.go
T
Patrick East 8688bc0ff6 bundle: Make the DirectoryLoader public
Previously we had it in an internal package but used by a public API,
which basically means it can't actually be used outside of OPA.
Initial thinking was that this was an OK situation, but by popular
demand we are making it available to everyone so OPA as a lib users
can use the bundle loading API's.

Fixes: #1840
Signed-off-by: Patrick East <east.patrick@gmail.com>
2019-11-14 10:49:05 -08:00

167 lines
3.9 KiB
Go

package bundle
import (
"archive/tar"
"compress/gzip"
"io"
"os"
"path/filepath"
"strings"
"sync"
"github.com/pkg/errors"
)
// Descriptor contains information about a file and
// can be used to read the file contents.
type Descriptor struct {
path string
reader io.Reader
closer io.Closer
closeOnce *sync.Once
}
func newDescriptor(path string, reader io.Reader) *Descriptor {
return &Descriptor{
path: path,
reader: reader,
}
}
func (d *Descriptor) withCloser(closer io.Closer) *Descriptor {
d.closer = closer
d.closeOnce = new(sync.Once)
return d
}
// Path returns the path of the file.
func (d *Descriptor) Path() string {
return d.path
}
// Read will read all the contents from the file the Descriptor refers to
// into the dest writer up n bytes. Will return an io.EOF error
// if EOF is encountered before n bytes are read.
func (d *Descriptor) Read(dest io.Writer, n int64) (int64, error) {
n, err := io.CopyN(dest, d.reader, n)
return n, err
}
// Close the file, on some Loader implementations this might be a no-op.
// It should *always* be called regardless of file.
func (d *Descriptor) Close() error {
var err error
if d.closer != nil {
d.closeOnce.Do(func() {
err = d.closer.Close()
})
}
return err
}
// DirectoryLoader defines an interface which can be used to load
// files from a directory by iterating over each one in the tree.
type DirectoryLoader interface {
// NextFile must return io.EOF if there is no next value. The returned
// descriptor should *always* be closed when no longer needed.
NextFile() (*Descriptor, error)
}
type dirLoader struct {
root string
files []string
idx int
}
// NewDirectoryLoader returns a basic DirectoryLoader implementation
// that will load files from a given root directory path.
func NewDirectoryLoader(root string) DirectoryLoader {
d := dirLoader{
root: root,
}
return &d
}
// NextFile iterates to the next file in the directory tree
// and returns a file Descriptor for the file.
func (d *dirLoader) NextFile() (*Descriptor, error) {
// build a list of all files we will iterate over and read, but only one time
if d.files == nil {
d.files = []string{}
err := filepath.Walk(d.root, func(path string, info os.FileInfo, err error) error {
if info != nil && info.Mode().IsRegular() {
d.files = append(d.files, filepath.ToSlash(path))
}
return nil
})
if err != nil {
return nil, errors.Wrap(err, "failed to list files")
}
}
// If done reading files then just return io.EOF
// errors for each NextFile() call
if d.idx >= len(d.files) {
return nil, io.EOF
}
fileName := d.files[d.idx]
d.idx++
fh, err := os.Open(fileName)
if err != nil {
return nil, errors.Wrapf(err, "failed to open file %s", fileName)
}
// Trim off the root directory and return path as if chrooted
cleanedPath := strings.TrimPrefix(fileName, d.root)
if !strings.HasPrefix(cleanedPath, "/") {
cleanedPath = "/" + cleanedPath
}
f := newDescriptor(cleanedPath, fh).withCloser(fh)
return f, nil
}
type tarballLoader struct {
r io.Reader
tr *tar.Reader
}
// NewTarballLoader returns a new DirectoryLoader that reads
// files out of a gzipped tar archive.
func NewTarballLoader(r io.Reader) DirectoryLoader {
l := tarballLoader{
r: r,
}
return &l
}
// NextFile iterates to the next file in the directory tree
// and returns a file Descriptor for the file.
func (t *tarballLoader) NextFile() (*Descriptor, error) {
if t.tr == nil {
gr, err := gzip.NewReader(t.r)
if err != nil {
return nil, errors.Wrap(err, "archive read failed")
}
t.tr = tar.NewReader(gr)
}
for {
header, err := t.tr.Next()
// Eventually we will get an io.EOF error when finished
// iterating through the archive
if err != nil {
return nil, err
}
// Keep iterating on the archive until we find a normal file
if header.Typeflag == tar.TypeReg {
// no need to close this descriptor after reading
f := newDescriptor(header.Name, t.tr)
return f, nil
}
}
}