~gitpocket/gitpocket

feat(backup): bundle changed repositories and ship systemd and nginx configs

commit
3b1371e84b8b46e3c441a3dae54a34d06d1012a3 browse tree
parent
d84220b05c231b806099a8a038b008519ae864f8
author
james <j@cpl0.zip>
signature
Verified ~james signed with a key registered by ~james
date
2026-11-13 11:27 UTC

5 files changed, +152 −0

backup.go added

@@ -0,0 +1,75 @@
1+package main
2+
3+import (
4+ "context"
5+ "fmt"
6+ "os"
7+ "os/exec"
8+ "path/filepath"
9+ "strings"
10+ "time"
11+)
12+
13+func backup(root, dir string, keep int) error {
14+ if strings.HasSuffix(dir, ".db") {
15+ return mustStore(root).Backup(dir)
16+ }
17+ if err := os.MkdirAll(filepath.Join(dir, "repos"), 0o750); err != nil {
18+ return err
19+ }
20+ snapshot := filepath.Join(dir, "gitpocket-"+time.Now().UTC().Format("2006-01-02")+".db")
21+ os.Remove(snapshot)
22+ if err := mustStore(root).Backup(snapshot); err != nil {
23+ return err
24+ }
25+ old, _ := filepath.Glob(filepath.Join(dir, "gitpocket-*.db"))
26+ for i := 0; i+keep < len(old); i++ {
27+ os.Remove(old[i])
28+ }
29+ var failed []string
30+ repos, _ := filepath.Glob(filepath.Join(root, "repos", "*", "*.git"))
31+ live := map[string]bool{}
32+ for _, repo := range repos {
33+ rel, _ := filepath.Rel(filepath.Join(root, "repos"), repo)
34+ out := filepath.Join(dir, "repos", strings.TrimSuffix(rel, ".git")+".bundle")
35+ live[out] = true
36+ if info, err := os.Stat(out); err == nil && !repoUpdated(repo).After(info.ModTime()) {
37+ continue
38+ }
39+ if err := bundle(repo, out); err != nil {
40+ failed = append(failed, rel+": "+err.Error())
41+ }
42+ }
43+ stale, _ := filepath.Glob(filepath.Join(dir, "repos", "*", "*.bundle"))
44+ for _, b := range stale {
45+ if !live[b] {
46+ os.Remove(b)
47+ }
48+ }
49+ if len(failed) > 0 {
50+ return fmt.Errorf("could not bundle %s", strings.Join(failed, "; "))
51+ }
52+ return nil
53+}
54+
55+func bundle(repo, out string) error {
56+ g := Git{Dir: repo}
57+ if _, err := g.run(nil, "rev-parse", "--verify", "--quiet", "HEAD"); err != nil {
58+ if refs, _ := g.run(nil, "for-each-ref", "--count=1"); len(refs) == 0 {
59+ return nil
60+ }
61+ }
62+ if err := os.MkdirAll(filepath.Dir(out), 0o750); err != nil {
63+ return err
64+ }
65+ tmp := out + ".tmp"
66+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
67+ defer cancel()
68+ cmd := exec.CommandContext(ctx, "git", "-C", repo, "bundle", "create", "--quiet", tmp, "--all")
69+ cmd.Env = []string{"GIT_CONFIG_NOSYSTEM=1", "HOME=/nonexistent", "PATH=" + os.Getenv("PATH")}
70+ if out, err := cmd.CombinedOutput(); err != nil {
71+ os.Remove(tmp)
72+ return fmt.Errorf("%v: %s", err, strings.TrimSpace(string(out)))
73+ }
74+ return os.Rename(tmp, out)
75+}

deploy/gitpocket-backup.service added

@@ -0,0 +1,9 @@
1+[Unit]
2+Description=gitpocket database snapshot and repository bundles
3+
4+[Service]
5+Type=oneshot
6+User=git
7+Nice=10
8+IOSchedulingClass=idle
9+ExecStart=/usr/local/bin/gitpocket -root /srv/gitpocket backup /srv/gitpocket/backups

deploy/gitpocket-backup.timer added

@@ -0,0 +1,9 @@
1+[Unit]
2+Description=daily gitpocket backup
3+
4+[Timer]
5+OnCalendar=daily
6+Persistent=true
7+
8+[Install]
9+WantedBy=timers.target

deploy/gitpocket.service added

@@ -0,0 +1,22 @@
1+[Unit]
2+Description=gitpocket git host
3+After=network.target
4+
5+[Service]
6+User=git
7+Group=git
8+ExecStart=/usr/local/bin/gitpocket -root /srv/gitpocket serve -listen 127.0.0.1:3200 -host git.example.org
9+Restart=always
10+RestartSec=2
11+Environment=GOMEMLIMIT=192MiB
12+MemoryHigh=384M
13+MemoryMax=512M
14+TasksMax=256
15+NoNewPrivileges=yes
16+PrivateTmp=yes
17+ProtectSystem=strict
18+ProtectHome=yes
19+ReadWritePaths=/srv/gitpocket
20+
21+[Install]
22+WantedBy=multi-user.target

deploy/nginx.conf added

@@ -0,0 +1,37 @@
1+limit_req_zone $binary_remote_addr zone=gitpocket:10m rate=10r/s;
2+
3+server {
4+ listen 80;
5+ listen [::]:80;
6+ server_name git.example.org;
7+ location / { return 301 https://$host$request_uri; }
8+}
9+
10+server {
11+ listen 443 ssl;
12+ listen [::]:443 ssl;
13+ http2 on;
14+ server_name git.example.org;
15+ ssl_certificate /etc/letsencrypt/live/gitpocket/fullchain.pem;
16+ ssl_certificate_key /etc/letsencrypt/live/gitpocket/privkey.pem;
17+ ssl_protocols TLSv1.2 TLSv1.3;
18+ client_max_body_size 512m;
19+ add_header Strict-Transport-Security "max-age=31536000" always;
20+ location / {
21+ limit_req zone=gitpocket burst=60 nodelay;
22+ limit_req_status 429;
23+ proxy_pass http://127.0.0.1:3200;
24+ proxy_http_version 1.1;
25+ proxy_set_header Host $host;
26+ proxy_set_header X-Forwarded-Proto https;
27+ proxy_set_header X-Forwarded-For $remote_addr;
28+ proxy_request_buffering off;
29+ proxy_buffering off;
30+ proxy_read_timeout 600s;
31+ }
32+ location /static/ {
33+ proxy_pass http://127.0.0.1:3200;
34+ proxy_set_header Host $host;
35+ expires 1d;
36+ }
37+}