├── fixture └── resource │ ├── reports │ └── 2018.txt │ ├── templates │ ├── yml │ │ └── schema.yml │ └── html │ │ └── index.html │ └── scripts │ └── schema.sql ├── integration ├── integration.coverprofile ├── suite_test.go └── parcel_test.go ├── example ├── public │ ├── package.go │ ├── document │ │ └── message.txt │ ├── website │ │ └── index.html │ └── resource.go ├── .envrc ├── main.go └── README.md ├── doc └── img │ └── logo.png ├── script └── cover.sh ├── suite_test.go ├── .github ├── CONTRIBUTING.md ├── PULL_REQUEST_TEMPLATE.md ├── workflows │ ├── release.yaml │ └── main.yaml └── CODE_OF_CONDUCT.md ├── .gitignore ├── go.mod ├── common.go ├── .goreleaser.yml ├── CONTRIBUTORS ├── embedder.go ├── LICENSE ├── bundler.go ├── dir.go ├── fake ├── Composer.go ├── Compressor.go ├── FileSystem.go ├── FileSystemManager.go └── File.go ├── embedder_test.go ├── generator.go ├── dir_test.go ├── bundler_test.go ├── compressor.go ├── cmd └── parcello │ └── main.go ├── model_test.go ├── generator_test.go ├── model.go ├── README.md ├── compressor_test.go ├── go.sum ├── manager.go └── manager_test.go /fixture/resource/reports/2018.txt: -------------------------------------------------------------------------------- 1 | Report 2018 2 | -------------------------------------------------------------------------------- /integration/integration.coverprofile: -------------------------------------------------------------------------------- 1 | mode: atomic 2 | -------------------------------------------------------------------------------- /example/public/package.go: -------------------------------------------------------------------------------- 1 | package public 2 | 3 | //go:generate parcello -r 4 | -------------------------------------------------------------------------------- /fixture/resource/templates/yml/schema.yml: -------------------------------------------------------------------------------- 1 | title: my-title 2 | logo: no-logo 3 | -------------------------------------------------------------------------------- /doc/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phogolabs/parcello/HEAD/doc/img/logo.png -------------------------------------------------------------------------------- /example/.envrc: -------------------------------------------------------------------------------- 1 | export PARCELLO_DEV_ENABLED=1 2 | export PARCELLO_RESOURCE_DIR="./public" 3 | -------------------------------------------------------------------------------- /fixture/resource/scripts/schema.sql: -------------------------------------------------------------------------------- 1 | -- Auto-generated at Fri Apr 6 19:11:37 CEST 2018 2 | -- name: show-users 3 | SELECT * FROM users; 4 | -------------------------------------------------------------------------------- /fixture/resource/templates/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | parcello 4 | 5 | 6 | Welcome 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/public/document/message.txt: -------------------------------------------------------------------------------- 1 | 2 | Starting Webserver on http://localhost:8080 3 | 4 | You can access the embedded resource by visiting: 5 | 6 | http://localhost:8080/ 7 | 8 | -------------------------------------------------------------------------------- /example/public/website/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

Welcome to Parcello

5 |

This webpage is served from embedded resource

6 | 7 | 8 | -------------------------------------------------------------------------------- /script/cover.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "mode: atomic" > coverage.txt 4 | 5 | for profile in $(find . -name '*.coverprofile' -maxdepth 10 -type f); do 6 | grep -v "mode: " < "$profile" >> coverage.txt 7 | rm "$profile" 8 | done 9 | -------------------------------------------------------------------------------- /suite_test.go: -------------------------------------------------------------------------------- 1 | package parcello_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestEmbedo(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Parcello Suite") 13 | } 14 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, 4 | email, or any other method with the owners of this repository before making a change. 5 | 6 | Please note we have a code of conduct, please follow it in all your interactions with the project. 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | vendor/ 16 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | Please explain the changes you made here. 3 | 4 | ### Checklist 5 | - [ ] Code compiles correctly 6 | - [ ] Created tests which fail without the change (if possible) 7 | - [ ] All tests passing 8 | - [ ] Extended the README.md / documentation, if necessary 9 | - [ ] Added myself / the copyright holder to the CONTRIBUTORS file 10 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/phogolabs/parcello 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/blang/vfs v1.0.0 7 | github.com/daaku/go.zipexe v1.0.1 8 | github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 9 | github.com/mattn/go-sqlite3 v1.14.4 10 | github.com/onsi/ginkgo v1.14.0 11 | github.com/onsi/gomega v1.10.1 12 | github.com/phogolabs/cli v0.0.0-20191212161310-ce689d871370 13 | ) 14 | -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "os" 7 | 8 | "github.com/phogolabs/parcello" 9 | _ "github.com/phogolabs/parcello/example/public" 10 | ) 11 | 12 | func main() { 13 | file, err := parcello.Open("document/message.txt") 14 | if err != nil { 15 | panic(err) 16 | } 17 | 18 | if _, err = io.Copy(os.Stdout, file); err != nil { 19 | panic(err) 20 | } 21 | 22 | http.ListenAndServe(":8080", http.FileServer(parcello.ManagerAt("/website"))) 23 | } 24 | -------------------------------------------------------------------------------- /common.go: -------------------------------------------------------------------------------- 1 | package parcello 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | ) 7 | 8 | func match(pattern, path, name string) (bool, error) { 9 | matched, err := filepath.Match(pattern, path) 10 | if err != nil { 11 | return false, err 12 | } 13 | 14 | try, _ := filepath.Match(pattern, name) 15 | return matched || try, nil 16 | } 17 | 18 | func getenv(key, fallback string) string { 19 | value := os.Getenv(key) 20 | if len(value) == 0 { 21 | return fallback 22 | } 23 | return value 24 | } 25 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | before: 2 | hooks: 3 | - go mod download 4 | builds: 5 | - main: ./cmd/parcello/main.go 6 | env: 7 | - CGO_ENABLED=0 8 | goos: 9 | - linux 10 | - darwin 11 | - windows 12 | changelog: 13 | sort: asc 14 | filters: 15 | exclude: 16 | - '^docs:' 17 | - '^test:' 18 | brews: 19 | - github: 20 | owner: phogolabs 21 | name: homebrew-tap 22 | name: parcello 23 | description: Golang Resource Bundler 24 | homepage: https://github.com/phogolabs/parcello 25 | test: | 26 | system "#{bin}/parcello -v" 27 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | # This is the list of Parcell contributors for copyright purposes. 2 | # 3 | # This does not necessarily list everyone who has contributed code, since in 4 | # some cases, their employer may be the copyright holder. To see the full list 5 | # of contributors, see the revision history in source control. 6 | 7 | Svetlin Ralchev - https://github.com/svett 8 | Steve Kemp - https://github.com/skx 9 | Ashish Acharya - https://github.com/anarchyrucks 10 | Tobi Fuhrimann - https://github.com/mastertinner 11 | Michael Christenson II - https://github.com/m3talsmith 12 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # Parcello Example 2 | 3 | You should start the application with the following command: 4 | 5 | ```console 6 | $ go run main.go 7 | ``` 8 | 9 | This example illustrates how to embed resource in Golang application. If you 10 | want to enable dev mode, which enables editing content on a fly, you should set 11 | the following environment variables before you start the application: 12 | 13 | ```console 14 | $ export PARCELLO_DEV_ENABLED=1 15 | $ export PARCELLO_RESOURCE_DIR=./public 16 | ``` 17 | 18 | or use tools like [direnv](https://direnv.net): 19 | 20 | ```console 21 | $ direnv allow 22 | ``` 23 | 24 | 25 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | branches: 6 | - "!*" 7 | tags: 8 | - "v*.*.*" 9 | 10 | jobs: 11 | pipeline: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Check out Code 15 | uses: actions/checkout@v1 16 | - name: Set up Golang 17 | uses: actions/setup-go@v1 18 | with: 19 | go-version: '1.13.x' 20 | - name: Release Application 21 | uses: goreleaser/goreleaser-action@v1 22 | with: 23 | version: latest 24 | args: release 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GORELEASE_GITHUB_TOKEN }} 27 | -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | name: main 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | ignore-tags: 8 | - 'v*' 9 | pull_request: 10 | 11 | jobs: 12 | pipeline: 13 | name: pipeline 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Check out code 17 | uses: actions/checkout@v1 18 | - name: Set up Golang 19 | uses: actions/setup-go@v1 20 | with: 21 | go-version: '1.13.x' 22 | - name: Run Tests 23 | run: go test -race -coverprofile=coverage.txt -covermode=atomic ./... 24 | - name: Upload tests coverage to codeconv.io 25 | uses: codecov/codecov-action@v1 26 | with: 27 | token: ${{ secrets.CODECOV_TOKEN }} 28 | -------------------------------------------------------------------------------- /integration/suite_test.go: -------------------------------------------------------------------------------- 1 | package integration_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | _ "github.com/mattn/go-sqlite3" 8 | . "github.com/onsi/ginkgo" 9 | . "github.com/onsi/gomega" 10 | "github.com/onsi/gomega/gexec" 11 | ) 12 | 13 | var embedoPath string 14 | 15 | func TestIntegration(t *testing.T) { 16 | RegisterFailHandler(Fail) 17 | RunSpecs(t, "Integration Suite") 18 | } 19 | 20 | var _ = SynchronizedBeforeSuite(func() []byte { 21 | binPath, err := gexec.Build("github.com/phogolabs/parcello/cmd/parcello") 22 | Expect(err).NotTo(HaveOccurred()) 23 | 24 | return []byte(binPath) 25 | }, func(data []byte) { 26 | embedoPath = string(data) 27 | SetDefaultEventuallyTimeout(10 * time.Second) 28 | }) 29 | 30 | var _ = SynchronizedAfterSuite(func() { 31 | }, func() { 32 | gexec.CleanupBuildArtifacts() 33 | }) 34 | -------------------------------------------------------------------------------- /embedder.go: -------------------------------------------------------------------------------- 1 | package parcello 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | ) 7 | 8 | // Embedder embeds the resources to the provided package 9 | type Embedder struct { 10 | // Logger prints each step of compression 11 | Logger io.Writer 12 | // Composer composes the resources 13 | Composer Composer 14 | // Compressor compresses the resources 15 | Compressor Compressor 16 | // FileSystem represents the underlying file system 17 | FileSystem FileSystem 18 | } 19 | 20 | // Embed embeds the resources to the provided package 21 | func (e *Embedder) Embed() error { 22 | ctx := &CompressorContext{ 23 | FileSystem: e.FileSystem, 24 | } 25 | 26 | bundle, err := e.Compressor.Compress(ctx) 27 | if err != nil { 28 | return err 29 | } 30 | 31 | if bundle == nil { 32 | return nil 33 | } 34 | 35 | fmt.Fprintf(e.Logger, "Embedding %d resource(s) at 'resource.go'\n", bundle.Count) 36 | err = e.Composer.Compose(bundle) 37 | return err 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Phogo Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /bundler.go: -------------------------------------------------------------------------------- 1 | package parcello 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | ) 8 | 9 | // BundlerContext the context of this bundler 10 | type BundlerContext struct { 11 | // Name of the binary 12 | Name string 13 | // FileSystem represents the underlying file system 14 | FileSystem FileSystem 15 | } 16 | 17 | // Bundler bundles the resources to the provided binary 18 | type Bundler struct { 19 | // Logger prints each step of compression 20 | Logger io.Writer 21 | // Compressor compresses the resources 22 | Compressor Compressor 23 | // FileSystem represents the underlying file system 24 | FileSystem FileSystem 25 | } 26 | 27 | // Bundle bundles the resources to the provided binary 28 | func (e *Bundler) Bundle(ctx *BundlerContext) error { 29 | file, err := ctx.FileSystem.OpenFile(ctx.Name, os.O_WRONLY|os.O_APPEND, 0600) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | defer file.Close() 35 | 36 | finfo, ferr := file.Stat() 37 | if ferr != nil { 38 | return ferr 39 | } 40 | 41 | if finfo.IsDir() { 42 | return fmt.Errorf("'%s' is not a regular file", ctx.Name) 43 | } 44 | 45 | cctx := &CompressorContext{ 46 | FileSystem: e.FileSystem, 47 | Offset: finfo.Size(), 48 | } 49 | 50 | fmt.Fprintf(e.Logger, "Bundling resource(s) at '%s'\n", ctx.Name) 51 | bundle, cerr := e.Compressor.Compress(cctx) 52 | if cerr != nil { 53 | return cerr 54 | } 55 | 56 | if _, err = file.Write(bundle.Body); err != nil { 57 | return err 58 | } 59 | 60 | fmt.Fprintf(e.Logger, "Bundled %d resource(s) at '%s'\n", bundle.Count, ctx.Name) 61 | return nil 62 | } 63 | -------------------------------------------------------------------------------- /dir.go: -------------------------------------------------------------------------------- 1 | package parcello 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | ) 7 | 8 | var _ FileSystemManager = Dir("") 9 | 10 | // Dir implements FileSystem using the native file system restricted to a 11 | // specific directory tree. 12 | type Dir string 13 | 14 | // Open opens the named file for reading. If successful, methods on 15 | // the returned file can be used for reading; the associated file 16 | // descriptor has mode O_RDONLY. 17 | // If there is an error, it will be of type *PathError. 18 | func (d Dir) Open(name string) (ReadOnlyFile, error) { 19 | return d.OpenFile(name, os.O_RDONLY, 0) 20 | } 21 | 22 | // OpenFile is the generalized open call; most users will use Open 23 | func (d Dir) OpenFile(name string, flag int, perm os.FileMode) (File, error) { 24 | dir := filepath.Join(string(d), filepath.Dir(name)) 25 | 26 | if hasFlag(os.O_CREATE, flag) { 27 | if err := os.MkdirAll(dir, 0700); err != nil { 28 | return nil, err 29 | } 30 | } 31 | 32 | name = filepath.Join(dir, filepath.Base(name)) 33 | return os.OpenFile(name, flag, perm) 34 | } 35 | 36 | // Walk walks the file tree rooted at root, calling walkFn for each file or 37 | // directory in the tree, including root. 38 | func (d Dir) Walk(dir string, fn filepath.WalkFunc) error { 39 | dir = filepath.Join(string(d), dir) 40 | 41 | return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { 42 | path, _ = filepath.Rel(string(d), path) 43 | return fn(path, info, err) 44 | }) 45 | } 46 | 47 | // Dir returns a sub-manager for given path 48 | func (d Dir) Dir(name string) (FileSystemManager, error) { 49 | return Dir(filepath.Join(string(d), name)), nil 50 | } 51 | 52 | // Add adds resource bundle to the dir. (noop) 53 | func (d Dir) Add(resource *Resource) error { 54 | return nil 55 | } 56 | -------------------------------------------------------------------------------- /fake/Composer.go: -------------------------------------------------------------------------------- 1 | // This file was generated by counterfeiter 2 | package fake 3 | 4 | import ( 5 | "sync" 6 | 7 | "github.com/phogolabs/parcello" 8 | ) 9 | 10 | type Composer struct { 11 | ComposeStub func(bundle *parcello.Bundle) error 12 | composeMutex sync.RWMutex 13 | composeArgsForCall []struct { 14 | bundle *parcello.Bundle 15 | } 16 | composeReturns struct { 17 | result1 error 18 | } 19 | invocations map[string][][]interface{} 20 | invocationsMutex sync.RWMutex 21 | } 22 | 23 | func (fake *Composer) Compose(bundle *parcello.Bundle) error { 24 | fake.composeMutex.Lock() 25 | fake.composeArgsForCall = append(fake.composeArgsForCall, struct { 26 | bundle *parcello.Bundle 27 | }{bundle}) 28 | fake.recordInvocation("Compose", []interface{}{bundle}) 29 | fake.composeMutex.Unlock() 30 | if fake.ComposeStub != nil { 31 | return fake.ComposeStub(bundle) 32 | } 33 | return fake.composeReturns.result1 34 | } 35 | 36 | func (fake *Composer) ComposeCallCount() int { 37 | fake.composeMutex.RLock() 38 | defer fake.composeMutex.RUnlock() 39 | return len(fake.composeArgsForCall) 40 | } 41 | 42 | func (fake *Composer) ComposeArgsForCall(i int) *parcello.Bundle { 43 | fake.composeMutex.RLock() 44 | defer fake.composeMutex.RUnlock() 45 | return fake.composeArgsForCall[i].bundle 46 | } 47 | 48 | func (fake *Composer) ComposeReturns(result1 error) { 49 | fake.ComposeStub = nil 50 | fake.composeReturns = struct { 51 | result1 error 52 | }{result1} 53 | } 54 | 55 | func (fake *Composer) Invocations() map[string][][]interface{} { 56 | fake.invocationsMutex.RLock() 57 | defer fake.invocationsMutex.RUnlock() 58 | fake.composeMutex.RLock() 59 | defer fake.composeMutex.RUnlock() 60 | return fake.invocations 61 | } 62 | 63 | func (fake *Composer) recordInvocation(key string, args []interface{}) { 64 | fake.invocationsMutex.Lock() 65 | defer fake.invocationsMutex.Unlock() 66 | if fake.invocations == nil { 67 | fake.invocations = map[string][][]interface{}{} 68 | } 69 | if fake.invocations[key] == nil { 70 | fake.invocations[key] = [][]interface{}{} 71 | } 72 | fake.invocations[key] = append(fake.invocations[key], args) 73 | } 74 | 75 | var _ parcello.Composer = new(Composer) 76 | -------------------------------------------------------------------------------- /fake/Compressor.go: -------------------------------------------------------------------------------- 1 | // This file was generated by counterfeiter 2 | package fake 3 | 4 | import ( 5 | "sync" 6 | 7 | "github.com/phogolabs/parcello" 8 | ) 9 | 10 | type Compressor struct { 11 | CompressStub func(ctx *parcello.CompressorContext) (*parcello.Bundle, error) 12 | compressMutex sync.RWMutex 13 | compressArgsForCall []struct { 14 | ctx *parcello.CompressorContext 15 | } 16 | compressReturns struct { 17 | result1 *parcello.Bundle 18 | result2 error 19 | } 20 | invocations map[string][][]interface{} 21 | invocationsMutex sync.RWMutex 22 | } 23 | 24 | func (fake *Compressor) Compress(ctx *parcello.CompressorContext) (*parcello.Bundle, error) { 25 | fake.compressMutex.Lock() 26 | fake.compressArgsForCall = append(fake.compressArgsForCall, struct { 27 | ctx *parcello.CompressorContext 28 | }{ctx}) 29 | fake.recordInvocation("Compress", []interface{}{ctx}) 30 | fake.compressMutex.Unlock() 31 | if fake.CompressStub != nil { 32 | return fake.CompressStub(ctx) 33 | } 34 | return fake.compressReturns.result1, fake.compressReturns.result2 35 | } 36 | 37 | func (fake *Compressor) CompressCallCount() int { 38 | fake.compressMutex.RLock() 39 | defer fake.compressMutex.RUnlock() 40 | return len(fake.compressArgsForCall) 41 | } 42 | 43 | func (fake *Compressor) CompressArgsForCall(i int) *parcello.CompressorContext { 44 | fake.compressMutex.RLock() 45 | defer fake.compressMutex.RUnlock() 46 | return fake.compressArgsForCall[i].ctx 47 | } 48 | 49 | func (fake *Compressor) CompressReturns(result1 *parcello.Bundle, result2 error) { 50 | fake.CompressStub = nil 51 | fake.compressReturns = struct { 52 | result1 *parcello.Bundle 53 | result2 error 54 | }{result1, result2} 55 | } 56 | 57 | func (fake *Compressor) Invocations() map[string][][]interface{} { 58 | fake.invocationsMutex.RLock() 59 | defer fake.invocationsMutex.RUnlock() 60 | fake.compressMutex.RLock() 61 | defer fake.compressMutex.RUnlock() 62 | return fake.invocations 63 | } 64 | 65 | func (fake *Compressor) recordInvocation(key string, args []interface{}) { 66 | fake.invocationsMutex.Lock() 67 | defer fake.invocationsMutex.Unlock() 68 | if fake.invocations == nil { 69 | fake.invocations = map[string][][]interface{}{} 70 | } 71 | if fake.invocations[key] == nil { 72 | fake.invocations[key] = [][]interface{}{} 73 | } 74 | fake.invocations[key] = append(fake.invocations[key], args) 75 | } 76 | 77 | var _ parcello.Compressor = new(Compressor) 78 | -------------------------------------------------------------------------------- /embedder_test.go: -------------------------------------------------------------------------------- 1 | package parcello_test 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | 7 | . "github.com/onsi/ginkgo" 8 | . "github.com/onsi/gomega" 9 | "github.com/phogolabs/parcello" 10 | "github.com/phogolabs/parcello/fake" 11 | ) 12 | 13 | var _ = Describe("Embedder", func() { 14 | var ( 15 | embedder *parcello.Embedder 16 | composer *fake.Composer 17 | compressor *fake.Compressor 18 | fileSystem *fake.FileSystem 19 | resource *parcello.ResourceFile 20 | bundle *parcello.Bundle 21 | ) 22 | 23 | BeforeEach(func() { 24 | data := []byte("data") 25 | node := &parcello.Node{ 26 | Name: "resource", 27 | Content: &data, 28 | Mutex: &sync.RWMutex{}, 29 | } 30 | 31 | resource = parcello.NewResourceFile(node) 32 | 33 | bundle = &parcello.Bundle{ 34 | Name: "resource", 35 | Count: 20, 36 | Body: []byte("resource"), 37 | } 38 | 39 | compressor = &fake.Compressor{} 40 | compressor.CompressStub = func(ctx *parcello.CompressorContext) (*parcello.Bundle, error) { 41 | return bundle, nil 42 | } 43 | 44 | composer = &fake.Composer{} 45 | 46 | fileSystem = &fake.FileSystem{} 47 | fileSystem.OpenFileReturns(resource, nil) 48 | 49 | embedder = &parcello.Embedder{ 50 | Logger: GinkgoWriter, 51 | Compressor: compressor, 52 | Composer: composer, 53 | FileSystem: fileSystem, 54 | } 55 | }) 56 | 57 | It("embeds the provided source successfully", func() { 58 | Expect(embedder.Embed()).To(Succeed()) 59 | Expect(compressor.CompressCallCount()).To(Equal(1)) 60 | 61 | ctx := compressor.CompressArgsForCall(0) 62 | Expect(ctx.FileSystem).To(Equal(fileSystem)) 63 | 64 | Expect(composer.ComposeCallCount()).To(Equal(1)) 65 | Expect(composer.ComposeArgsForCall(0)).To(Equal(bundle)) 66 | }) 67 | 68 | Context("when the bundle is nil", func() { 69 | It("does not compose it", func() { 70 | compressor.CompressReturns(nil, nil) 71 | 72 | Expect(embedder.Embed()).To(Succeed()) 73 | Expect(compressor.CompressCallCount()).To(Equal(1)) 74 | ctx := compressor.CompressArgsForCall(0) 75 | Expect(ctx.FileSystem).To(Equal(fileSystem)) 76 | Expect(composer.ComposeCallCount()).To(BeZero()) 77 | }) 78 | }) 79 | 80 | Context("when the compressor fails", func() { 81 | It("returns the error", func() { 82 | compressor.CompressReturns(nil, fmt.Errorf("Oh no!")) 83 | Expect(embedder.Embed()).To(MatchError("Oh no!")) 84 | }) 85 | }) 86 | 87 | Context("when the composer fails", func() { 88 | It("returns the error", func() { 89 | composer.ComposeReturns(fmt.Errorf("Oh no!")) 90 | Expect(embedder.Embed()).To(MatchError("Oh no!")) 91 | }) 92 | }) 93 | }) 94 | -------------------------------------------------------------------------------- /example/public/resource.go: -------------------------------------------------------------------------------- 1 | // Code generated by parcello; DO NOT EDIT. 2 | 3 | // Package public contains embedded resources 4 | package public 5 | 6 | import "github.com/phogolabs/parcello" 7 | 8 | func init() { 9 | parcello.AddResource([]byte{ 10 | 80, 75, 3, 4, 20, 0, 8, 0, 8, 0, 44, 111, 146, 76, 0, 0, 0, 11 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 0, 9, 0, 100, 111, 99, 117, 12 | 109, 101, 110, 116, 47, 109, 101, 115, 115, 97, 103, 101, 13 | 46, 116, 120, 116, 85, 84, 5, 0, 1, 196, 78, 215, 90, 108, 14 | 204, 189, 13, 194, 48, 16, 5, 224, 254, 166, 120, 27, 56, 15 | 101, 228, 53, 40, 16, 165, 127, 158, 176, 165, 224, 67, 119, 16 | 151, 72, 108, 143, 232, 89, 224, 147, 91, 20, 139, 185, 158, 17 | 184, 179, 58, 237, 162, 65, 23, 70, 196, 59, 167, 116, 104, 18 | 43, 199, 80, 143, 188, 111, 251, 38, 242, 208, 19, 173, 44, 19 | 148, 214, 232, 142, 24, 4, 95, 149, 189, 179, 195, 232, 122, 20 | 90, 35, 234, 7, 215, 244, 249, 67, 179, 200, 95, 41, 137, 21 | 124, 3, 0, 0, 255, 255, 80, 75, 7, 8, 255, 101, 15, 123, 98, 22 | 0, 0, 0, 121, 0, 0, 0, 80, 75, 3, 4, 20, 0, 8, 0, 8, 0, 185, 23 | 109, 146, 76, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 9, 24 | 0, 119, 101, 98, 115, 105, 116, 101, 47, 105, 110, 100, 101, 25 | 120, 46, 104, 116, 109, 108, 85, 84, 5, 0, 1, 14, 76, 215, 26 | 90, 44, 205, 177, 10, 194, 48, 20, 133, 225, 221, 167, 56, 27 | 62, 65, 232, 126, 233, 162, 206, 118, 40, 136, 99, 147, 28, 28 | 77, 33, 225, 134, 155, 170, 248, 246, 162, 113, 58, 103, 248, 29 | 225, 147, 253, 241, 124, 152, 175, 211, 9, 105, 43, 121, 220, 30 | 73, 31, 64, 188, 198, 247, 247, 0, 146, 134, 241, 194, 28, 31 | 180, 16, 155, 98, 90, 44, 48, 103, 21, 151, 134, 127, 80, 32 | 199, 57, 173, 13, 47, 250, 186, 220, 137, 181, 161, 209, 158, 33 | 140, 184, 153, 22, 176, 120, 198, 200, 8, 99, 211, 135, 5, 34 | 138, 171, 63, 194, 117, 67, 92, 71, 63, 1, 0, 0, 255, 255, 35 | 80, 75, 7, 8, 174, 55, 85, 131, 117, 0, 0, 0, 140, 0, 0, 0, 36 | 80, 75, 1, 2, 20, 3, 20, 0, 8, 0, 8, 0, 44, 111, 146, 76, 37 | 255, 101, 15, 123, 98, 0, 0, 0, 121, 0, 0, 0, 20, 0, 9, 0, 38 | 0, 0, 0, 0, 0, 0, 0, 0, 164, 129, 0, 0, 0, 0, 100, 111, 99, 39 | 117, 109, 101, 110, 116, 47, 109, 101, 115, 115, 97, 103, 40 | 101, 46, 116, 120, 116, 85, 84, 5, 0, 1, 196, 78, 215, 90, 41 | 80, 75, 1, 2, 20, 3, 20, 0, 8, 0, 8, 0, 185, 109, 146, 76, 42 | 174, 55, 85, 131, 117, 0, 0, 0, 140, 0, 0, 0, 18, 0, 9, 0, 43 | 0, 0, 0, 0, 0, 0, 0, 0, 164, 129, 173, 0, 0, 0, 119, 101, 44 | 98, 115, 105, 116, 101, 47, 105, 110, 100, 101, 120, 46, 104, 45 | 116, 109, 108, 85, 84, 5, 0, 1, 14, 76, 215, 90, 80, 75, 5, 46 | 6, 0, 0, 0, 0, 2, 0, 2, 0, 148, 0, 0, 0, 107, 1, 0, 0, 0, 47 | 0, 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /generator.go: -------------------------------------------------------------------------------- 1 | package parcello 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "go/format" 8 | "io" 9 | "os" 10 | "strings" 11 | ) 12 | 13 | var _ Composer = &Generator{} 14 | 15 | // GeneratorConfig controls how the code generation happens 16 | type GeneratorConfig struct { 17 | // Package determines the name of the package 18 | Package string 19 | // InlcudeDocs determines whether to include documentation 20 | InlcudeDocs bool 21 | } 22 | 23 | // Generator generates an embedable resource 24 | type Generator struct { 25 | // FileSystem represents the underlying file system 26 | FileSystem FileSystem 27 | // Config controls how the code generation happens 28 | Config *GeneratorConfig 29 | } 30 | 31 | // Compose generates an embedable resource for given directory 32 | func (g *Generator) Compose(bundle *Bundle) error { 33 | template := &bytes.Buffer{} 34 | 35 | if g.Config.InlcudeDocs { 36 | fmt.Fprintln(template, "// Code generated by parcello; DO NOT EDIT.") 37 | fmt.Fprintln(template, "") 38 | fmt.Fprintln(template, "// Package", g.Config.Package, "contains embedded resources") 39 | } 40 | 41 | fmt.Fprintln(template, "package", g.Config.Package) 42 | fmt.Fprintln(template) 43 | fmt.Fprintf(template, "import \"github.com/phogolabs/parcello\"") 44 | fmt.Fprintln(template) 45 | fmt.Fprintln(template) 46 | fmt.Fprintln(template, "func init() {") 47 | fmt.Fprintln(template, "\tparcello.AddResource([]byte{") 48 | 49 | template.Write(g.prepare(bundle.Body)) 50 | 51 | fmt.Fprintln(template, "\t})") 52 | fmt.Fprintln(template, "}") 53 | 54 | return g.write(bundle.Name, template.Bytes()) 55 | } 56 | 57 | func (g *Generator) prepare(data []byte) []byte { 58 | prepared := &bytes.Buffer{} 59 | body := bytes.NewBuffer(data) 60 | reader := bufio.NewReader(body) 61 | buffer := &bytes.Buffer{} 62 | 63 | for { 64 | bit, rErr := reader.ReadByte() 65 | if rErr == io.EOF { 66 | line := strings.TrimSpace(buffer.String()) 67 | fmt.Fprintln(prepared, line) 68 | return prepared.Bytes() 69 | } 70 | 71 | if buffer.Len() == 0 { 72 | fmt.Fprint(buffer, "\t\t") 73 | } 74 | 75 | fmt.Fprintf(buffer, "%d, ", int(bit)) 76 | 77 | if buffer.Len() >= 60 { 78 | line := strings.TrimSpace(buffer.String()) 79 | fmt.Fprintln(prepared, line) 80 | buffer.Reset() 81 | continue 82 | } 83 | } 84 | } 85 | 86 | func (g *Generator) write(name string, data []byte) error { 87 | var err error 88 | 89 | if data, err = format.Source(data); err != nil { 90 | return err 91 | } 92 | 93 | filename := fmt.Sprintf("%s.go", name) 94 | 95 | file, err := g.FileSystem.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) 96 | if err != nil { 97 | return err 98 | } 99 | 100 | defer func() { 101 | if ioErr := file.Close(); err == nil { 102 | err = ioErr 103 | } 104 | }() 105 | 106 | _, err = file.Write(data) 107 | return err 108 | } 109 | -------------------------------------------------------------------------------- /dir_test.go: -------------------------------------------------------------------------------- 1 | package parcello_test 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | 9 | . "github.com/onsi/ginkgo" 10 | . "github.com/onsi/gomega" 11 | "github.com/phogolabs/parcello" 12 | ) 13 | 14 | var _ = Describe("Dir", func() { 15 | var dir parcello.Dir 16 | 17 | BeforeEach(func() { 18 | path, err := ioutil.TempDir("", "gom_generator") 19 | Expect(err).To(BeNil()) 20 | 21 | dir = parcello.Dir(path) 22 | Expect(ioutil.WriteFile(filepath.Join(path, "sample.txt"), []byte("test"), 0600)).To(Succeed()) 23 | }) 24 | 25 | Context("Open", func() { 26 | It("opens a file successfully", func() { 27 | file, err := dir.Open("sample.txt") 28 | Expect(err).To(BeNil()) 29 | 30 | content, err := ioutil.ReadAll(file) 31 | Expect(err).To(BeNil()) 32 | Expect(string(content)).To(Equal("test")) 33 | Expect(file.Close()).To(Succeed()) 34 | }) 35 | }) 36 | 37 | Context("Add", func() { 38 | It("adds the resource to the manager", func() { 39 | Expect(dir.Add(parcello.BinaryResource([]byte{}))).To(Succeed()) 40 | }) 41 | }) 42 | 43 | Context("Dir", func() { 44 | It("creates a sub file system", func() { 45 | d, err := dir.Dir("root") 46 | Expect(err).To(BeNil()) 47 | Expect(fmt.Sprintf("%v", d)).To(Equal(filepath.Join(string(dir), "root"))) 48 | }) 49 | }) 50 | 51 | Context("OpenFile", func() { 52 | It("opens a file successfully", func() { 53 | file, err := dir.OpenFile("sample.txt", os.O_RDONLY, 0) 54 | Expect(err).To(BeNil()) 55 | 56 | content, err := ioutil.ReadAll(file) 57 | Expect(err).To(BeNil()) 58 | Expect(string(content)).To(Equal("test")) 59 | Expect(file.Close()).To(Succeed()) 60 | }) 61 | 62 | Context("when the underlying file system fails", func() { 63 | It("returns an error", func() { 64 | dir = parcello.Dir("/hello") 65 | file, err := dir.OpenFile("report.txt", os.O_CREATE, 0) 66 | Expect(file).To(BeNil()) 67 | Expect(err).To(HaveOccurred()) 68 | Expect(err.Error()).To(ContainSubstring("mkdir /hello: permission denied")) 69 | }) 70 | }) 71 | 72 | Context("when the file does not exists", func() { 73 | It("returns an error", func() { 74 | file, err := dir.OpenFile("report.txt", os.O_RDONLY, 0) 75 | Expect(file).To(BeNil()) 76 | Expect(err).To(HaveOccurred()) 77 | Expect(err.Error()).To(ContainSubstring("no such file or directory")) 78 | }) 79 | }) 80 | }) 81 | 82 | Context("Walk", func() { 83 | It("walks through the hierarchy successfully", func() { 84 | count := 0 85 | err := dir.Walk("/", func(path string, info os.FileInfo, err error) error { 86 | count = count + 1 87 | 88 | if info.IsDir() { 89 | Expect(path).To(Equal(".")) 90 | } else { 91 | Expect(path).To(Equal("sample.txt")) 92 | } 93 | 94 | return nil 95 | }) 96 | 97 | Expect(count).To(Equal(2)) 98 | Expect(err).NotTo(HaveOccurred()) 99 | }) 100 | 101 | Context("when the walking fails", func() { 102 | It("returns an error", func() { 103 | err := dir.Walk("/wrong", func(path string, info os.FileInfo, err error) error { 104 | return fmt.Errorf("Oh no!") 105 | }) 106 | 107 | Expect(err).To(MatchError("Oh no!")) 108 | }) 109 | }) 110 | }) 111 | }) 112 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at contact@phogolabs.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /bundler_test.go: -------------------------------------------------------------------------------- 1 | package parcello_test 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "sync" 7 | 8 | . "github.com/onsi/ginkgo" 9 | . "github.com/onsi/gomega" 10 | "github.com/phogolabs/parcello" 11 | "github.com/phogolabs/parcello/fake" 12 | ) 13 | 14 | var _ = Describe("Bundler", func() { 15 | var ( 16 | bundler *parcello.Bundler 17 | compressor *fake.Compressor 18 | source *fake.FileSystem 19 | target *fake.FileSystem 20 | binary *fake.File 21 | binaryInfo *parcello.ResourceFileInfo 22 | ctx *parcello.BundlerContext 23 | ) 24 | 25 | BeforeEach(func() { 26 | content := []byte("file") 27 | binaryInfo = &parcello.ResourceFileInfo{ 28 | Node: &parcello.Node{ 29 | Mutex: &sync.RWMutex{}, 30 | IsDir: false, 31 | Content: &content, 32 | }, 33 | } 34 | 35 | binary = &fake.File{} 36 | binary.StatReturns(binaryInfo, nil) 37 | 38 | source = &fake.FileSystem{} 39 | target = &fake.FileSystem{} 40 | target.OpenFileReturns(binary, nil) 41 | 42 | bundle := &parcello.Bundle{ 43 | Name: "app", 44 | Count: 1, 45 | Body: []byte("content"), 46 | } 47 | 48 | compressor = &fake.Compressor{} 49 | compressor.CompressReturns(bundle, nil) 50 | 51 | bundler = &parcello.Bundler{ 52 | Logger: GinkgoWriter, 53 | Compressor: compressor, 54 | FileSystem: source, 55 | } 56 | 57 | ctx = &parcello.BundlerContext{ 58 | Name: "app", 59 | FileSystem: target, 60 | } 61 | }) 62 | 63 | It("bunles the binary successfully", func() { 64 | Expect(bundler.Bundle(ctx)).To(Succeed()) 65 | Expect(target.OpenFileCallCount()).To(Equal(1)) 66 | 67 | name, opts, perm := target.OpenFileArgsForCall(0) 68 | Expect(name).To(Equal(ctx.Name)) 69 | Expect(opts).To(Equal(os.O_WRONLY | os.O_APPEND)) 70 | Expect(perm).To(Equal(os.FileMode(0600))) 71 | 72 | Expect(compressor.CompressCallCount()).To(Equal(1)) 73 | 74 | cctx := compressor.CompressArgsForCall(0) 75 | Expect(cctx.FileSystem).To(Equal(source)) 76 | Expect(cctx.Offset).To(Equal(binaryInfo.Size())) 77 | }) 78 | 79 | Context("when writing to the fail fails", func() { 80 | BeforeEach(func() { 81 | f := &fake.File{} 82 | f.StatReturns(binaryInfo, nil) 83 | f.WriteReturns(0, fmt.Errorf("Oh no!")) 84 | target.OpenFileReturns(f, nil) 85 | }) 86 | 87 | It("returns an error", func() { 88 | Expect(bundler.Bundle(ctx)).To(MatchError("Oh no!")) 89 | }) 90 | }) 91 | 92 | Context("when opening the binary fails", func() { 93 | BeforeEach(func() { 94 | target.OpenFileReturns(nil, fmt.Errorf("Oh no!")) 95 | }) 96 | 97 | It("returns an error", func() { 98 | Expect(bundler.Bundle(ctx)).To(MatchError("Oh no!")) 99 | }) 100 | }) 101 | 102 | Context("when getting the binary information fails", func() { 103 | BeforeEach(func() { 104 | binary.StatReturns(nil, fmt.Errorf("Oh no!")) 105 | }) 106 | 107 | It("returns an error", func() { 108 | Expect(bundler.Bundle(ctx)).To(MatchError("Oh no!")) 109 | }) 110 | }) 111 | 112 | Context("when the target is directory", func() { 113 | BeforeEach(func() { 114 | binaryInfo.Node.IsDir = true 115 | }) 116 | 117 | It("returns an error", func() { 118 | Expect(bundler.Bundle(ctx)).To(MatchError("'app' is not a regular file")) 119 | }) 120 | }) 121 | 122 | Context("when the compressor fails", func() { 123 | It("returns an error", func() { 124 | compressor.CompressReturns(nil, fmt.Errorf("Oh no!")) 125 | Expect(bundler.Bundle(ctx)).To(MatchError("Oh no!")) 126 | }) 127 | }) 128 | }) 129 | -------------------------------------------------------------------------------- /compressor.go: -------------------------------------------------------------------------------- 1 | package parcello 2 | 3 | import ( 4 | "archive/zip" 5 | "bytes" 6 | "fmt" 7 | "io" 8 | "os" 9 | "path/filepath" 10 | ) 11 | 12 | var _ Compressor = &ZipCompressor{} 13 | 14 | // ErrSkipResource skips a particular file from processing 15 | var ErrSkipResource = fmt.Errorf("Skip Resource Error") 16 | 17 | // CompressorConfig controls how the code generation happens 18 | type CompressorConfig struct { 19 | // Logger prints each step of compression 20 | Logger io.Writer 21 | // Filename is the name of the compressed bundle 22 | Filename string 23 | // IgnorePatterns provides a list of all files that has to be ignored 24 | IgnorePatterns []string 25 | // Recurive enables embedding the resources recursively 26 | Recurive bool 27 | } 28 | 29 | // ZipCompressor compresses content as GZip tarball 30 | type ZipCompressor struct { 31 | // Config controls how the compression is made 32 | Config *CompressorConfig 33 | } 34 | 35 | // Compress compresses given source in tar.gz 36 | func (e *ZipCompressor) Compress(ctx *CompressorContext) (*Bundle, error) { 37 | buffer := &bytes.Buffer{} 38 | count, err := e.write(buffer, ctx) 39 | 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | if count == 0 { 45 | return nil, nil 46 | } 47 | 48 | return &Bundle{ 49 | Name: e.Config.Filename, 50 | Body: buffer.Bytes(), 51 | Count: count, 52 | }, nil 53 | } 54 | 55 | func (e *ZipCompressor) write(w io.Writer, ctx *CompressorContext) (int, error) { 56 | compressor := zip.NewWriter(w) 57 | if ctx.Offset > 0 { 58 | compressor.SetOffset(ctx.Offset) 59 | } 60 | 61 | count := 0 62 | 63 | err := ctx.FileSystem.Walk("/", func(path string, info os.FileInfo, err error) error { 64 | if err != nil { 65 | return err 66 | } 67 | 68 | err = e.filter(path, info) 69 | 70 | switch err { 71 | case ErrSkipResource: 72 | return nil 73 | default: 74 | if err != nil { 75 | return err 76 | } 77 | } 78 | 79 | if err = e.walk(compressor, ctx.FileSystem, path, info); err != nil { 80 | return err 81 | } 82 | 83 | count = count + 1 84 | return nil 85 | }) 86 | 87 | if err != nil { 88 | return count, err 89 | } 90 | 91 | _ = compressor.Flush() 92 | 93 | if ioErr := compressor.Close(); err == nil { 94 | err = ioErr 95 | } 96 | 97 | return count, err 98 | } 99 | 100 | func (e *ZipCompressor) walk(compressor *zip.Writer, fileSystem FileSystem, path string, info os.FileInfo) error { 101 | fmt.Fprintln(e.Config.Logger, fmt.Sprintf("Compressing '%s'", path)) 102 | 103 | header, _ := zip.FileInfoHeader(info) 104 | header.Method = zip.Deflate 105 | header.Name = path 106 | 107 | writer, _ := compressor.CreateHeader(header) 108 | resource, err := fileSystem.OpenFile(path, os.O_RDONLY, 0) 109 | if err != nil { 110 | return err 111 | } 112 | 113 | defer func() { 114 | if ioErr := resource.Close(); err == nil { 115 | err = ioErr 116 | } 117 | }() 118 | 119 | _, err = io.Copy(writer, resource) 120 | return err 121 | } 122 | 123 | func (e *ZipCompressor) filter(path string, info os.FileInfo) error { 124 | if info == nil { 125 | return ErrSkipResource 126 | } 127 | 128 | if err := e.ignore(path, info); err != nil { 129 | return err 130 | } 131 | 132 | if !info.IsDir() { 133 | return nil 134 | } 135 | 136 | if !e.Config.Recurive && path != "." { 137 | return filepath.SkipDir 138 | } 139 | 140 | return ErrSkipResource 141 | } 142 | 143 | func (e *ZipCompressor) ignore(path string, info os.FileInfo) error { 144 | ignore := append(e.Config.IgnorePatterns, "*.go") 145 | 146 | for _, pattern := range ignore { 147 | matched, err := match(pattern, path, info.Name()) 148 | 149 | if err != nil { 150 | return err 151 | } 152 | 153 | if !matched { 154 | continue 155 | } 156 | 157 | if info.IsDir() { 158 | return filepath.SkipDir 159 | } 160 | 161 | return ErrSkipResource 162 | } 163 | 164 | return nil 165 | } 166 | -------------------------------------------------------------------------------- /cmd/parcello/main.go: -------------------------------------------------------------------------------- 1 | // Command Line Interface of Embedo. 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "os" 9 | "path/filepath" 10 | "sort" 11 | "strings" 12 | 13 | "github.com/phogolabs/cli" 14 | "github.com/phogolabs/parcello" 15 | ) 16 | 17 | // version is injected by gorelease.com 18 | var version string = "unknown" 19 | 20 | const ( 21 | // ErrCodeArg is returned when an invalid argument is passed to CLI 22 | ErrCodeArg = 101 23 | ) 24 | 25 | func main() { 26 | app := &cli.App{ 27 | Name: "parcello", 28 | HelpName: "parcello", 29 | Usage: "Golang Resource Bundler and Embedder", 30 | UsageText: "parcello [global options]", 31 | Version: version, 32 | Writer: os.Stdout, 33 | ErrWriter: os.Stderr, 34 | Action: run, 35 | Flags: []cli.Flag{ 36 | &cli.BoolFlag{ 37 | Name: "quiet, q", 38 | Usage: "disable logging", 39 | }, 40 | &cli.BoolFlag{ 41 | Name: "recursive, r", 42 | Usage: "embed or bundle the resources recursively", 43 | }, 44 | &cli.StringFlag{ 45 | Name: "resource-dir, d", 46 | Usage: "path to directory", 47 | Value: ".", 48 | }, 49 | &cli.StringFlag{ 50 | Name: "bundle-path, b", 51 | Usage: "path to the bundle directory or binary", 52 | Value: ".", 53 | }, 54 | &cli.StringFlag{ 55 | Name: "resource-type, t", 56 | Usage: "resource type. (supported: bundle, source-code)", 57 | Value: "source-code", 58 | }, 59 | &cli.StringSliceFlag{ 60 | Name: "ignore, i", 61 | Usage: "ignore file name", 62 | }, 63 | &cli.BoolFlag{ 64 | Name: "include-docs", 65 | Usage: "include API documentation in generated source code", 66 | Value: true, 67 | }, 68 | }, 69 | } 70 | 71 | sort.Sort(cli.FlagsByName(app.Flags)) 72 | sort.Sort(cli.CommandsByName(app.Commands)) 73 | 74 | app.Run(os.Args) 75 | } 76 | 77 | func run(ctx *cli.Context) error { 78 | rType := ctx.String("resource-type") 79 | 80 | switch strings.ToLower(rType) { 81 | case "source-code": 82 | return embed(ctx) 83 | case "bundle": 84 | return bundle(ctx) 85 | default: 86 | err := fmt.Errorf("Invalid resource type '%s'", rType) 87 | return cli.NewExitError(err.Error(), ErrCodeArg) 88 | } 89 | } 90 | 91 | func embed(ctx *cli.Context) error { 92 | resourceDir, err := filepath.Abs(ctx.String("resource-dir")) 93 | if err != nil { 94 | return cli.NewExitError(err.Error(), ErrCodeArg) 95 | } 96 | 97 | bundlePath, err := filepath.Abs(ctx.String("bundle-path")) 98 | if err != nil { 99 | return cli.NewExitError(err.Error(), ErrCodeArg) 100 | } 101 | 102 | _, packageName := filepath.Split(bundlePath) 103 | 104 | embedder := &parcello.Embedder{ 105 | Logger: logger(ctx), 106 | FileSystem: parcello.Dir(resourceDir), 107 | Composer: &parcello.Generator{ 108 | FileSystem: parcello.Dir(bundlePath), 109 | Config: &parcello.GeneratorConfig{ 110 | Package: packageName, 111 | InlcudeDocs: ctx.Bool("include-docs"), 112 | }, 113 | }, 114 | Compressor: &parcello.ZipCompressor{ 115 | Config: &parcello.CompressorConfig{ 116 | Logger: logger(ctx), 117 | Filename: "resource", 118 | IgnorePatterns: ctx.StringSlice("ignore"), 119 | Recurive: ctx.Bool("recursive"), 120 | }, 121 | }, 122 | } 123 | 124 | if err := embedder.Embed(); err != nil { 125 | return cli.NewExitError(err.Error(), ErrCodeArg) 126 | } 127 | 128 | return nil 129 | } 130 | 131 | func bundle(ctx *cli.Context) error { 132 | resourceDir, err := filepath.Abs(ctx.String("resource-dir")) 133 | if err != nil { 134 | return cli.NewExitError(err.Error(), ErrCodeArg) 135 | } 136 | 137 | bundlePath, err := filepath.Abs(ctx.String("bundle-path")) 138 | if err != nil { 139 | return cli.NewExitError(err.Error(), ErrCodeArg) 140 | } 141 | 142 | bundler := &parcello.Bundler{ 143 | Logger: logger(ctx), 144 | FileSystem: parcello.Dir(resourceDir), 145 | Compressor: &parcello.ZipCompressor{ 146 | Config: &parcello.CompressorConfig{ 147 | Logger: logger(ctx), 148 | Filename: "resource", 149 | IgnorePatterns: ctx.StringSlice("ignore"), 150 | Recurive: ctx.Bool("recursive"), 151 | }, 152 | }, 153 | } 154 | 155 | bundleDir, bundleName := filepath.Split(bundlePath) 156 | 157 | bctx := &parcello.BundlerContext{ 158 | Name: bundleName, 159 | FileSystem: parcello.Dir(bundleDir), 160 | } 161 | 162 | if err := bundler.Bundle(bctx); err != nil { 163 | return cli.NewExitError(err.Error(), ErrCodeArg) 164 | } 165 | 166 | return nil 167 | } 168 | 169 | func logger(ctx *cli.Context) io.Writer { 170 | if ctx.GlobalBool("quiet") { 171 | return ioutil.Discard 172 | } 173 | 174 | return os.Stdout 175 | } 176 | -------------------------------------------------------------------------------- /model_test.go: -------------------------------------------------------------------------------- 1 | package parcello_test 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/ioutil" 7 | "sync" 8 | "time" 9 | 10 | . "github.com/onsi/ginkgo" 11 | . "github.com/onsi/gomega" 12 | "github.com/phogolabs/parcello" 13 | ) 14 | 15 | var _ = Describe("Model", func() { 16 | Describe("ResourceFileInfo", func() { 17 | var ( 18 | info *parcello.ResourceFileInfo 19 | node *parcello.Node 20 | ) 21 | 22 | BeforeEach(func() { 23 | data := []byte("hello") 24 | 25 | node = &parcello.Node{ 26 | Name: "node", 27 | ModTime: time.Now(), 28 | Mutex: &sync.RWMutex{}, 29 | IsDir: false, 30 | Content: &data, 31 | } 32 | 33 | info = &parcello.ResourceFileInfo{Node: node} 34 | }) 35 | 36 | It("returns the Name successfully", func() { 37 | Expect(info.Name()).To(Equal("node")) 38 | }) 39 | 40 | It("returns the Size successfully", func() { 41 | Expect(info.Size()).To(Equal(int64(len(*node.Content)))) 42 | }) 43 | 44 | It("returns the Mode successfully", func() { 45 | Expect(info.Mode()).To(BeZero()) 46 | }) 47 | 48 | It("returns the ModTime successfully", func() { 49 | Expect(info.ModTime()).To(Equal(node.ModTime)) 50 | }) 51 | 52 | It("returns the IsDir successfully", func() { 53 | Expect(info.IsDir()).To(BeFalse()) 54 | }) 55 | 56 | It("returns the Sys successfully", func() { 57 | Expect(info.Sys()).To(BeNil()) 58 | }) 59 | }) 60 | 61 | Describe("ResourceFile", func() { 62 | var ( 63 | file *parcello.ResourceFile 64 | node *parcello.Node 65 | ) 66 | 67 | Context("when the node is file", func() { 68 | BeforeEach(func() { 69 | data := []byte("hello") 70 | 71 | node = &parcello.Node{ 72 | Name: "sample.txt", 73 | ModTime: time.Now(), 74 | Mutex: &sync.RWMutex{}, 75 | IsDir: false, 76 | Content: &data, 77 | } 78 | 79 | file = parcello.NewResourceFile(node) 80 | 81 | _, err := file.Seek(int64(len(data)), io.SeekStart) 82 | Expect(err).NotTo(HaveOccurred()) 83 | }) 84 | 85 | It("reads successfully", func() { 86 | _, err := file.Seek(0, io.SeekStart) 87 | Expect(err).NotTo(HaveOccurred()) 88 | 89 | data, err := ioutil.ReadAll(file) 90 | Expect(err).NotTo(HaveOccurred()) 91 | Expect(string(data)).To(Equal("hello")) 92 | }) 93 | 94 | It("writes successfully", func() { 95 | fmt.Fprintf(file, ",jack") 96 | 97 | _, err := file.Seek(0, io.SeekStart) 98 | Expect(err).NotTo(HaveOccurred()) 99 | 100 | data, err := ioutil.ReadAll(file) 101 | Expect(err).NotTo(HaveOccurred()) 102 | Expect(string(data)).To(Equal("hello,jack")) 103 | }) 104 | 105 | It("closes successfully", func() { 106 | Expect(file.Close()).To(Succeed()) 107 | }) 108 | 109 | It("seeks successfully", func() { 110 | n, err := file.Seek(1, 0) 111 | Expect(err).To(BeNil()) 112 | Expect(n).To(Equal(int64(1))) 113 | }) 114 | 115 | It("reads the directory fails", func() { 116 | files, err := file.Readdir(-1) 117 | Expect(err).To(MatchError("Not supported")) 118 | Expect(files).To(HaveLen(0)) 119 | }) 120 | 121 | It("returns the information successfully", func() { 122 | info, err := file.Stat() 123 | Expect(err).To(BeNil()) 124 | Expect(info.IsDir()).To(BeFalse()) 125 | Expect(info.Name()).To(Equal("sample.txt")) 126 | }) 127 | }) 128 | 129 | Context("when the node is directory", func() { 130 | BeforeEach(func() { 131 | data1 := []byte("hello") 132 | data2 := []byte("world") 133 | node = &parcello.Node{ 134 | Name: "documents", 135 | IsDir: true, 136 | Children: []*parcello.Node{ 137 | { 138 | Name: "sample.txt", 139 | Content: &data1, 140 | }, 141 | { 142 | Name: "report.txt", 143 | Content: &data2, 144 | }, 145 | }, 146 | } 147 | 148 | file = parcello.NewResourceFile(node) 149 | }) 150 | 151 | It("reads the directory successfully", func() { 152 | files, err := file.Readdir(-1) 153 | Expect(err).To(BeNil()) 154 | Expect(files).To(HaveLen(2)) 155 | 156 | info := files[0] 157 | Expect(info.Name()).To(Equal("sample.txt")) 158 | 159 | info = files[1] 160 | Expect(info.Name()).To(Equal("report.txt")) 161 | }) 162 | 163 | Context("when the n is 1", func() { 164 | It("reads the directory successfully", func() { 165 | files, err := file.Readdir(1) 166 | Expect(err).To(BeNil()) 167 | Expect(files).To(HaveLen(1)) 168 | 169 | info := files[0] 170 | Expect(info.Name()).To(Equal("sample.txt")) 171 | }) 172 | }) 173 | 174 | It("returns the information successfully", func() { 175 | info, err := file.Stat() 176 | Expect(err).To(BeNil()) 177 | Expect(info.IsDir()).To(BeTrue()) 178 | Expect(info.Name()).To(Equal("documents")) 179 | Expect(info.Size()).To(BeZero()) 180 | }) 181 | }) 182 | }) 183 | }) 184 | -------------------------------------------------------------------------------- /fake/FileSystem.go: -------------------------------------------------------------------------------- 1 | // This file was generated by counterfeiter 2 | package fake 3 | 4 | import ( 5 | "net/http" 6 | "os" 7 | "path/filepath" 8 | "sync" 9 | 10 | "github.com/phogolabs/parcello" 11 | ) 12 | 13 | type FileSystem struct { 14 | OpenStub func(name string) (http.File, error) 15 | openMutex sync.RWMutex 16 | openArgsForCall []struct { 17 | name string 18 | } 19 | openReturns struct { 20 | result1 http.File 21 | result2 error 22 | } 23 | WalkStub func(dir string, fn filepath.WalkFunc) error 24 | walkMutex sync.RWMutex 25 | walkArgsForCall []struct { 26 | dir string 27 | fn filepath.WalkFunc 28 | } 29 | walkReturns struct { 30 | result1 error 31 | } 32 | OpenFileStub func(name string, flag int, perm os.FileMode) (parcello.File, error) 33 | openFileMutex sync.RWMutex 34 | openFileArgsForCall []struct { 35 | name string 36 | flag int 37 | perm os.FileMode 38 | } 39 | openFileReturns struct { 40 | result1 parcello.File 41 | result2 error 42 | } 43 | invocations map[string][][]interface{} 44 | invocationsMutex sync.RWMutex 45 | } 46 | 47 | func (fake *FileSystem) Open(name string) (http.File, error) { 48 | fake.openMutex.Lock() 49 | fake.openArgsForCall = append(fake.openArgsForCall, struct { 50 | name string 51 | }{name}) 52 | fake.recordInvocation("Open", []interface{}{name}) 53 | fake.openMutex.Unlock() 54 | if fake.OpenStub != nil { 55 | return fake.OpenStub(name) 56 | } 57 | return fake.openReturns.result1, fake.openReturns.result2 58 | } 59 | 60 | func (fake *FileSystem) OpenCallCount() int { 61 | fake.openMutex.RLock() 62 | defer fake.openMutex.RUnlock() 63 | return len(fake.openArgsForCall) 64 | } 65 | 66 | func (fake *FileSystem) OpenArgsForCall(i int) string { 67 | fake.openMutex.RLock() 68 | defer fake.openMutex.RUnlock() 69 | return fake.openArgsForCall[i].name 70 | } 71 | 72 | func (fake *FileSystem) OpenReturns(result1 http.File, result2 error) { 73 | fake.OpenStub = nil 74 | fake.openReturns = struct { 75 | result1 http.File 76 | result2 error 77 | }{result1, result2} 78 | } 79 | 80 | func (fake *FileSystem) Walk(dir string, fn filepath.WalkFunc) error { 81 | fake.walkMutex.Lock() 82 | fake.walkArgsForCall = append(fake.walkArgsForCall, struct { 83 | dir string 84 | fn filepath.WalkFunc 85 | }{dir, fn}) 86 | fake.recordInvocation("Walk", []interface{}{dir, fn}) 87 | fake.walkMutex.Unlock() 88 | if fake.WalkStub != nil { 89 | return fake.WalkStub(dir, fn) 90 | } 91 | return fake.walkReturns.result1 92 | } 93 | 94 | func (fake *FileSystem) WalkCallCount() int { 95 | fake.walkMutex.RLock() 96 | defer fake.walkMutex.RUnlock() 97 | return len(fake.walkArgsForCall) 98 | } 99 | 100 | func (fake *FileSystem) WalkArgsForCall(i int) (string, filepath.WalkFunc) { 101 | fake.walkMutex.RLock() 102 | defer fake.walkMutex.RUnlock() 103 | return fake.walkArgsForCall[i].dir, fake.walkArgsForCall[i].fn 104 | } 105 | 106 | func (fake *FileSystem) WalkReturns(result1 error) { 107 | fake.WalkStub = nil 108 | fake.walkReturns = struct { 109 | result1 error 110 | }{result1} 111 | } 112 | 113 | func (fake *FileSystem) OpenFile(name string, flag int, perm os.FileMode) (parcello.File, error) { 114 | fake.openFileMutex.Lock() 115 | fake.openFileArgsForCall = append(fake.openFileArgsForCall, struct { 116 | name string 117 | flag int 118 | perm os.FileMode 119 | }{name, flag, perm}) 120 | fake.recordInvocation("OpenFile", []interface{}{name, flag, perm}) 121 | fake.openFileMutex.Unlock() 122 | if fake.OpenFileStub != nil { 123 | return fake.OpenFileStub(name, flag, perm) 124 | } 125 | return fake.openFileReturns.result1, fake.openFileReturns.result2 126 | } 127 | 128 | func (fake *FileSystem) OpenFileCallCount() int { 129 | fake.openFileMutex.RLock() 130 | defer fake.openFileMutex.RUnlock() 131 | return len(fake.openFileArgsForCall) 132 | } 133 | 134 | func (fake *FileSystem) OpenFileArgsForCall(i int) (string, int, os.FileMode) { 135 | fake.openFileMutex.RLock() 136 | defer fake.openFileMutex.RUnlock() 137 | return fake.openFileArgsForCall[i].name, fake.openFileArgsForCall[i].flag, fake.openFileArgsForCall[i].perm 138 | } 139 | 140 | func (fake *FileSystem) OpenFileReturns(result1 parcello.File, result2 error) { 141 | fake.OpenFileStub = nil 142 | fake.openFileReturns = struct { 143 | result1 parcello.File 144 | result2 error 145 | }{result1, result2} 146 | } 147 | 148 | func (fake *FileSystem) Invocations() map[string][][]interface{} { 149 | fake.invocationsMutex.RLock() 150 | defer fake.invocationsMutex.RUnlock() 151 | fake.openMutex.RLock() 152 | defer fake.openMutex.RUnlock() 153 | fake.walkMutex.RLock() 154 | defer fake.walkMutex.RUnlock() 155 | fake.openFileMutex.RLock() 156 | defer fake.openFileMutex.RUnlock() 157 | return fake.invocations 158 | } 159 | 160 | func (fake *FileSystem) recordInvocation(key string, args []interface{}) { 161 | fake.invocationsMutex.Lock() 162 | defer fake.invocationsMutex.Unlock() 163 | if fake.invocations == nil { 164 | fake.invocations = map[string][][]interface{}{} 165 | } 166 | if fake.invocations[key] == nil { 167 | fake.invocations[key] = [][]interface{}{} 168 | } 169 | fake.invocations[key] = append(fake.invocations[key], args) 170 | } 171 | 172 | var _ parcello.FileSystem = new(FileSystem) 173 | -------------------------------------------------------------------------------- /generator_test.go: -------------------------------------------------------------------------------- 1 | package parcello_test 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/ioutil" 7 | "os" 8 | "sync" 9 | 10 | . "github.com/onsi/ginkgo" 11 | . "github.com/onsi/gomega" 12 | "github.com/phogolabs/parcello" 13 | "github.com/phogolabs/parcello/fake" 14 | ) 15 | 16 | var _ = Describe("Generator", func() { 17 | var ( 18 | generator *parcello.Generator 19 | bundle *parcello.Bundle 20 | node *parcello.Node 21 | buffer *parcello.ResourceFile 22 | fileSystem *fake.FileSystem 23 | ) 24 | 25 | BeforeEach(func() { 26 | bundle = &parcello.Bundle{ 27 | Name: "bundle", 28 | Body: []byte{ 29 | 31, 139, 8, 0, 0, 0, 0, 0, 0, 255, 212, 146, 223, 171, 218, 30 | 48, 28, 197, 251, 156, 191, 226, 60, 110, 96, 103, 90, 107, 31 | 156, 250, 212, 185, 12, 202, 172, 150, 54, 3, 125, 146, 216, 32 | 70, 45, 104, 219, 37, 41, 251, 247, 71, 117, 212, 135, 93, 132, 33 | 43, 23, 46, 247, 243, 114, 242, 227, 36, 57, 95, 242, 45, 164, 34 | 149, 123, 105, 212, 240, 82, 30, 181, 180, 101, 93, 13, 41, 35 | 165, 140, 142, 253, 128, 94, 217, 25, 101, 219, 230, 139, 249, 36 | 125, 118, 158, 165, 187, 134, 5, 193, 85, 39, 108, 124, 85, 37 | 234, 223, 230, 29, 1, 27, 59, 222, 200, 103, 62, 155, 140, 188, 38 | 17, 115, 168, 79, 3, 202, 28, 208, 167, 95, 124, 5, 173, 177, 39 | 82, 59, 148, 106, 121, 206, 79, 15, 124, 198, 202, 195, 225, 40 | 193, 254, 191, 90, 122, 253, 32, 184, 46, 194, 214, 214, 238, 41 | 81, 85, 74, 75, 171, 10, 72, 139, 184, 174, 16, 54, 26, 152, 42 | 194, 99, 51, 111, 58, 243, 40, 22, 60, 19, 240, 169, 247, 149, 43 | 184, 46, 146, 179, 146, 70, 161, 168, 81, 213, 22, 249, 73, 44 | 86, 71, 5, 123, 82, 168, 228, 69, 65, 90, 171, 203, 125, 107, 45 | 149, 33, 157, 185, 91, 155, 161, 109, 8, 89, 164, 60, 20, 28, 46 | 34, 252, 182, 228, 136, 126, 96, 181, 22, 224, 155, 40, 19, 47 | 25, 250, 246, 51, 248, 68, 80, 22, 232, 17, 124, 35, 110, 163, 48 | 206, 190, 250, 181, 92, 34, 73, 163, 56, 76, 183, 248, 201, 49 | 183, 3, 130, 66, 153, 92, 151, 77, 119, 248, 5, 243, 128, 32, 50 | 215, 170, 171, 108, 39, 45, 32, 162, 152, 103, 34, 140, 147, 51 | 222, 64, 62, 207, 239, 41, 139, 250, 79, 69, 200, 247, 116, 52 | 157, 220, 83, 254, 151, 112, 254, 222, 159, 246, 134, 252, 5, 53 | 0, 0, 255, 255, 194, 146, 255, 65, 145, 12, 202, 128, 134, 150, 54 | 6, 6, 134, 166, 241, 165, 197, 169, 69, 197, 52, 205, 255, 198, 55 | 230, 38, 240, 252, 111, 96, 110, 12, 206, 255, 198, 70, 163, 56 | 249, 159, 30, 0, 107, 254, 119, 43, 202, 132, 228, 127, 51, 57 | 5, 67, 75, 43, 3, 3, 43, 67, 83, 106, 228, 127, 148, 236, 15, 58 | 78, 86, 160, 188, 14, 202, 236, 158, 126, 33, 200, 153, 26, 59 | 57, 243, 42, 164, 101, 22, 21, 151, 196, 131, 13, 6, 231, 110, 60 | 100, 185, 156, 68, 100, 41, 80, 78, 70, 203, 202, 88, 115, 50, 61 | 216, 106, 107, 46, 174, 129, 14, 251, 193, 0, 0, 0, 0, 0, 255, 62 | 255, 130, 231, 127, 72, 25, 138, 148, 249, 13, 13, 141, 205, 63 | 41, 202, 246, 112, 64, 40, 255, 27, 26, 155, 162, 230, 127, 64 | 67, 51, 83, 35, 211, 209, 252, 79, 15, 64, 68, 254, 55, 52, 65 | 180, 50, 54, 71, 205, 255, 144, 28, 86, 156, 145, 95, 174, 11, 66 | 206, 76, 92, 193, 174, 62, 174, 206, 33, 10, 90, 10, 110, 65, 67 | 254, 190, 163, 25, 108, 232, 0, 0, 0, 0, 0, 255, 255, 68 | }, 69 | } 70 | 71 | node = &parcello.Node{ 72 | Name: "resource", 73 | Content: &[]byte{}, 74 | Mutex: &sync.RWMutex{}, 75 | } 76 | 77 | buffer = parcello.NewResourceFile(node) 78 | 79 | fileSystem = &fake.FileSystem{} 80 | fileSystem.OpenFileReturns(buffer, nil) 81 | 82 | generator = &parcello.Generator{ 83 | FileSystem: fileSystem, 84 | Config: &parcello.GeneratorConfig{ 85 | Package: "mypackage", 86 | }, 87 | } 88 | }) 89 | 90 | It("writes the bundle to the destination successfully", func() { 91 | Expect(generator.Compose(bundle)).To(Succeed()) 92 | Expect(fileSystem.OpenFileCallCount()).To(Equal(1)) 93 | 94 | filename, flag, mode := fileSystem.OpenFileArgsForCall(0) 95 | Expect(filename).To(Equal("bundle.go")) 96 | Expect(flag).To(Equal(os.O_WRONLY | os.O_CREATE | os.O_TRUNC)) 97 | Expect(mode).To(Equal(os.FileMode(0600))) 98 | 99 | _, err := buffer.Seek(0, io.SeekStart) 100 | Expect(err).To(BeNil()) 101 | content, err := ioutil.ReadAll(buffer) 102 | Expect(err).To(BeNil()) 103 | 104 | Expect(content).To(ContainSubstring("package mypackage")) 105 | Expect(content).To(ContainSubstring("func init()")) 106 | Expect(content).To(ContainSubstring("parcello.AddResource")) 107 | Expect(content).NotTo(ContainSubstring("// Code generated by parcello; DO NOT EDIT.")) 108 | }) 109 | 110 | Context("when include API documentation is enabled", func() { 111 | BeforeEach(func() { 112 | generator.Config.InlcudeDocs = true 113 | }) 114 | 115 | It("includes the documentation", func() { 116 | Expect(generator.Compose(bundle)).To(Succeed()) 117 | 118 | _, err := buffer.Seek(0, io.SeekStart) 119 | Expect(err).To(BeNil()) 120 | content, err := ioutil.ReadAll(buffer) 121 | Expect(err).To(BeNil()) 122 | 123 | Expect(content).To(ContainSubstring("package mypackage")) 124 | Expect(content).To(ContainSubstring("func init()")) 125 | Expect(content).To(ContainSubstring("parcello.AddResource")) 126 | Expect(content).To(ContainSubstring("// Code generated by parcello; DO NOT EDIT.")) 127 | }) 128 | }) 129 | 130 | Context("when the package name is not provided", func() { 131 | BeforeEach(func() { 132 | generator.Config.Package = "" 133 | }) 134 | 135 | It("returns the error", func() { 136 | Expect(generator.Compose(bundle)).To(MatchError("3:1: expected 'IDENT', found 'import'")) 137 | }) 138 | }) 139 | 140 | Context("when the file system fails", func() { 141 | It("returns the error", func() { 142 | fileSystem.OpenFileReturns(nil, fmt.Errorf("Oh no!")) 143 | Expect(generator.Compose(bundle)).To(MatchError("Oh no!")) 144 | }) 145 | }) 146 | 147 | Context("when writing the bundle fails", func() { 148 | It("returns the error", func() { 149 | buffer := &fake.File{} 150 | buffer.WriteReturns(0, fmt.Errorf("Oh no!")) 151 | fileSystem.OpenFileReturns(buffer, nil) 152 | 153 | Expect(generator.Compose(bundle)).To(MatchError("Oh no!")) 154 | }) 155 | }) 156 | }) 157 | -------------------------------------------------------------------------------- /model.go: -------------------------------------------------------------------------------- 1 | package parcello 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "os" 9 | "path/filepath" 10 | "sync" 11 | "time" 12 | 13 | "github.com/blang/vfs/memfs" 14 | ) 15 | 16 | //go:generate counterfeiter -fake-name FileSystem -o ./fake/FileSystem.go . FileSystem 17 | //go:generate counterfeiter -fake-name FileSystemManager -o ./fake/FileSystemManager.go . FileSystemManager 18 | //go:generate counterfeiter -fake-name File -o ./fake/File.go . File 19 | //go:generate counterfeiter -fake-name Composer -o ./fake/Composer.go . Composer 20 | //go:generate counterfeiter -fake-name Compressor -o ./fake/Compressor.go . Compressor 21 | 22 | // FileSystem provides primitives to work with the underlying file system 23 | type FileSystem interface { 24 | // A FileSystem implements access to a collection of named files. 25 | http.FileSystem 26 | // Walk walks the file tree rooted at root, calling walkFn for each file or 27 | // directory in the tree, including root. 28 | Walk(dir string, fn filepath.WalkFunc) error 29 | // OpenFile is the generalized open call; most users will use Open 30 | OpenFile(name string, flag int, perm os.FileMode) (File, error) 31 | } 32 | 33 | // FileSystemManager is a file system that can create sub-file-systems 34 | type FileSystemManager interface { 35 | // FileSystem is the underlying file system 36 | FileSystem 37 | // Dir returns a sub-file-system 38 | Dir(name string) (FileSystemManager, error) 39 | // Add resource bundle to the manager 40 | Add(resource *Resource) error 41 | } 42 | 43 | // Resource represents a resource 44 | type Resource struct { 45 | // Body of the resource 46 | Body io.ReaderAt 47 | // Size of the body 48 | Size int64 49 | } 50 | 51 | // BinaryResource creates a binary resource 52 | func BinaryResource(data []byte) *Resource { 53 | return &Resource{ 54 | Body: bytes.NewReader(data), 55 | Size: int64(len(data)), 56 | } 57 | } 58 | 59 | // ReadOnlyFile is the bundle file 60 | type ReadOnlyFile = http.File 61 | 62 | // File is the bundle file 63 | type File interface { 64 | // Close() error 65 | // Read(p []byte) (n int, err error) 66 | // Seek(offset int64, whence int) (int64, error) 67 | // Readdir(count int) ([]os.FileInfo, error) 68 | // Stat() (os.FileInfo, error) 69 | // Write(p []byte) (n int, err error) 70 | // ReadAt(p []byte, off int64) (n int, err error) 71 | 72 | // A File is returned by a FileSystem's Open method and can be 73 | ReadOnlyFile 74 | // Writer is the interface that wraps the basic Write method. 75 | io.Writer 76 | // ReaderAt reads at specific position 77 | io.ReaderAt 78 | } 79 | 80 | // Composer composes the resources 81 | type Composer interface { 82 | // Compose composes from an archive 83 | Compose(bundle *Bundle) error 84 | } 85 | 86 | // CompressorContext used for the compression 87 | type CompressorContext struct { 88 | // FileSystem file system that contain the files which will be compressed 89 | FileSystem FileSystem 90 | // Offset that should be applied 91 | Offset int64 92 | } 93 | 94 | // Compressor compresses given resource 95 | type Compressor interface { 96 | // Compress compresses given source 97 | Compress(ctx *CompressorContext) (*Bundle, error) 98 | } 99 | 100 | // Bundle represents a bundled resource 101 | type Bundle struct { 102 | // Name of the resource 103 | Name string 104 | // Count returns the count of files in the bundle 105 | Count int 106 | // Body of the resource 107 | Body []byte 108 | } 109 | 110 | // Node represents a node in resource tree 111 | type Node struct { 112 | // Name of the node 113 | Name string 114 | // IsDir returns true if the node is directory 115 | IsDir bool 116 | // Mutext keeps the node thread safe 117 | Mutex *sync.RWMutex 118 | // ModTime returns the last modified time 119 | ModTime time.Time 120 | // Content of the node 121 | Content *[]byte 122 | // Children of the node 123 | Children []*Node 124 | } 125 | 126 | var _ os.FileInfo = &ResourceFileInfo{} 127 | 128 | // ResourceFileInfo represents a hierarchy node in the resource manager 129 | type ResourceFileInfo struct { 130 | Node *Node 131 | } 132 | 133 | // Name returns the base name of the file 134 | func (n *ResourceFileInfo) Name() string { 135 | return n.Node.Name 136 | } 137 | 138 | // Size returns the length in bytes for regular files 139 | func (n *ResourceFileInfo) Size() int64 { 140 | if n.Node.IsDir { 141 | return 0 142 | } 143 | 144 | n.Node.Mutex.RLock() 145 | defer n.Node.Mutex.RUnlock() 146 | l := len(*(n.Node.Content)) 147 | return int64(l) 148 | } 149 | 150 | // Mode returns the file mode bits 151 | func (n *ResourceFileInfo) Mode() os.FileMode { 152 | return 0 153 | } 154 | 155 | // ModTime returns the modification time 156 | func (n *ResourceFileInfo) ModTime() time.Time { 157 | return n.Node.ModTime 158 | } 159 | 160 | // IsDir returns true if the node is directory 161 | func (n *ResourceFileInfo) IsDir() bool { 162 | return n.Node.IsDir 163 | } 164 | 165 | // Sys returns the underlying data source 166 | func (n *ResourceFileInfo) Sys() interface{} { 167 | return nil 168 | } 169 | 170 | var _ File = &ResourceFile{} 171 | 172 | // ResourceFile represents a *bytes.Buffer that can be closed 173 | type ResourceFile struct { 174 | *memfs.MemFile 175 | node *Node 176 | } 177 | 178 | // NewResourceFile creates a new Buffer 179 | func NewResourceFile(node *Node) *ResourceFile { 180 | return &ResourceFile{ 181 | MemFile: memfs.NewMemFile(node.Name, node.Mutex, node.Content), 182 | node: node, 183 | } 184 | } 185 | 186 | // Readdir reads the contents of the directory associated with file and 187 | // returns a slice of up to n FileInfo values, as would be returned 188 | func (b *ResourceFile) Readdir(n int) ([]os.FileInfo, error) { 189 | info := []os.FileInfo{} 190 | 191 | if !b.node.IsDir { 192 | return info, fmt.Errorf("Not supported") 193 | } 194 | 195 | for index, node := range b.node.Children { 196 | if index >= n && n > 0 { 197 | break 198 | } 199 | 200 | info = append(info, &ResourceFileInfo{Node: node}) 201 | } 202 | 203 | return info, nil 204 | } 205 | 206 | // Stat returns the FileInfo structure describing file. 207 | // If there is an error, it will be of type *PathError. 208 | func (b *ResourceFile) Stat() (os.FileInfo, error) { 209 | return &ResourceFileInfo{Node: b.node}, nil 210 | } 211 | 212 | // ExecutableFunc returns the executable path 213 | type ExecutableFunc func() (string, error) 214 | -------------------------------------------------------------------------------- /integration/parcel_test.go: -------------------------------------------------------------------------------- 1 | package integration_test 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "os/exec" 8 | "path/filepath" 9 | 10 | . "github.com/onsi/ginkgo" 11 | . "github.com/onsi/gomega" 12 | "github.com/onsi/gomega/gbytes" 13 | "github.com/onsi/gomega/gexec" 14 | ) 15 | 16 | var _ = Describe("Parcel", func() { 17 | var ( 18 | cmd *exec.Cmd 19 | dir string 20 | binaryPath string 21 | args []string 22 | resource string 23 | ) 24 | 25 | BeforeEach(func() { 26 | args = []string{} 27 | 28 | binaryDir, err := ioutil.TempDir("", "gom") 29 | Expect(err).To(BeNil()) 30 | 31 | binaryPath = filepath.Join(binaryDir, "file") 32 | Expect(ioutil.WriteFile(binaryPath, []byte("content"), 0700)).To(Succeed()) 33 | }) 34 | 35 | JustBeforeEach(func() { 36 | var err error 37 | 38 | dir, err = ioutil.TempDir("", "gom") 39 | Expect(err).To(BeNil()) 40 | 41 | cmd = exec.Command(embedoPath, args...) 42 | cmd.Dir = dir 43 | 44 | path := filepath.Join(cmd.Dir, "/database") 45 | Expect(os.MkdirAll(path, 0700)).To(Succeed()) 46 | 47 | path = filepath.Join(path, "main.sql") 48 | Expect(ioutil.WriteFile(path, []byte("main"), 0700)).To(Succeed()) 49 | 50 | path = filepath.Join(cmd.Dir, "/database/command") 51 | Expect(os.MkdirAll(path, 0700)).To(Succeed()) 52 | 53 | path = filepath.Join(path, "commands.sql") 54 | Expect(ioutil.WriteFile(path, []byte("command"), 0700)).To(Succeed()) 55 | 56 | resource = filepath.Join(cmd.Dir, "/resource.go") 57 | }) 58 | 59 | Describe("Embed", func() { 60 | It("embeds resource on root level", func() { 61 | cmd.Args = append(cmd.Args, "-r") 62 | 63 | session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter) 64 | Expect(err).NotTo(HaveOccurred()) 65 | Eventually(session).Should(gexec.Exit(0)) 66 | 67 | Expect(session.Out).To(gbytes.Say("Compressing 'database/main.sql'")) 68 | Expect(session.Out).NotTo(gbytes.Say("Compressing 'database/command/commands.sql'")) 69 | Expect(resource).To(BeARegularFile()) 70 | 71 | data, err := ioutil.ReadFile(resource) 72 | Expect(err).NotTo(HaveOccurred()) 73 | Expect(string(data)).To(ContainSubstring("// Code generated by parcello; DO NOT EDIT.")) 74 | Expect(string(data)).To(ContainSubstring(fmt.Sprintf("package %s", filepath.Base(cmd.Dir)))) 75 | Expect(string(data)).To(ContainSubstring("parcello.AddResource")) 76 | }) 77 | 78 | Context("when the commands.sql is ignored", func() { 79 | BeforeEach(func() { 80 | args = append(args, "-r", "-i", "commands.sql") 81 | }) 82 | 83 | It("does not embed embedded resource for it", func() { 84 | session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter) 85 | Expect(err).NotTo(HaveOccurred()) 86 | Eventually(session).Should(gexec.Exit(0)) 87 | 88 | Expect(session.Out).To(gbytes.Say("Compressing 'database/main.sql'")) 89 | Expect(session.Out).NotTo(gbytes.Say("Compressing 'database/command/commands.sql'")) 90 | Expect(resource).To(BeARegularFile()) 91 | }) 92 | }) 93 | 94 | Context("when the documentation is disabled", func() { 95 | BeforeEach(func() { 96 | args = append(args, "-r", "-include-docs=false") 97 | }) 98 | 99 | It("does not include API documentation", func() { 100 | session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter) 101 | Expect(err).NotTo(HaveOccurred()) 102 | Eventually(session).Should(gexec.Exit(0)) 103 | 104 | Expect(session.Out).To(gbytes.Say("Compressing 'database/main.sql'")) 105 | Expect(resource).To(BeARegularFile()) 106 | 107 | data, err := ioutil.ReadFile(resource) 108 | Expect(err).NotTo(HaveOccurred()) 109 | Expect(string(data)).To(ContainSubstring("parcello.AddResource")) 110 | Expect(string(data)).NotTo(ContainSubstring("Code embedd by parcello; DO NOT EDIT.")) 111 | }) 112 | }) 113 | 114 | Context("when quite model is enabled", func() { 115 | BeforeEach(func() { 116 | args = append(args, "-r", "-q") 117 | }) 118 | 119 | It("does not print anything on stdout", func() { 120 | session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter) 121 | Expect(err).NotTo(HaveOccurred()) 122 | Eventually(session).Should(gexec.Exit(0)) 123 | 124 | Expect(session.Out).NotTo(gbytes.Say("Compressing 'database/main.sql'")) 125 | Expect(session.Out).NotTo(gbytes.Say("Compressing 'database/command/commands.sql'")) 126 | Expect(resource).To(BeARegularFile()) 127 | }) 128 | }) 129 | 130 | Context("when the recursion is disabled", func() { 131 | It("embeds resource for all directories", func() { 132 | session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter) 133 | Expect(err).NotTo(HaveOccurred()) 134 | Eventually(session).Should(gexec.Exit(0)) 135 | 136 | Expect(session.Out).NotTo(gbytes.Say("Compressing")) 137 | Expect(resource).NotTo(BeARegularFile()) 138 | }) 139 | }) 140 | 141 | Context("when the directory is provided", func() { 142 | BeforeEach(func() { 143 | args = []string{"-r", "-d", "./database"} 144 | }) 145 | 146 | It("compresses the directory successfully", func() { 147 | session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter) 148 | Expect(err).NotTo(HaveOccurred()) 149 | Eventually(session).Should(gexec.Exit(0)) 150 | 151 | Expect(session.Out).To(gbytes.Say("Compressing 'main.sql'")) 152 | Expect(resource).To(BeARegularFile()) 153 | }) 154 | }) 155 | 156 | Context("when the bundle-dir is provided", func() { 157 | BeforeEach(func() { 158 | args = []string{"-r", "-b", "./database"} 159 | }) 160 | 161 | It("returns an error", func() { 162 | session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter) 163 | Expect(err).NotTo(HaveOccurred()) 164 | Eventually(session).Should(gexec.Exit(0)) 165 | 166 | resource = filepath.Join(cmd.Dir, "database", "resource.go") 167 | Expect(resource).To(BeARegularFile()) 168 | 169 | data, err := ioutil.ReadFile(resource) 170 | Expect(err).NotTo(HaveOccurred()) 171 | Expect(string(data)).To(ContainSubstring("package database")) 172 | }) 173 | }) 174 | 175 | Context("when the resource type is bundle", func() { 176 | BeforeEach(func() { 177 | args = []string{"-r", "-t", "bundle", "-b", binaryPath} 178 | }) 179 | 180 | It("bundles the resource into binary", func() { 181 | session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter) 182 | Expect(err).NotTo(HaveOccurred()) 183 | Eventually(session).Should(gexec.Exit(0)) 184 | }) 185 | }) 186 | }) 187 | }) 188 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Parcello 2 | 3 | [![Documentation][godoc-img]][godoc-url] 4 | ![License][license-img] 5 | [![Build Status][action-img]][action-url] 6 | [![Coverage][codecov-img]][codecov-url] 7 | [![Go Report Card][report-img]][report-url] 8 | 9 | **The tool is depracted. Golang officially supports embedded resources.** 10 | 11 | **For more information have a look at [embed](https://golang.org/pkg/embed/).** 12 | 13 | [![Parcel][parcello-img]][parcello-url] 14 | 15 | Parcello is a simple resource manager for Golang that allows embedding asset 16 | like SQL, bash scripts and images. That allows easy release management by 17 | deploying just a single binary rather than many files. 18 | 19 | ## Roadmap 20 | 21 | Note that we may introduce breaking changes until we reach v1.0. 22 | 23 | - [x] Rename the tool in order not to clash with [parcel-bundler](https://github.com/parcel-bundler/parcel) 24 | - [x] Support [http.FileSystem](https://golang.org/pkg/net/http/#FileSystem) 25 | - [x] Bundle resource as ZIP archive in the end of built Golang binary 26 | - [ ] ~~Support embedded COFF resources~~ (postponed until we accomplish a spike that works on all platforms) 27 | 28 | ## Installation 29 | 30 | #### GitHub 31 | 32 | ```console 33 | $ go get -u github.com/phogolabs/parcello 34 | $ go install github.com/phogolabs/parcello/cmd/parcello 35 | ``` 36 | #### Homebrew (for Mac OS X) 37 | 38 | ```console 39 | $ brew tap phogolabs/tap 40 | $ brew install parcello 41 | ``` 42 | 43 | ## Usage 44 | 45 | You can use the parcello command line interface to bundle the desired resources 46 | recursively: 47 | 48 | ```console 49 | $ parcello -r -d -b 50 | ``` 51 | 52 | However, the best way to use the tool is via `go generate`. In order to embed all 53 | resource in particular directory, you should make it a package that has the 54 | following comment: 55 | 56 | ```golang 57 | // Package database contains the database artefacts of GOM as embedded resource 58 | package database 59 | 60 | //go:generate parcello -r 61 | ``` 62 | 63 | Alternatively, if you don't wish to install the cli for any reason, you can use this `go generate` comment instead: 64 | 65 | ```golang 66 | // Package database contains the database artefacts of GOM as embedded resource 67 | package database 68 | 69 | //go:generate go run github.com/phogolabs/parcello/cmd/parcello -r 70 | ``` 71 | 72 | When you run: 73 | 74 | ```console 75 | $ go generate ./... 76 | ``` 77 | 78 | The tools will create a `resource.go` file that contains 79 | all embedded resource in that directory and its 80 | subdirectories as `zip` archive which is registered in 81 | [parcello.ResourceManager](https://github.com/phogolabs/parcello/blob/master/common.go#L6). 82 | 83 | You can read the content in the following way: 84 | 85 | ```golang 86 | // Import the package that includes 'resource.go' 87 | import _ "database" 88 | 89 | file, err := parcello.Open("your_sub_directory_name/your_file_name") 90 | ``` 91 | 92 | The `parcello` package provides an abstraction of 93 | [FileSystem](https://godoc.org/github.com/phogolabs/parcello#FileSystem) 94 | interface: 95 | 96 | ```golang 97 | // FileSystem provides primitives to work with the underlying file system 98 | type FileSystem interface { 99 | // A FileSystem implements access to a collection of named files. 100 | http.FileSystem 101 | // Walk walks the file tree rooted at root, calling walkFn for each file or 102 | // directory in the tree, including root. 103 | Walk(dir string, fn filepath.WalkFunc) error 104 | // OpenFile is the generalized open call; most users will use Open 105 | OpenFile(name string, flag int, perm os.FileMode) (File, error) 106 | } 107 | ``` 108 | 109 | That is implemented by the following: 110 | 111 | - [parcello.ResourceManager](https://godoc.org/github.com/phogolabs/parcello#ResourceManager) which provides an access to the bundled resources. 112 | - [parcello.Dir](https://godoc.org/github.com/phogolabs/parcello#Dir) which provides an access to the underlying file system. 113 | 114 | That allows easy replacement of the file system with the bundled resources and 115 | vice versa. 116 | 117 | If you want to work in dev mode, you should set the following environment 118 | variables before you start your application: 119 | 120 | ```console 121 | $ export PARCELLO_DEV_ENABLED=1 122 | $ # if the application resource directory is different than the current working directory 123 | $ export PARCELLO_RESOURCE_DIR=./public 124 | ``` 125 | 126 | Note that downsides of this resource embedding approach are that your compile 127 | time may increase significantly. 128 | 129 | If you have such a issue, you can bundle the resource at the end of your binary 130 | as zip archive. You can do this via `parcello` CLI: 131 | 132 | ```console 133 | $ go build your_binary 134 | $ parcello -r -d -b -t bundle 135 | ``` 136 | 137 | ## Command Line Interface 138 | 139 | ```console 140 | $ parcello -h 141 | 142 | NAME: 143 | parcello - Golang Resource Bundler and Embedder 144 | 145 | USAGE: 146 | parcello [global options] 147 | 148 | VERSION: 149 | 0.8 150 | 151 | COMMANDS: 152 | help, h Shows a list of commands or help for one command 153 | 154 | GLOBAL OPTIONS: 155 | --bundle-path value, -b value path to the bundle directory or binary (default: ".") 156 | --ignore value, -i value ignore file name 157 | --include-docs include API documentation in generated source code 158 | --quiet, -q disable logging 159 | --recursive, -r embed or bundle the resources recursively 160 | --resource-dir value, -d value path to directory (default: ".") 161 | --resource-type value, -t value resource type. (supported: bundle, source-code) (default: "source-code") 162 | --help, -h show help 163 | --version, -v print the version 164 | ``` 165 | 166 | ## Example 167 | 168 | You can check working [example](example). 169 | 170 | ## Contributing 171 | 172 | We are open for any contributions. Just fork the 173 | [project](https://github.com/phogolabs/parcello). 174 | 175 | *logo made by [Good Wave][logo-author-url] [CC 3.0][logo-license]* 176 | 177 | [report-img]: https://goreportcard.com/badge/github.com/phogolabs/parcello 178 | [report-url]: https://goreportcard.com/report/github.com/phogolabs/parcello 179 | [logo-author-url]: https://www.flaticon.com/authors/good-ware 180 | [logo-license]: http://creativecommons.org/licenses/by/3.0/ 181 | [parcello-url]: https://github.com/phogolabs/parcello 182 | [parcello-img]: doc/img/logo.png 183 | [codecov-url]: https://codecov.io/gh/phogolabs/parcello 184 | [codecov-img]: https://codecov.io/gh/phogolabs/parcello/branch/master/graph/badge.svg 185 | [action-img]: https://github.com/phogolabs/parcello/workflows/main/badge.svg 186 | [action-url]: https://github.com/phogolabs/parcello/actions 187 | [parcello-url]: https://github.com/phogolabs/parcello 188 | [godoc-url]: https://godoc.org/github.com/phogolabs/parcello 189 | [godoc-img]: https://godoc.org/github.com/phogolabs/parcello?status.svg 190 | [license-img]: https://img.shields.io/badge/license-MIT-blue.svg 191 | [software-license-url]: LICENSE 192 | -------------------------------------------------------------------------------- /compressor_test.go: -------------------------------------------------------------------------------- 1 | package parcello_test 2 | 3 | import ( 4 | "archive/zip" 5 | "bytes" 6 | "fmt" 7 | "path/filepath" 8 | 9 | . "github.com/onsi/ginkgo" 10 | . "github.com/onsi/gomega" 11 | "github.com/phogolabs/parcello" 12 | "github.com/phogolabs/parcello/fake" 13 | ) 14 | 15 | var _ = Describe("ZipCompressor", func() { 16 | var ( 17 | compressor *parcello.ZipCompressor 18 | ) 19 | 20 | BeforeEach(func() { 21 | compressor = &parcello.ZipCompressor{ 22 | Config: &parcello.CompressorConfig{ 23 | Logger: GinkgoWriter, 24 | Filename: "bundle", 25 | Recurive: true, 26 | }, 27 | } 28 | }) 29 | 30 | It("compresses a given hierarchy", func() { 31 | fileSystem := parcello.Dir("./fixture") 32 | 33 | ctx := &parcello.CompressorContext{ 34 | FileSystem: fileSystem, 35 | } 36 | 37 | bundle, err := compressor.Compress(ctx) 38 | Expect(err).To(BeNil()) 39 | Expect(bundle).NotTo(BeNil()) 40 | Expect(bundle.Name).To(Equal("bundle")) 41 | 42 | reader, err := zip.NewReader(bytes.NewReader(bundle.Body), int64(len(bundle.Body))) 43 | Expect(err).To(BeNil()) 44 | 45 | Expect(reader.File).To(HaveLen(4)) 46 | Expect(reader.File[0].Name).To(Equal("resource/reports/2018.txt")) 47 | Expect(reader.File[1].Name).To(Equal("resource/scripts/schema.sql")) 48 | Expect(reader.File[2].Name).To(Equal("resource/templates/html/index.html")) 49 | Expect(reader.File[3].Name).To(Equal("resource/templates/yml/schema.yml")) 50 | }) 51 | 52 | Context("when the offset is provided", func() { 53 | It("compresses a given hierarchy", func() { 54 | fileSystem := parcello.Dir("./fixture") 55 | 56 | ctx := &parcello.CompressorContext{ 57 | FileSystem: fileSystem, 58 | Offset: 1, 59 | } 60 | 61 | bundle, err := compressor.Compress(ctx) 62 | Expect(err).To(BeNil()) 63 | Expect(bundle).NotTo(BeNil()) 64 | }) 65 | }) 66 | 67 | Context("whene ingore pattern is provided", func() { 68 | It("ignores that files", func() { 69 | compressor.Config.IgnorePatterns = []string{"*/**/*.txt"} 70 | fileSystem := parcello.Dir("./fixture") 71 | 72 | ctx := &parcello.CompressorContext{ 73 | FileSystem: fileSystem, 74 | } 75 | 76 | bundle, err := compressor.Compress(ctx) 77 | Expect(err).To(BeNil()) 78 | Expect(bundle).NotTo(BeNil()) 79 | Expect(bundle.Name).To(Equal("bundle")) 80 | 81 | reader, err := zip.NewReader(bytes.NewReader(bundle.Body), int64(len(bundle.Body))) 82 | Expect(err).To(BeNil()) 83 | 84 | Expect(reader.File).To(HaveLen(3)) 85 | Expect(reader.File[0].Name).To(Equal("resource/scripts/schema.sql")) 86 | Expect(reader.File[1].Name).To(Equal("resource/templates/html/index.html")) 87 | Expect(reader.File[2].Name).To(Equal("resource/templates/yml/schema.yml")) 88 | }) 89 | 90 | Context("when the pattern is directory", func() { 91 | It("ignores the directory and its files", func() { 92 | compressor.Config.IgnorePatterns = []string{"resource/templates/**/*"} 93 | fileSystem := parcello.Dir("./fixture") 94 | ctx := &parcello.CompressorContext{ 95 | FileSystem: fileSystem, 96 | } 97 | 98 | bundle, err := compressor.Compress(ctx) 99 | Expect(err).To(BeNil()) 100 | Expect(bundle).NotTo(BeNil()) 101 | Expect(bundle.Name).To(Equal("bundle")) 102 | 103 | reader, err := zip.NewReader(bytes.NewReader(bundle.Body), int64(len(bundle.Body))) 104 | Expect(err).To(BeNil()) 105 | 106 | Expect(reader.File[0].Name).To(Equal("resource/reports/2018.txt")) 107 | Expect(reader.File[1].Name).To(Equal("resource/scripts/schema.sql")) 108 | }) 109 | 110 | It("ignores the whole directory", func() { 111 | compressor.Config.IgnorePatterns = []string{"resource/templates"} 112 | fileSystem := parcello.Dir("./fixture") 113 | ctx := &parcello.CompressorContext{ 114 | FileSystem: fileSystem, 115 | } 116 | 117 | bundle, err := compressor.Compress(ctx) 118 | Expect(err).To(BeNil()) 119 | Expect(bundle).NotTo(BeNil()) 120 | Expect(bundle.Name).To(Equal("bundle")) 121 | 122 | reader, err := zip.NewReader(bytes.NewReader(bundle.Body), int64(len(bundle.Body))) 123 | Expect(err).To(BeNil()) 124 | 125 | Expect(reader.File[0].Name).To(Equal("resource/reports/2018.txt")) 126 | Expect(reader.File[1].Name).To(Equal("resource/scripts/schema.sql")) 127 | }) 128 | }) 129 | }) 130 | 131 | Context("when the pattern is invalid", func() { 132 | It("returns an error", func() { 133 | compressor.Config.IgnorePatterns = []string{"[*"} 134 | fileSystem := parcello.Dir("./fixture") 135 | ctx := &parcello.CompressorContext{ 136 | FileSystem: fileSystem, 137 | } 138 | 139 | bundle, err := compressor.Compress(ctx) 140 | Expect(err).To(MatchError("syntax error in pattern")) 141 | Expect(bundle).To(BeNil()) 142 | }) 143 | }) 144 | 145 | Context("when the recursion is disabled", func() { 146 | It("does not go through the hierarchy", func() { 147 | compressor.Config.Recurive = false 148 | 149 | fileSystem := parcello.Dir("./fixture") 150 | ctx := &parcello.CompressorContext{ 151 | FileSystem: fileSystem, 152 | } 153 | 154 | bundle, err := compressor.Compress(ctx) 155 | Expect(err).To(BeNil()) 156 | Expect(bundle).To(BeNil()) 157 | }) 158 | }) 159 | 160 | Context("when opening file fails", func() { 161 | It("return the error", func() { 162 | fileSystem := &fake.FileSystem{} 163 | fileSystem.WalkStub = parcello.Dir("./fixture").Walk 164 | fileSystem.OpenFileReturns(nil, fmt.Errorf("Oh no!")) 165 | 166 | ctx := &parcello.CompressorContext{ 167 | FileSystem: fileSystem, 168 | } 169 | 170 | binary, err := compressor.Compress(ctx) 171 | Expect(err).To(MatchError("Oh no!")) 172 | Expect(binary).To(BeNil()) 173 | }) 174 | }) 175 | 176 | Context("when the walker returns an nil file info", func() { 177 | It("return the error", func() { 178 | fileSystem := &fake.FileSystem{} 179 | fileSystem.WalkStub = func(dir string, fn filepath.WalkFunc) error { 180 | return fn("/", nil, nil) 181 | } 182 | 183 | ctx := &parcello.CompressorContext{ 184 | FileSystem: fileSystem, 185 | } 186 | 187 | bundle, err := compressor.Compress(ctx) 188 | Expect(err).To(BeNil()) 189 | Expect(bundle).To(BeNil()) 190 | }) 191 | }) 192 | 193 | Context("when the walker callback has an error", func() { 194 | It("return the error", func() { 195 | fileSystem := &fake.FileSystem{} 196 | fileSystem.WalkStub = func(dir string, fn filepath.WalkFunc) error { 197 | return fn("path", nil, fmt.Errorf("Oh no!")) 198 | } 199 | 200 | ctx := &parcello.CompressorContext{ 201 | FileSystem: fileSystem, 202 | } 203 | 204 | bundle, err := compressor.Compress(ctx) 205 | Expect(err).To(MatchError("Oh no!")) 206 | Expect(bundle).To(BeNil()) 207 | }) 208 | }) 209 | 210 | Context("when the traversing fails", func() { 211 | It("return the error", func() { 212 | fileSystem := &fake.FileSystem{} 213 | fileSystem.WalkReturns(fmt.Errorf("Oh no!")) 214 | 215 | ctx := &parcello.CompressorContext{ 216 | FileSystem: fileSystem, 217 | } 218 | 219 | bundle, err := compressor.Compress(ctx) 220 | Expect(err).To(MatchError("Oh no!")) 221 | Expect(bundle).To(BeNil()) 222 | }) 223 | }) 224 | }) 225 | -------------------------------------------------------------------------------- /fake/FileSystemManager.go: -------------------------------------------------------------------------------- 1 | // This file was generated by counterfeiter 2 | package fake 3 | 4 | import ( 5 | "net/http" 6 | "os" 7 | "path/filepath" 8 | "sync" 9 | 10 | "github.com/phogolabs/parcello" 11 | ) 12 | 13 | type FileSystemManager struct { 14 | OpenStub func(name string) (http.File, error) 15 | openMutex sync.RWMutex 16 | openArgsForCall []struct { 17 | name string 18 | } 19 | openReturns struct { 20 | result1 http.File 21 | result2 error 22 | } 23 | WalkStub func(dir string, fn filepath.WalkFunc) error 24 | walkMutex sync.RWMutex 25 | walkArgsForCall []struct { 26 | dir string 27 | fn filepath.WalkFunc 28 | } 29 | walkReturns struct { 30 | result1 error 31 | } 32 | OpenFileStub func(name string, flag int, perm os.FileMode) (parcello.File, error) 33 | openFileMutex sync.RWMutex 34 | openFileArgsForCall []struct { 35 | name string 36 | flag int 37 | perm os.FileMode 38 | } 39 | openFileReturns struct { 40 | result1 parcello.File 41 | result2 error 42 | } 43 | DirStub func(name string) (parcello.FileSystemManager, error) 44 | dirMutex sync.RWMutex 45 | dirArgsForCall []struct { 46 | name string 47 | } 48 | dirReturns struct { 49 | result1 parcello.FileSystemManager 50 | result2 error 51 | } 52 | AddStub func(resource *parcello.Resource) error 53 | addMutex sync.RWMutex 54 | addArgsForCall []struct { 55 | resource *parcello.Resource 56 | } 57 | addReturns struct { 58 | result1 error 59 | } 60 | invocations map[string][][]interface{} 61 | invocationsMutex sync.RWMutex 62 | } 63 | 64 | func (fake *FileSystemManager) Open(name string) (http.File, error) { 65 | fake.openMutex.Lock() 66 | fake.openArgsForCall = append(fake.openArgsForCall, struct { 67 | name string 68 | }{name}) 69 | fake.recordInvocation("Open", []interface{}{name}) 70 | fake.openMutex.Unlock() 71 | if fake.OpenStub != nil { 72 | return fake.OpenStub(name) 73 | } 74 | return fake.openReturns.result1, fake.openReturns.result2 75 | } 76 | 77 | func (fake *FileSystemManager) OpenCallCount() int { 78 | fake.openMutex.RLock() 79 | defer fake.openMutex.RUnlock() 80 | return len(fake.openArgsForCall) 81 | } 82 | 83 | func (fake *FileSystemManager) OpenArgsForCall(i int) string { 84 | fake.openMutex.RLock() 85 | defer fake.openMutex.RUnlock() 86 | return fake.openArgsForCall[i].name 87 | } 88 | 89 | func (fake *FileSystemManager) OpenReturns(result1 http.File, result2 error) { 90 | fake.OpenStub = nil 91 | fake.openReturns = struct { 92 | result1 http.File 93 | result2 error 94 | }{result1, result2} 95 | } 96 | 97 | func (fake *FileSystemManager) Walk(dir string, fn filepath.WalkFunc) error { 98 | fake.walkMutex.Lock() 99 | fake.walkArgsForCall = append(fake.walkArgsForCall, struct { 100 | dir string 101 | fn filepath.WalkFunc 102 | }{dir, fn}) 103 | fake.recordInvocation("Walk", []interface{}{dir, fn}) 104 | fake.walkMutex.Unlock() 105 | if fake.WalkStub != nil { 106 | return fake.WalkStub(dir, fn) 107 | } 108 | return fake.walkReturns.result1 109 | } 110 | 111 | func (fake *FileSystemManager) WalkCallCount() int { 112 | fake.walkMutex.RLock() 113 | defer fake.walkMutex.RUnlock() 114 | return len(fake.walkArgsForCall) 115 | } 116 | 117 | func (fake *FileSystemManager) WalkArgsForCall(i int) (string, filepath.WalkFunc) { 118 | fake.walkMutex.RLock() 119 | defer fake.walkMutex.RUnlock() 120 | return fake.walkArgsForCall[i].dir, fake.walkArgsForCall[i].fn 121 | } 122 | 123 | func (fake *FileSystemManager) WalkReturns(result1 error) { 124 | fake.WalkStub = nil 125 | fake.walkReturns = struct { 126 | result1 error 127 | }{result1} 128 | } 129 | 130 | func (fake *FileSystemManager) OpenFile(name string, flag int, perm os.FileMode) (parcello.File, error) { 131 | fake.openFileMutex.Lock() 132 | fake.openFileArgsForCall = append(fake.openFileArgsForCall, struct { 133 | name string 134 | flag int 135 | perm os.FileMode 136 | }{name, flag, perm}) 137 | fake.recordInvocation("OpenFile", []interface{}{name, flag, perm}) 138 | fake.openFileMutex.Unlock() 139 | if fake.OpenFileStub != nil { 140 | return fake.OpenFileStub(name, flag, perm) 141 | } 142 | return fake.openFileReturns.result1, fake.openFileReturns.result2 143 | } 144 | 145 | func (fake *FileSystemManager) OpenFileCallCount() int { 146 | fake.openFileMutex.RLock() 147 | defer fake.openFileMutex.RUnlock() 148 | return len(fake.openFileArgsForCall) 149 | } 150 | 151 | func (fake *FileSystemManager) OpenFileArgsForCall(i int) (string, int, os.FileMode) { 152 | fake.openFileMutex.RLock() 153 | defer fake.openFileMutex.RUnlock() 154 | return fake.openFileArgsForCall[i].name, fake.openFileArgsForCall[i].flag, fake.openFileArgsForCall[i].perm 155 | } 156 | 157 | func (fake *FileSystemManager) OpenFileReturns(result1 parcello.File, result2 error) { 158 | fake.OpenFileStub = nil 159 | fake.openFileReturns = struct { 160 | result1 parcello.File 161 | result2 error 162 | }{result1, result2} 163 | } 164 | 165 | func (fake *FileSystemManager) Dir(name string) (parcello.FileSystemManager, error) { 166 | fake.dirMutex.Lock() 167 | fake.dirArgsForCall = append(fake.dirArgsForCall, struct { 168 | name string 169 | }{name}) 170 | fake.recordInvocation("Dir", []interface{}{name}) 171 | fake.dirMutex.Unlock() 172 | if fake.DirStub != nil { 173 | return fake.DirStub(name) 174 | } 175 | return fake.dirReturns.result1, fake.dirReturns.result2 176 | } 177 | 178 | func (fake *FileSystemManager) DirCallCount() int { 179 | fake.dirMutex.RLock() 180 | defer fake.dirMutex.RUnlock() 181 | return len(fake.dirArgsForCall) 182 | } 183 | 184 | func (fake *FileSystemManager) DirArgsForCall(i int) string { 185 | fake.dirMutex.RLock() 186 | defer fake.dirMutex.RUnlock() 187 | return fake.dirArgsForCall[i].name 188 | } 189 | 190 | func (fake *FileSystemManager) DirReturns(result1 parcello.FileSystemManager, result2 error) { 191 | fake.DirStub = nil 192 | fake.dirReturns = struct { 193 | result1 parcello.FileSystemManager 194 | result2 error 195 | }{result1, result2} 196 | } 197 | 198 | func (fake *FileSystemManager) Add(resource *parcello.Resource) error { 199 | fake.addMutex.Lock() 200 | fake.addArgsForCall = append(fake.addArgsForCall, struct { 201 | resource *parcello.Resource 202 | }{resource}) 203 | fake.recordInvocation("Add", []interface{}{resource}) 204 | fake.addMutex.Unlock() 205 | if fake.AddStub != nil { 206 | return fake.AddStub(resource) 207 | } 208 | return fake.addReturns.result1 209 | } 210 | 211 | func (fake *FileSystemManager) AddCallCount() int { 212 | fake.addMutex.RLock() 213 | defer fake.addMutex.RUnlock() 214 | return len(fake.addArgsForCall) 215 | } 216 | 217 | func (fake *FileSystemManager) AddArgsForCall(i int) *parcello.Resource { 218 | fake.addMutex.RLock() 219 | defer fake.addMutex.RUnlock() 220 | return fake.addArgsForCall[i].resource 221 | } 222 | 223 | func (fake *FileSystemManager) AddReturns(result1 error) { 224 | fake.AddStub = nil 225 | fake.addReturns = struct { 226 | result1 error 227 | }{result1} 228 | } 229 | 230 | func (fake *FileSystemManager) Invocations() map[string][][]interface{} { 231 | fake.invocationsMutex.RLock() 232 | defer fake.invocationsMutex.RUnlock() 233 | fake.openMutex.RLock() 234 | defer fake.openMutex.RUnlock() 235 | fake.walkMutex.RLock() 236 | defer fake.walkMutex.RUnlock() 237 | fake.openFileMutex.RLock() 238 | defer fake.openFileMutex.RUnlock() 239 | fake.dirMutex.RLock() 240 | defer fake.dirMutex.RUnlock() 241 | fake.addMutex.RLock() 242 | defer fake.addMutex.RUnlock() 243 | return fake.invocations 244 | } 245 | 246 | func (fake *FileSystemManager) recordInvocation(key string, args []interface{}) { 247 | fake.invocationsMutex.Lock() 248 | defer fake.invocationsMutex.Unlock() 249 | if fake.invocations == nil { 250 | fake.invocations = map[string][][]interface{}{} 251 | } 252 | if fake.invocations[key] == nil { 253 | fake.invocations[key] = [][]interface{}{} 254 | } 255 | fake.invocations[key] = append(fake.invocations[key], args) 256 | } 257 | 258 | var _ parcello.FileSystemManager = new(FileSystemManager) 259 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= 2 | github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= 3 | github.com/aws/aws-sdk-go v1.25.43/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= 4 | github.com/blang/vfs v1.0.0 h1:AUZUgulCDzbaNjTRWEP45X7m/J10brAptZpSRKRZBZc= 5 | github.com/blang/vfs v1.0.0/go.mod h1:jjuNUc/IKcRNNWC9NUCvz4fR9PZLPIKxEygtPs/4tSI= 6 | github.com/daaku/go.zipexe v1.0.1 h1:wV4zMsDOI2SZ2m7Tdz1Ps96Zrx+TzaK15VbUaGozw0M= 7 | github.com/daaku/go.zipexe v1.0.1/go.mod h1:5xWogtqlYnfBXkSB1o9xysukNP9GTvaNkqzUZbt3Bw8= 8 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 9 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 10 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 11 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 12 | github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= 13 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 14 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 15 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 16 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 17 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 18 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 19 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 20 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 21 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 22 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 23 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= 24 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 25 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= 26 | github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA= 27 | github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= 28 | github.com/mattn/go-sqlite3 v1.11.0 h1:LDdKkqtYlom37fkvqs8rMPFKAMe8+SgjbwZ6ex1/A/Q= 29 | github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= 30 | github.com/mattn/go-sqlite3 v1.14.1 h1:AHx9Ra40wIzl+GelgX2X6AWxmT5tfxhI1PL0523HcSw= 31 | github.com/mattn/go-sqlite3 v1.14.1/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= 32 | github.com/mattn/go-sqlite3 v1.14.4 h1:4rQjbDxdu9fSgI/r3KN72G3c2goxknAqHHgPWWs8UlI= 33 | github.com/mattn/go-sqlite3 v1.14.4/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI= 34 | github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= 35 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 36 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 37 | github.com/onsi/ginkgo v1.10.3 h1:OoxbjfXVZyod1fmWYhI7SEyaD8B00ynP3T+D5GiyHOY= 38 | github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 39 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 40 | github.com/onsi/ginkgo v1.12.3 h1:+RYp9QczoWz9zfUyLP/5SLXQVhfr6gZOoKGfQqHuLZQ= 41 | github.com/onsi/ginkgo v1.12.3/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= 42 | github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA= 43 | github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= 44 | github.com/onsi/gomega v1.7.1 h1:K0jcRCwNQM3vFGh1ppMtDh/+7ApJrjldlX8fA0jDTLQ= 45 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 46 | github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= 47 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 48 | github.com/phogolabs/cli v0.0.0-20191212161310-ce689d871370 h1:jGx4KpaIpen14V5GR/valO9BoaDjqiqSSlS0l4WLGJ4= 49 | github.com/phogolabs/cli v0.0.0-20191212161310-ce689d871370/go.mod h1:grzrc/EIac+v5wd6EjBB4a9obKGGIdsgWhPIsqjBGLo= 50 | github.com/phogolabs/parcello v0.8.1/go.mod h1:/HlY+yKSdyM8MUX9YvwT3+sED9SKXizc5zfuHDh6+to= 51 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 52 | golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 53 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA= 54 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 55 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 56 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 57 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7 h1:AeiKBIuRw3UomYXSbLy0Mc2dDLfdtbT/IVn4keq83P0= 58 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 59 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= 60 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 61 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs= 62 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 63 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 64 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 65 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 66 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 67 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 68 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 h1:DYfZAGf2WMFjMxbgTjaC+2HC7NkNAQs+6Q8b9WEB/F4= 69 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 70 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 71 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 72 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 73 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 74 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 75 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 76 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 77 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 78 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 79 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 80 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 81 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 82 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 83 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 84 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= 85 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 86 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 87 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 88 | gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= 89 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 90 | gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= 91 | gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 92 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 93 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 94 | -------------------------------------------------------------------------------- /manager.go: -------------------------------------------------------------------------------- 1 | package parcello 2 | 3 | import ( 4 | "archive/zip" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | "sync" 13 | "time" 14 | 15 | zipexe "github.com/daaku/go.zipexe" 16 | "github.com/kardianos/osext" 17 | ) 18 | 19 | var ( 20 | // ErrReadOnly is returned if the file is read-only and write operations are disabled. 21 | ErrReadOnly = errors.New("File is read-only") 22 | // ErrWriteOnly is returned if the file is write-only and read operations are disabled. 23 | ErrWriteOnly = errors.New("File is write-only") 24 | // ErrIsDirectory is returned if the file under operation is not a regular file but a directory. 25 | ErrIsDirectory = errors.New("Is directory") 26 | ) 27 | 28 | var ( 29 | // Manager keeps track of all resources 30 | Manager = DefaultManager(osext.Executable) 31 | // Make sure the ResourceManager implements the FileSystemManager interface 32 | _ FileSystemManager = &ResourceManager{} 33 | ) 34 | 35 | // Open opens an embedded resource for read 36 | func Open(name string) (File, error) { 37 | return Manager.OpenFile(name, os.O_RDONLY, 0) 38 | } 39 | 40 | // ManagerAt returns manager at given path 41 | func ManagerAt(path string) FileSystemManager { 42 | mngr, err := Manager.Dir(path) 43 | if err != nil { 44 | panic(err) 45 | } 46 | return mngr 47 | } 48 | 49 | // AddResource adds resource to the default resource manager 50 | // Note that the method may panic if the resource not exists 51 | func AddResource(resource []byte) { 52 | if err := Manager.Add(BinaryResource(resource)); err != nil { 53 | panic(err) 54 | } 55 | } 56 | 57 | // ResourceManagerConfig represents the configuration for Resource Manager 58 | type ResourceManagerConfig struct { 59 | // Path to the archive 60 | Path string 61 | // FileSystem that stores the archive 62 | FileSystem FileSystem 63 | } 64 | 65 | // ResourceManager represents a virtual in memory file system 66 | type ResourceManager struct { 67 | cfg *ResourceManagerConfig 68 | rw sync.RWMutex 69 | root *Node 70 | // NewReader creates a new ZIP Reader 71 | NewReader func(io.ReaderAt, int64) (*zip.Reader, error) 72 | } 73 | 74 | // DefaultManager creates a FileSystemManager based on whether dev mode is enabled 75 | func DefaultManager(executable ExecutableFunc) FileSystemManager { 76 | mode := os.Getenv("PARCELLO_DEV_ENABLED") 77 | 78 | if mode != "" { 79 | return Dir(getenv("PARCELLO_RESOURCE_DIR", ".")) 80 | } 81 | 82 | path, err := executable() 83 | if err != nil { 84 | panic(err) 85 | } 86 | 87 | dir, path := filepath.Split(path) 88 | 89 | cfg := &ResourceManagerConfig{ 90 | Path: path, 91 | FileSystem: Dir(dir), 92 | } 93 | 94 | manager, err := NewResourceManager(cfg) 95 | if err != nil { 96 | panic(err) 97 | } 98 | 99 | return manager 100 | } 101 | 102 | // NewResourceManager creates a new manager 103 | func NewResourceManager(cfg *ResourceManagerConfig) (*ResourceManager, error) { 104 | manager := &ResourceManager{cfg: cfg} 105 | 106 | file, err := cfg.FileSystem.OpenFile(cfg.Path, os.O_RDONLY, 0) 107 | if err != nil { 108 | return nil, err 109 | } 110 | 111 | defer file.Close() 112 | 113 | info, err := file.Stat() 114 | if err != nil { 115 | return nil, err 116 | } 117 | 118 | resource := &Resource{ 119 | Body: file, 120 | Size: info.Size(), 121 | } 122 | 123 | _ = manager.Add(resource) 124 | 125 | return manager, nil 126 | } 127 | 128 | // Add adds resource to the manager 129 | func (m *ResourceManager) Add(resource *Resource) error { 130 | m.rw.Lock() 131 | defer m.rw.Unlock() 132 | 133 | if m.root == nil { 134 | m.root = &Node{Name: "/", IsDir: true} 135 | } 136 | 137 | newReader := zipexe.NewReader 138 | 139 | if m.NewReader != nil { 140 | newReader = m.NewReader 141 | } 142 | 143 | reader, err := newReader(resource.Body, resource.Size) 144 | if err != nil { 145 | return err 146 | } 147 | 148 | return m.uncompress(reader) 149 | } 150 | 151 | func (m *ResourceManager) uncompress(reader *zip.Reader) error { 152 | for _, header := range reader.File { 153 | path := split(header.Name) 154 | node := add(path, m.root) 155 | 156 | if node == m.root || node == nil { 157 | return fmt.Errorf("invalid path: '%s'", header.Name) 158 | } 159 | 160 | file, err := header.Open() 161 | if err != nil { 162 | return err 163 | } 164 | defer file.Close() 165 | 166 | content, err := ioutil.ReadAll(file) 167 | if err != nil { 168 | return err 169 | } 170 | 171 | node.IsDir = false 172 | node.Content = &content 173 | } 174 | 175 | return nil 176 | } 177 | 178 | // Dir returns a sub-manager for given path 179 | func (m *ResourceManager) Dir(name string) (FileSystemManager, error) { 180 | if _, node := find(split(name), nil, m.root); node != nil { 181 | if node.IsDir { 182 | return &ResourceManager{root: node}, nil 183 | } 184 | } 185 | 186 | return nil, os.ErrNotExist 187 | } 188 | 189 | // Open opens an embedded resource for read 190 | func (m *ResourceManager) Open(name string) (ReadOnlyFile, error) { 191 | return m.OpenFile(name, os.O_RDONLY, 0) 192 | } 193 | 194 | // OpenFile is the generalized open call; most users will use Open 195 | func (m *ResourceManager) OpenFile(name string, flag int, perm os.FileMode) (File, error) { 196 | parent, node, err := m.open(name) 197 | if err != nil { 198 | return nil, err 199 | } 200 | 201 | if isWritable(flag) && node != nil && node.IsDir { 202 | return nil, &os.PathError{Op: "open", Path: name, Err: ErrIsDirectory} 203 | } 204 | 205 | if hasFlag(os.O_CREATE, flag) { 206 | if node != nil && !hasFlag(os.O_TRUNC, flag) { 207 | return nil, &os.PathError{Op: "open", Path: name, Err: os.ErrExist} 208 | } 209 | 210 | node = newNode(filepath.Base(name), parent) 211 | } 212 | 213 | if node == nil { 214 | return nil, &os.PathError{Op: "open", Path: name, Err: os.ErrNotExist} 215 | } 216 | 217 | return newFile(node, flag) 218 | } 219 | 220 | func (m *ResourceManager) open(name string) (*Node, *Node, error) { 221 | parent, node := find(split(name), nil, m.root) 222 | if node != m.root && parent == nil { 223 | return nil, nil, &os.PathError{Op: "open", Path: name, Err: os.ErrNotExist} 224 | } 225 | 226 | return parent, node, nil 227 | } 228 | 229 | // Walk walks the file tree rooted at root, calling walkFn for each file or 230 | // directory in the tree, including root. 231 | func (m *ResourceManager) Walk(dir string, fn filepath.WalkFunc) error { 232 | if _, node := find(split(dir), nil, m.root); node != nil { 233 | return walk(dir, node, fn) 234 | } 235 | 236 | return os.ErrNotExist 237 | } 238 | 239 | func add(path []string, node *Node) *Node { 240 | if !node.IsDir || node.Content != nil { 241 | return nil 242 | } 243 | 244 | if len(path) == 0 { 245 | return node 246 | } 247 | 248 | name := path[0] 249 | 250 | for _, child := range node.Children { 251 | if child.Name == name { 252 | return add(path[1:], child) 253 | } 254 | } 255 | 256 | child := &Node{ 257 | Mutex: &sync.RWMutex{}, 258 | Name: name, 259 | IsDir: true, 260 | ModTime: time.Now(), 261 | } 262 | 263 | node.Children = append(node.Children, child) 264 | return add(path[1:], child) 265 | } 266 | 267 | func split(path string) []string { 268 | parts := []string{} 269 | 270 | for _, part := range strings.Split(path, string(os.PathSeparator)) { 271 | if part != "" && part != "/" { 272 | parts = append(parts, part) 273 | } 274 | } 275 | 276 | return parts 277 | } 278 | 279 | func find(path []string, parent, node *Node) (*Node, *Node) { 280 | if len(path) == 0 || node == nil { 281 | return parent, node 282 | } 283 | 284 | for _, child := range node.Children { 285 | if path[0] == child.Name { 286 | if len(path) == 1 { 287 | return node, child 288 | } 289 | return find(path[1:], node, child) 290 | } 291 | } 292 | 293 | return parent, nil 294 | } 295 | 296 | func walk(path string, node *Node, fn filepath.WalkFunc) error { 297 | if err := fn(path, &ResourceFileInfo{Node: node}, nil); err != nil { 298 | return err 299 | } 300 | 301 | for _, child := range node.Children { 302 | if err := walk(filepath.Join(path, child.Name), child, fn); err != nil { 303 | return err 304 | } 305 | } 306 | 307 | return nil 308 | } 309 | 310 | func newNode(name string, parent *Node) *Node { 311 | node := &Node{ 312 | Name: name, 313 | IsDir: false, 314 | ModTime: time.Now(), 315 | } 316 | 317 | parent.Children = append(parent.Children, node) 318 | return node 319 | } 320 | 321 | func newFile(node *Node, flag int) (File, error) { 322 | if isWritable(flag) { 323 | node.ModTime = time.Now() 324 | } 325 | 326 | if node.Content == nil || hasFlag(os.O_TRUNC, flag) { 327 | buf := make([]byte, 0) 328 | node.Content = &buf 329 | node.Mutex = &sync.RWMutex{} 330 | } 331 | 332 | f := NewResourceFile(node) 333 | 334 | if hasFlag(os.O_APPEND, flag) { 335 | _, _ = f.Seek(0, io.SeekEnd) 336 | } 337 | 338 | if hasFlag(os.O_RDWR, flag) { 339 | return f, nil 340 | } 341 | if hasFlag(os.O_WRONLY, flag) { 342 | return &woFile{f}, nil 343 | } 344 | 345 | return &roFile{f}, nil 346 | } 347 | 348 | func hasFlag(flag int, flags int) bool { 349 | return flags&flag == flag 350 | } 351 | 352 | func isWritable(flag int) bool { 353 | return hasFlag(os.O_WRONLY, flag) || hasFlag(os.O_RDWR, flag) || hasFlag(os.O_APPEND, flag) 354 | } 355 | 356 | type roFile struct { 357 | *ResourceFile 358 | } 359 | 360 | // Write is disabled and returns ErrorReadOnly 361 | func (f *roFile) Write(p []byte) (n int, err error) { 362 | return 0, ErrReadOnly 363 | } 364 | 365 | // woFile wraps the given file and disables Read(..) operation. 366 | type woFile struct { 367 | *ResourceFile 368 | } 369 | 370 | // Read is disabled and returns ErrorWroteOnly 371 | func (f *woFile) Read(p []byte) (n int, err error) { 372 | return 0, ErrWriteOnly 373 | } 374 | -------------------------------------------------------------------------------- /fake/File.go: -------------------------------------------------------------------------------- 1 | // This file was generated by counterfeiter 2 | package fake 3 | 4 | import ( 5 | "os" 6 | "sync" 7 | 8 | "github.com/phogolabs/parcello" 9 | ) 10 | 11 | type File struct { 12 | CloseStub func() error 13 | closeMutex sync.RWMutex 14 | closeArgsForCall []struct{} 15 | closeReturns struct { 16 | result1 error 17 | } 18 | ReadStub func(p []byte) (n int, err error) 19 | readMutex sync.RWMutex 20 | readArgsForCall []struct { 21 | p []byte 22 | } 23 | readReturns struct { 24 | result1 int 25 | result2 error 26 | } 27 | SeekStub func(offset int64, whence int) (int64, error) 28 | seekMutex sync.RWMutex 29 | seekArgsForCall []struct { 30 | offset int64 31 | whence int 32 | } 33 | seekReturns struct { 34 | result1 int64 35 | result2 error 36 | } 37 | ReaddirStub func(count int) ([]os.FileInfo, error) 38 | readdirMutex sync.RWMutex 39 | readdirArgsForCall []struct { 40 | count int 41 | } 42 | readdirReturns struct { 43 | result1 []os.FileInfo 44 | result2 error 45 | } 46 | StatStub func() (os.FileInfo, error) 47 | statMutex sync.RWMutex 48 | statArgsForCall []struct{} 49 | statReturns struct { 50 | result1 os.FileInfo 51 | result2 error 52 | } 53 | WriteStub func(p []byte) (n int, err error) 54 | writeMutex sync.RWMutex 55 | writeArgsForCall []struct { 56 | p []byte 57 | } 58 | writeReturns struct { 59 | result1 int 60 | result2 error 61 | } 62 | ReadAtStub func(p []byte, off int64) (n int, err error) 63 | readAtMutex sync.RWMutex 64 | readAtArgsForCall []struct { 65 | p []byte 66 | off int64 67 | } 68 | readAtReturns struct { 69 | result1 int 70 | result2 error 71 | } 72 | invocations map[string][][]interface{} 73 | invocationsMutex sync.RWMutex 74 | } 75 | 76 | func (fake *File) Close() error { 77 | fake.closeMutex.Lock() 78 | fake.closeArgsForCall = append(fake.closeArgsForCall, struct{}{}) 79 | fake.recordInvocation("Close", []interface{}{}) 80 | fake.closeMutex.Unlock() 81 | if fake.CloseStub != nil { 82 | return fake.CloseStub() 83 | } 84 | return fake.closeReturns.result1 85 | } 86 | 87 | func (fake *File) CloseCallCount() int { 88 | fake.closeMutex.RLock() 89 | defer fake.closeMutex.RUnlock() 90 | return len(fake.closeArgsForCall) 91 | } 92 | 93 | func (fake *File) CloseReturns(result1 error) { 94 | fake.CloseStub = nil 95 | fake.closeReturns = struct { 96 | result1 error 97 | }{result1} 98 | } 99 | 100 | func (fake *File) Read(p []byte) (n int, err error) { 101 | var pCopy []byte 102 | if p != nil { 103 | pCopy = make([]byte, len(p)) 104 | copy(pCopy, p) 105 | } 106 | fake.readMutex.Lock() 107 | fake.readArgsForCall = append(fake.readArgsForCall, struct { 108 | p []byte 109 | }{pCopy}) 110 | fake.recordInvocation("Read", []interface{}{pCopy}) 111 | fake.readMutex.Unlock() 112 | if fake.ReadStub != nil { 113 | return fake.ReadStub(p) 114 | } 115 | return fake.readReturns.result1, fake.readReturns.result2 116 | } 117 | 118 | func (fake *File) ReadCallCount() int { 119 | fake.readMutex.RLock() 120 | defer fake.readMutex.RUnlock() 121 | return len(fake.readArgsForCall) 122 | } 123 | 124 | func (fake *File) ReadArgsForCall(i int) []byte { 125 | fake.readMutex.RLock() 126 | defer fake.readMutex.RUnlock() 127 | return fake.readArgsForCall[i].p 128 | } 129 | 130 | func (fake *File) ReadReturns(result1 int, result2 error) { 131 | fake.ReadStub = nil 132 | fake.readReturns = struct { 133 | result1 int 134 | result2 error 135 | }{result1, result2} 136 | } 137 | 138 | func (fake *File) Seek(offset int64, whence int) (int64, error) { 139 | fake.seekMutex.Lock() 140 | fake.seekArgsForCall = append(fake.seekArgsForCall, struct { 141 | offset int64 142 | whence int 143 | }{offset, whence}) 144 | fake.recordInvocation("Seek", []interface{}{offset, whence}) 145 | fake.seekMutex.Unlock() 146 | if fake.SeekStub != nil { 147 | return fake.SeekStub(offset, whence) 148 | } 149 | return fake.seekReturns.result1, fake.seekReturns.result2 150 | } 151 | 152 | func (fake *File) SeekCallCount() int { 153 | fake.seekMutex.RLock() 154 | defer fake.seekMutex.RUnlock() 155 | return len(fake.seekArgsForCall) 156 | } 157 | 158 | func (fake *File) SeekArgsForCall(i int) (int64, int) { 159 | fake.seekMutex.RLock() 160 | defer fake.seekMutex.RUnlock() 161 | return fake.seekArgsForCall[i].offset, fake.seekArgsForCall[i].whence 162 | } 163 | 164 | func (fake *File) SeekReturns(result1 int64, result2 error) { 165 | fake.SeekStub = nil 166 | fake.seekReturns = struct { 167 | result1 int64 168 | result2 error 169 | }{result1, result2} 170 | } 171 | 172 | func (fake *File) Readdir(count int) ([]os.FileInfo, error) { 173 | fake.readdirMutex.Lock() 174 | fake.readdirArgsForCall = append(fake.readdirArgsForCall, struct { 175 | count int 176 | }{count}) 177 | fake.recordInvocation("Readdir", []interface{}{count}) 178 | fake.readdirMutex.Unlock() 179 | if fake.ReaddirStub != nil { 180 | return fake.ReaddirStub(count) 181 | } 182 | return fake.readdirReturns.result1, fake.readdirReturns.result2 183 | } 184 | 185 | func (fake *File) ReaddirCallCount() int { 186 | fake.readdirMutex.RLock() 187 | defer fake.readdirMutex.RUnlock() 188 | return len(fake.readdirArgsForCall) 189 | } 190 | 191 | func (fake *File) ReaddirArgsForCall(i int) int { 192 | fake.readdirMutex.RLock() 193 | defer fake.readdirMutex.RUnlock() 194 | return fake.readdirArgsForCall[i].count 195 | } 196 | 197 | func (fake *File) ReaddirReturns(result1 []os.FileInfo, result2 error) { 198 | fake.ReaddirStub = nil 199 | fake.readdirReturns = struct { 200 | result1 []os.FileInfo 201 | result2 error 202 | }{result1, result2} 203 | } 204 | 205 | func (fake *File) Stat() (os.FileInfo, error) { 206 | fake.statMutex.Lock() 207 | fake.statArgsForCall = append(fake.statArgsForCall, struct{}{}) 208 | fake.recordInvocation("Stat", []interface{}{}) 209 | fake.statMutex.Unlock() 210 | if fake.StatStub != nil { 211 | return fake.StatStub() 212 | } 213 | return fake.statReturns.result1, fake.statReturns.result2 214 | } 215 | 216 | func (fake *File) StatCallCount() int { 217 | fake.statMutex.RLock() 218 | defer fake.statMutex.RUnlock() 219 | return len(fake.statArgsForCall) 220 | } 221 | 222 | func (fake *File) StatReturns(result1 os.FileInfo, result2 error) { 223 | fake.StatStub = nil 224 | fake.statReturns = struct { 225 | result1 os.FileInfo 226 | result2 error 227 | }{result1, result2} 228 | } 229 | 230 | func (fake *File) Write(p []byte) (n int, err error) { 231 | var pCopy []byte 232 | if p != nil { 233 | pCopy = make([]byte, len(p)) 234 | copy(pCopy, p) 235 | } 236 | fake.writeMutex.Lock() 237 | fake.writeArgsForCall = append(fake.writeArgsForCall, struct { 238 | p []byte 239 | }{pCopy}) 240 | fake.recordInvocation("Write", []interface{}{pCopy}) 241 | fake.writeMutex.Unlock() 242 | if fake.WriteStub != nil { 243 | return fake.WriteStub(p) 244 | } 245 | return fake.writeReturns.result1, fake.writeReturns.result2 246 | } 247 | 248 | func (fake *File) WriteCallCount() int { 249 | fake.writeMutex.RLock() 250 | defer fake.writeMutex.RUnlock() 251 | return len(fake.writeArgsForCall) 252 | } 253 | 254 | func (fake *File) WriteArgsForCall(i int) []byte { 255 | fake.writeMutex.RLock() 256 | defer fake.writeMutex.RUnlock() 257 | return fake.writeArgsForCall[i].p 258 | } 259 | 260 | func (fake *File) WriteReturns(result1 int, result2 error) { 261 | fake.WriteStub = nil 262 | fake.writeReturns = struct { 263 | result1 int 264 | result2 error 265 | }{result1, result2} 266 | } 267 | 268 | func (fake *File) ReadAt(p []byte, off int64) (n int, err error) { 269 | var pCopy []byte 270 | if p != nil { 271 | pCopy = make([]byte, len(p)) 272 | copy(pCopy, p) 273 | } 274 | fake.readAtMutex.Lock() 275 | fake.readAtArgsForCall = append(fake.readAtArgsForCall, struct { 276 | p []byte 277 | off int64 278 | }{pCopy, off}) 279 | fake.recordInvocation("ReadAt", []interface{}{pCopy, off}) 280 | fake.readAtMutex.Unlock() 281 | if fake.ReadAtStub != nil { 282 | return fake.ReadAtStub(p, off) 283 | } 284 | return fake.readAtReturns.result1, fake.readAtReturns.result2 285 | } 286 | 287 | func (fake *File) ReadAtCallCount() int { 288 | fake.readAtMutex.RLock() 289 | defer fake.readAtMutex.RUnlock() 290 | return len(fake.readAtArgsForCall) 291 | } 292 | 293 | func (fake *File) ReadAtArgsForCall(i int) ([]byte, int64) { 294 | fake.readAtMutex.RLock() 295 | defer fake.readAtMutex.RUnlock() 296 | return fake.readAtArgsForCall[i].p, fake.readAtArgsForCall[i].off 297 | } 298 | 299 | func (fake *File) ReadAtReturns(result1 int, result2 error) { 300 | fake.ReadAtStub = nil 301 | fake.readAtReturns = struct { 302 | result1 int 303 | result2 error 304 | }{result1, result2} 305 | } 306 | 307 | func (fake *File) Invocations() map[string][][]interface{} { 308 | fake.invocationsMutex.RLock() 309 | defer fake.invocationsMutex.RUnlock() 310 | fake.closeMutex.RLock() 311 | defer fake.closeMutex.RUnlock() 312 | fake.readMutex.RLock() 313 | defer fake.readMutex.RUnlock() 314 | fake.seekMutex.RLock() 315 | defer fake.seekMutex.RUnlock() 316 | fake.readdirMutex.RLock() 317 | defer fake.readdirMutex.RUnlock() 318 | fake.statMutex.RLock() 319 | defer fake.statMutex.RUnlock() 320 | fake.writeMutex.RLock() 321 | defer fake.writeMutex.RUnlock() 322 | fake.readAtMutex.RLock() 323 | defer fake.readAtMutex.RUnlock() 324 | return fake.invocations 325 | } 326 | 327 | func (fake *File) recordInvocation(key string, args []interface{}) { 328 | fake.invocationsMutex.Lock() 329 | defer fake.invocationsMutex.Unlock() 330 | if fake.invocations == nil { 331 | fake.invocations = map[string][][]interface{}{} 332 | } 333 | if fake.invocations[key] == nil { 334 | fake.invocations[key] = [][]interface{}{} 335 | } 336 | fake.invocations[key] = append(fake.invocations[key], args) 337 | } 338 | 339 | var _ parcello.File = new(File) 340 | -------------------------------------------------------------------------------- /manager_test.go: -------------------------------------------------------------------------------- 1 | package parcello_test 2 | 3 | import ( 4 | "archive/zip" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "os" 9 | "path/filepath" 10 | "time" 11 | 12 | zipexe "github.com/daaku/go.zipexe" 13 | "github.com/kardianos/osext" 14 | . "github.com/onsi/ginkgo" 15 | . "github.com/onsi/gomega" 16 | "github.com/phogolabs/parcello" 17 | "github.com/phogolabs/parcello/fake" 18 | ) 19 | 20 | var _ = Describe("ResourceManager", func() { 21 | var ( 22 | manager *parcello.ResourceManager 23 | resource *parcello.Resource 24 | bundle *parcello.Bundle 25 | ) 26 | 27 | BeforeEach(func() { 28 | var err error 29 | 30 | compressor := parcello.ZipCompressor{ 31 | Config: &parcello.CompressorConfig{ 32 | Logger: ioutil.Discard, 33 | Filename: "bundle", 34 | Recurive: true, 35 | }, 36 | } 37 | 38 | fileSystem := parcello.Dir("./fixture") 39 | 40 | ctx := &parcello.CompressorContext{ 41 | FileSystem: fileSystem, 42 | } 43 | 44 | bundle, err = compressor.Compress(ctx) 45 | Expect(err).NotTo(HaveOccurred()) 46 | 47 | manager = &parcello.ResourceManager{} 48 | }) 49 | 50 | JustBeforeEach(func() { 51 | resource = parcello.BinaryResource(bundle.Body) 52 | Expect(manager.Add(resource)).To(Succeed()) 53 | }) 54 | 55 | Describe("NewResourceManager", func() { 56 | var ( 57 | name string 58 | fileSystem parcello.FileSystem 59 | ) 60 | 61 | BeforeEach(func() { 62 | path, err := osext.Executable() 63 | Expect(err).To(Succeed()) 64 | 65 | path, name = filepath.Split(path) 66 | fileSystem = parcello.Dir(path) 67 | }) 68 | 69 | It("creates new manager successfully", func() { 70 | cfg := &parcello.ResourceManagerConfig{ 71 | Path: name, 72 | FileSystem: fileSystem, 73 | } 74 | m, err := parcello.NewResourceManager(cfg) 75 | Expect(m).NotTo(BeNil()) 76 | Expect(err).NotTo(HaveOccurred()) 77 | }) 78 | 79 | Context("when file stat fails", func() { 80 | BeforeEach(func() { 81 | exec := &fake.File{} 82 | exec.StatReturns(nil, fmt.Errorf("oh no!")) 83 | 84 | fs := &fake.FileSystem{} 85 | fs.OpenFileReturns(exec, nil) 86 | 87 | fileSystem = fs 88 | }) 89 | 90 | It("returns the error", func() { 91 | cfg := &parcello.ResourceManagerConfig{ 92 | Path: name, 93 | FileSystem: fileSystem, 94 | } 95 | 96 | m, err := parcello.NewResourceManager(cfg) 97 | Expect(m).To(BeNil()) 98 | Expect(err).To(MatchError("oh no!")) 99 | }) 100 | }) 101 | }) 102 | 103 | Describe("Add", func() { 104 | Context("when the resource is added second time", func() { 105 | It("returns an error", func() { 106 | Expect(manager.Add(resource)).To(MatchError("invalid path: 'resource/reports/2018.txt'")) 107 | }) 108 | }) 109 | 110 | Context("when the algorithm is unsupported", func() { 111 | JustBeforeEach(func() { 112 | manager = &parcello.ResourceManager{} 113 | manager.NewReader = func(r io.ReaderAt, s int64) (*zip.Reader, error) { 114 | reader, err := zipexe.NewReader(r, s) 115 | if err != nil { 116 | return nil, err 117 | } 118 | reader.File[0].FileHeader.Method = 2000 119 | return reader, nil 120 | } 121 | }) 122 | 123 | It("returns an error", func() { 124 | Expect(manager.Add(resource)).To(MatchError("zip: unsupported compression algorithm")) 125 | }) 126 | }) 127 | 128 | Context("when the file is corrupted", func() { 129 | JustBeforeEach(func() { 130 | manager = &parcello.ResourceManager{} 131 | manager.NewReader = func(r io.ReaderAt, s int64) (*zip.Reader, error) { 132 | reader, err := zipexe.NewReader(r, s) 133 | if err != nil { 134 | return nil, err 135 | } 136 | reader.File[0].FileHeader.CRC32 = 123 137 | return reader, nil 138 | } 139 | }) 140 | 141 | It("returns an error", func() { 142 | Expect(manager.Add(resource)).To(MatchError("zip: checksum error")) 143 | }) 144 | }) 145 | 146 | Context("when the resource is not zip", func() { 147 | It("returns an error", func() { 148 | Expect(manager.Add(parcello.BinaryResource([]byte("lol")))).To(MatchError("Couldn't Open As Executable")) 149 | }) 150 | 151 | It("panics", func() { 152 | Expect(func() { parcello.AddResource([]byte("lol")) }).To(Panic()) 153 | }) 154 | }) 155 | }) 156 | 157 | Describe("Dir", func() { 158 | It("returns a valid sub-manager", func() { 159 | group, err := manager.Dir("/resource") 160 | Expect(err).To(BeNil()) 161 | 162 | file, err := group.Open("/reports/2018.txt") 163 | Expect(file).NotTo(BeNil()) 164 | Expect(err).NotTo(HaveOccurred()) 165 | 166 | data, err := ioutil.ReadAll(file) 167 | Expect(err).NotTo(HaveOccurred()) 168 | Expect(string(data)).To(Equal("Report 2018\n")) 169 | }) 170 | 171 | Context("when group is a file not a directory", func() { 172 | It("returns an error", func() { 173 | group, err := manager.Dir("/resource/reports/2018.txt") 174 | Expect(group).To(BeNil()) 175 | Expect(err).To(MatchError(os.ErrNotExist)) 176 | }) 177 | }) 178 | 179 | Context("when the manager is global", func() { 180 | var ( 181 | original parcello.FileSystemManager 182 | manager *fake.FileSystemManager 183 | ) 184 | 185 | BeforeEach(func() { 186 | manager = &fake.FileSystemManager{} 187 | 188 | original = parcello.Manager 189 | parcello.Manager = manager 190 | }) 191 | 192 | AfterEach(func() { 193 | parcello.Manager = original 194 | }) 195 | 196 | It("returns a sub-manager", func() { 197 | manager.DirReturns(manager, nil) 198 | Expect(parcello.ManagerAt("/nil")).To(Equal(parcello.Manager)) 199 | }) 200 | 201 | Context("when the directory does not exist", func() { 202 | It("panics", func() { 203 | manager.DirReturns(nil, fmt.Errorf("oh no!")) 204 | Expect(func() { parcello.ManagerAt("/i/do/not/exist") }).To(Panic()) 205 | }) 206 | }) 207 | }) 208 | }) 209 | 210 | Describe("Open", func() { 211 | It("opens the root successfully", func() { 212 | file, err := manager.Open("/") 213 | Expect(file).NotTo(BeNil()) 214 | Expect(err).To(BeNil()) 215 | }) 216 | 217 | Context("when the resource is empty", func() { 218 | It("returns an error", func() { 219 | file, err := manager.Open("/migration.sql") 220 | Expect(file).To(BeNil()) 221 | Expect(err).To(MatchError("open /migration.sql: file does not exist")) 222 | }) 223 | }) 224 | 225 | Context("when the file is directory", func() { 226 | It("returns an error", func() { 227 | file, err := manager.Open("/resource/reports") 228 | Expect(file).NotTo(BeNil()) 229 | Expect(err).To(BeNil()) 230 | }) 231 | }) 232 | 233 | Context("when the global resource is empty", func() { 234 | It("returns an error", func() { 235 | file, err := parcello.Open("migration.sql") 236 | Expect(file).To(BeNil()) 237 | Expect(err).To(MatchError("open migration.sql: file does not exist")) 238 | }) 239 | }) 240 | 241 | It("returns the resource successfully", func() { 242 | file, err := manager.Open("/resource/reports/2018.txt") 243 | Expect(file).NotTo(BeNil()) 244 | Expect(err).NotTo(HaveOccurred()) 245 | 246 | data, err := ioutil.ReadAll(file) 247 | Expect(err).NotTo(HaveOccurred()) 248 | Expect(string(data)).To(Equal("Report 2018\n")) 249 | }) 250 | 251 | Context("when the file is open more than once for read", func() { 252 | It("does not change the mod time", func() { 253 | file, err := manager.Open("/resource/reports/2018.txt") 254 | Expect(file).NotTo(BeNil()) 255 | Expect(err).NotTo(HaveOccurred()) 256 | 257 | info, err := file.Stat() 258 | Expect(err).NotTo(HaveOccurred()) 259 | 260 | file, err = manager.Open("/resource/reports/2018.txt") 261 | Expect(file).NotTo(BeNil()) 262 | Expect(err).NotTo(HaveOccurred()) 263 | 264 | info2, err := file.Stat() 265 | Expect(err).NotTo(HaveOccurred()) 266 | 267 | Expect(info.ModTime()).To(Equal(info2.ModTime())) 268 | }) 269 | }) 270 | 271 | It("returns a readonly resource", func() { 272 | file, err := manager.Open("/resource/reports/2018.txt") 273 | Expect(file).NotTo(BeNil()) 274 | Expect(err).NotTo(HaveOccurred()) 275 | 276 | _, err = fmt.Fprintln(file.(io.Writer), "hello") 277 | Expect(err).To(MatchError("File is read-only")) 278 | }) 279 | 280 | Context("when the file with the requested name does not exist", func() { 281 | It("returns an error", func() { 282 | file, err := manager.Open("/resource/migration.sql") 283 | Expect(file).To(BeNil()) 284 | Expect(err).To(MatchError("open /resource/migration.sql: file does not exist")) 285 | }) 286 | }) 287 | }) 288 | 289 | Describe("OpenFile", func() { 290 | Context("when the file does not exist", func() { 291 | It("creates the file", func() { 292 | file, err := manager.OpenFile("/resource/secrets.txt", os.O_CREATE, 0600) 293 | Expect(file).NotTo(BeNil()) 294 | Expect(err).NotTo(HaveOccurred()) 295 | }) 296 | }) 297 | 298 | Context("when the file is directory", func() { 299 | It("returns an error", func() { 300 | file, err := manager.OpenFile("/resource/reports", os.O_RDWR, 0600) 301 | Expect(file).To(BeNil()) 302 | Expect(err).To(MatchError("open /resource/reports: Is directory")) 303 | }) 304 | }) 305 | 306 | Context("when the file exists", func() { 307 | It("truncs the file content", func() { 308 | file, err := manager.OpenFile("/resource/reports/2018.txt", os.O_CREATE|os.O_TRUNC, 0600) 309 | Expect(file).NotTo(BeNil()) 310 | Expect(err).NotTo(HaveOccurred()) 311 | 312 | data, err := ioutil.ReadAll(file) 313 | Expect(err).NotTo(HaveOccurred()) 314 | Expect(data).To(BeEmpty()) 315 | }) 316 | 317 | Context("when the file is open more than once for write", func() { 318 | It("does not change the mod time", func() { 319 | start := time.Now() 320 | 321 | file, err := manager.OpenFile("/resource/reports/2018.txt", os.O_WRONLY, 0600) 322 | Expect(file).NotTo(BeNil()) 323 | Expect(err).NotTo(HaveOccurred()) 324 | 325 | info, err := file.Stat() 326 | Expect(err).NotTo(HaveOccurred()) 327 | modTime := info.ModTime() 328 | 329 | Expect(modTime.After(start)).To(BeTrue()) 330 | }) 331 | }) 332 | 333 | Context("when the os.O_TRUNC flag is not provided", func() { 334 | It("returns an error", func() { 335 | file, err := manager.OpenFile("/resource/reports/2018.txt", os.O_CREATE, 0600) 336 | Expect(file).To(BeNil()) 337 | Expect(err).To(MatchError("open /resource/reports/2018.txt: file already exists")) 338 | }) 339 | }) 340 | 341 | Context("when the file is open for append", func() { 342 | It("appends content successfully", func() { 343 | file, err := manager.OpenFile("/resource/reports/2018.txt", os.O_RDWR|os.O_APPEND, 0600) 344 | Expect(file).NotTo(BeNil()) 345 | Expect(err).NotTo(HaveOccurred()) 346 | 347 | _, err = fmt.Fprint(file, "hello") 348 | Expect(err).NotTo(HaveOccurred()) 349 | 350 | _, err = file.Seek(0, io.SeekStart) 351 | Expect(err).NotTo(HaveOccurred()) 352 | 353 | data, err := ioutil.ReadAll(file) 354 | Expect(err).NotTo(HaveOccurred()) 355 | Expect(string(data)).To(Equal("Report 2018\nhello")) 356 | }) 357 | }) 358 | 359 | Context("when the file is open for WRITE only", func() { 360 | Context("when we try to read", func() { 361 | It("returns an error", func() { 362 | file, err := manager.OpenFile("/resource/reports/2018.txt", os.O_WRONLY, 0600) 363 | Expect(file).NotTo(BeNil()) 364 | Expect(err).NotTo(HaveOccurred()) 365 | 366 | _, err = ioutil.ReadAll(file) 367 | Expect(err).To(MatchError("File is write-only")) 368 | }) 369 | }) 370 | }) 371 | }) 372 | }) 373 | 374 | Describe("Walk", func() { 375 | Context("when the resource is empty", func() { 376 | It("returns an error", func() { 377 | err := manager.Walk("/documents", func(path string, info os.FileInfo, err error) error { 378 | return nil 379 | }) 380 | 381 | Expect(err).To(MatchError(os.ErrNotExist)) 382 | }) 383 | }) 384 | 385 | Context("when the resource has hierarchy of directories and files", func() { 386 | It("walks through all of them", func() { 387 | paths := []string{} 388 | err := manager.Walk("/", func(path string, info os.FileInfo, err error) error { 389 | paths = append(paths, path) 390 | return nil 391 | }) 392 | 393 | Expect(paths).To(HaveLen(11)) 394 | Expect(paths[0]).To(Equal("/")) 395 | Expect(paths[1]).To(Equal("/resource")) 396 | Expect(paths[2]).To(Equal("/resource/reports")) 397 | Expect(paths[3]).To(Equal("/resource/reports/2018.txt")) 398 | Expect(paths[4]).To(Equal("/resource/scripts")) 399 | Expect(paths[5]).To(Equal("/resource/scripts/schema.sql")) 400 | Expect(paths[6]).To(Equal("/resource/templates")) 401 | Expect(paths[7]).To(Equal("/resource/templates/html")) 402 | Expect(paths[8]).To(Equal("/resource/templates/html/index.html")) 403 | Expect(paths[9]).To(Equal("/resource/templates/yml")) 404 | Expect(paths[10]).To(Equal("/resource/templates/yml/schema.yml")) 405 | Expect(err).NotTo(HaveOccurred()) 406 | }) 407 | 408 | Context("when the start node is file", func() { 409 | It("walks through the file only", func() { 410 | cnt := 0 411 | err := manager.Walk("/resource/reports/2018.txt", func(path string, info os.FileInfo, err error) error { 412 | cnt = cnt + 1 413 | Expect(path).To(Equal("/resource/reports/2018.txt")) 414 | Expect(info.Name()).To(Equal("2018.txt")) 415 | Expect(info.Size()).NotTo(BeZero()) 416 | return nil 417 | }) 418 | 419 | Expect(err).NotTo(HaveOccurred()) 420 | Expect(cnt).To(Equal(1)) 421 | }) 422 | }) 423 | 424 | It("walks through all of root children", func() { 425 | cnt := 0 426 | paths := []string{} 427 | err := manager.Walk("/resource/templates", func(path string, info os.FileInfo, err error) error { 428 | paths = append(paths, path) 429 | cnt = cnt + 1 430 | return nil 431 | }) 432 | 433 | Expect(paths).To(HaveLen(5)) 434 | Expect(paths[0]).To(Equal("/resource/templates")) 435 | Expect(paths[1]).To(Equal("/resource/templates/html")) 436 | Expect(paths[2]).To(Equal("/resource/templates/html/index.html")) 437 | Expect(paths[3]).To(Equal("/resource/templates/yml")) 438 | Expect(paths[4]).To(Equal("/resource/templates/yml/schema.yml")) 439 | Expect(err).NotTo(HaveOccurred()) 440 | }) 441 | 442 | Context("when the walker returns an error", func() { 443 | It("returns the error", func() { 444 | err := manager.Walk("/resource", func(path string, info os.FileInfo, err error) error { 445 | return fmt.Errorf("Oh no!") 446 | }) 447 | 448 | Expect(err).To(MatchError("Oh no!")) 449 | }) 450 | 451 | Context("when the walk returns an error for sub-directory", func() { 452 | It("returns the error", func() { 453 | err := manager.Walk("/resource", func(path string, info os.FileInfo, err error) error { 454 | if path == "/resource/templates" { 455 | return fmt.Errorf("Oh no!") 456 | } 457 | return nil 458 | }) 459 | 460 | Expect(err).To(MatchError("Oh no!")) 461 | }) 462 | }) 463 | }) 464 | }) 465 | }) 466 | }) 467 | 468 | var _ = Describe("DefaultManager", func() { 469 | It("creates a new manager successfully", func() { 470 | manager := parcello.DefaultManager(osext.Executable) 471 | Expect(manager).NotTo(BeNil()) 472 | _, ok := manager.(*parcello.ResourceManager) 473 | Expect(ok).To(BeTrue()) 474 | }) 475 | 476 | Context("when the executable cannot be found", func() { 477 | It("panics", func() { 478 | fn := func() (string, error) { return "", fmt.Errorf("oh no!") } 479 | Expect(func() { parcello.DefaultManager(fn) }).To(Panic()) 480 | }) 481 | }) 482 | 483 | Context("when the filesystem fails", func() { 484 | It("panics", func() { 485 | fn := func() (string, error) { return "/i/do/not/exist", nil } 486 | Expect(func() { parcello.DefaultManager(fn) }).To(Panic()) 487 | }) 488 | }) 489 | 490 | Context("when dev mode is enabled", func() { 491 | BeforeEach(func() { 492 | os.Setenv("PARCELLO_DEV_ENABLED", "1") 493 | }) 494 | 495 | AfterEach(func() { 496 | os.Unsetenv("PARCELLO_DEV_ENABLED") 497 | }) 498 | 499 | It("creates a new dir manager", func() { 500 | manager := parcello.DefaultManager(osext.Executable) 501 | Expect(manager).NotTo(BeNil()) 502 | dir, ok := manager.(parcello.Dir) 503 | Expect(ok).To(BeTrue()) 504 | Expect(string(dir)).To(Equal(".")) 505 | }) 506 | 507 | Context("when the directory is provided", func() { 508 | BeforeEach(func() { 509 | os.Setenv("PARCELLO_RESOURCE_DIR", "./root") 510 | }) 511 | 512 | AfterEach(func() { 513 | os.Unsetenv("PRACELLO_RESOURCE_DIR") 514 | }) 515 | 516 | It("creates a new dir manager", func() { 517 | manager := parcello.DefaultManager(osext.Executable) 518 | Expect(manager).NotTo(BeNil()) 519 | dir, ok := manager.(parcello.Dir) 520 | Expect(ok).To(BeTrue()) 521 | Expect(string(dir)).To(Equal("./root")) 522 | }) 523 | }) 524 | }) 525 | }) 526 | --------------------------------------------------------------------------------