├── 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 |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