feat: initial commit

This commit is contained in:
2023-02-09 12:16:36 +01:00
commit 81dc1adfef
126 changed files with 11551 additions and 0 deletions

11
pkg/bundle/bundle.go Normal file
View 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
View 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)
}
}

View 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
View File

@ -0,0 +1,5 @@
package bundle
import "errors"
var ErrUnknownBundleArchiveExt = errors.New("unknown bundle archive extension")

101
pkg/bundle/filesystem.go Normal file
View 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
View 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
View 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

Binary file not shown.

BIN
pkg/bundle/testdata/bundle.zip vendored Normal file

Binary file not shown.

View File

@ -0,0 +1 @@
bar

98
pkg/bundle/zip_bundle.go Normal file
View 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,
}
}