Skip to main content

gitbackup-github-desktop

Desktop application to back up all GitHub repositories locally and optionally to AWS S3 or Cloudflare R2 cloud storage

설치로 이동

소스 정보

저장소
reason-machines/trending-skills
최근 소스 활동
2026년 4월 28일 14:19
감지된 SKILL.md 언어
영어
스타
80
포크
15

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
gitbackup-github-desktop
description
Desktop application to back up all GitHub repositories locally and optionally to AWS S3 or Cloudflare R2 cloud storage
triggers
["backup my github repos","clone all my github repositories","set up gitbackup","configure github backup to s3","schedule automatic github backup","add cloud storage to gitbackup","gitbackup electron app","backup github account locally"]
# GitBackup — GitHub Repository Backup Desktop App > Skill by [ara.so](https://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. ## Installation ### Download Pre-built Binary Download from [Releases](https://github.com/hiteshchoudhary/gitbackup/releases/latest): | Platform | File | |----------|------| | 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. ### Build from Source ```bash git clone https://github.com/hiteshchoudhary/gitbackup.git cd gitbackup npm install # Development with hot reload npm run dev # Package for current platform npm run package # Platform-specific builds npm run package:mac npm run package:win npm run package:linux ``` ## GitHub Token Setup 1. Go to [github.com/settings/tokens](https://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**. ## 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 ``` ## Tech Stack | Layer | Technology | |-------|-----------| | Framework | Electron 35 | | Frontend | React 19 + Tailwind CSS | | Language | TypeScript 5 | | Bundler | Vite 8 | | GitHub API | @octokit/rest | | Git operations | simple-git | | Cloud storage | AWS SDK v3 (S3-compatible) | | Settings | electron-store (encrypted) | | Scheduling | node-cron | | Packaging | electron-builder | ## Core Services — Code Examples ### GitHub Service (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; } } ``` ### Git Service (Clone & Fetch) ```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]); } } ``` ### Cloud Service (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", }) ); } } ``` ### 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; } } ``` ### 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); } } ``` ### 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; } } ``` ### 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!, }); ``` ### IPC Bridge (Preload → Renderer) ```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"), });
GitHub에서 보기
이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기