├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── example ├── README.md ├── main.go ├── public │ ├── hello.txt │ ├── ignoreFile.csv │ └── img │ │ └── friends.jpg └── statik │ └── statik.go ├── fs ├── bench_test.go ├── fs.go ├── fs_test.go └── walk.go ├── go.mod ├── go.sum ├── statik.go ├── statik_test.go └── testdata ├── deep ├── a └── aa │ └── bb │ └── c ├── file └── file.txt ├── image └── pixel.gif ├── index ├── index.html └── sub_dir │ └── index.html └── readdir ├── aa ├── bb └── cc /.gitignore: -------------------------------------------------------------------------------- 1 | statik -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.9.x 5 | - 1.10.3 6 | - 1.11.x 7 | - 1.12.x 8 | 9 | go_import_path: github.com/rakyll/statik 10 | 11 | install: 12 | - go build -v 13 | - ./statik -f -src=./example/public -dest=./example/ -include=*.jpg,*.txt,*.html,*.css,*.js -ns=web 14 | 15 | script: 16 | - go test -v -bench=. ./... 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2014 Google Inc. 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # statik 2 | 3 | [](https://travis-ci.org/rakyll/statik) 4 | 5 | statik allows you to embed a directory of static files into your Go binary to be later served from an http.FileSystem. 6 | 7 | Is this a crazy idea? No, not necessarily. If you're building a tool that has a Web component, you typically want to serve some images, CSS and JavaScript. You like the comfort of distributing a single binary, so you don't want to mess with deploying them elsewhere. If your static files are not large in size and will be browsed by a few people, statik is a solution you are looking for. 8 | 9 | ## Usage 10 | 11 | Install the command line tool first. 12 | 13 | go get github.com/rakyll/statik 14 | 15 | statik is a tiny program that reads a directory and generates a source file that contains its contents. The generated source file registers the directory contents to be used by statik file system. 16 | 17 | The command below will walk on the public path and generate a package called `statik` under the current working directory. 18 | 19 | $ statik -src=/path/to/your/project/public 20 | 21 | The command below will filter only files on listed extensions. 22 | 23 | $ statik -include=*.jpg,*.txt,*.html,*.css,*.js 24 | 25 | In your program, all your need to do is to import the generated package, initialize a new statik file system and serve. 26 | 27 | ~~~ go 28 | import ( 29 | "github.com/rakyll/statik/fs" 30 | 31 | _ "./statik" // TODO: Replace with the absolute import path 32 | ) 33 | 34 | // ... 35 | 36 | statikFS, err := fs.New() 37 | if err != nil { 38 | log.Fatal(err) 39 | } 40 | 41 | // Serve the contents over HTTP. 42 | http.Handle("/public/", http.StripPrefix("/public/", http.FileServer(statikFS))) 43 | http.ListenAndServe(":8080", nil) 44 | ~~~ 45 | 46 | Visit http://localhost:8080/public/path/to/file to see your file. 47 | 48 | You can also read the content of a single file: 49 | 50 | ~~~ go 51 | import ( 52 | "github.com/rakyll/statik/fs" 53 | 54 | _ "./statik" // TODO: Replace with the absolute import path 55 | ) 56 | 57 | // ... 58 | 59 | statikFS, err := fs.New() 60 | if err != nil { 61 | log.Fatal(err) 62 | } 63 | 64 | // Access individual files by their paths. 65 | r, err := statikFS.Open("/hello.txt") 66 | if err != nil { 67 | log.Fatal(err) 68 | } 69 | defer r.Close() 70 | contents, err := ioutil.ReadAll(r) 71 | if err != nil { 72 | log.Fatal(err) 73 | } 74 | 75 | fmt.Println(string(contents)) 76 | ~~~ 77 | 78 | There is also a working example under [example](https://github.com/rakyll/statik/tree/master/example) directory, follow the instructions to build and run it. 79 | 80 | Note: The idea and the implementation are hijacked from [camlistore](http://camlistore.org/). I decided to decouple it from its codebase due to the fact I'm actively in need of a similar solution for many of my projects. 81 | 82 | ## Deterministic output 83 | 84 | By default, statik includes the "last modified" (mtime) time on files that it packs. This allows an HTTP FileServer to present the correct file modification times to clients. 85 | 86 | However, if you have a continuous integration task that checks that your checked-in static files in a git repository match the code that is generated on your CI system, you'll run into a problem: The mtime on the git checkout does not match what you have locally, causing tests to fail. 87 | 88 | You can fix the test in one of two ways: 89 | 90 | 1. In CI, manually set the mtime on the freshly checked out tree: [here's a stackoverflow answer](https://stackoverflow.com/a/22638823/93405) that provides a shell command to do that; or, 91 | 2. Instruct statik not to store the "last modified" time. 92 | 93 | To ignore the last modified time, use the `-m` to statik, like so: 94 | 95 | $ statik -m -include=*.jpg,*.txt,*.html,*.css,*.js 96 | 97 | Note that this will cause http.FileServer to consider the file to always have changed & serve it with a "Last-Modified" of the time of the request. 98 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # How to run? 2 | 3 | Run `go generate` to create a statik package that embeds the binary data underneath the public directory. 4 | 5 | $ go generate 6 | 7 | Once the statik package is generated, run the web server: 8 | 9 | $ go run main.go 10 | 11 | Visit [http://localhost:8080/public/hello.txt](http://localhost:8080/public/hello.txt) to see the file. 12 | -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | //go:generate statik -src=./public -include=*.jpg,*.txt,*.html,*.css,*.js 2 | 3 | package main 4 | 5 | import ( 6 | "log" 7 | "net/http" 8 | 9 | _ "github.com/rakyll/statik/example/statik" 10 | "github.com/rakyll/statik/fs" 11 | ) 12 | 13 | // Before buildling, run go generate. 14 | // Then, run the main program and visit http://localhost:8080/public/hello.txt 15 | func main() { 16 | statikFS, err := fs.New() 17 | if err != nil { 18 | log.Fatal(err) 19 | } 20 | 21 | http.Handle("/public/", http.StripPrefix("/public/", http.FileServer(statikFS))) 22 | http.ListenAndServe(":8080", nil) 23 | } 24 | -------------------------------------------------------------------------------- /example/public/hello.txt: -------------------------------------------------------------------------------- 1 | Hello World 2 | 3 | -------------------------------------------------------------------------------- /example/public/ignoreFile.csv: -------------------------------------------------------------------------------- 1 | ignored,file 2 | -------------------------------------------------------------------------------- /example/public/img/friends.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rakyll/statik/e5988123cababc33df1352cba7df8535f1337326/example/public/img/friends.jpg -------------------------------------------------------------------------------- /fs/bench_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package fs 16 | 17 | import "testing" 18 | 19 | func BenchmarkOpen(b *testing.B) { 20 | Register(mustZipTree("../testdata/index")) 21 | fs, err := New() 22 | if err != nil { 23 | b.Fatalf("New() = %v", err) 24 | } 25 | b.RunParallel(func(pb *testing.PB) { 26 | for pb.Next() { 27 | const name = "/index.html" 28 | _, err := fs.Open(name) 29 | if err != nil { 30 | b.Errorf("fs.Open(%v) = %v", name, err) 31 | } 32 | } 33 | }) 34 | } 35 | 36 | func BenchmarkOpenDeep(b *testing.B) { 37 | Register(mustZipTree("../testdata/deep")) 38 | fs, err := New() 39 | if err != nil { 40 | b.Fatalf("New() = %v", err) 41 | } 42 | for i := 0; i < b.N; i++ { 43 | const name = "/aa/bb/c" 44 | _, err := fs.Open(name) 45 | if err != nil { 46 | b.Errorf("fs.Open(%v) = %v", name, err) 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /fs/fs.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package fs contains an HTTP file system that works with zip contents. 16 | package fs 17 | 18 | import ( 19 | "archive/zip" 20 | "bytes" 21 | "errors" 22 | "fmt" 23 | "io" 24 | "io/ioutil" 25 | "net/http" 26 | "os" 27 | "path" 28 | "path/filepath" 29 | "sort" 30 | "strings" 31 | "time" 32 | ) 33 | 34 | var zipData = map[string]string{} 35 | 36 | // file holds unzipped read-only file contents and file metadata. 37 | type file struct { 38 | os.FileInfo 39 | data []byte 40 | fs *statikFS 41 | } 42 | 43 | type statikFS struct { 44 | files map[string]file 45 | dirs map[string][]string 46 | } 47 | 48 | const defaultNamespace = "default" 49 | 50 | // IsDefaultNamespace returns true if the assetNamespace is 51 | // the default one 52 | func IsDefaultNamespace(assetNamespace string) bool { 53 | return assetNamespace == defaultNamespace 54 | } 55 | 56 | // Register registers zip contents data, later used to initialize 57 | // the statik file system. 58 | func Register(data string) { 59 | RegisterWithNamespace(defaultNamespace, data) 60 | } 61 | 62 | // RegisterWithNamespace registers zip contents data and set asset namespace, 63 | // later used to initialize the statik file system. 64 | func RegisterWithNamespace(assetNamespace string, data string) { 65 | zipData[assetNamespace] = data 66 | } 67 | 68 | // New creates a new file system with the default registered zip contents data. 69 | // It unzips all files and stores them in an in-memory map. 70 | func New() (http.FileSystem, error) { 71 | return NewWithNamespace(defaultNamespace) 72 | } 73 | 74 | // NewWithNamespace creates a new file system with the registered zip contents data. 75 | // It unzips all files and stores them in an in-memory map. 76 | func NewWithNamespace(assetNamespace string) (http.FileSystem, error) { 77 | asset, ok := zipData[assetNamespace] 78 | if !ok { 79 | return nil, errors.New("statik/fs: no zip data registered") 80 | } 81 | zipReader, err := zip.NewReader(strings.NewReader(asset), int64(len(asset))) 82 | if err != nil { 83 | return nil, err 84 | } 85 | files := make(map[string]file, len(zipReader.File)) 86 | dirs := make(map[string][]string) 87 | fs := &statikFS{files: files, dirs: dirs} 88 | for _, zipFile := range zipReader.File { 89 | fi := zipFile.FileInfo() 90 | f := file{FileInfo: fi, fs: fs} 91 | f.data, err = unzip(zipFile) 92 | if err != nil { 93 | return nil, fmt.Errorf("statik/fs: error unzipping file %q: %s", zipFile.Name, err) 94 | } 95 | files["/"+zipFile.Name] = f 96 | } 97 | for fn := range files { 98 | // go up directories recursively in order to care deep directory 99 | for dn := path.Dir(fn); dn != fn; { 100 | if _, ok := files[dn]; !ok { 101 | files[dn] = file{FileInfo: dirInfo{dn}, fs: fs} 102 | } else { 103 | break 104 | } 105 | fn, dn = dn, path.Dir(dn) 106 | } 107 | } 108 | for fn := range files { 109 | dn := path.Dir(fn) 110 | if fn != dn { 111 | fs.dirs[dn] = append(fs.dirs[dn], path.Base(fn)) 112 | } 113 | } 114 | for _, s := range fs.dirs { 115 | sort.Strings(s) 116 | } 117 | return fs, nil 118 | } 119 | 120 | var _ = os.FileInfo(dirInfo{}) 121 | 122 | type dirInfo struct { 123 | name string 124 | } 125 | 126 | func (di dirInfo) Name() string { return path.Base(di.name) } 127 | func (di dirInfo) Size() int64 { return 0 } 128 | func (di dirInfo) Mode() os.FileMode { return 0755 | os.ModeDir } 129 | func (di dirInfo) ModTime() time.Time { return time.Time{} } 130 | func (di dirInfo) IsDir() bool { return true } 131 | func (di dirInfo) Sys() interface{} { return nil } 132 | 133 | // Open returns a file matching the given file name, or os.ErrNotExists if 134 | // no file matching the given file name is found in the archive. 135 | // If a directory is requested, Open returns the file named "index.html" 136 | // in the requested directory, if that file exists. 137 | func (fs *statikFS) Open(name string) (http.File, error) { 138 | name = filepath.ToSlash(filepath.Clean(name)) 139 | if f, ok := fs.files[name]; ok { 140 | return newHTTPFile(f), nil 141 | } 142 | return nil, os.ErrNotExist 143 | } 144 | 145 | func newHTTPFile(file file) *httpFile { 146 | if file.IsDir() { 147 | return &httpFile{file: file, isDir: true} 148 | } 149 | return &httpFile{file: file, reader: bytes.NewReader(file.data)} 150 | } 151 | 152 | // httpFile represents an HTTP file and acts as a bridge 153 | // between file and http.File. 154 | type httpFile struct { 155 | file 156 | 157 | reader *bytes.Reader 158 | isDir bool 159 | dirIdx int 160 | } 161 | 162 | // Read reads bytes into p, returns the number of read bytes. 163 | func (f *httpFile) Read(p []byte) (n int, err error) { 164 | if f.reader == nil && f.isDir { 165 | return 0, io.EOF 166 | } 167 | return f.reader.Read(p) 168 | } 169 | 170 | // Seek seeks to the offset. 171 | func (f *httpFile) Seek(offset int64, whence int) (ret int64, err error) { 172 | return f.reader.Seek(offset, whence) 173 | } 174 | 175 | // Stat stats the file. 176 | func (f *httpFile) Stat() (os.FileInfo, error) { 177 | return f, nil 178 | } 179 | 180 | // IsDir returns true if the file location represents a directory. 181 | func (f *httpFile) IsDir() bool { 182 | return f.isDir 183 | } 184 | 185 | // Readdir returns an empty slice of files, directory 186 | // listing is disabled. 187 | func (f *httpFile) Readdir(count int) ([]os.FileInfo, error) { 188 | var fis []os.FileInfo 189 | if !f.isDir { 190 | return fis, nil 191 | } 192 | di, ok := f.FileInfo.(dirInfo) 193 | if !ok { 194 | return nil, fmt.Errorf("failed to read directory: %q", f.Name()) 195 | } 196 | 197 | // If count is positive, the specified number of files will be returned, 198 | // and if non-positive, all remaining files will be returned. 199 | // The reading position of which file is returned is held in dirIndex. 200 | fnames := f.file.fs.dirs[di.name] 201 | flen := len(fnames) 202 | 203 | // If dirIdx reaches the end and the count is a positive value, 204 | // an io.EOF error is returned. 205 | // In other cases, no error will be returned even if, for example, 206 | // you specified more counts than the number of remaining files. 207 | start := f.dirIdx 208 | if start >= flen && count > 0 { 209 | return fis, io.EOF 210 | } 211 | var end int 212 | if count <= 0 { 213 | end = flen 214 | } else { 215 | end = start + count 216 | } 217 | if end > flen { 218 | end = flen 219 | } 220 | for i := start; i < end; i++ { 221 | fis = append(fis, f.file.fs.files[path.Join(di.name, fnames[i])].FileInfo) 222 | } 223 | f.dirIdx += len(fis) 224 | return fis, nil 225 | } 226 | 227 | func (f *httpFile) Close() error { 228 | return nil 229 | } 230 | 231 | func unzip(zf *zip.File) ([]byte, error) { 232 | rc, err := zf.Open() 233 | if err != nil { 234 | return nil, err 235 | } 236 | defer rc.Close() 237 | return ioutil.ReadAll(rc) 238 | } 239 | -------------------------------------------------------------------------------- /fs/fs_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | package fs 15 | 16 | import ( 17 | "archive/zip" 18 | "bytes" 19 | "errors" 20 | "io" 21 | "io/ioutil" 22 | "os" 23 | "path" 24 | "path/filepath" 25 | "reflect" 26 | "sort" 27 | "strings" 28 | "sync" 29 | "testing" 30 | "time" 31 | ) 32 | 33 | type wantFile struct { 34 | data []byte 35 | isDir bool 36 | modTime time.Time 37 | mode os.FileMode 38 | name string 39 | size int64 40 | err error 41 | } 42 | 43 | func TestRegisterWithNamespace(t *testing.T) { 44 | tests := []struct { 45 | description string 46 | assetName string 47 | zipData string 48 | condition func() error 49 | }{ 50 | { 51 | description: "RegisterWithNamespace() should set zipData with assetName to be key", 52 | assetName: "file", 53 | zipData: "file test", 54 | condition: func() error { 55 | data, ok := zipData["file"] 56 | if !ok { 57 | return errors.New("fail to register zipData") 58 | } 59 | if data != "file test" { 60 | return errors.New("fail to register zipData[\"file\"]") 61 | } 62 | return nil 63 | }, 64 | }, 65 | { 66 | description: "zipData[\"default\"] should be able to open by Open()", 67 | assetName: "default", 68 | zipData: mustZipTree("../testdata/file"), 69 | condition: func() error { 70 | fs, err := New() 71 | if err != nil { 72 | return err 73 | } 74 | if _, err := fs.Open("/file.txt"); err != nil { 75 | return err 76 | } 77 | return nil 78 | }, 79 | }, 80 | { 81 | description: "zipData[\"foo\"] should be able to open by Open()", 82 | assetName: "foo", 83 | zipData: mustZipTree("../testdata/file"), 84 | condition: func() error { 85 | fs, err := NewWithNamespace("foo") 86 | if err != nil { 87 | return err 88 | } 89 | if _, err := fs.Open("/file.txt"); err != nil { 90 | return err 91 | } 92 | return nil 93 | }, 94 | }, 95 | } 96 | for _, tc := range tests { 97 | t.Run(tc.description, func(t *testing.T) { 98 | RegisterWithNamespace(tc.assetName, tc.zipData) 99 | if err := tc.condition(); err != nil { 100 | t.Error(err) 101 | } 102 | }) 103 | delete(zipData, tc.assetName) 104 | } 105 | } 106 | 107 | func TestOpen(t *testing.T) { 108 | fileTxtHeader := mustFileHeader("../testdata/file/file.txt") 109 | pixelGifHeader := mustFileHeader("../testdata/image/pixel.gif") 110 | indexHTMLHeader := mustFileHeader("../testdata/index/index.html") 111 | subdirIndexHTMLHeader := mustFileHeader("../testdata/index/sub_dir/index.html") 112 | deepAHTMLHeader := mustFileHeader("../testdata/deep/a") 113 | deepCHTMLHeader := mustFileHeader("../testdata/deep/aa/bb/c") 114 | tests := []struct { 115 | description string 116 | zipData string 117 | wantFiles map[string]wantFile 118 | }{ 119 | { 120 | description: "Files should retain their original file mode and modified time", 121 | zipData: mustZipTree("../testdata/file"), 122 | wantFiles: map[string]wantFile{ 123 | "/file.txt": { 124 | data: mustReadFile("../testdata/file/file.txt"), 125 | isDir: false, 126 | modTime: fileTxtHeader.ModTime(), 127 | mode: fileTxtHeader.Mode(), 128 | name: fileTxtHeader.Name, 129 | size: int64(fileTxtHeader.UncompressedSize64), 130 | }, 131 | }, 132 | }, 133 | { 134 | description: "Images should successfully unpack", 135 | zipData: mustZipTree("../testdata/image"), 136 | wantFiles: map[string]wantFile{ 137 | "/pixel.gif": { 138 | data: mustReadFile("../testdata/image/pixel.gif"), 139 | isDir: false, 140 | modTime: pixelGifHeader.ModTime(), 141 | mode: pixelGifHeader.Mode(), 142 | name: pixelGifHeader.Name, 143 | size: int64(pixelGifHeader.UncompressedSize64), 144 | }, 145 | }, 146 | }, 147 | { 148 | description: "'index.html' files should be returned at their original path and their directory path", 149 | zipData: mustZipTree("../testdata/index"), 150 | wantFiles: map[string]wantFile{ 151 | "/index.html": { 152 | data: mustReadFile("../testdata/index/index.html"), 153 | isDir: false, 154 | modTime: indexHTMLHeader.ModTime(), 155 | mode: indexHTMLHeader.Mode(), 156 | name: indexHTMLHeader.Name, 157 | size: int64(indexHTMLHeader.UncompressedSize64), 158 | }, 159 | "/sub_dir/index.html": { 160 | data: mustReadFile("../testdata/index/sub_dir/index.html"), 161 | isDir: false, 162 | modTime: subdirIndexHTMLHeader.ModTime(), 163 | mode: subdirIndexHTMLHeader.Mode(), 164 | name: subdirIndexHTMLHeader.Name, 165 | size: int64(subdirIndexHTMLHeader.UncompressedSize64), 166 | }, 167 | "/": { 168 | isDir: true, 169 | mode: os.ModeDir | 0755, 170 | name: "/", 171 | }, 172 | "/sub_dir": { 173 | isDir: true, 174 | mode: os.ModeDir | 0755, 175 | name: "/sub_dir", 176 | }, 177 | }, 178 | }, 179 | { 180 | description: "Missing files should return os.ErrNotExist", 181 | zipData: mustZipTree("../testdata/file"), 182 | wantFiles: map[string]wantFile{ 183 | "/missing.txt": { 184 | err: os.ErrNotExist, 185 | }, 186 | }, 187 | }, 188 | { 189 | description: "listed all sub directories in deep directory", 190 | zipData: mustZipTree("../testdata/deep"), 191 | wantFiles: map[string]wantFile{ 192 | "/a": { 193 | data: mustReadFile("../testdata/deep/a"), 194 | isDir: false, 195 | modTime: deepAHTMLHeader.ModTime(), 196 | mode: deepAHTMLHeader.Mode(), 197 | name: deepAHTMLHeader.Name, 198 | size: int64(deepAHTMLHeader.UncompressedSize64), 199 | }, 200 | "/aa/bb/c": { 201 | data: mustReadFile("../testdata/deep/aa/bb/c"), 202 | isDir: false, 203 | modTime: deepCHTMLHeader.ModTime(), 204 | mode: deepCHTMLHeader.Mode(), 205 | name: deepCHTMLHeader.Name, 206 | size: int64(deepCHTMLHeader.UncompressedSize64), 207 | }, 208 | "/": { 209 | isDir: true, 210 | mode: os.ModeDir | 0755, 211 | name: "/", 212 | }, 213 | "/aa": { 214 | isDir: true, 215 | mode: os.ModeDir | 0755, 216 | name: "/aa", 217 | }, 218 | "/aa/bb": { 219 | isDir: true, 220 | mode: os.ModeDir | 0755, 221 | name: "/aa/bb", 222 | }, 223 | }, 224 | }, 225 | { 226 | description: "Paths containing dots should be properly sanitized", 227 | zipData: mustZipTree("../testdata"), 228 | wantFiles: map[string]wantFile{ 229 | "/../file/../file/../file/.//file.txt": { 230 | data: mustReadFile("../testdata/file/file.txt"), 231 | isDir: false, 232 | modTime: fileTxtHeader.ModTime(), 233 | mode: fileTxtHeader.Mode(), 234 | name: fileTxtHeader.Name, 235 | size: int64(fileTxtHeader.UncompressedSize64), 236 | }, 237 | }, 238 | }, 239 | } 240 | for _, tc := range tests { 241 | t.Run(tc.description, func(t *testing.T) { 242 | Register(tc.zipData) 243 | fs, err := New() 244 | if err != nil { 245 | t.Errorf("New() = %v", err) 246 | return 247 | } 248 | for name, wantFile := range tc.wantFiles { 249 | f, err := fs.Open(name) 250 | if wantFile.err != err { 251 | t.Errorf("fs.Open(%v) = %v; want %v", name, err, wantFile.err) 252 | } 253 | if err != nil { 254 | continue 255 | } 256 | if !wantFile.isDir { 257 | b, err := ioutil.ReadAll(f) 258 | if err != nil { 259 | t.Errorf("ioutil.ReadAll(%v) = %v", name, err) 260 | continue 261 | } 262 | if !reflect.DeepEqual(wantFile.data, b) { 263 | t.Errorf("%v data = %q; want %q", name, b, wantFile.data) 264 | } 265 | } 266 | stat, err := f.Stat() 267 | if err != nil { 268 | t.Errorf("Stat(%v) = %v", name, err) 269 | } 270 | if got, want := stat.IsDir(), wantFile.isDir; got != want { 271 | t.Errorf("IsDir(%v) = %t; want %t", name, got, want) 272 | } 273 | if got, want := stat.ModTime(), wantFile.modTime; got != want { 274 | t.Errorf("ModTime(%v) = %v; want %v", name, got, want) 275 | } 276 | if got, want := stat.Mode(), wantFile.mode; got != want { 277 | t.Errorf("Mode(%v) = %v; want %v", name, got, want) 278 | } 279 | if got, want := stat.Name(), path.Base(wantFile.name); got != want { 280 | t.Errorf("Name(%v) = %v; want %v", name, got, want) 281 | } 282 | if got, want := stat.Size(), wantFile.size; got != want { 283 | t.Errorf("Size(%v) = %v; want %v", name, got, want) 284 | } 285 | } 286 | }) 287 | } 288 | } 289 | 290 | func TestWalk(t *testing.T) { 291 | Register(mustZipTree("../testdata/deep")) 292 | fs, err := New() 293 | if err != nil { 294 | t.Errorf("New() = %v", err) 295 | return 296 | } 297 | var files []string 298 | err = Walk(fs, "/", func(path string, fi os.FileInfo, err error) error { 299 | if err != nil { 300 | return err 301 | } 302 | files = append(files, path) 303 | return nil 304 | }) 305 | if err != nil { 306 | t.Errorf("Walk(fs, /) = %v", err) 307 | return 308 | } 309 | wantDirs := []string{ 310 | "/", 311 | "/a", 312 | "/aa", 313 | "/aa/bb", 314 | "/aa/bb/c", 315 | } 316 | sort.Strings(files) 317 | if !reflect.DeepEqual(files, wantDirs) { 318 | t.Errorf("got: %v\nexpect: %v", files, wantDirs) 319 | } 320 | } 321 | 322 | func TestHTTPFile_Readdir(t *testing.T) { 323 | Register(mustZipTree("../testdata/readdir")) 324 | fs, err := New() 325 | if err != nil { 326 | t.Errorf("New() = %v", err) 327 | return 328 | } 329 | t.Run("Readdir(-1)", func(t *testing.T) { 330 | dir, err := fs.Open("/") 331 | if err != nil { 332 | t.Errorf("fs.Open(/) = %v", err) 333 | return 334 | } 335 | fis, err := dir.Readdir(-1) 336 | if err != nil { 337 | t.Errorf("dir.Readdir(-1) = %v", err) 338 | return 339 | } 340 | if len(fis) != 3 { 341 | t.Errorf("got: %d, expect: 3", len(fis)) 342 | } 343 | }) 344 | t.Run("Readdir(0)", func(t *testing.T) { 345 | dir, err := fs.Open("/") 346 | if err != nil { 347 | t.Errorf("fs.Open(/) = %v", err) 348 | return 349 | } 350 | fis, err := dir.Readdir(0) 351 | if err != nil { 352 | t.Errorf("dir.Readdir(0) = %v", err) 353 | return 354 | } 355 | if len(fis) != 3 { 356 | t.Errorf("got: %d, expect: 3", len(fis)) 357 | } 358 | }) 359 | t.Run("Readdir(>0)", func(t *testing.T) { 360 | dir, err := fs.Open("/") 361 | if err != nil { 362 | t.Errorf("fs.Open(/) = %v", err) 363 | return 364 | } 365 | fis, err := dir.Readdir(1) 366 | if err != nil { 367 | t.Errorf("dir.Readdir(1) = %v", err) 368 | return 369 | } 370 | if len(fis) != 1 { 371 | t.Errorf("got: %d, expect: 1", len(fis)) 372 | } 373 | if fis[0].Name() != "aa" { 374 | t.Errorf("got: %s, expect: aa", fis[0].Name()) 375 | } 376 | fis, err = dir.Readdir(1) 377 | if err != nil { 378 | t.Errorf("dir.Readdir(1) = %v", err) 379 | return 380 | } 381 | if len(fis) != 1 { 382 | t.Errorf("got: %d, expect: 1", len(fis)) 383 | } 384 | if fis[0].Name() != "bb" { 385 | t.Errorf("got: %s, expect: bb", fis[0].Name()) 386 | } 387 | fis, err = dir.Readdir(-1) // take rest entries 388 | if err != nil { 389 | t.Errorf("dir.Readdir(1) = %v", err) 390 | return 391 | } 392 | if len(fis) != 1 { 393 | t.Errorf("got: %d, expect: 1", len(fis)) 394 | } 395 | if fis[0].Name() != "cc" { 396 | t.Errorf("got: %s, expect: cc", fis[0].Name()) 397 | } 398 | fis, err = dir.Readdir(-1) 399 | if err != nil { 400 | t.Errorf("dir.Readdir(1) = %v", err) 401 | return 402 | } 403 | if len(fis) != 0 { 404 | t.Errorf("got: %d, expect: 0", len(fis)) 405 | } 406 | fis, err = dir.Readdir(1) 407 | if err != io.EOF { 408 | t.Errorf("error should be io.EOF, but: %s", err) 409 | return 410 | } 411 | if len(fis) != 0 { 412 | t.Errorf("got: %d, expect: 0", len(fis)) 413 | } 414 | }) 415 | } 416 | 417 | // Test that calling Open by many goroutines concurrently continues 418 | // to return the expected result. 419 | func TestOpen_Parallel(t *testing.T) { 420 | indexHTMLData := mustReadFile("../testdata/index/index.html") 421 | Register(mustZipTree("../testdata/index")) 422 | fs, err := New() 423 | if err != nil { 424 | t.Fatalf("New() = %v", err) 425 | } 426 | wg := sync.WaitGroup{} 427 | for i := 0; i < 128; i++ { 428 | wg.Add(1) 429 | go func() { 430 | defer wg.Done() 431 | name := "/index.html" 432 | f, err := fs.Open(name) 433 | if err != nil { 434 | t.Errorf("fs.Open(%v) = %v", name, err) 435 | return 436 | } 437 | b, err := ioutil.ReadAll(f) 438 | if err != nil { 439 | t.Errorf("ioutil.ReadAll(%v) = %v", name, err) 440 | return 441 | } 442 | if !reflect.DeepEqual(indexHTMLData, b) { 443 | t.Errorf("%v data = %q; want %q", name, b, indexHTMLData) 444 | } 445 | }() 446 | } 447 | wg.Wait() 448 | } 449 | 450 | // mustZipTree walks on the source path and returns the zipped file contents 451 | // as a string. Panics on any errors. 452 | func mustZipTree(srcPath string) string { 453 | var out bytes.Buffer 454 | w := zip.NewWriter(&out) 455 | if err := filepath.Walk(srcPath, func(path string, fi os.FileInfo, err error) error { 456 | if err != nil { 457 | return err 458 | } 459 | // Ignore directories and hidden files. 460 | // No entry is needed for directories in a zip file. 461 | // Each file is represented with a path, no directory 462 | // entities are required to build the hierarchy. 463 | if fi.IsDir() || strings.HasPrefix(fi.Name(), ".") { 464 | return nil 465 | } 466 | relPath, err := filepath.Rel(srcPath, path) 467 | if err != nil { 468 | return err 469 | } 470 | b, err := ioutil.ReadFile(path) 471 | if err != nil { 472 | return err 473 | } 474 | fHeader, err := zip.FileInfoHeader(fi) 475 | if err != nil { 476 | return err 477 | } 478 | fHeader.Name = filepath.ToSlash(relPath) 479 | fHeader.Method = zip.Deflate 480 | f, err := w.CreateHeader(fHeader) 481 | if err != nil { 482 | return err 483 | } 484 | _, err = f.Write(b) 485 | return err 486 | }); err != nil { 487 | panic(err) 488 | } 489 | if err := w.Close(); err != nil { 490 | panic(err) 491 | } 492 | return out.String() 493 | } 494 | 495 | // mustReadFile returns the file contents. Panics on any errors. 496 | func mustReadFile(filename string) []byte { 497 | b, err := ioutil.ReadFile(filename) 498 | if err != nil { 499 | panic(err) 500 | } 501 | return b 502 | } 503 | 504 | // mustFileHeader returns the zip file info header. Panics on any errors. 505 | func mustFileHeader(filename string) *zip.FileHeader { 506 | info, err := os.Stat(filename) 507 | if err != nil { 508 | panic(err) 509 | } 510 | header, err := zip.FileInfoHeader(info) 511 | if err != nil { 512 | panic(err) 513 | } 514 | return header 515 | } 516 | -------------------------------------------------------------------------------- /fs/walk.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package fs 16 | 17 | import ( 18 | "bytes" 19 | "io" 20 | "net/http" 21 | "path" 22 | "path/filepath" 23 | ) 24 | 25 | // Walk walks the file tree rooted at root, 26 | // calling walkFn for each file or directory in the tree, including root. 27 | // All errors that arise visiting files and directories are filtered by walkFn. 28 | // 29 | // As with filepath.Walk, if the walkFn returns filepath.SkipDir, then the directory is skipped. 30 | func Walk(hfs http.FileSystem, root string, walkFn filepath.WalkFunc) error { 31 | dh, err := hfs.Open(root) 32 | if err != nil { 33 | return err 34 | } 35 | di, err := dh.Stat() 36 | if err != nil { 37 | return err 38 | } 39 | fis, err := dh.Readdir(-1) 40 | dh.Close() 41 | if err = walkFn(root, di, err); err != nil { 42 | if err == filepath.SkipDir { 43 | return nil 44 | } 45 | return err 46 | } 47 | for _, fi := range fis { 48 | fn := path.Join(root, fi.Name()) 49 | if fi.IsDir() { 50 | if err = Walk(hfs, fn, walkFn); err != nil { 51 | if err == filepath.SkipDir { 52 | continue 53 | } 54 | return err 55 | } 56 | continue 57 | } 58 | if err = walkFn(fn, fi, nil); err != nil { 59 | if err == filepath.SkipDir { 60 | continue 61 | } 62 | return err 63 | } 64 | } 65 | return nil 66 | } 67 | 68 | // ReadFile reads the contents of the file of hfs specified by name. 69 | // Just as ioutil.ReadFile does. 70 | func ReadFile(hfs http.FileSystem, name string) ([]byte, error) { 71 | fh, err := hfs.Open(name) 72 | if err != nil { 73 | return nil, err 74 | } 75 | var buf bytes.Buffer 76 | _, err = io.Copy(&buf, fh) 77 | fh.Close() 78 | return buf.Bytes(), err 79 | } 80 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/rakyll/statik 2 | 3 | go 1.12 4 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rakyll/statik/e5988123cababc33df1352cba7df8535f1337326/go.sum -------------------------------------------------------------------------------- /statik.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package contains a program that generates code to register 16 | // a directory and its contents as zip data for statik file system. 17 | package main 18 | 19 | import ( 20 | "archive/zip" 21 | "bytes" 22 | "flag" 23 | "fmt" 24 | "io" 25 | "io/ioutil" 26 | "os" 27 | "path" 28 | spath "path" 29 | "path/filepath" 30 | "strings" 31 | "time" 32 | "unicode" 33 | 34 | "github.com/rakyll/statik/fs" 35 | ) 36 | 37 | const nameSourceFile = "statik.go" 38 | 39 | var namePackage string 40 | 41 | var ( 42 | flagSrc = flag.String("src", path.Join(".", "public"), "") 43 | flagDest = flag.String("dest", ".", "") 44 | flagNoMtime = flag.Bool("m", false, "") 45 | flagNoCompress = flag.Bool("Z", false, "") 46 | flagForce = flag.Bool("f", false, "") 47 | flagTags = flag.String("tags", "", "") 48 | flagPkg = flag.String("p", "statik", "") 49 | flagNamespace = flag.String("ns", "default", "") 50 | flagPkgCmt = flag.String("c", "", "") 51 | flagInclude = flag.String("include", "*.*", "") 52 | ) 53 | 54 | const helpText = `statik [options] 55 | 56 | Options: 57 | -src The source directory of the assets, "public" by default. 58 | -dest The destination directory of the generated package, "." by default. 59 | 60 | -ns The namespace where assets will exist, "default" by default. 61 | -f Override destination if it already exists, false by default. 62 | -include Wildcard to filter files to include, "*.*" by default. 63 | -m Ignore modification times for deterministic output, false by default. 64 | -Z Do not use compression, false by default. 65 | 66 | -p Name of the generated package, "statik" by default. 67 | -tags Build tags for the generated package. 68 | -c Godoc for the generated package. 69 | 70 | -help Prints this text. 71 | 72 | Examples: 73 | 74 | Generates a statik package from ./assets directory. Overrides 75 | if there is already an existing package. 76 | 77 | $ statik -src=assets -f 78 | 79 | Generates a statik package only with the ".js" files 80 | from the ./public directory. 81 | 82 | $ statik -include=*.js 83 | ` 84 | 85 | // mtimeDate holds the arbitrary mtime that we assign to files when 86 | // flagNoMtime is set. 87 | var mtimeDate = time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC) 88 | 89 | func main() { 90 | flag.Usage = help 91 | flag.Parse() 92 | 93 | namePackage = *flagPkg 94 | 95 | file, err := generateSource(*flagSrc, *flagInclude) 96 | if err != nil { 97 | exitWithError(err) 98 | } 99 | 100 | destDir := path.Join(*flagDest, namePackage) 101 | err = os.MkdirAll(destDir, 0755) 102 | if err != nil { 103 | exitWithError(err) 104 | } 105 | 106 | err = rename(file.Name(), path.Join(destDir, nameSourceFile)) 107 | if err != nil { 108 | exitWithError(err) 109 | } 110 | } 111 | 112 | // rename tries to os.Rename, but fall backs to copying from src 113 | // to dest and unlink the source if os.Rename fails. 114 | func rename(src, dest string) error { 115 | // Try to rename generated source. 116 | if err := os.Rename(src, dest); err == nil { 117 | return nil 118 | } 119 | // If the rename failed (might do so due to temporary file residing on a 120 | // different device), try to copy byte by byte. 121 | rc, err := os.Open(src) 122 | if err != nil { 123 | return err 124 | } 125 | defer func() { 126 | rc.Close() 127 | os.Remove(src) // ignore the error, source is in tmp. 128 | }() 129 | 130 | if _, err = os.Stat(dest); !os.IsNotExist(err) { 131 | if *flagForce { 132 | if err = os.Remove(dest); err != nil { 133 | return fmt.Errorf("file %q could not be deleted", dest) 134 | } 135 | } else { 136 | return fmt.Errorf("file %q already exists; use -f to overwrite", dest) 137 | } 138 | } 139 | 140 | wc, err := os.Create(dest) 141 | if err != nil { 142 | return err 143 | } 144 | defer wc.Close() 145 | 146 | if _, err = io.Copy(wc, rc); err != nil { 147 | // Delete remains of failed copy attempt. 148 | os.Remove(dest) 149 | } 150 | return err 151 | } 152 | 153 | // Check if an array contains an item 154 | func contains(slice []string, item string) bool { 155 | set := make(map[string]struct{}, len(slice)) 156 | for _, s := range slice { 157 | set[s] = struct{}{} 158 | } 159 | 160 | _, ok := set[item] 161 | return ok 162 | } 163 | 164 | // Match a path with some of inclusions 165 | func match(incs []string, path string) (bool, error) { 166 | var err error 167 | for _, inc := range incs { 168 | matches, e := filepath.Glob(spath.Join(filepath.Dir(path), inc)) 169 | 170 | if e != nil { 171 | err = e 172 | } 173 | 174 | if matches != nil && len(matches) != 0 && contains(matches, path) { 175 | return true, nil 176 | } 177 | } 178 | 179 | return false, err 180 | } 181 | 182 | // Walks on the source path and generates source code 183 | // that contains source directory's contents as zip contents. 184 | // Generates source registers generated zip contents data to 185 | // be read by the statik/fs HTTP file system. 186 | func generateSource(srcPath string, includes string) (file *os.File, err error) { 187 | var ( 188 | buffer bytes.Buffer 189 | zipWriter io.Writer 190 | ) 191 | 192 | zipWriter = &buffer 193 | f, err := ioutil.TempFile("", namePackage) 194 | if err != nil { 195 | return 196 | } 197 | 198 | zipWriter = io.MultiWriter(zipWriter, f) 199 | defer f.Close() 200 | 201 | w := zip.NewWriter(zipWriter) 202 | if err = filepath.Walk(srcPath, func(path string, fi os.FileInfo, err error) error { 203 | if err != nil { 204 | return err 205 | } 206 | // Ignore directories and hidden files. 207 | // No entry is needed for directories in a zip file. 208 | // Each file is represented with a path, no directory 209 | // entities are required to build the hierarchy. 210 | if fi.IsDir() || strings.HasPrefix(fi.Name(), ".") { 211 | return nil 212 | } 213 | relPath, err := filepath.Rel(srcPath, path) 214 | if err != nil { 215 | return err 216 | } 217 | b, err := ioutil.ReadFile(path) 218 | if err != nil { 219 | return err 220 | } 221 | 222 | incs := strings.Split(includes, ",") 223 | 224 | if b, e := match(incs, path); e != nil { 225 | return err 226 | } else if !b { 227 | return nil 228 | } 229 | 230 | fHeader, err := zip.FileInfoHeader(fi) 231 | if err != nil { 232 | return err 233 | } 234 | if *flagNoMtime { 235 | // Always use the same modification time so that 236 | // the output is deterministic with respect to the file contents. 237 | // Do NOT use fHeader.Modified as it only works on go >= 1.10 238 | fHeader.SetModTime(mtimeDate) 239 | } 240 | fHeader.Name = filepath.ToSlash(relPath) 241 | if !*flagNoCompress { 242 | fHeader.Method = zip.Deflate 243 | } 244 | f, err := w.CreateHeader(fHeader) 245 | if err != nil { 246 | return err 247 | } 248 | _, err = f.Write(b) 249 | return err 250 | }); err != nil { 251 | return 252 | } 253 | if err = w.Close(); err != nil { 254 | return 255 | } 256 | 257 | var tags string 258 | if *flagTags != "" { 259 | tags = "\n// +build " + *flagTags + "\n" 260 | } 261 | 262 | var comment string 263 | if *flagPkgCmt != "" { 264 | comment = "\n" + commentLines(*flagPkgCmt) 265 | } 266 | 267 | // e.g.) 268 | // assetNamespaceIdentify is "AbcDeF_G" 269 | // when assetNamespace is "abc de f-g" 270 | assetNamespace := *flagNamespace 271 | assetNamespaceIdentify := toSymbolSafe(assetNamespace) 272 | 273 | // then embed it as a quoted string 274 | var qb bytes.Buffer 275 | fmt.Fprintf(&qb, `// Code generated by statik. DO NOT EDIT. 276 | %s%s 277 | package %s 278 | 279 | import ( 280 | "github.com/rakyll/statik/fs" 281 | ) 282 | 283 | `, tags, comment, namePackage) 284 | if !fs.IsDefaultNamespace(assetNamespace) { 285 | fmt.Fprintf(&qb, ` 286 | const %s = "%s" // static asset namespace 287 | `, assetNamespaceIdentify, assetNamespace) 288 | } 289 | fmt.Fprint(&qb, ` 290 | func init() { 291 | data := "`) 292 | FprintZipData(&qb, buffer.Bytes()) 293 | if fs.IsDefaultNamespace(assetNamespace) { 294 | fmt.Fprint(&qb, `" 295 | fs.Register(data) 296 | } 297 | `) 298 | 299 | } else { 300 | fmt.Fprintf(&qb, `" 301 | fs.RegisterWithNamespace("%s", data) 302 | } 303 | `, assetNamespace) 304 | } 305 | 306 | if err = ioutil.WriteFile(f.Name(), qb.Bytes(), 0644); err != nil { 307 | return 308 | } 309 | return f, nil 310 | } 311 | 312 | // FprintZipData converts zip binary contents to a string literal. 313 | func FprintZipData(dest *bytes.Buffer, zipData []byte) { 314 | for _, b := range zipData { 315 | if b == '\n' { 316 | dest.WriteString(`\n`) 317 | continue 318 | } 319 | if b == '\\' { 320 | dest.WriteString(`\\`) 321 | continue 322 | } 323 | if b == '"' { 324 | dest.WriteString(`\"`) 325 | continue 326 | } 327 | if (b >= 32 && b <= 126) || b == '\t' { 328 | dest.WriteByte(b) 329 | continue 330 | } 331 | fmt.Fprintf(dest, "\\x%02x", b) 332 | } 333 | } 334 | 335 | // comment lines prefixes each line in lines with "// ". 336 | func commentLines(lines string) string { 337 | lines = "// " + strings.Replace(lines, "\n", "\n// ", -1) 338 | return lines 339 | } 340 | 341 | // Prints out the error message and exists with a non-success signal. 342 | func exitWithError(err error) { 343 | fmt.Println(err) 344 | os.Exit(1) 345 | } 346 | 347 | // convert src to symbol safe string with upper camel case 348 | func toSymbolSafe(str string) string { 349 | isBeforeRuneNoGeneralCase := false 350 | replace := func(r rune) rune { 351 | if unicode.IsLetter(r) { 352 | if isBeforeRuneNoGeneralCase { 353 | isBeforeRuneNoGeneralCase = true 354 | return r 355 | } else { 356 | isBeforeRuneNoGeneralCase = true 357 | return unicode.ToTitle(r) 358 | } 359 | } else if unicode.IsDigit(r) { 360 | if isBeforeRuneNoGeneralCase { 361 | isBeforeRuneNoGeneralCase = true 362 | return r 363 | } else { 364 | isBeforeRuneNoGeneralCase = false 365 | return -1 366 | } 367 | } else { 368 | isBeforeRuneNoGeneralCase = false 369 | return -1 370 | } 371 | } 372 | return strings.TrimSpace(strings.Map(replace, str)) 373 | } 374 | 375 | func help() { 376 | fmt.Println(helpText) 377 | os.Exit(1) 378 | } 379 | -------------------------------------------------------------------------------- /statik_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func TestToSymbolSafe(t *testing.T) { 6 | testCase := [][]string{ 7 | {"abc", "Abc"}, 8 | {"_abc", "Abc"}, 9 | {"3abc", "Abc"}, 10 | {"abc3", "Abc3"}, 11 | {"/abc", "Abc"}, 12 | {"abc abc", "AbcAbc"}, 13 | } 14 | for i, test := range testCase { 15 | got := toSymbolSafe(test[0]) 16 | wont := test[1] 17 | if got != wont { 18 | t.Errorf("#%02d toSymbolSafe(%s) => %s != %s", i, test[0], got, wont) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /testdata/deep/a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rakyll/statik/e5988123cababc33df1352cba7df8535f1337326/testdata/deep/a -------------------------------------------------------------------------------- /testdata/deep/aa/bb/c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rakyll/statik/e5988123cababc33df1352cba7df8535f1337326/testdata/deep/aa/bb/c -------------------------------------------------------------------------------- /testdata/file/file.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rakyll/statik/e5988123cababc33df1352cba7df8535f1337326/testdata/file/file.txt -------------------------------------------------------------------------------- /testdata/image/pixel.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rakyll/statik/e5988123cababc33df1352cba7df8535f1337326/testdata/image/pixel.gif -------------------------------------------------------------------------------- /testdata/index/index.html: -------------------------------------------------------------------------------- 1 |