gitbackup-github-desktop

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

GitBackup — GitHub Repository Backup Desktop App

GitBackup — GitHub仓库备份桌面应用

Skill by ara.so — Daily 2026 Skills collection.
GitBackup is an Electron + React desktop application that clones all your GitHub repositories locally and optionally uploads compressed
.tar.gz
archives to AWS S3 or Cloudflare R2. It supports incremental updates, scheduled backups, concurrent processing, and encrypted local settings storage.
ara.so开发的技能工具 — 2026每日技能合集。
GitBackup是一款基于Electron + React的桌面应用,可将你的所有GitHub仓库克隆到本地,还可选择将压缩后的
.tar.gz
归档文件上传至AWS S3或Cloudflare R2。它支持增量更新、定时备份、并发处理以及加密本地设置存储。

Installation

安装

Download Pre-built Binary

下载预构建二进制文件

Download from Releases:
PlatformFile
macOS
GitBackup-x.x.x.dmg
Windows
GitBackup-Setup-x.x.x.exe
Linux
GitBackup-x.x.x.AppImage
Prerequisite: Git must be installed and available in PATH.
发布页面下载:
平台文件
macOS
GitBackup-x.x.x.dmg
Windows
GitBackup-Setup-x.x.x.exe
Linux
GitBackup-x.x.x.AppImage
前提条件: 必须安装Git且其路径已添加至系统PATH。

Build from Source

从源码构建

bash
git clone https://github.com/hiteshchoudhary/gitbackup.git
cd gitbackup
npm install
bash
git clone https://github.com/hiteshchoudhary/gitbackup.git
cd gitbackup
npm install

Development with hot reload

热重载开发模式

npm run dev
npm run dev

Package for current platform

为当前平台打包

npm run package
npm run package

Platform-specific builds

针对特定平台构建

npm run package:mac npm run package:win npm run package:linux
undefined
npm run package:mac npm run package:win npm run package:linux
undefined

GitHub Token Setup

GitHub令牌设置

  1. Go to github.com/settings/tokens
  2. Tokens (classic)Generate new token (classic)
  3. Select scopes:
    repo
    (full access) +
    read:org
    (for org repos)
  4. Copy the token and paste into the app's Setup page
Fine-grained tokens also work — grant Repository access → All repositories.
  1. 访问github.com/settings/tokens
  2. Tokens (classic)Generate new token (classic)
  3. 选择权限范围:
    repo
    (完全访问权限) +
    read:org
    (用于组织仓库)
  4. 复制令牌并粘贴到应用的设置页面
细粒度令牌同样适用 — 授予Repository access → All repositories权限。

Project Structure

项目结构

gitbackup/
├── electron/                       # Main process (Node.js)
│   ├── main.ts                     # App window, tray, lifecycle
│   ├── preload.ts                  # Secure IPC bridge
│   ├── tray.ts                     # System tray icon
│   ├── ipc/                        # IPC handler modules
│   ├── services/
│   │   ├── github.service.ts       # GitHub API via Octokit
│   │   ├── git.service.ts          # Clone & fetch repos (simple-git)
│   │   ├── compress.service.ts     # tar.gz archiving
│   │   ├── cloud.service.ts        # S3/R2 uploads (AWS SDK v3)
│   │   ├── backup-orchestrator.ts  # Core backup pipeline
│   │   └── scheduler.service.ts    # Cron scheduling (node-cron)
│   └── store/store.ts              # Encrypted settings (electron-store)
└── src/                            # Renderer process (React 19)
    ├── pages/                      # Setup, Repos, Backup, Settings
    ├── components/                 # UI components
    └── hooks/                      # IPC & state hooks
gitbackup/
├── electron/                       # 主进程(Node.js)
│   ├── main.ts                     # 应用窗口、托盘、生命周期管理
│   ├── preload.ts                  # 安全IPC桥接
│   ├── tray.ts                     # 系统托盘图标
│   ├── ipc/                        # IPC处理器模块
│   ├── services/
│   │   ├── github.service.ts       # 基于Octokit的GitHub API封装
│   │   ├── git.service.ts          # 克隆与拉取仓库(simple-git)
│   │   ├── compress.service.ts     # tar.gz归档处理
│   │   ├── cloud.service.ts        # S3/R2上传(AWS SDK v3)
│   │   ├── backup-orchestrator.ts  # 核心备份流水线
│   │   └── scheduler.service.ts    # Cron定时任务(node-cron)
│   └── store/store.ts              # 加密设置存储(electron-store)
└── src/                            # 渲染进程(React 19)
    ├── pages/                      # 设置、仓库、备份、设置页面
    ├── components/                 # UI组件
    └── hooks/                      # IPC与状态钩子

Tech Stack

技术栈

LayerTechnology
FrameworkElectron 35
FrontendReact 19 + Tailwind CSS
LanguageTypeScript 5
BundlerVite 8
GitHub API@octokit/rest
Git operationssimple-git
Cloud storageAWS SDK v3 (S3-compatible)
Settingselectron-store (encrypted)
Schedulingnode-cron
Packagingelectron-builder
层级技术
框架Electron 35
前端React 19 + Tailwind CSS
语言TypeScript 5
打包工具Vite 8
GitHub API@octokit/rest
Git操作simple-git
云存储AWS SDK v3(兼容S3)
设置存储electron-store(加密)
定时任务node-cron
应用打包electron-builder

Core Services — Code Examples

核心服务 — 代码示例

GitHub Service (Octokit)

GitHub服务(Octokit)

typescript
// electron/services/github.service.ts
import { Octokit } from "@octokit/rest";

export class GitHubService {
  private octokit: Octokit;

  constructor(token: string) {
    this.octokit = new Octokit({ auth: token });
  }

  async getAuthenticatedUser() {
    const { data } = await this.octokit.users.getAuthenticated();
    return data;
  }

  // Fetch all repos with pagination — handles 200-300+ repos
  async fetchAllRepositories(filters: RepoFilters): Promise<Repository[]> {
    const repos: Repository[] = [];

    if (filters.owned) {
      for await (const response of this.octokit.paginate.iterator(
        this.octokit.repos.listForAuthenticatedUser,
        { affiliation: "owner", per_page: 100 }
      )) {
        repos.push(...response.data);
      }
    }

    if (filters.organizations) {
      const orgs = await this.octokit.orgs.listForAuthenticatedUser();
      for (const org of orgs.data) {
        for await (const response of this.octokit.paginate.iterator(
          this.octokit.repos.listForOrg,
          { org: org.login, per_page: 100 }
        )) {
          repos.push(...response.data);
        }
      }
    }

    if (filters.starred) {
      for await (const response of this.octokit.paginate.iterator(
        this.octokit.activity.listReposStarredByAuthenticatedUser,
        { per_page: 100 }
      )) {
        repos.push(...response.data as any);
      }
    }

    return repos;
  }
}
typescript
// electron/services/github.service.ts
import { Octokit } from "@octokit/rest";

export class GitHubService {
  private octokit: Octokit;

  constructor(token: string) {
    this.octokit = new Octokit({ auth: token });
  }

  async getAuthenticatedUser() {
    const { data } = await this.octokit.users.getAuthenticated();
    return data;
  }

  // 分页获取所有仓库 — 支持200-300+仓库
  async fetchAllRepositories(filters: RepoFilters): Promise<Repository[]> {
    const repos: Repository[] = [];

    if (filters.owned) {
      for await (const response of this.octokit.paginate.iterator(
        this.octokit.repos.listForAuthenticatedUser,
        { affiliation: "owner", per_page: 100 }
      )) {
        repos.push(...response.data);
      }
    }

    if (filters.organizations) {
      const orgs = await this.octokit.orgs.listForAuthenticatedUser();
      for (const org of orgs.data) {
        for await (const response of this.octokit.paginate.iterator(
          this.octokit.repos.listForOrg,
          { org: org.login, per_page: 100 }
        )) {
          repos.push(...response.data);
        }
      }
    }

    if (filters.starred) {
      for await (const response of this.octokit.paginate.iterator(
        this.octokit.activity.listReposStarredByAuthenticatedUser,
        { per_page: 100 }
      )) {
        repos.push(...response.data as any);
      }
    }

    return repos;
  }
}

Git Service (Clone & Fetch)

Git服务(克隆与拉取)

typescript
// electron/services/git.service.ts
import simpleGit, { SimpleGit } from "simple-git";
import path from "path";
import fs from "fs";

export class GitService {
  // Clone with all branches or fetch updates if already exists
  async backupRepository(
    repoUrl: string,
    backupPath: string,
    token: string
  ): Promise<void> {
    // Embed token in URL (cleaned after operation)
    const authenticatedUrl = repoUrl.replace(
      "https://",
      `https://x-access-token:${token}@`
    );

    const repoExists = fs.existsSync(path.join(backupPath, ".git"));

    if (repoExists) {
      // Incremental update — only fetch changes
      const git: SimpleGit = simpleGit(backupPath);
      await git.fetch(["--all", "--prune"]);
    } else {
      // First run — full clone with all branches
      fs.mkdirSync(backupPath, { recursive: true });
      const git: SimpleGit = simpleGit();
      await git.clone(authenticatedUrl, backupPath, ["--mirror"]);
    }

    // Remove token from remote URL after operation
    const git: SimpleGit = simpleGit(backupPath);
    await git.remote(["set-url", "origin", repoUrl]);
  }
}
typescript
// electron/services/git.service.ts
import simpleGit, { SimpleGit } from "simple-git";
import path from "path";
import fs from "fs";

export class GitService {
  // 克隆所有分支,若已存在则仅拉取更新
  async backupRepository(
    repoUrl: string,
    backupPath: string,
    token: string
  ): Promise<void> {
    // 在URL中嵌入令牌(操作完成后清除)
    const authenticatedUrl = repoUrl.replace(
      "https://",
      `https://x-access-token:${token}@`
    );

    const repoExists = fs.existsSync(path.join(backupPath, ".git"));

    if (repoExists) {
      // 增量更新 — 仅拉取变更
      const git: SimpleGit = simpleGit(backupPath);
      await git.fetch(["--all", "--prune"]);
    } else {
      // 首次运行 — 完整克隆所有分支
      fs.mkdirSync(backupPath, { recursive: true });
      const git: SimpleGit = simpleGit();
      await git.clone(authenticatedUrl, backupPath, ["--mirror"]);
    }

    // 操作完成后移除远程URL中的令牌
    const git: SimpleGit = simpleGit(backupPath);
    await git.remote(["set-url", "origin", repoUrl]);
  }
}

Cloud Service (S3 / Cloudflare R2)

云服务(S3 / Cloudflare R2)

typescript
// electron/services/cloud.service.ts
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import fs from "fs";

export interface CloudConfig {
  provider: "s3" | "r2";
  bucket: string;
  region?: string;         // AWS S3
  endpoint?: string;       // Cloudflare R2 endpoint
  accessKeyId: string;     // from env: process.env.AWS_ACCESS_KEY_ID
  secretAccessKey: string; // from env: process.env.AWS_SECRET_ACCESS_KEY
}

export class CloudService {
  private client: S3Client;
  private bucket: string;

  constructor(config: CloudConfig) {
    this.bucket = config.bucket;
    this.client = new S3Client({
      region: config.region ?? "auto",
      endpoint: config.endpoint,       // Set for Cloudflare R2
      credentials: {
        accessKeyId: config.accessKeyId,
        secretAccessKey: config.secretAccessKey,
      },
    });
  }

  async uploadArchive(archivePath: string, key: string): Promise<void> {
    const fileStream = fs.createReadStream(archivePath);
    await this.client.send(
      new PutObjectCommand({
        Bucket: this.bucket,
        Key: key,              // e.g. "owner/repo-name.tar.gz"
        Body: fileStream,
        ContentType: "application/gzip",
      })
    );
  }
}
typescript
// electron/services/cloud.service.ts
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import fs from "fs";

export interface CloudConfig {
  provider: "s3" | "r2";
  bucket: string;
  region?: string;         // AWS S3专用
  endpoint?: string;       // Cloudflare R2端点
  accessKeyId: string;     // 来自环境变量: process.env.AWS_ACCESS_KEY_ID
  secretAccessKey: string; // 来自环境变量: process.env.AWS_SECRET_ACCESS_KEY
}

export class CloudService {
  private client: S3Client;
  private bucket: string;

  constructor(config: CloudConfig) {
    this.bucket = config.bucket;
    this.client = new S3Client({
      region: config.region ?? "auto",
      endpoint: config.endpoint,       // 为Cloudflare R2设置
      credentials: {
        accessKeyId: config.accessKeyId,
        secretAccessKey: config.secretAccessKey,
      },
    });
  }

  async uploadArchive(archivePath: string, key: string): Promise<void> {
    const fileStream = fs.createReadStream(archivePath);
    await this.client.send(
      new PutObjectCommand({
        Bucket: this.bucket,
        Key: key,              // 例如: "owner/repo-name.tar.gz"
        Body: fileStream,
        ContentType: "application/gzip",
      })
    );
  }
}

Compress Service

压缩服务

typescript
// electron/services/compress.service.ts
import tar from "tar";
import path from "path";

export class CompressService {
  async createArchive(
    sourceDir: string,
    outputPath: string
  ): Promise<string> {
    const archiveName = `${path.basename(sourceDir)}.tar.gz`;
    const archivePath = path.join(outputPath, archiveName);

    await tar.create(
      { gzip: true, file: archivePath, cwd: path.dirname(sourceDir) },
      [path.basename(sourceDir)]
    );

    return archivePath;
  }
}
typescript
// electron/services/compress.service.ts
import tar from "tar";
import path from "path";

export class CompressService {
  async createArchive(
    sourceDir: string,
    outputPath: string
  ): Promise<string> {
    const archiveName = `${path.basename(sourceDir)}.tar.gz`;
    const archivePath = path.join(outputPath, archiveName);

    await tar.create(
      { gzip: true, file: archivePath, cwd: path.dirname(sourceDir) },
      [path.basename(sourceDir)]
    );

    return archivePath;
  }
}

Backup Orchestrator (Pipeline)

备份编排器(流水线)

typescript
// electron/services/backup-orchestrator.ts
import pLimit from "p-limit";

export interface BackupOptions {
  repos: Repository[];
  backupPath: string;
  token: string;
  concurrency: number;       // 1-10
  cloudConfig?: CloudConfig;
  onProgress: (repoName: string, status: RepoStatus) => void;
}

export class BackupOrchestrator {
  async run(options: BackupOptions): Promise<void> {
    const limit = pLimit(options.concurrency);
    const gitService = new GitService();
    const compressService = new CompressService();
    const cloudService = options.cloudConfig
      ? new CloudService(options.cloudConfig)
      : null;

    const tasks = options.repos.map((repo) =>
      limit(async () => {
        const repoPath = path.join(
          options.backupPath,
          repo.owner.login,
          repo.name
        );

        try {
          options.onProgress(repo.full_name, { status: "cloning" });
          await gitService.backupRepository(
            repo.clone_url,
            repoPath,
            options.token
          );

          if (cloudService) {
            options.onProgress(repo.full_name, { status: "compressing" });
            const archivePath = await compressService.createArchive(
              repoPath,
              options.backupPath
            );

            options.onProgress(repo.full_name, { status: "uploading" });
            await cloudService.uploadArchive(
              archivePath,
              `${repo.owner.login}/${repo.name}.tar.gz`
            );
          }

          options.onProgress(repo.full_name, { status: "done" });
        } catch (error) {
          options.onProgress(repo.full_name, {
            status: "error",
            error: (error as Error).message,
          });
        }
      })
    );

    await Promise.all(tasks);
  }
}
typescript
// electron/services/backup-orchestrator.ts
import pLimit from "p-limit";

export interface BackupOptions {
  repos: Repository[];
  backupPath: string;
  token: string;
  concurrency: number;       // 1-10
  cloudConfig?: CloudConfig;
  onProgress: (repoName: string, status: RepoStatus) => void;
}

export class BackupOrchestrator {
  async run(options: BackupOptions): Promise<void> {
    const limit = pLimit(options.concurrency);
    const gitService = new GitService();
    const compressService = new CompressService();
    const cloudService = options.cloudConfig
      ? new CloudService(options.cloudConfig)
      : null;

    const tasks = options.repos.map((repo) =>
      limit(async () => {
        const repoPath = path.join(
          options.backupPath,
          repo.owner.login,
          repo.name
        );

        try {
          options.onProgress(repo.full_name, { status: "cloning" });
          await gitService.backupRepository(
            repo.clone_url,
            repoPath,
            options.token
          );

          if (cloudService) {
            options.onProgress(repo.full_name, { status: "compressing" });
            const archivePath = await compressService.createArchive(
              repoPath,
              options.backupPath
            );

            options.onProgress(repo.full_name, { status: "uploading" });
            await cloudService.uploadArchive(
              archivePath,
              `${repo.owner.login}/${repo.name}.tar.gz`
            );
          }

          options.onProgress(repo.full_name, { status: "done" });
        } catch (error) {
          options.onProgress(repo.full_name, {
            status: "error",
            error: (error as Error).message,
          });
        }
      })
    );

    await Promise.all(tasks);
  }
}

Scheduler Service

定时任务服务

typescript
// electron/services/scheduler.service.ts
import cron from "node-cron";

export type ScheduleFrequency = "daily" | "weekly" | "monthly";

export class SchedulerService {
  private task: cron.ScheduledTask | null = null;

  schedule(
    frequency: ScheduleFrequency,
    time: string,             // "HH:MM" format
    callback: () => void
  ): void {
    this.stop();

    const [hour, minute] = time.split(":").map(Number);

    const cronExpressions: Record<ScheduleFrequency, string> = {
      daily:   `${minute} ${hour} * * *`,
      weekly:  `${minute} ${hour} * * 0`,
      monthly: `${minute} ${hour} 1 * *`,
    };

    this.task = cron.schedule(cronExpressions[frequency], callback);
  }

  stop(): void {
    this.task?.stop();
    this.task = null;
  }
}
typescript
// electron/services/scheduler.service.ts
import cron from "node-cron";

export type ScheduleFrequency = "daily" | "weekly" | "monthly";

export class SchedulerService {
  private task: cron.ScheduledTask | null = null;

  schedule(
    frequency: ScheduleFrequency,
    time: string,             // "HH:MM"格式
    callback: () => void
  ): void {
    this.stop();

    const [hour, minute] = time.split(":").map(Number);

    const cronExpressions: Record<ScheduleFrequency, string> = {
      daily:   `${minute} ${hour} * * *`,
      weekly:  `${minute} ${hour} * * 0`,
      monthly: `${minute} ${hour} 1 * *`,
    };

    this.task = cron.schedule(cronExpressions[frequency], callback);
  }

  stop(): void {
    this.task?.stop();
    this.task = null;
  }
}

Encrypted Settings Store

加密设置存储

typescript
// electron/store/store.ts
import Store from "electron-store";

interface AppSettings {
  githubToken: string;
  backupPath: string;
  concurrency: number;
  schedule?: {
    enabled: boolean;
    frequency: "daily" | "weekly" | "monthly";
    time: string;
  };
  cloud?: {
    provider: "s3" | "r2";
    bucket: string;
    region?: string;
    endpoint?: string;
    accessKeyId: string;
    secretAccessKey: string;
  };
}

export const store = new Store<AppSettings>({
  encryptionKey: "your-app-encryption-key", // electron-store encrypts at rest
  defaults: {
    githubToken: "",
    backupPath: "",
    concurrency: 3,
  },
});

// Usage
store.set("githubToken", token);
store.get("backupPath");
store.set("cloud", {
  provider: "r2",
  bucket: process.env.R2_BUCKET_NAME!,
  endpoint: process.env.R2_ENDPOINT!,
  accessKeyId: process.env.R2_ACCESS_KEY_ID!,
  secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
});
typescript
// electron/store/store.ts
import Store from "electron-store";

interface AppSettings {
  githubToken: string;
  backupPath: string;
  concurrency: number;
  schedule?: {
    enabled: boolean;
    frequency: "daily" | "weekly" | "monthly";
    time: string;
  };
  cloud?: {
    provider: "s3" | "r2";
    bucket: string;
    region?: string;
    endpoint?: string;
    accessKeyId: string;
    secretAccessKey: string;
  };
}

export const store = new Store<AppSettings>({
  encryptionKey: "your-app-encryption-key", // electron-store会在存储时加密
  defaults: {
    githubToken: "",
    backupPath: "",
    concurrency: 3,
  },
});

// 使用示例
store.set("githubToken", token);
store.get("backupPath");
store.set("cloud", {
  provider: "r2",
  bucket: process.env.R2_BUCKET_NAME!,
  endpoint: process.env.R2_ENDPOINT!,
  accessKeyId: process.env.R2_ACCESS_KEY_ID!,
  secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
});

IPC Bridge (Preload → Renderer)

IPC桥接(预加载→渲染进程)

typescript
// electron/preload.ts
import { contextBridge, ipcRenderer } from "electron";

contextBridge.exposeInMainWorld("electronAPI", {
  // GitHub
  validateToken: (token: string) =>
    ipcRenderer.invoke("github:validate-token", token),
  fetchRepos: (filters: RepoFilters) =>
    ipcRenderer.invoke("github:fetch-repos", filters),

  // Backup
  startBackup: (options: BackupOptions) =>
    ipcRenderer.invoke("backup:start", options),
  onProgress: (callback: (data: ProgressEvent) => void) =>
    ipcRenderer.on("backup:progress", (_event, data) => callback(data)),

  // Settings
  getSettings: () => ipcRenderer.invoke("settings:get"),
  saveSettings: (settings: Partial<AppSettings>) =>
    ipcRenderer.invoke("settings:save", settings),

  // Folder picker
  selectFolder: () => ipcRenderer.invoke("dialog:select-folder"),
});
typescript
// src/hooks/useBackup.ts — React renderer side
import { useState } from "react";

export function useBackup() {
  const [progress, setProgress] = useState<Record<string, RepoStatus>>({});

  const startBackup = async (repos: Repository[]) => {
    // Listen for per-repo progress events
    window.electronAPI.onProgress((data) => {
      setProgress((prev) => ({
        ...prev,
        [data.repoName]: data.status,
      }));
    });

    await window.electronAPI.startBackup({
      repos,
      concurrency: 5,
    });
  };

  return { startBackup, progress };
}
typescript
// electron/preload.ts
import { contextBridge, ipcRenderer } from "electron";

contextBridge.exposeInMainWorld("electronAPI", {
  // GitHub相关
  validateToken: (token: string) =>
    ipcRenderer.invoke("github:validate-token", token),
  fetchRepos: (filters: RepoFilters) =>
    ipcRenderer.invoke("github:fetch-repos", filters),

  // 备份相关
  startBackup: (options: BackupOptions) =>
    ipcRenderer.invoke("backup:start", options),
  onProgress: (callback: (data: ProgressEvent) => void) =>
    ipcRenderer.on("backup:progress", (_event, data) => callback(data)),

  // 设置相关
  getSettings: () => ipcRenderer.invoke("settings:get"),
  saveSettings: (settings: Partial<AppSettings>) =>
    ipcRenderer.invoke("settings:save", settings),

  // 文件夹选择器
  selectFolder: () => ipcRenderer.invoke("dialog:select-folder"),
});
typescript
// src/hooks/useBackup.ts — React渲染进程侧
import { useState } from "react";

export function useBackup() {
  const [progress, setProgress] = useState<Record<string, RepoStatus>>({});

  const startBackup = async (repos: Repository[]) => {
    // 监听每个仓库的进度事件
    window.electronAPI.onProgress((data) => {
      setProgress((prev) => ({
        ...prev,
        [data.repoName]: data.status,
      }));
    });

    await window.electronAPI.startBackup({
      repos,
      concurrency: 5,
    });
  };

  return { startBackup, progress };
}

Configuration Reference

配置参考

SettingDescriptionDefault
githubToken
PAT with
repo
+
read:org
scopes
backupPath
Local directory for cloned repos
concurrency
Parallel repo operations (1–10)
3
schedule.frequency
daily
/
weekly
/
monthly
schedule.time
Time in
HH:MM
format
cloud.provider
s3
or
r2
cloud.bucket
S3/R2 bucket name
cloud.region
AWS region (S3 only)
us-east-1
cloud.endpoint
Custom endpoint (R2:
https://<id>.r2.cloudflarestorage.com
)
设置项描述默认值
githubToken
拥有
repo
+
read:org
权限范围的PAT令牌
backupPath
克隆仓库的本地目录
concurrency
并行仓库操作数(1–10)
3
schedule.frequency
daily
/
weekly
/
monthly
schedule.time
时间,格式为
HH:MM
cloud.provider
s3
r2
cloud.bucket
S3/R2存储桶名称
cloud.region
AWS区域(仅S3适用)
us-east-1
cloud.endpoint
自定义端点(R2格式:
https://<id>.r2.cloudflarestorage.com

Cloudflare R2 vs AWS S3 Configuration

Cloudflare R2与AWS S3配置对比

typescript
// AWS S3
const s3Config: CloudConfig = {
  provider: "s3",
  bucket: process.env.S3_BUCKET!,
  region: process.env.AWS_REGION ?? "us-east-1",
  accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
  secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
};

// Cloudflare R2 — uses S3-compatible API
const r2Config: CloudConfig = {
  provider: "r2",
  bucket: process.env.R2_BUCKET!,
  endpoint: `https://${process.env.CF_ACCOUNT_ID}.r2.cloudflarestorage.com`,
  accessKeyId: process.env.R2_ACCESS_KEY_ID!,
  secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
};
typescript
// AWS S3配置
const s3Config: CloudConfig = {
  provider: "s3",
  bucket: process.env.S3_BUCKET!,
  region: process.env.AWS_REGION ?? "us-east-1",
  accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
  secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
};

// Cloudflare R2配置 — 兼容S3 API
const r2Config: CloudConfig = {
  provider: "r2",
  bucket: process.env.R2_BUCKET!,
  endpoint: `https://${process.env.CF_ACCOUNT_ID}.r2.cloudflarestorage.com`,
  accessKeyId: process.env.R2_ACCESS_KEY_ID!,
  secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
};

Repository Filters

仓库筛选器

typescript
interface RepoFilters {
  owned: boolean;          // Repos you own
  organizations: boolean;  // Repos in your orgs
  starred: boolean;        // Repos you've starred
  forked: boolean;         // Forked repos
  collaborator: boolean;   // Repos you collaborate on
}

// Example: back up only owned + org repos
const filters: RepoFilters = {
  owned: true,
  organizations: true,
  starred: false,
  forked: false,
  collaborator: false,
};
typescript
interface RepoFilters {
  owned: boolean;          // 你拥有的仓库
  organizations: boolean;  // 你所在组织的仓库
  starred: boolean;        // 你已标星的仓库
  forked: boolean;         // 复刻的仓库
  collaborator: boolean;   // 你参与协作的仓库
}

// 示例:仅备份个人拥有的仓库 + 组织仓库
const filters: RepoFilters = {
  owned: true,
  organizations: true,
  starred: false,
  forked: false,
  collaborator: false,
};

Repo Storage Layout

仓库存储结构

~/GitBackup/
├── hiteshchoudhary/
│   ├── gitbackup/        # Mirror clone (all branches)
│   ├── project-a/
│   └── project-b/
├── some-org/
│   └── org-repo/
└── archives/             # .tar.gz files (if cloud upload enabled)
    ├── hiteshchoudhary/
    │   ├── gitbackup.tar.gz
    │   └── project-a.tar.gz
    └── some-org/
        └── org-repo.tar.gz
~/GitBackup/
├── hiteshchoudhary/
│   ├── gitbackup/        // 镜像克隆(包含所有分支)
│   ├── project-a/
│   └── project-b/
├── some-org/
│   └── org-repo/
└── archives/             // .tar.gz文件(若启用云上传)
    ├── hiteshchoudhary/
    │   ├── gitbackup.tar.gz
    │   └── project-a.tar.gz
    └── some-org/
        └── org-repo.tar.gz

Common Patterns

常见模式

Adding a New IPC Handler

添加新的IPC处理器

typescript
// electron/ipc/backup.handler.ts
import { ipcMain } from "electron";
import { BackupOrchestrator } from "../services/backup-orchestrator";

export function registerBackupHandlers(mainWindow: BrowserWindow) {
  ipcMain.handle("backup:start", async (_event, options: BackupOptions) => {
    const orchestrator = new BackupOrchestrator();

    await orchestrator.run({
      ...options,
      onProgress: (repoName, status) => {
        // Send progress back to renderer
        mainWindow.webContents.send("backup:progress", { repoName, status });
      },
    });
  });
}
typescript
// electron/ipc/backup.handler.ts
import { ipcMain } from "electron";
import { BackupOrchestrator } from "../services/backup-orchestrator";

export function registerBackupHandlers(mainWindow: BrowserWindow) {
  ipcMain.handle("backup:start", async (_event, options: BackupOptions) => {
    const orchestrator = new BackupOrchestrator();

    await orchestrator.run({
      ...options,
      onProgress: (repoName, status) => {
        // 将进度发送回渲染进程
        mainWindow.webContents.send("backup:progress", { repoName, status });
      },
    });
  });
}

Adding a New Settings Page (React)

添加新的设置页面(React)

typescript
// src/pages/Settings.tsx
import { useEffect, useState } from "react";

export default function Settings() {
  const [settings, setSettings] = useState<AppSettings | null>(null);

  useEffect(() => {
    window.electronAPI.getSettings().then(setSettings);
  }, []);

  const handleSave = async () => {
    await window.electronAPI.saveSettings(settings!);
  };

  return (
    <div>
      <label>Concurrency (1-10)</label>
      <input
        type="number"
        min={1}
        max={10}
        value={settings?.concurrency ?? 3}
        onChange={(e) =>
          setSettings((s) => ({ ...s!, concurrency: Number(e.target.value) }))
        }
      />
      <button onClick={handleSave}>Save</button>
    </div>
  );
}
typescript
// src/pages/Settings.tsx
import { useEffect, useState } from "react";

export default function Settings() {
  const [settings, setSettings] = useState<AppSettings | null>(null);

  useEffect(() => {
    window.electronAPI.getSettings().then(setSettings);
  }, []);

  const handleSave = async () => {
    await window.electronAPI.saveSettings(settings!);
  };

  return (
    <div>
      <label>并发数(1-10</label>
      <input
        type="number"
        min={1}
        max={10}
        value={settings?.concurrency ?? 3}
        onChange={(e) =>
          setSettings((s) => ({ ...s!, concurrency: Number(e.target.value) }))
        }
      />
      <button onClick={handleSave}>保存</button>
    </div>
  );
}

Troubleshooting

故障排除

IssueFix
git: command not found
Install Git and ensure it's in system PATH
Token validation failsConfirm
repo
+
read:org
scopes are granted
macOS "app can't be opened"Right-click → Open to bypass Gatekeeper
Windows Defender warningClick "More info" → "Run anyway"
Rate limit errors (large accounts)Reduce concurrency to 1–2; app handles pagination automatically
R2 upload 403 errorsVerify
CF_ACCOUNT_ID
in endpoint URL and correct R2 API token permissions
Incremental fetch not workingEnsure
backupPath
still contains
.git
directory
Scheduled backup not triggeringKeep app running in system tray (don't fully quit)
问题解决方法
git: command not found
安装Git并确保其路径已添加至系统PATH
令牌验证失败确认已授予
repo
+
read:org
权限范围
macOS提示“无法打开应用”右键点击→打开以绕过Gatekeeper安全限制
Windows Defender警告点击“更多信息”→“仍要运行”
速率限制错误(大型账号)将并发数降低至1–2;应用会自动处理分页
R2上传403错误验证端点URL中的
CF_ACCOUNT_ID
及R2 API令牌权限是否正确
增量拉取不生效确保
backupPath
目录下仍存在
.git
文件夹
定时备份未触发保持应用在系统托盘运行(不要完全退出)

Development Tips

开发技巧

bash
undefined
bash
undefined

Run dev server (Vite + Electron with hot reload)

运行开发服务器(Vite + Electron热重载)

npm run dev
npm run dev

Type-check without building

仅类型检查不构建

npx tsc --noEmit
npx tsc --noEmit

Build renderer only

仅构建渲染进程

npx vite build
npx vite build

Inspect electron-store saved data location

查看electron-store保存的数据位置

macOS: ~/Library/Application Support/GitBackup/config.json

macOS: ~/Library/Application Support/GitBackup/config.json

Windows: %APPDATA%\GitBackup\config.json

Windows: %APPDATA%\GitBackup\config.json

Linux: ~/.config/GitBackup/config.json

Linux: ~/.config/GitBackup/config.json

undefined
undefined