diff --git a/.drone.yml b/.drone.yml index 1ba6f730..2e375596 100644 --- a/.drone.yml +++ b/.drone.yml @@ -42,6 +42,17 @@ pipeline: when: event: tag + artifacts_tag: + image: plugins/docker + dockerfile: Dockerfile.artifacts + build_args: + - DRONE=${DRONE} + - DRONE_TAG=${DRONE_TAG} + - DRONE_COMMIT=${DRONE_COMMIT} + - GITHUB_TOKEN=${GITHUB_TOKEN} + when: + event: tag + docker_branch: image: plugins/docker repo: umputun/remark42 diff --git a/Dockerfile.artifacts b/Dockerfile.artifacts new file mode 100644 index 00000000..2ab65127 --- /dev/null +++ b/Dockerfile.artifacts @@ -0,0 +1,87 @@ +FROM node:10.6-alpine as build-frontend-deps + +ARG CI +ENV SKIP_FRONTEND_TEST=true + +RUN apk add --no-cache --update git +ADD web/package.json /srv/web/package.json +ADD web/package-lock.json /srv/web/package-lock.json +RUN cd /srv/web && CI=true npm ci + +FROM node:10.6-alpine as build-frontend + +ARG CI +ARG NODE_ENV=production +ENV SKIP_FRONTEND_TEST=true + +COPY --from=build-frontend-deps /srv/web/node_modules /srv/web/node_modules +ADD web /srv/web +RUN cd /srv/web && \ + npm run build && \ + rm -rf ./node_modules + + +FROM umputun/baseimage:buildgo-latest as build-backend + +ARG GITHUB_TOKEN +ENV SKIP_BACKEND_TEST=true + +WORKDIR /go/src/github.com/umputun/remark/backend +ADD backend /go/src/github.com/umputun/remark/backend +ADD README.md /go/src/github.com/umputun/remark/ +ADD LICENSE /go/src/github.com/umputun/remark/ + +COPY --from=build-frontend /srv/web web + +RUN \ + go get -v github.com/rakyll/statik && \ + statik --src=/go/src/github.com/umputun/remark/backend/web --dest=/go/src/github.com/umputun/remark/backend/app/rest -p api -f && \ + ls -la /go/src/github.com/umputun/remark/backend/app/rest/api/statik.go + +# if DRONE presented use DRONE_* git env to make version +RUN \ + if [ -z "$DRONE" ] ; then \ + echo "runs outside of drone" && version="local"; \ + else version=${DRONE_TAG}${DRONE_BRANCH}${DRONE_PULL_REQUEST}-${DRONE_COMMIT:0:7}-$(date +%Y%m%d-%H:%M:%S); fi && \ + echo "version=$version" && \ + GOOS=linux GOARCH=amd64 go build -o remark42.linux-amd64 -ldflags "-X main.revision=${version} -s -w" ./app && \ + GOOS=linux GOARCH=386 go build -o remark42.linux-386 -ldflags "-X main.revision=${version} -s -w" ./app && \ + GOOS=linux GOARCH=arm64 go build -o remark42.linux-arm64 -ldflags "-X main.revision=${version} -s -w" ./app && \ + GOOS=windows GOARCH=amd64 go build -o remark42.windows-amd64.exe -ldflags "-X main.revision=${version} -s -w" ./app && \ + GOOS=darwin GOARCH=amd64 go build -o remark42.darwin-amd64 -ldflags "-X main.revision=${version} -s -w" ./app + +RUN \ + if [ -z "$DRONE_TAG" ] ; then \ + echo "runs outside of drone" && tag=""; \ + else tag=_${DRONE_TAG}; fi && \ + apk add --no-cache --update zip && \ + tar cvzf remark42${tag}.linux-amd64.tar.gz remark42.linux-amd64 ../LICENSE ../README.md && \ + tar cvzf remark42${tag}.linux-386.tar.gz remark42.linux-386 ../LICENSE ../README.md && \ + tar cvzf remark42${tag}.linux-arm64.tar.gz remark42.linux-arm64 ../LICENSE ../README.md && \ + tar cvzf remark42${tag}.darwin-amd64.tar.gz remark42.darwin-amd64 ../LICENSE ../README.md && \ + zip remark${tag}.windows-amd64.zip remark42.windows-amd64.exe ../LICENSE ../README.md + +# upload to github +RUN \ + if [ -z "$DRONE_TAG" ] ; then \ + echo "skip upload to github" ; \ + else \ + curl -H "Authorization: token $GITHUB_TOKEN" -H "Accept: application/vnd.github.manifold-preview" \ + -H "Content-Type: application/gzip" --data-binary @remark42_${DRONE_TAG}.linux-amd64.tar.gz \ + "https://uploads.github.com/repos/umputun/remark/releases/${DRONE_TAG}/assets?name=remark_${DRONE_TAG}.linux-amd64.tar.gz" && \ + curl -H "Authorization: token $GITHUB_TOKEN" -H "Accept: application/vnd.github.manifold-preview" \ + -H "Content-Type: application/gzip" --data-binary @remark42_${DRONE_TAG}.linux-386.tar.gz \ + "https://uploads.github.com/repos/umputun/remark/releases/${DRONE_TAG}/assets?name=remark_${DRONE_TAG}.linux-386.tar.gz" && \ + curl -H "Authorization: token $GITHUB_TOKEN" -H "Accept: application/vnd.github.manifold-preview" \ + -H "Content-Type: application/gzip" --data-binary @remark42_${DRONE_TAG}.linux-arm64.tar.gz \ + "https://uploads.github.com/repos/umputun/remark/releases/${DRONE_TAG}/assets?name=remark_${DRONE_TAG}.linux-arm64.tar.gz" && \ + curl -H "Authorization: token $GITHUB_TOKEN" -H "Accept: application/vnd.github.manifold-preview" \ + -H "Content-Type: application/gzip" --data-binary @remark42_${DRONE_TAG}.darwin-amd64.tar.gz \ + "https://uploads.github.com/repos/umputun/remark/releases/${DRONE_TAG}/assets?name=remark_${DRONE_TAG}.darwin-amd64.tar.gz" && \ + curl -H "Authorization: token $GITHUB_TOKEN" -H "Accept: application/vnd.github.manifold-preview" \ + -H "Content-Type: application/zip" --data-binary @remark42_${DRONE_TAG}.windows-amd64.zip \ + "https://uploads.github.com/repos/umputun/remark/releases/${DRONE_TAG}/assets?name=remark_${DRONE_TAG}.windows-amd64.zip"; fi + +FROM alpine +COPY --from=build-backend /go/src/github.com/umputun/remark/backend/remark42.* /artifacts/ +CMD ["sleep", "100"] diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..fc5b9690 --- /dev/null +++ b/Makefile @@ -0,0 +1,11 @@ +OS=linux +ARCH=amd64 + +bin: + docker build -f Dockerfile.artifacts -t remark42.bin . + docker run -d --name=remark42.bin remark42.bin + docker cp remark42.bin:/artifacts/remark42.$(OS)-$(ARCH) remark42 + docker rm -f remark42.bin + +docker: + docker build -t umputun/remark42 --build-arg SKIP_FRONTEND_TEST=true --build-arg SKIP_BACKEND_TEST=true . diff --git a/backend/Gopkg.lock b/backend/Gopkg.lock index c07a44c5..26598748 100644 --- a/backend/Gopkg.lock +++ b/backend/Gopkg.lock @@ -237,6 +237,14 @@ revision = "792786c7400a136282c1664665ae0a8db921c6c2" version = "v1.0.0" +[[projects]] + digest = "1:bc91590d3e20673d5e33267fc140e7dadddde0b84f2e9030547ba86859d2d13e" + name = "github.com/rakyll/statik" + packages = ["fs"] + pruneopts = "UT" + revision = "19b88da8fc15428620782ba18f68423130e7ac7d" + version = "v0.1.3" + [[projects]] branch = "master" digest = "1:def689e73e9252f6f7fe66834a76751a41b767e03daab299e607e7226c58a855" @@ -367,6 +375,7 @@ "github.com/nullrocks/identicon", "github.com/patrickmn/go-cache", "github.com/pkg/errors", + "github.com/rakyll/statik/fs", "github.com/stretchr/testify/assert", "github.com/stretchr/testify/require", "golang.org/x/image/draw", diff --git a/backend/app/rest/api/rest.go b/backend/app/rest/api/rest.go index 0a4b71cf..57c18d2d 100644 --- a/backend/app/rest/api/rest.go +++ b/backend/app/rest/api/rest.go @@ -20,6 +20,7 @@ import ( "github.com/go-chi/cors" "github.com/go-chi/render" "github.com/pkg/errors" + "github.com/rakyll/statik/fs" "github.com/umputun/remark/backend/app/migrator" "github.com/umputun/remark/backend/app/rest" @@ -213,11 +214,24 @@ func (s *Rest) routes() chi.Router { return router } -// serves static files from /web +// serves static files from /web or embedded by statik func addFileServer(r chi.Router, path string, root http.FileSystem) { - log.Printf("[INFO] run file server for %s, path %s", root, path) + + var webFS http.Handler + + statikFS, err := fs.New() + if err == nil { + log.Printf("[INFO] run file server for %s, embedded", root) + webFS = http.FileServer(statikFS) + } + if err != nil { + log.Printf("[DEBUG] no embedded assets loaded, %s", err) + log.Printf("[INFO] run file server for %s, path %s", root, path) + webFS = http.FileServer(root) + } + origPath := path - fs := http.StripPrefix(path, http.FileServer(root)) + webFS = http.StripPrefix(path, webFS) if path != "/" && path[len(path)-1] != '/' { r.Get(path, http.RedirectHandler(path+"/", 301).ServeHTTP) path += "/" @@ -231,7 +245,7 @@ func addFileServer(r chi.Router, path string, root http.FileSystem) { http.NotFound(w, r) return } - fs.ServeHTTP(w, r) + webFS.ServeHTTP(w, r) })) } diff --git a/backend/app/rest/api/rest_private_test.go b/backend/app/rest/api/rest_private_test.go index 26d09935..7b775ad8 100644 --- a/backend/app/rest/api/rest_private_test.go +++ b/backend/app/rest/api/rest_private_test.go @@ -24,10 +24,10 @@ func TestRest_Create(t *testing.T) { resp, err := post(t, ts.URL+"/api/v1/comment", `{"text": "test 123", "locator":{"url": "https://radio-t.com/blah1", "site": "radio-t"}}`) assert.Nil(t, err) - require.Equal(t, http.StatusCreated, resp.StatusCode) - b, err := ioutil.ReadAll(resp.Body) assert.Nil(t, err) + require.Equal(t, http.StatusCreated, resp.StatusCode, string(b)) + c := JSON{} err = json.Unmarshal(b, &c) assert.Nil(t, err) @@ -114,6 +114,7 @@ func TestRest_CreateRejected(t *testing.T) { assert.Nil(t, err) assert.Equal(t, 401, resp.StatusCode) } + func TestRest_CreateAndGet(t *testing.T) { srv, ts := prep(t) assert.NotNil(t, srv) @@ -121,7 +122,7 @@ func TestRest_CreateAndGet(t *testing.T) { // create comment resp, err := post(t, ts.URL+"/api/v1/comment", - `{"text": "**test** *123* http://radio-t.com", "locator":{"url": "https://radio-t.com/blah1", "site": "radio-t"}}`) + `{"text": "**test** *123*\n\n http://radio-t.com", "locator":{"url": "https://radio-t.com/blah1", "site": "radio-t"}}`) require.Nil(t, err) require.Equal(t, http.StatusCreated, resp.StatusCode) b, err := ioutil.ReadAll(resp.Body) @@ -138,8 +139,8 @@ func TestRest_CreateAndGet(t *testing.T) { comment := store.Comment{} err = json.Unmarshal([]byte(res), &comment) assert.Nil(t, err) - assert.Equal(t, `
test 123 http://radio-t.com
`+"\n", comment.Text) - assert.Equal(t, "**test** *123* http://radio-t.com", comment.Orig) + assert.Equal(t, "test 123
\n\n\n", comment.Text) + assert.Equal(t, "**test** *123*\n\n http://radio-t.com", comment.Orig) assert.Equal(t, store.User{Name: "developer one", ID: "dev", Picture: "/api/v1/avatar/remark.image", Admin: true, Blocked: false, IP: "dbc7c999343f003f189f70aaf52cc04443f90790"}, comment.User) diff --git a/backend/app/store/comment.go b/backend/app/store/comment.go index 51c5fb38..a3b02557 100644 --- a/backend/app/store/comment.go +++ b/backend/app/store/comment.go @@ -94,7 +94,7 @@ func (c *Comment) SetDeleted(mode DeleteMode) { } } -// Sanitize clean dangerous html/js from the comment, shorten autolinks. +// Sanitize clean dangerous html/js from the comment func (c *Comment) Sanitize() { p := bluemonday.UGCPolicy() p.AllowAttrs("class").Matching(regexp.MustCompile("^language-[a-zA-Z0-9]+$")).OnElements("code") diff --git a/backend/app/store/formater_test.go b/backend/app/store/formater_test.go index 38465114..08685dc1 100644 --- a/backend/app/store/formater_test.go +++ b/backend/app/store/formater_test.go @@ -35,7 +35,7 @@ func TestFormatter_FormatTextNoConvertor(t *testing.T) { func TestFormatter_FormatComment(t *testing.T) { comment := Comment{ - Text: `blah`, + Text: "blah\n\nxyz", User: User{ID: "username"}, ParentID: "p123", ID: "123", @@ -49,7 +49,7 @@ func TestFormatter_FormatComment(t *testing.T) { f := NewCommentFormatter(mockConverter{}) exp := comment - exp.Text = "blah
\n!converted" + exp.Text = "blah
\n\nxyz
\n!converted" assert.Equal(t, exp, f.Format(comment)) } diff --git a/backend/app/store/user.go b/backend/app/store/user.go index eec073e0..626521b4 100644 --- a/backend/app/store/user.go +++ b/backend/app/store/user.go @@ -37,7 +37,7 @@ func HashValue(val string, secret string) string { } key := []byte(secret) h := hmac.New(sha1.New, key) - return hashWithFailback(h, val) + return hashWithFallback(h, val) } // EncodeID hashes id to sha1. The function intentionally left outside of User struct because in some cases @@ -47,11 +47,11 @@ func EncodeID(id string) string { return id // already hashed or empty } h := sha1.New() - return hashWithFailback(h, id) + return hashWithFallback(h, id) } -// hashWithFailback tries to has val with hash.Hash and failback to crc if needed -func hashWithFailback(h hash.Hash, val string) string { +// hashWithFallback tries to has val with hash.Hash and failback to crc if needed +func hashWithFallback(h hash.Hash, val string) string { if _, err := io.WriteString(h, val); err != nil { // fail back to crc64 log.Printf("[WARN] can't hash id %s, %s", val, err) diff --git a/backend/vendor/github.com/rakyll/statik/LICENSE b/backend/vendor/github.com/rakyll/statik/LICENSE new file mode 100644 index 00000000..a4c5efd8 --- /dev/null +++ b/backend/vendor/github.com/rakyll/statik/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2014 Google Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/backend/vendor/github.com/rakyll/statik/fs/fs.go b/backend/vendor/github.com/rakyll/statik/fs/fs.go new file mode 100644 index 00000000..82cda3a8 --- /dev/null +++ b/backend/vendor/github.com/rakyll/statik/fs/fs.go @@ -0,0 +1,146 @@ +// Copyright 2014 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package fs contains an HTTP file system that works with zip contents. +package fs + +import ( + "archive/zip" + "bytes" + "errors" + "fmt" + "io/ioutil" + "net/http" + "os" + "strings" +) + +var zipData string + +// file holds unzipped read-only file contents and file metadata. +type file struct { + os.FileInfo + data []byte +} + +type statikFS struct { + files map[string]file +} + +// Register registers zip contents data, later used to initialize +// the statik file system. +func Register(data string) { + zipData = data +} + +// New creates a new file system with the registered zip contents data. +// It unzips all files and stores them in an in-memory map. +func New() (http.FileSystem, error) { + if zipData == "" { + return nil, errors.New("statik/fs: no zip data registered") + } + zipReader, err := zip.NewReader(strings.NewReader(zipData), int64(len(zipData))) + if err != nil { + return nil, err + } + files := make(map[string]file) + for _, zipFile := range zipReader.File { + unzipped, err := unzip(zipFile) + if err != nil { + return nil, fmt.Errorf("statik/fs: error unzipping file %q: %s", zipFile.Name, err) + } + files["/"+zipFile.Name] = file{ + FileInfo: zipFile.FileInfo(), + data: unzipped, + } + } + return &statikFS{files: files}, nil +} + +func unzip(zf *zip.File) ([]byte, error) { + rc, err := zf.Open() + if err != nil { + return nil, err + } + defer rc.Close() + return ioutil.ReadAll(rc) +} + +// Open returns a file matching the given file name, or os.ErrNotExists if +// no file matching the given file name is found in the archive. +// If a directory is requested, Open returns the file named "index.html" +// in the requested directory, if that file exists. +func (fs *statikFS) Open(name string) (http.File, error) { + name = strings.Replace(name, "//", "/", -1) + f, ok := fs.files[name] + if ok { + return newHTTPFile(f, false), nil + } + // The file doesn't match, but maybe it's a directory, + // thus we should look for index.html + indexName := strings.Replace(name+"/index.html", "//", "/", -1) + f, ok = fs.files[indexName] + if !ok { + return nil, os.ErrNotExist + } + return newHTTPFile(f, true), nil +} + +func newHTTPFile(file file, isDir bool) *httpFile { + return &httpFile{ + file: file, + reader: bytes.NewReader(file.data), + isDir: isDir, + } +} + +// httpFile represents an HTTP file and acts as a bridge +// between file and http.File. +type httpFile struct { + file + + reader *bytes.Reader + isDir bool +} + +// Read reads bytes into p, returns the number of read bytes. +func (f *httpFile) Read(p []byte) (n int, err error) { + return f.reader.Read(p) +} + +// Seek seeks to the offset. +func (f *httpFile) Seek(offset int64, whence int) (ret int64, err error) { + return f.reader.Seek(offset, whence) +} + +// Stat stats the file. +func (f *httpFile) Stat() (os.FileInfo, error) { + return f, nil +} + +// IsDir returns true if the file location represents a directory. +func (f *httpFile) IsDir() bool { + return f.isDir +} + +// Readdir returns an empty slice of files, directory +// listing is disabled. +func (f *httpFile) Readdir(count int) ([]os.FileInfo, error) { + // directory listing is disabled. + return make([]os.FileInfo, 0), nil +} + +func (f *httpFile) Close() error { + return nil +}