feat(backup): bundle changed repositories and ship systemd and nginx configs
5 files changed, +152 −0
- backup.go added +75 −0
- deploy/gitpocket-backup.service added +9 −0
- deploy/gitpocket-backup.timer added +9 −0
- deploy/gitpocket.service added +22 −0
- deploy/nginx.conf added +37 −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 | +} | |