package forge import ( "context" "fmt" "os" "os/exec" "path/filepath" "strings" "time" ) func backup(root, dir string, keep int) error { if strings.HasSuffix(dir, ".db") { return mustStore(root).Backup(dir) } if err := os.MkdirAll(filepath.Join(dir, "repos"), 0o750); err != nil { return err } snapshot := filepath.Join(dir, "gitpocket-"+time.Now().UTC().Format("2006-01-02")+".db") os.Remove(snapshot) if err := mustStore(root).Backup(snapshot); err != nil { return err } old, _ := filepath.Glob(filepath.Join(dir, "gitpocket-*.db")) for i := 0; i+keep < len(old); i++ { os.Remove(old[i]) } var failed []string repos, _ := filepath.Glob(filepath.Join(root, "repos", "*", "*.git")) live := map[string]bool{} for _, repo := range repos { rel, _ := filepath.Rel(filepath.Join(root, "repos"), repo) out := filepath.Join(dir, "repos", strings.TrimSuffix(rel, ".git")+".bundle") live[out] = true if info, err := os.Stat(out); err == nil && !repoUpdated(repo).After(info.ModTime()) { continue } if err := bundle(repo, out); err != nil { failed = append(failed, rel+": "+err.Error()) } } stale, _ := filepath.Glob(filepath.Join(dir, "repos", "*", "*.bundle")) for _, b := range stale { if !live[b] { os.Remove(b) } } if err := copyAttachments(filepath.Join(root, "attachments"), filepath.Join(dir, "attachments")); err != nil { failed = append(failed, "attachments: "+err.Error()) } if len(failed) > 0 { return fmt.Errorf("could not bundle %s", strings.Join(failed, "; ")) } return nil } func bundle(repo, out string) error { g := Git{Dir: repo} if _, err := g.run(nil, "rev-parse", "--verify", "--quiet", "HEAD"); err != nil { if refs, _ := g.run(nil, "for-each-ref", "--count=1"); len(refs) == 0 { return nil } } if err := os.MkdirAll(filepath.Dir(out), 0o750); err != nil { return err } tmp := out + ".tmp" ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute) defer cancel() cmd := exec.CommandContext(ctx, "git", "-C", repo, "bundle", "create", "--quiet", tmp, "--all") cmd.Env = []string{"GIT_CONFIG_NOSYSTEM=1", "HOME=/nonexistent", "PATH=" + os.Getenv("PATH")} if out, err := cmd.CombinedOutput(); err != nil { os.Remove(tmp) return fmt.Errorf("%v: %s", err, strings.TrimSpace(string(out))) } return os.Rename(tmp, out) } func copyAttachments(src, dst string) error { files, err := os.ReadDir(src) if err != nil || len(files) == 0 { return nil } if err := os.MkdirAll(dst, 0o750); err != nil { return err } for _, f := range files { out := filepath.Join(dst, f.Name()) if !attachName.MatchString(f.Name()) { continue } if _, err := os.Stat(out); err == nil { continue } data, err := os.ReadFile(filepath.Join(src, f.Name())) if err == nil { err = os.WriteFile(out, data, 0o640) } if err != nil { return err } } return nil }