diff --git a/.forgejo/workflows/build-docker.yml b/.forgejo/workflows/build-docker.yml index e24bcf2..91ac6b3 100644 --- a/.forgejo/workflows/build-docker.yml +++ b/.forgejo/workflows/build-docker.yml @@ -5,11 +5,11 @@ on: push: - branches: - - master + tags: + - 'v*' jobs: - basic: + docker: runs-on: any steps: - name: Checkout code @@ -45,15 +45,6 @@ jobs: push: true pull: true tags: git.ronmi.tw/ronmi/forgejo-pages:arm64,ronmi/forgejo-pages:arm64 - - name: Build arm64 image (git) - uses: docker/build-push-action@v6 - with: - context: . - file: git.dockerfile - platforms: linux/arm64 - push: true - pull: true - tags: git.ronmi.tw/ronmi/forgejo-pages:git-arm64,ronmi/forgejo-pages:git-arm64 - name: Build amd64 binary run: GOARCH=amd64 go build - name: Build amd64 image @@ -64,24 +55,14 @@ jobs: push: true pull: true tags: git.ronmi.tw/ronmi/forgejo-pages:amd64,ronmi/forgejo-pages:amd64 - - name: Build amd64 image (git) - uses: docker/build-push-action@v6 - with: - context: . - file: git.dockerfile - platforms: linux/amd64 - push: true - pull: true - tags: git.ronmi.tw/ronmi/forgejo-pages:git-amd64,ronmi/forgejo-pages:git-amd64 - name: Create multiarch image run: | docker buildx imagetools create -t git.ronmi.tw/ronmi/forgejo-pages git.ronmi.tw/ronmi/forgejo-pages:arm64 git.ronmi.tw/ronmi/forgejo-pages:amd64 docker buildx imagetools create -t ronmi/forgejo-pages ronmi/forgejo-pages:arm64 ronmi/forgejo-pages:amd64 - docker buildx imagetools create -t git.ronmi.tw/ronmi/forgejo-pages:git git.ronmi.tw/ronmi/forgejo-pages:git-arm64 git.ronmi.tw/ronmi/forgejo-pages:git-amd64 - docker buildx imagetools create -t ronmi/forgejo-pages:git ronmi/forgejo-pages:git-arm64 ronmi/forgejo-pages:git-amd64 - name: Update readme to docker hub uses: https://github.com/peter-evans/dockerhub-description@v4 with: username: ${{ secrets.DOCKER_HUB_USER }} password: ${{ secrets.DOCKER_HUB_PASSWORD }} repository: ronmi/forgejo-pages + diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 194951c..0000000 --- a/.gitignore +++ /dev/null @@ -1 +0,0 @@ -forgejo-pages diff --git a/README.md b/README.md index e91b1f1..8c628cd 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ This mode is good for simple setup, like, you have small number of viewers, or y ### Webhook mode -Webhook mode is a tool which helps you to download latest content via git. You'll have to setup a webhook in forgejo server in order to notify it when to download new content. You have to have `git` binary in your path. +Webhook mode is a tool which helps you to download latest content via git. You'll have to setup a webhook in forgejo server in order to notify it when to download new content. To serve downloaded pages, you'll have to use a web server like Nginx. @@ -41,13 +41,9 @@ Take care about permissions of the API token. For serve mode, repositories the k ``` docker run -p 8080:8080 --user 1000:1000 ronmi/forgejo-pages serve -s https://git.example.com -k my-secret-token -docker run -p 8080:8080 --user 1000:1000 -v `pwd`/data:/data ronmi/forgejo-pages:git listen -u myuser -k my-secret-token -s https://git.example.com -a :8080 -b static-pages -d /data +docker run -p 8080:8080 --user 1000:1000 -v `pwd`/data:/data ronmi/forgejo-pages listen -u myuser -k my-secret-token -s https://git.example.com -a :8080 -b static-pages -d /data ``` -Serve mode could use `latest` tag, a minimal image contains only libc, CA certificates, timezone data and page server binary. - -Webhook mode should use `git` tag. - # FAQ ### Can I use user.example.com/repo/path format? diff --git a/cmd/listen.go b/cmd/listen.go index 4da2bca..e079383 100644 --- a/cmd/listen.go +++ b/cmd/listen.go @@ -15,8 +15,6 @@ import ( "time" "git.ronmi.tw/ronmi/forgejo-pages/lib" - "github.com/raohwork/task" - "github.com/raohwork/task/httptask" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -70,7 +68,7 @@ is up to you, eg. Nginx, Apache, or even a simple Go server. GitPass: token, } - s, r, err := lib.UseWebhook(bind, cfg) + s, err := lib.UseWebhook(bind, cfg) if err != nil { fmt.Println("cannot create server: ", err) return @@ -85,11 +83,14 @@ is up to you, eg. Nginx, Apache, or even a simple Go server. defer stop() fmt.Println("starting server") - task.Wait( - httptask.Server(s, task.Timeout(10*time.Second)), - r.Run, - ).Run(ctx) - + go func() { + <-ctx.Done() + stop() + ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second) + defer cancel() + s.Shutdown(ctx) + }() + s.ListenAndServe() }, } diff --git a/cmd/serve.go b/cmd/serve.go index c400476..7970908 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -14,8 +14,6 @@ import ( "time" "git.ronmi.tw/ronmi/forgejo-pages/lib" - "github.com/raohwork/task" - "github.com/raohwork/task/httptask" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -72,7 +70,14 @@ cache/protection layer like Cloudflare in front of the server. defer stop() fmt.Println("starting server") - httptask.Server(s, task.Timeout(10*time.Second)).Run(ctx) + go func() { + <-ctx.Done() + stop() + ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second) + defer cancel() + s.Shutdown(ctx) + }() + s.ListenAndServe() }, } diff --git a/git.dockerfile b/git.dockerfile deleted file mode 100644 index 4259929..0000000 --- a/git.dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at https://mozilla.org/MPL/2.0/. - -FROM debian:stable-slim - -# Install git -RUN apt-get update && apt-get install -y \ - ca-certificates git tzdata \ - && apt-get clean -y && rm -rf /var/lib/apt/lists/* - -ADD forgejo-pages /usr/bin/forgejo-pages -ENTRYPOINT ["/usr/bin/forgejo-pages"] diff --git a/go.mod b/go.mod index a963e88..640e949 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module git.ronmi.tw/ronmi/forgejo-pages go 1.21.6 require ( - github.com/raohwork/task v0.3.0 github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.19.0 ) diff --git a/go.sum b/go.sum index c0b39f8..aba9163 100644 --- a/go.sum +++ b/go.sum @@ -26,8 +26,6 @@ github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/raohwork/task v0.3.0 h1:4j0jT1a+f5O+g6q22o42sFEdSlYGYtgUHLbg3cF/7VE= -github.com/raohwork/task v0.3.0/go.mod h1:QkVxY/Q/w6bW5Xjhcp8vn5qLgoS+70jUTyGueI0nzMo= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= diff --git a/lib/git.go b/lib/git.go deleted file mode 100644 index 5c03fe6..0000000 --- a/lib/git.go +++ /dev/null @@ -1,159 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -package lib - -import ( - "context" - "fmt" - "net/url" - "os" - "os/exec" - "path" - "path/filepath" - "sync" -) - -type repoSpec struct { - user string - name string -} - -type GitRunner struct { - ch <-chan repoSpec - cfg *WebhookCFG -} - -func (g *GitRunner) Run(ctx context.Context) error { - // graceful: exit only if all tasks are done - for { - select { - case repo := <-g.ch: - err := g.cfg.download(repo.user, repo.name) - if err != nil { - fmt.Println("Failed to download repo: ", err) - } - case <-ctx.Done(): - return ctx.Err() - } - } -} - -func (c *WebhookCFG) pageDir(user, repo string) string { - return filepath.Join(c.PageDir, user, repo) -} - -func (c *WebhookCFG) gitDir(user, repo string) string { - return filepath.Join(c.GitDir, user, repo) -} - -func (c *WebhookCFG) checkout(user, repo string) (err error) { - pageDir := c.pageDir(user, repo) - gitDir := c.gitDir(user, repo) - - git := exec.Command( - "git", - "--git-dir", gitDir, - "--work-tree", pageDir, - "checkout", "origin/"+c.Branch, - ) - output, err := git.CombinedOutput() - if err != nil { - fmt.Println("git checkout failed: ", err) - fmt.Println("=========== Dump output: ============") - fmt.Println(string(output)) - fmt.Println("=====================================") - } - - return -} - -func (c *WebhookCFG) fetch(user, repo string) (err error) { - gitDir := c.gitDir(user, repo) - git := exec.Command( - "git", - "--git-dir", gitDir, - "fetch", "origin", c.Branch, - ) - output, err := git.CombinedOutput() - if err != nil { - fmt.Println("git fetch failed: ", err) - fmt.Println("=========== Dump output: ============") - fmt.Println(string(output)) - fmt.Println("=====================================") - } - - return -} - -func (c *WebhookCFG) clone(user, repo string) (err error) { - pageDir := c.pageDir(user, repo) - err = os.MkdirAll(filepath.Dir(pageDir), 0755) - if err != nil { - return - } - - gitDir := c.gitDir(user, repo) - err = os.MkdirAll(filepath.Dir(gitDir), 0700) - if err != nil { - return - } - - uri := c.Server - uri.Path = "/" + path.Join(user, repo) + ".git" - uri.User = url.UserPassword(c.GitUser, c.GitPass) - - git := exec.Command( - "git", - "clone", - "-b", c.Branch, - "--single-branch", - "--no-tags", - "--separate-git-dir", gitDir, - uri.String(), - pageDir, - ) - output, err := git.CombinedOutput() - if err != nil { - fmt.Println("git clone failed: ", err) - fmt.Println("=========== Dump output: ============") - fmt.Println(string(output)) - fmt.Println("=====================================") - } - - os.Remove(filepath.Join(pageDir, ".git")) - - return -} - -var repoLock = &sync.Map{} - -func (c *WebhookCFG) lock(user, repo string) func() { - key := user + "/" + repo - - lock, _ := repoLock.LoadOrStore(key, &sync.Mutex{}) - m := lock.(*sync.Mutex) - m.Lock() - return m.Unlock -} - -func (c *WebhookCFG) download(user, repo string) (err error) { - unlock := c.lock(user, repo) - defer unlock() - fmt.Println("Pulling ", user, repo) - - gitDir := c.gitDir(user, repo) - if _, err = os.Stat(gitDir); os.IsNotExist(err) { - err = c.clone(user, repo) - } else { - err = c.fetch(user, repo) - } - - if err != nil { - return - } - - err = c.checkout(user, repo) - return -} diff --git a/lib/hook.go b/lib/hook.go index fe0d592..b0b6bb4 100644 --- a/lib/hook.go +++ b/lib/hook.go @@ -10,7 +10,12 @@ import ( "fmt" "io" "net/http" + "net/url" + "os" + "os/exec" "path" + "path/filepath" + "sync" ) type WebhookCFG struct { @@ -19,7 +24,123 @@ type WebhookCFG struct { GitDir string GitUser string GitPass string - ch chan<- repoSpec +} + +func (c *WebhookCFG) pageDir(user, repo string) string { + return filepath.Join(c.PageDir, user, repo) +} + +func (c *WebhookCFG) gitDir(user, repo string) string { + return filepath.Join(c.GitDir, user, repo) +} + +func (c *WebhookCFG) checkout(user, repo string) (err error) { + pageDir := c.pageDir(user, repo) + gitDir := c.gitDir(user, repo) + + git := exec.Command( + "git", + "--git-dir", gitDir, + "--work-tree", pageDir, + "checkout", "origin/"+c.Branch, + ) + output, err := git.CombinedOutput() + if err != nil { + fmt.Println("git checkout failed: ", err) + fmt.Println("=========== Dump output: ============") + fmt.Println(string(output)) + fmt.Println("=====================================") + } + + return +} + +func (c *WebhookCFG) fetch(user, repo string) (err error) { + gitDir := c.gitDir(user, repo) + git := exec.Command( + "git", + "--git-dir", gitDir, + "fetch", "origin", c.Branch, + ) + output, err := git.CombinedOutput() + if err != nil { + fmt.Println("git fetch failed: ", err) + fmt.Println("=========== Dump output: ============") + fmt.Println(string(output)) + fmt.Println("=====================================") + } + + return +} + +func (c *WebhookCFG) clone(user, repo string) (err error) { + pageDir := c.pageDir(user, repo) + err = os.MkdirAll(filepath.Dir(pageDir), 0755) + if err != nil { + return + } + + gitDir := c.gitDir(user, repo) + err = os.MkdirAll(filepath.Dir(gitDir), 0700) + if err != nil { + return + } + + uri := c.Server + uri.Path = "/" + path.Join(user, repo) + ".git" + uri.User = url.UserPassword(c.GitUser, c.GitPass) + + git := exec.Command( + "git", + "clone", + "-b", c.Branch, + "--single-branch", + "--no-tags", + "--separate-git-dir", gitDir, + uri.String(), + pageDir, + ) + output, err := git.CombinedOutput() + if err != nil { + fmt.Println("git clone failed: ", err) + fmt.Println("=========== Dump output: ============") + fmt.Println(string(output)) + fmt.Println("=====================================") + } + + os.Remove(filepath.Join(pageDir, ".git")) + + return +} + +var repoLock = &sync.Map{} + +func (c *WebhookCFG) lock(user, repo string) func() { + key := user + "/" + repo + + lock, _ := repoLock.LoadOrStore(key, &sync.Mutex{}) + m := lock.(*sync.Mutex) + m.Lock() + return m.Unlock +} + +func (c *WebhookCFG) download(user, repo string) (err error) { + unlock := c.lock(user, repo) + defer unlock() + + gitDir := c.gitDir(user, repo) + if _, err = os.Stat(gitDir); os.IsNotExist(err) { + err = c.clone(user, repo) + } else { + err = c.fetch(user, repo) + } + + if err != nil { + return + } + + err = c.checkout(user, repo) + return } type webhookPayload struct { @@ -64,19 +185,22 @@ func (c *WebhookCFG) handle(w http.ResponseWriter, r *http.Request) { user, repo := path.Split(payload.Repository.FullName) user = user[:len(user)-1] go func() { - c.ch <- repoSpec{user: user, name: repo} + fmt.Println("Pulling ", user, repo) + err = c.download(user, repo) + if err != nil { + fmt.Println("Failed to download repository: ", err) + return + } }() } -func UseWebhook(bind string, cfg *WebhookCFG) (*http.Server, *GitRunner, error) { +func UseWebhook(bind string, cfg *WebhookCFG) (*http.Server, error) { if cfg == nil { - return nil, nil, errors.New("webhook config is nil") + return nil, errors.New("webhook config is nil") } s := &http.Server{ Addr: bind, Handler: http.HandlerFunc(cfg.handle), } - ch := make(chan repoSpec, 5) - cfg.ch = ch - return s, &GitRunner{ch: ch, cfg: cfg}, nil + return s, nil } diff --git a/lib/web.go b/lib/web.go index ab0da19..5616a9f 100644 --- a/lib/web.go +++ b/lib/web.go @@ -12,8 +12,6 @@ import ( "net/http" "net/url" "strings" - "mime" - "path/filepath" ) type Forgejo struct { @@ -36,7 +34,7 @@ func (f *Forgejo) GetFile(ctx context.Context, headers map[string]string, user, if err != nil { return } - + for k, v := range headers { if v == "" { continue @@ -52,7 +50,7 @@ func (f *Forgejo) GetFile(ctx context.Context, headers map[string]string, user, err = ErrNotFound return } - + return resp, nil } @@ -104,43 +102,24 @@ func (f *Forgejo) handle(w http.ResponseWriter, r *http.Request) { } file = strings.Join(arr[2:], "/") } - + headers := map[string]string{} headers["If-None-Match"] = r.Header.Get("If-None-Match") headers["If-Modified-Since"] = r.Header.Get("If-Modified-Since") headers["If-Range"] = r.Header.Get("If-Range") headers["Range"] = r.Header.Get("Range") - headers["X-Forwarded-For"] = r.Header.Get("X-Forwarded-For") - headers["X-Forwarded-Host"] = r.Header.Get("X-Forwarded-Host") - headers["X-Forwarded-Proto"] = r.Header.Get("X-Forwarded-Proto") - headers["X-Real-IP"] = r.Header.Get("X-Real-IP") - headers["X-Host"] = r.Header.Get("X-Host") - headers["CF-Connecting-IP"] = r.Header.Get("CF-Connecting-IP") - headers["CF-IPCountry"] = r.Header.Get("CF-IPCountry") - headers["CF-Visitor"] = r.Header.Get("CF-Visitor") - headers["CF-Request-ID"] = r.Header.Get("CF-Request-ID") - headers["CF-Ray"] = r.Header.Get("CF-Ray") resp, err := f.GetFile(r.Context(), headers, user, repo, f.Branch, file) if err != nil { w.WriteHeader(http.StatusNotFound) return } defer resp.Body.Close() - + trySet(w.Header(), "Etag", resp.Header) trySet(w.Header(), "Last-Modified", resp.Header) trySet(w.Header(), "Content-Length", resp.Header) trySet(w.Header(), "Content-Range", resp.Header) - contentType := resp.Header.Get("Content-Type") - if contentType == "" || strings.HasPrefix(contentType, "text/plain") { - if ct := mime.TypeByExtension(filepath.Ext(file)); ct != "" { - contentType = ct - } - } - if contentType != "" { - w.Header().Set("Content-Type", contentType) - } - + w.WriteHeader(resp.StatusCode) io.Copy(w, resp.Body) }