Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
61b1dac
feat: add git backup package
EduardSchwarzkopf May 21, 2026
d534481
feat: add git backup CLI flags and initialization in cmd/leafwiki
EduardSchwarzkopf May 21, 2026
10362c0
feat: add backup admin API routes
EduardSchwarzkopf May 21, 2026
b610989
feat: add backup settings UI component
EduardSchwarzkopf May 21, 2026
e642ab5
feat: add docker backup documentation
EduardSchwarzkopf May 21, 2026
121cbaa
fix: add logging to backup module
EduardSchwarzkopf May 21, 2026
143eab2
fix: use insecure host key callback for SSH git backup
EduardSchwarzkopf May 21, 2026
53009da
fix: prevent time.NewTicker panic on zero interval in backup scheduler
EduardSchwarzkopf May 21, 2026
8944c7a
fix: update LastBackupAt only when content was actually backed up
EduardSchwarzkopf May 21, 2026
0775664
feat: add git backup variables
EduardSchwarzkopf May 21, 2026
49898be
feat: add backup settings to user dropdown
EduardSchwarzkopf May 21, 2026
a0a3e79
fix: align BackupSettings layout with other settings pages
EduardSchwarzkopf May 21, 2026
dc913a4
feat: make it prettier
EduardSchwarzkopf May 21, 2026
2f5b424
fix: handle empty commit error gracefully on first start
EduardSchwarzkopf May 21, 2026
4b7bcd9
fix: serialize LastBackupAt as null instead of zero time in JSON
EduardSchwarzkopf May 21, 2026
cd80813
chore: update backup settings hint with all required env vars
EduardSchwarzkopf May 21, 2026
6a3bd19
chore: only show Last error field when there is an actual error
EduardSchwarzkopf May 21, 2026
5801377
chore: remove separate docker-backup.md doc
EduardSchwarzkopf May 21, 2026
edbd97c
chore: document git backup feature in README
EduardSchwarzkopf May 21, 2026
816c86d
chore: formatting
EduardSchwarzkopf May 21, 2026
1023943
fix: push to configured branch instead of remote's default branch
EduardSchwarzkopf May 21, 2026
d7b57a9
fix: treat already up-to-date as successful backup (not an error)
EduardSchwarzkopf May 21, 2026
a010cfa
fix: use strings.Contains for already up-to-date error check
EduardSchwarzkopf May 21, 2026
bf0a9d0
fix: use case-insensitive check for already up-to-date error
EduardSchwarzkopf May 21, 2026
d0d75b3
feat: update manual push logic
EduardSchwarzkopf May 21, 2026
08d0a7a
feat: update manual push logic
EduardSchwarzkopf May 21, 2026
526fb4d
feat: add debug logging
EduardSchwarzkopf May 22, 2026
4b11f05
fix: error message wipe
EduardSchwarzkopf May 22, 2026
dc29036
docs: enhance README with backup feature
EduardSchwarzkopf May 22, 2026
26679ef
feat: add SSHKnownHosts and Duration() method
EduardSchwarzkopf May 22, 2026
f30ca81
feat: use value-type LastBackupAt and omitempty on LastError
EduardSchwarzkopf May 22, 2026
55cfe09
fix: add db-journal to gitignore and respect system umask
EduardSchwarzkopf May 22, 2026
28858ac
feat: add panic recovery, waitgroup, and race-free trigger
EduardSchwarzkopf May 22, 2026
c460800
fix: propagate errors, validate author fields, remove force-push
EduardSchwarzkopf May 22, 2026
0ebb3cf
Merge branch 'main' into main
perber May 22, 2026
5a08da9
Add backupRoutes to wiki service structure
perber May 22, 2026
16020e4
fix: cmd/leafwiki/main.go:344:53
EduardSchwarzkopf May 22, 2026
76f4c9f
fix: linter
EduardSchwarzkopf May 22, 2026
a6edb95
fix: linter
EduardSchwarzkopf May 22, 2026
82ee157
lint: prettier
EduardSchwarzkopf May 22, 2026
04386fa
Potential fix for pull request finding
EduardSchwarzkopf May 23, 2026
5e46fc8
chore: run go mod tidy to remove unused golang.org/x/exp dependency
EduardSchwarzkopf May 23, 2026
9362e85
fix(backup): fix infinite polling loop on first-ever backup
EduardSchwarzkopf May 23, 2026
073938a
fix(backup): use fetchWithAuth instead of raw fetch
EduardSchwarzkopf May 23, 2026
7e10231
fix(backup): add .leafwiki/ and schema.json to gitignore, remove sysc…
EduardSchwarzkopf May 23, 2026
9496372
fix(backup): don't set LastBackupAt on no-op skip, split misleading l…
EduardSchwarzkopf May 23, 2026
a7f5ba6
fix(backup): simplify TriggerNow to single non-blocking send
EduardSchwarzkopf May 23, 2026
3554fc2
fix(backup): replace direct repo.status.LastBackupAt with repo.Status…
EduardSchwarzkopf May 23, 2026
5bcbcae
fix: wire SSHKnownHosts from CLI/env, make remote optional
EduardSchwarzkopf May 23, 2026
1e1bbef
chore: add LEAFWIKI_GIT_BACKUP_SSH_KNOWN_HOSTS entry
EduardSchwarzkopf May 23, 2026
99c2ff0
docs: update description to say remote is optional
EduardSchwarzkopf May 23, 2026
18eb869
chore: remove obselete comment
EduardSchwarzkopf May 23, 2026
b8b12b8
fix: type casting
EduardSchwarzkopf May 23, 2026
b9a4c29
feat: refactoring
EduardSchwarzkopf May 24, 2026
3585237
Merge branch 'main' into main
EduardSchwarzkopf May 24, 2026
3192fdd
chore: fix versions
EduardSchwarzkopf May 24, 2026
e467deb
Merge branch 'main' into main
EduardSchwarzkopf May 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,34 @@ LEAFWIKI_MAX_REVISION_HISTORY=
# Enable link refactoring dialog and rewrite flow (true/false)
LEAFWIKI_ENABLE_LINK_REFACTOR=

# ─── Git Backup ────────────────────────────────────────────────
# Enable automated git backup to a remote repository (true/false)
LEAFWIKI_GIT_BACKUP=false

# SSH remote URL for the backup repository (e.g. git@github.com:user/wiki-backup.git)
# Required when LEAFWIKI_GIT_BACKUP=true
LEAFWIKI_GIT_BACKUP_REMOTE=

# Branch to push to (default: main)
LEAFWIKI_GIT_BACKUP_BRANCH=main

# Git commit author name
LEAFWIKI_GIT_BACKUP_AUTHOR_NAME=LeafWiki Backup

# Git commit author email
LEAFWIKI_GIT_BACKUP_AUTHOR_EMAIL=backup@leafwiki.local

# Backup interval in minutes (default: 60)
LEAFWIKI_GIT_BACKUP_INTERVAL=60

# Path to SSH private key file (alternative to LEAFWIKI_GIT_BACKUP_SSH_KEY)
LEAFWIKI_GIT_BACKUP_SSH_KEY_PATH=

# Raw SSH private key (PEM). Mount your key file and set this, OR use LEAFWIKI_GIT_BACKUP_SSH_KEY_PATH
# Example: LEAFWIKI_GIT_BACKUP_SSH_KEY="-----BEGIN OPENSSH PRIVATE KEY-----\n..."
LEAFWIKI_GIT_BACKUP_SSH_KEY=
Comment thread
EduardSchwarzkopf marked this conversation as resolved.

# Path to known_hosts file for SSH host key verification (MITM protection)
# If not set, host key checking is skipped (insecure)
LEAFWIKI_GIT_BACKUP_SSH_KNOWN_HOSTS=

79 changes: 79 additions & 0 deletions cmd/leafwiki/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@ import (
"log/slog"
"math"
"os"
"path/filepath"
"strings"
"time"

"github.com/dustin/go-humanize"
"github.com/perber/wiki/internal/backup"
wikibackup "github.com/perber/wiki/internal/wiki/backup"
"github.com/perber/wiki/internal/core/tools"
httpinternal "github.com/perber/wiki/internal/http"
authmw "github.com/perber/wiki/internal/http/middleware/auth"
Expand Down Expand Up @@ -51,6 +54,15 @@ func writeUsage(w io.Writer) {
--http-remote-user-header-name HTTP header carrying the username from a trusted proxy (default: Remote-User)
--trusted-proxy-ips Comma-separated trusted proxy IPs/CIDRs (e.g. 127.0.0.1,172.18.0.0/16)
--http-remote-user-logout-url URL the frontend redirects to after logout in proxy-auth mode (default: "")
--git-backup Enable git backup to a remote repository (default: false)
--git-backup-author-name Git commit author name for backups (default: LeafWiki Backup)
--git-backup-author-email Git commit author email for backups (default: backup@leafwiki.local)
--git-backup-remote Git remote URL (SSH) for backups (required when git-backup is enabled)
--git-backup-branch Git branch to push to (default: main)
--git-backup-ssh-key-path Path to SSH private key for git backup
--git-backup-ssh-key Raw SSH private key for git backup (env var preferred)
--git-backup-ssh-known-hosts Path to known_hosts file for SSH host key verification (MITM protection)
--git-backup-interval Git backup interval in minutes (default: 60)

Environment variables:
LEAFWIKI_HOST
Expand All @@ -76,6 +88,15 @@ func writeUsage(w io.Writer) {
LEAFWIKI_HTTP_REMOTE_USER_HEADER_NAME
LEAFWIKI_TRUSTED_PROXY_IPS
LEAFWIKI_HTTP_REMOTE_USER_LOGOUT_URL
LEAFWIKI_GIT_BACKUP
LEAFWIKI_GIT_BACKUP_AUTHOR_NAME
LEAFWIKI_GIT_BACKUP_AUTHOR_EMAIL
LEAFWIKI_GIT_BACKUP_REMOTE
LEAFWIKI_GIT_BACKUP_BRANCH
LEAFWIKI_GIT_BACKUP_SSH_KEY_PATH
LEAFWIKI_GIT_BACKUP_SSH_KEY
LEAFWIKI_GIT_BACKUP_SSH_KNOWN_HOSTS
LEAFWIKI_GIT_BACKUP_INTERVAL
`); err != nil {
panic(err)
}
Expand Down Expand Up @@ -131,6 +152,15 @@ type cliFlags struct {
httpRemoteUserHeader *string
trustedProxyIPs *string
httpRemoteUserLogoutURL *string
gitBackup *bool
gitBackupAuthorName *string
gitBackupAuthorEmail *string
gitBackupRemote *string
gitBackupBranch *string
gitBackupSSHKeyPath *string
gitBackupSSHKey *string
gitBackupSSHKnownHosts *string
gitBackupInterval *int
}

func registerFlags(fs *flag.FlagSet) *cliFlags {
Expand All @@ -157,6 +187,15 @@ func registerFlags(fs *flag.FlagSet) *cliFlags {
httpRemoteUserHeader: fs.String("http-remote-user-header-name", "Remote-User", "HTTP header name carrying the username from a trusted proxy (default: Remote-User)"),
trustedProxyIPs: fs.String("trusted-proxy-ips", "", "comma-separated list of trusted proxy IPs/CIDRs (e.g. 127.0.0.1,172.18.0.0/16)"),
httpRemoteUserLogoutURL: fs.String("http-remote-user-logout-url", "", "URL the frontend redirects to after logout when reverse-proxy auth is active (e.g. https://auth.example.com/logout)"),
gitBackup: fs.Bool("git-backup", false, "enable git backup to a remote repository (default: false)"),
gitBackupAuthorName: fs.String("git-backup-author-name", "", "git commit author name for backups (default: LeafWiki Backup)"),
gitBackupAuthorEmail: fs.String("git-backup-author-email", "", "git commit author email for backups (default: backup@leafwiki.local)"),
gitBackupRemote: fs.String("git-backup-remote", "", "git remote URL (SSH) for backups (required when git-backup is enabled)"),
gitBackupBranch: fs.String("git-backup-branch", "", "git branch to push to (default: main)"),
gitBackupSSHKeyPath: fs.String("git-backup-ssh-key-path", "", "path to SSH private key for git backup"),
gitBackupSSHKey: fs.String("git-backup-ssh-key", "", "raw SSH private key for git backup (env var preferred)"),
gitBackupSSHKnownHosts: fs.String("git-backup-ssh-known-hosts", "", "path to known_hosts file for SSH host key verification (MITM protection)"),
gitBackupInterval: fs.Int("git-backup-interval", 0, "git backup interval in minutes (default: 60)"),
}
}

Expand Down Expand Up @@ -199,11 +238,23 @@ func main() {
httpRemoteUserHeader := resolveString("http-remote-user-header-name", *flags.httpRemoteUserHeader, visited, "LEAFWIKI_HTTP_REMOTE_USER_HEADER_NAME", "Remote-User")
trustedProxyIPsRaw := resolveString("trusted-proxy-ips", *flags.trustedProxyIPs, visited, "LEAFWIKI_TRUSTED_PROXY_IPS", "")
httpRemoteUserLogoutURL := resolveString("http-remote-user-logout-url", *flags.httpRemoteUserLogoutURL, visited, "LEAFWIKI_HTTP_REMOTE_USER_LOGOUT_URL", "")
gitBackupEnabled := resolveBool("git-backup", *flags.gitBackup, visited, "LEAFWIKI_GIT_BACKUP")
gitBackupAuthorName := resolveString("git-backup-author-name", *flags.gitBackupAuthorName, visited, "LEAFWIKI_GIT_BACKUP_AUTHOR_NAME", "LeafWiki Backup")
gitBackupAuthorEmail := resolveString("git-backup-author-email", *flags.gitBackupAuthorEmail, visited, "LEAFWIKI_GIT_BACKUP_AUTHOR_EMAIL", "backup@leafwiki.local")
gitBackupRemote := resolveString("git-backup-remote", *flags.gitBackupRemote, visited, "LEAFWIKI_GIT_BACKUP_REMOTE", "")
gitBackupBranch := resolveString("git-backup-branch", *flags.gitBackupBranch, visited, "LEAFWIKI_GIT_BACKUP_BRANCH", "main")
gitBackupSSHKeyPath := resolveString("git-backup-ssh-key-path", *flags.gitBackupSSHKeyPath, visited, "LEAFWIKI_GIT_BACKUP_SSH_KEY_PATH", "")
gitBackupSSHKey := resolveString("git-backup-ssh-key", *flags.gitBackupSSHKey, visited, "LEAFWIKI_GIT_BACKUP_SSH_KEY", "")
gitBackupInterval := resolveInt("git-backup-interval", *flags.gitBackupInterval, visited, "LEAFWIKI_GIT_BACKUP_INTERVAL", 60)
gitBackupSSHKnownHosts := resolveString("git-backup-ssh-known-hosts", *flags.gitBackupSSHKnownHosts, visited, "LEAFWIKI_GIT_BACKUP_SSH_KNOWN_HOSTS", "")
trustedProxies, err := authmw.ParseTrustedProxies(trustedProxyIPsRaw)
if err != nil {
fail("invalid --trusted-proxy-ips value", "error", err)
}

// Validate git backup configuration
// Note: git-backup-remote is now optional (local-only mode is supported)

args := flag.Args()
if len(args) > 0 {
switch args[0] {
Expand Down Expand Up @@ -271,6 +322,34 @@ func main() {
}
}()

// Initialize git backup if enabled
var backupScheduler *backup.Scheduler
if gitBackupEnabled {
repoDir := dataDir
if err := backup.EnsureGitignore(repoDir); err != nil {
fail("git backup init failed: %v", err)
}
backupRepo, err := backup.Init(backup.Config{
Enabled: true,
RootDir: filepath.Join(dataDir, "root"),
AssetsDir: filepath.Join(dataDir, "assets"),
AuthorName: gitBackupAuthorName,
AuthorEmail: gitBackupAuthorEmail,
RemoteURL: gitBackupRemote,
Branch: gitBackupBranch,
SSHKeyPath: gitBackupSSHKeyPath,
SSHKey: gitBackupSSHKey,
SSHKnownHosts: gitBackupSSHKnownHosts,
IntervalMinutes: gitBackupInterval,
})
Comment thread
EduardSchwarzkopf marked this conversation as resolved.
if err != nil {
fail("git backup init failed: %v", err)
}
backupScheduler = backup.NewScheduler(backupRepo, time.Duration(gitBackupInterval)*time.Minute)
defer backupScheduler.Stop()
w.SetBackupRoutes(wikibackup.NewRoutes(backupRepo, backupScheduler, w.AuthService()))
}

router := httpinternal.NewRouter(w.Registrars(), w.FrontendConfig(), httpinternal.RouterOptions{
PublicAccess: publicAccess,
InjectCodeInHeader: injectCodeInHeader,
Expand Down
17 changes: 17 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.25.0
require (
github.com/dustin/go-humanize v1.0.1
github.com/gin-gonic/gin v1.12.0
github.com/go-git/go-git/v5 v5.19.1
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/gosimple/slug v1.15.0
github.com/microcosm-cc/bluemonday v1.0.27
Expand All @@ -16,40 +17,56 @@ require (
)

require (
dario.cat/mergo v1.0.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.1.6 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.15.0 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/cloudflare/circl v1.6.3 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.9.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.30.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/gosimple/unidecode v1.0.1 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pjbgf/sha1cd v0.6.0 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.59.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/skeema/knownhosts v1.3.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
golang.org/x/arch v0.22.0 // indirect
golang.org/x/net v0.54.0 // indirect
golang.org/x/sys v0.45.0 // indirect
golang.org/x/text v0.37.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
modernc.org/libc v1.72.3 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
Expand Down
Loading