feat: initial commit
This commit is contained in:
11
pkg/bundle/bundle.go
Normal file
11
pkg/bundle/bundle.go
Normal file
@ -0,0 +1,11 @@
|
||||
package bundle
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Bundle interface {
|
||||
File(string) (io.ReadCloser, os.FileInfo, error)
|
||||
Dir(string) ([]os.FileInfo, error)
|
||||
}
|
70
pkg/bundle/bundle_test.go
Normal file
70
pkg/bundle/bundle_test.go
Normal file
@ -0,0 +1,70 @@
|
||||
package bundle
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
func TestBundle(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
logger.SetLevel(logger.LevelDebug)
|
||||
|
||||
bundles := []Bundle{
|
||||
NewDirectoryBundle("testdata/bundle"),
|
||||
NewTarBundle("testdata/bundle.tar.gz"),
|
||||
NewZipBundle("testdata/bundle.zip"),
|
||||
}
|
||||
|
||||
for _, b := range bundles {
|
||||
func(b Bundle) {
|
||||
t.Run(fmt.Sprintf("'%T'", b), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
reader, info, err := b.File("data/test/foo.txt")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if reader == nil {
|
||||
t.Fatal("File(data/test/foo.txt): reader should not be nil")
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := reader.Close(); err != nil {
|
||||
t.Error(errors.WithStack(err))
|
||||
}
|
||||
}()
|
||||
|
||||
if info == nil {
|
||||
t.Error("File(data/test/foo.txt): info should not be nil")
|
||||
}
|
||||
|
||||
files, err := b.Dir("data")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if e, g := 1, len(files); e != g {
|
||||
t.Errorf("len(files): expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
files, err = b.Dir("data/test")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if e, g := 1, len(files); e != g {
|
||||
t.Fatalf("len(files): expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
if e, g := "foo.txt", files[0].Name(); e != g {
|
||||
t.Errorf("files[0].Name(): expected '%v', got '%v'", e, g)
|
||||
}
|
||||
})
|
||||
}(b)
|
||||
}
|
||||
}
|
54
pkg/bundle/directory_bundle.go
Normal file
54
pkg/bundle/directory_bundle.go
Normal file
@ -0,0 +1,54 @@
|
||||
package bundle
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type DirectoryBundle struct {
|
||||
baseDir string
|
||||
}
|
||||
|
||||
func (b *DirectoryBundle) File(filename string) (io.ReadCloser, os.FileInfo, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
fullPath := path.Join(b.baseDir, filename)
|
||||
|
||||
logger.Debug(ctx, "accessing bundle file", logger.F("file", fullPath))
|
||||
|
||||
info, err := os.Stat(fullPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return nil, nil, errors.Wrapf(err, "stat '%s'", fullPath)
|
||||
}
|
||||
|
||||
reader, err := os.Open(fullPath)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrapf(err, "open '%s'", fullPath)
|
||||
}
|
||||
|
||||
return reader, info, nil
|
||||
}
|
||||
|
||||
func (b *DirectoryBundle) Dir(dirname string) ([]os.FileInfo, error) {
|
||||
fullPath := path.Join(b.baseDir, dirname)
|
||||
ctx := context.Background()
|
||||
logger.Debug(ctx, "accessing bundle directory", logger.F("file", fullPath))
|
||||
return ioutil.ReadDir(fullPath)
|
||||
}
|
||||
|
||||
func NewDirectoryBundle(baseDir string) *DirectoryBundle {
|
||||
return &DirectoryBundle{
|
||||
baseDir: baseDir,
|
||||
}
|
||||
}
|
5
pkg/bundle/error.go
Normal file
5
pkg/bundle/error.go
Normal file
@ -0,0 +1,5 @@
|
||||
package bundle
|
||||
|
||||
import "errors"
|
||||
|
||||
var ErrUnknownBundleArchiveExt = errors.New("unknown bundle archive extension")
|
101
pkg/bundle/filesystem.go
Normal file
101
pkg/bundle/filesystem.go
Normal file
@ -0,0 +1,101 @@
|
||||
package bundle
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
type FileSystem struct {
|
||||
prefix string
|
||||
bundle Bundle
|
||||
}
|
||||
|
||||
func (fs *FileSystem) Open(name string) (http.File, error) {
|
||||
if filepath.Separator != '/' && strings.ContainsRune(name, filepath.Separator) ||
|
||||
strings.Contains(name, "\x00") {
|
||||
return nil, errors.New("http: invalid character in file path")
|
||||
}
|
||||
|
||||
p := path.Join(fs.prefix, strings.TrimPrefix(name, "/"))
|
||||
|
||||
ctx := logger.With(
|
||||
context.Background(),
|
||||
logger.F("filename", name),
|
||||
)
|
||||
|
||||
logger.Debug(ctx, "opening file")
|
||||
|
||||
readCloser, fileInfo, err := fs.bundle.File(p)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logger.Error(ctx, "could not open bundle file", logger.E(err))
|
||||
|
||||
return nil, errors.Wrapf(err, "could not open bundle file '%s'", p)
|
||||
}
|
||||
defer readCloser.Close()
|
||||
|
||||
file := &File{
|
||||
fi: fileInfo,
|
||||
}
|
||||
|
||||
if fileInfo.IsDir() {
|
||||
files, err := fs.bundle.Dir(p)
|
||||
if err != nil {
|
||||
logger.Error(ctx, "could not read bundle directory", logger.E(err))
|
||||
|
||||
return nil, errors.Wrapf(err, "could not read bundle directory '%s'", p)
|
||||
}
|
||||
|
||||
file.files = files
|
||||
} else {
|
||||
data, err := ioutil.ReadAll(readCloser)
|
||||
if err != nil {
|
||||
logger.Error(ctx, "could not read bundle file", logger.E(err))
|
||||
|
||||
return nil, errors.Wrapf(err, "could not read bundle file '%s'", p)
|
||||
}
|
||||
|
||||
file.Reader = bytes.NewReader(data)
|
||||
}
|
||||
|
||||
return file, nil
|
||||
}
|
||||
|
||||
func NewFileSystem(prefix string, bundle Bundle) *FileSystem {
|
||||
return &FileSystem{prefix, bundle}
|
||||
}
|
||||
|
||||
type File struct {
|
||||
*bytes.Reader
|
||||
fi os.FileInfo
|
||||
files []os.FileInfo
|
||||
}
|
||||
|
||||
// A noop-closer.
|
||||
func (f *File) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *File) Readdir(count int) ([]os.FileInfo, error) {
|
||||
if f.fi.IsDir() && f.files != nil {
|
||||
return f.files, nil
|
||||
}
|
||||
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
|
||||
func (f *File) Stat() (os.FileInfo, error) {
|
||||
return f.fi, nil
|
||||
}
|
60
pkg/bundle/from_path.go
Normal file
60
pkg/bundle/from_path.go
Normal file
@ -0,0 +1,60 @@
|
||||
package bundle
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type ArchiveExt string
|
||||
|
||||
const (
|
||||
ExtZip ArchiveExt = "zip"
|
||||
ExtTarGz ArchiveExt = "tar.gz"
|
||||
)
|
||||
|
||||
func FromPath(path string) (Bundle, error) {
|
||||
stat, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not stat file '%s'", path)
|
||||
}
|
||||
|
||||
var b Bundle
|
||||
|
||||
if stat.IsDir() {
|
||||
b = NewDirectoryBundle(path)
|
||||
} else {
|
||||
b, err = matchArchivePattern(path)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func matchArchivePattern(archivePath string) (Bundle, error) {
|
||||
base := filepath.Base(archivePath)
|
||||
|
||||
matches, err := filepath.Match(fmt.Sprintf("*.%s", ExtTarGz), base)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not match file archive '%s'", archivePath)
|
||||
}
|
||||
|
||||
if matches {
|
||||
return NewTarBundle(archivePath), nil
|
||||
}
|
||||
|
||||
matches, err = filepath.Match(fmt.Sprintf("*.%s", ExtZip), base)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not match file archive '%s'", archivePath)
|
||||
}
|
||||
|
||||
if matches {
|
||||
return NewZipBundle(archivePath), nil
|
||||
}
|
||||
|
||||
return nil, errors.WithStack(ErrUnknownBundleArchiveExt)
|
||||
}
|
146
pkg/bundle/tar_bundle.go
Normal file
146
pkg/bundle/tar_bundle.go
Normal file
@ -0,0 +1,146 @@
|
||||
package bundle
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
type TarBundle struct {
|
||||
archivePath string
|
||||
}
|
||||
|
||||
func (b *TarBundle) File(filename string) (io.ReadCloser, os.FileInfo, error) {
|
||||
reader, archive, err := b.openArchive()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
ctx := logger.With(
|
||||
context.Background(),
|
||||
logger.F("filename", filename),
|
||||
)
|
||||
|
||||
logger.Debug(ctx, "opening file")
|
||||
|
||||
for {
|
||||
header, err := reader.Next()
|
||||
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "could not get next tar file")
|
||||
}
|
||||
|
||||
p := strings.TrimPrefix(strings.TrimSuffix(header.Name, "/"), "./")
|
||||
|
||||
logger.Debug(ctx, "reading archive file", logger.F("path", p))
|
||||
|
||||
if filename != p {
|
||||
continue
|
||||
}
|
||||
|
||||
if header.Typeflag != tar.TypeReg && header.Typeflag != tar.TypeDir {
|
||||
continue
|
||||
}
|
||||
|
||||
rc := &archiveFile{reader, archive}
|
||||
|
||||
return rc, header.FileInfo(), nil
|
||||
}
|
||||
|
||||
return nil, nil, os.ErrNotExist
|
||||
}
|
||||
|
||||
func (b *TarBundle) Dir(dirname string) ([]os.FileInfo, error) {
|
||||
reader, archive, err := b.openArchive()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer archive.Close()
|
||||
|
||||
files := make([]os.FileInfo, 0)
|
||||
ctx := context.Background()
|
||||
|
||||
for {
|
||||
header, err := reader.Next()
|
||||
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not get next tar file")
|
||||
}
|
||||
|
||||
if header.Typeflag != tar.TypeReg && header.Typeflag != tar.TypeDir {
|
||||
continue
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(header.Name, dirname) {
|
||||
continue
|
||||
}
|
||||
|
||||
relPath, err := filepath.Rel(dirname, header.Name)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not get relative path")
|
||||
}
|
||||
|
||||
logger.Debug(
|
||||
ctx, "checking file prefix",
|
||||
logger.F("dirname", dirname),
|
||||
logger.F("filename", header.Name),
|
||||
logger.F("relpath", relPath),
|
||||
)
|
||||
|
||||
if relPath == filepath.Base(header.Name) {
|
||||
files = append(files, header.FileInfo())
|
||||
}
|
||||
}
|
||||
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (b *TarBundle) openArchive() (*tar.Reader, *os.File, error) {
|
||||
f, err := os.Open(b.archivePath)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrapf(err, "could not open '%v'", b.archivePath)
|
||||
}
|
||||
|
||||
gzf, err := gzip.NewReader(f)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrapf(err, "could not decompress '%v'", b.archivePath)
|
||||
}
|
||||
|
||||
tr := tar.NewReader(gzf)
|
||||
|
||||
return tr, f, nil
|
||||
}
|
||||
|
||||
func NewTarBundle(archivePath string) *TarBundle {
|
||||
return &TarBundle{
|
||||
archivePath: archivePath,
|
||||
}
|
||||
}
|
||||
|
||||
type archiveFile struct {
|
||||
reader io.Reader
|
||||
closer io.Closer
|
||||
}
|
||||
|
||||
func (f *archiveFile) Read(p []byte) (n int, err error) {
|
||||
return f.reader.Read(p)
|
||||
}
|
||||
|
||||
func (f *archiveFile) Close() error {
|
||||
return f.closer.Close()
|
||||
}
|
BIN
pkg/bundle/testdata/bundle.tar.gz
vendored
Normal file
BIN
pkg/bundle/testdata/bundle.tar.gz
vendored
Normal file
Binary file not shown.
BIN
pkg/bundle/testdata/bundle.zip
vendored
Normal file
BIN
pkg/bundle/testdata/bundle.zip
vendored
Normal file
Binary file not shown.
1
pkg/bundle/testdata/bundle/data/test/foo.txt
vendored
Normal file
1
pkg/bundle/testdata/bundle/data/test/foo.txt
vendored
Normal file
@ -0,0 +1 @@
|
||||
bar
|
98
pkg/bundle/zip_bundle.go
Normal file
98
pkg/bundle/zip_bundle.go
Normal file
@ -0,0 +1,98 @@
|
||||
package bundle
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
type ZipBundle struct {
|
||||
archivePath string
|
||||
}
|
||||
|
||||
func (b *ZipBundle) File(filename string) (io.ReadCloser, os.FileInfo, error) {
|
||||
reader, err := b.openArchive()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
ctx := logger.With(
|
||||
context.Background(),
|
||||
logger.F("filename", filename),
|
||||
)
|
||||
|
||||
logger.Debug(ctx, "opening file")
|
||||
|
||||
f, err := reader.Open(filename)
|
||||
if err != nil {
|
||||
return nil, nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
stat, err := f.Stat()
|
||||
if err != nil {
|
||||
return nil, nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return f, stat, nil
|
||||
}
|
||||
|
||||
func (b *ZipBundle) Dir(dirname string) ([]os.FileInfo, error) {
|
||||
reader, err := b.openArchive()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := reader.Close(); err != nil {
|
||||
panic(errors.WithStack(err))
|
||||
}
|
||||
}()
|
||||
|
||||
files := make([]os.FileInfo, 0)
|
||||
ctx := context.Background()
|
||||
|
||||
for _, f := range reader.File {
|
||||
if !strings.HasPrefix(f.Name, dirname) {
|
||||
continue
|
||||
}
|
||||
|
||||
relPath, err := filepath.Rel(dirname, f.Name)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not get relative path")
|
||||
}
|
||||
|
||||
logger.Debug(
|
||||
ctx, "checking file prefix",
|
||||
logger.F("dirname", dirname),
|
||||
logger.F("filename", f.Name),
|
||||
logger.F("relpath", relPath),
|
||||
)
|
||||
|
||||
if relPath == filepath.Base(f.Name) {
|
||||
files = append(files, f.FileInfo())
|
||||
}
|
||||
}
|
||||
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (b *ZipBundle) openArchive() (*zip.ReadCloser, error) {
|
||||
zr, err := zip.OpenReader(b.archivePath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not decompress '%v'", b.archivePath)
|
||||
}
|
||||
|
||||
return zr, nil
|
||||
}
|
||||
|
||||
func NewZipBundle(archivePath string) *ZipBundle {
|
||||
return &ZipBundle{
|
||||
archivePath: archivePath,
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user