mirror of
https://github.com/morten-olsen/github-backup.git
synced 2026-02-08 01:36:24 +01:00
Sceduled mode
This commit is contained in:
@@ -8,8 +8,9 @@ COPY . /app/
|
|||||||
RUN yarn bundle
|
RUN yarn bundle
|
||||||
|
|
||||||
FROM node:alpine
|
FROM node:alpine
|
||||||
RUN apk update && apk add git
|
COPY entry.sh /entry.sh
|
||||||
|
RUN chmod 711 /entry.sh && apk update && apk add git
|
||||||
COPY --from=BuildEnv /app/dist /app
|
COPY --from=BuildEnv /app/dist /app
|
||||||
RUN ls /app
|
CMD ["schedule"]
|
||||||
CMD ["node", "/app/index.js"]
|
ENTRYPOINT ["/entry.sh"]
|
||||||
VOLUME /backup
|
VOLUME /backup
|
||||||
|
|||||||
10
README.md
10
README.md
@@ -1,6 +1,6 @@
|
|||||||
# Simple Github Backup
|
# Simple GitHub Backup
|
||||||
|
|
||||||
A simple Github backup image, which will fetch a mirror backup of all repos the user is associated with from Github
|
A simple GitHub backup image, which will fetch a mirror backup of all repos the user is associated with from GitHub
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
```
|
```
|
||||||
@@ -9,11 +9,11 @@ docker run -it --rm \
|
|||||||
-v "$PWD/backup:/backup" \
|
-v "$PWD/backup:/backup" \
|
||||||
--cap-drop=all \
|
--cap-drop=all \
|
||||||
--user "$UID:$GID" \
|
--user "$UID:$GID" \
|
||||||
ghcr.io/morten-olsen/github-backup
|
ghcr.io/morten-olsen/github-backup run
|
||||||
```
|
```
|
||||||
_note: `--user` is not required, but recommended instead of running as root. Remember to give the user write access to the backup directory_
|
_Note: `--user` is not required, but recommended instead of running as root. Remember to give the user write access to the backup directory_
|
||||||
|
|
||||||
You can also limit which repositories to backup using the environment variabled `INCLUDE` and `EXCLUDE`, which supports a list or repos separated by `,` and with `*` as wildcard
|
You can also limit which repositories to back up using the environment variables `INCLUDE` and `EXCLUDE`, which supports a list or repos separated by `,` and with `*` as wildcard
|
||||||
|
|
||||||
```
|
```
|
||||||
-e INCLUDE="morten-olsen/*,morten-olsen-env/dotfiles" -e EXCLUDE="morten-olsen/something,*/test"
|
-e INCLUDE="morten-olsen/*,morten-olsen-env/dotfiles" -e EXCLUDE="morten-olsen/something,*/test"
|
||||||
|
|||||||
@@ -12,8 +12,11 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/fs-extra": "^9.0.12",
|
"@types/fs-extra": "^9.0.12",
|
||||||
"@types/node": "^16.4.0",
|
"@types/node": "^16.4.0",
|
||||||
|
"@types/node-cron": "^3.0.0",
|
||||||
"@types/rimraf": "^3.0.1",
|
"@types/rimraf": "^3.0.1",
|
||||||
"@vercel/ncc": "^0.29.0",
|
"@vercel/ncc": "^0.29.0",
|
||||||
|
"commander": "^8.3.0",
|
||||||
|
"node-cron": "^3.0.0",
|
||||||
"ts-node": "^10.1.0",
|
"ts-node": "^10.1.0",
|
||||||
"typescript": "^4.3.5"
|
"typescript": "^4.3.5"
|
||||||
},
|
},
|
||||||
|
|||||||
99
src/index.ts
99
src/index.ts
@@ -1,89 +1,20 @@
|
|||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
import { Octokit } from '@octokit/rest';
|
import runBackup from './run';
|
||||||
import simpleGit from 'simple-git';
|
import { program } from 'commander';
|
||||||
import fs from 'fs-extra';
|
import cron from 'node-cron';
|
||||||
import path from 'path';
|
|
||||||
import ora from 'ora';
|
|
||||||
|
|
||||||
const token = process.env.GITHUB_TOKEN;
|
const run = program.command('run');
|
||||||
const backupLocation = path.resolve(process.env.GITHUB_BACKUP_LOCATION || '/backup');
|
run.action(runBackup)
|
||||||
const included = (process.env.INCLUDE || '*/*').split(',');
|
|
||||||
const excluded = (process.env.EXCLUDE || '').split(',');
|
|
||||||
|
|
||||||
const mirror = async (target: string, repo: string) => {
|
const schedule = program.command('schedule');
|
||||||
const authRemote = `https://foo:${token}@github.com/${repo}`
|
schedule.action(() => {
|
||||||
|
const schedule = process.env.SCHEDULE || '0 0 3 * * Sunday';
|
||||||
if (fs.existsSync(target)) {
|
cron.schedule(schedule, () => {
|
||||||
const git = simpleGit(target);
|
runBackup().catch((err) => {
|
||||||
const remotes = await git.getRemotes();
|
console.error(err);
|
||||||
const origin = remotes.find(r => r.name === 'origin');
|
|
||||||
if (origin) {
|
|
||||||
await git.remote(['set-url', 'origin', authRemote]);
|
|
||||||
} else {
|
|
||||||
await git.addRemote('origin', authRemote);
|
|
||||||
}
|
|
||||||
await git.remote(['update']);
|
|
||||||
await git.remote(['set-url', 'origin', `https://github.com/${repo}`]);
|
|
||||||
} else {
|
|
||||||
await fs.mkdirp(target);
|
|
||||||
const git = simpleGit(target);
|
|
||||||
await git.mirror(authRemote, target);
|
|
||||||
await git.remote(['set-url', 'origin', `https://github.com/${repo}`]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const shouldRun = (repoUser: string, repoName: string) => {
|
|
||||||
const isIncluded = included.reduce((result, current) => {
|
|
||||||
if (result) return result;
|
|
||||||
const [user = '*', repo = '*'] = current.split('/');
|
|
||||||
return (user === '*' || user === repoUser) && (repo === '*' || repo === repoName);
|
|
||||||
}, false)
|
|
||||||
|
|
||||||
const isExcluded = excluded.reduce((result, current) => {
|
|
||||||
if (result) return result;
|
|
||||||
const [user = '', repo = ''] = current.split('/');
|
|
||||||
return (user === '*' || user === repoUser) && (repo === '*' || repo === repoName);
|
|
||||||
}, false)
|
|
||||||
|
|
||||||
return isIncluded && !isExcluded;
|
|
||||||
}
|
|
||||||
|
|
||||||
const run = async () => {
|
|
||||||
const github = new Octokit({
|
|
||||||
auth: process.env.GITHUB_TOKEN,
|
|
||||||
});
|
|
||||||
|
|
||||||
const action = github.repos.listForAuthenticatedUser;
|
|
||||||
const errors: any[] = [];
|
|
||||||
for await (const repos of github.paginate.iterator(action, { visibility: 'all' })) {
|
|
||||||
for (const repo of repos.data) {
|
|
||||||
if (!shouldRun(repo.owner.login, repo.name)) {
|
|
||||||
console.log(`skipping ${repo.full_name}`)
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const loader = ora('preparing');
|
|
||||||
loader.prefixText = repo.full_name;
|
|
||||||
loader.start();
|
|
||||||
try {
|
|
||||||
const repoBackupLocation = path.join(backupLocation, repo.full_name);
|
|
||||||
const infoLocation = path.join(repoBackupLocation, 'info.json');
|
|
||||||
const gitLocation = path.join(repoBackupLocation, 'git');
|
|
||||||
await fs.mkdirp(repoBackupLocation);
|
|
||||||
loader.text = 'fething info';
|
|
||||||
await fs.writeFile(infoLocation, JSON.stringify(repo, null, ' '), 'utf-8');
|
|
||||||
loader.text = 'mirroring';
|
|
||||||
await mirror(gitLocation, repo.full_name);
|
|
||||||
loader.text = '';
|
|
||||||
loader.succeed();
|
|
||||||
} catch (err: any) {
|
|
||||||
loader.fail(err.toString());
|
|
||||||
errors.push(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (errors.length > 0) {
|
|
||||||
process.exit(-1);
|
process.exit(-1);
|
||||||
}
|
});
|
||||||
};
|
});
|
||||||
|
});
|
||||||
|
|
||||||
run().catch(console.error);
|
program.parse(process.argv);
|
||||||
|
|||||||
89
src/run.ts
Normal file
89
src/run.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import { Octokit } from '@octokit/rest';
|
||||||
|
import simpleGit from 'simple-git';
|
||||||
|
import fs from 'fs-extra';
|
||||||
|
import path from 'path';
|
||||||
|
import ora from 'ora';
|
||||||
|
|
||||||
|
const token = process.env.GITHUB_TOKEN;
|
||||||
|
const backupLocation = path.resolve(process.env.GITHUB_BACKUP_LOCATION || '/backup');
|
||||||
|
const included = (process.env.INCLUDE || '*/*').split(',');
|
||||||
|
const excluded = (process.env.EXCLUDE || '').split(',');
|
||||||
|
|
||||||
|
const mirror = async (target: string, repo: string) => {
|
||||||
|
const authRemote = `https://foo:${token}@github.com/${repo}`
|
||||||
|
|
||||||
|
if (fs.existsSync(target)) {
|
||||||
|
const git = simpleGit(target);
|
||||||
|
const remotes = await git.getRemotes();
|
||||||
|
const origin = remotes.find(r => r.name === 'origin');
|
||||||
|
if (origin) {
|
||||||
|
await git.remote(['set-url', 'origin', authRemote]);
|
||||||
|
} else {
|
||||||
|
await git.addRemote('origin', authRemote);
|
||||||
|
}
|
||||||
|
await git.remote(['update']);
|
||||||
|
await git.remote(['set-url', 'origin', `https://github.com/${repo}`]);
|
||||||
|
} else {
|
||||||
|
await fs.mkdirp(target);
|
||||||
|
const git = simpleGit(target);
|
||||||
|
await git.mirror(authRemote, target);
|
||||||
|
await git.remote(['set-url', 'origin', `https://github.com/${repo}`]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const shouldRun = (repoUser: string, repoName: string) => {
|
||||||
|
const isIncluded = included.reduce((result, current) => {
|
||||||
|
if (result) return result;
|
||||||
|
const [user = '*', repo = '*'] = current.split('/');
|
||||||
|
return (user === '*' || user === repoUser) && (repo === '*' || repo === repoName);
|
||||||
|
}, false)
|
||||||
|
|
||||||
|
const isExcluded = excluded.reduce((result, current) => {
|
||||||
|
if (result) return result;
|
||||||
|
const [user = '', repo = ''] = current.split('/');
|
||||||
|
return (user === '*' || user === repoUser) && (repo === '*' || repo === repoName);
|
||||||
|
}, false)
|
||||||
|
|
||||||
|
return isIncluded && !isExcluded;
|
||||||
|
}
|
||||||
|
|
||||||
|
const run = async () => {
|
||||||
|
const github = new Octokit({
|
||||||
|
auth: process.env.GITHUB_TOKEN,
|
||||||
|
});
|
||||||
|
|
||||||
|
const action = github.repos.listForAuthenticatedUser;
|
||||||
|
const errors: any[] = [];
|
||||||
|
for await (const repos of github.paginate.iterator(action, { visibility: 'all' })) {
|
||||||
|
for (const repo of repos.data) {
|
||||||
|
if (!shouldRun(repo.owner.login, repo.name)) {
|
||||||
|
console.log(`skipping ${repo.full_name}`)
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const loader = ora('preparing');
|
||||||
|
loader.prefixText = repo.full_name;
|
||||||
|
loader.start();
|
||||||
|
try {
|
||||||
|
const repoBackupLocation = path.join(backupLocation, repo.full_name);
|
||||||
|
const infoLocation = path.join(repoBackupLocation, 'info.json');
|
||||||
|
const gitLocation = path.join(repoBackupLocation, 'git');
|
||||||
|
await fs.mkdirp(repoBackupLocation);
|
||||||
|
loader.text = 'fething info';
|
||||||
|
await fs.writeFile(infoLocation, JSON.stringify(repo, null, ' '), 'utf-8');
|
||||||
|
loader.text = 'mirroring';
|
||||||
|
await mirror(gitLocation, repo.full_name);
|
||||||
|
loader.text = '';
|
||||||
|
loader.succeed();
|
||||||
|
} catch (err: any) {
|
||||||
|
loader.fail(err.toString());
|
||||||
|
errors.push(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (errors.length > 0) {
|
||||||
|
process.exit(-1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default run;
|
||||||
|
|
||||||
42
yarn.lock
42
yarn.lock
@@ -256,6 +256,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@types/node-cron@npm:^3.0.0":
|
||||||
|
version: 3.0.0
|
||||||
|
resolution: "@types/node-cron@npm:3.0.0"
|
||||||
|
checksum: 24cf0cdb5aa93092c71e92474d69082ea392f28b2814876a5b6ef60c1a904da635e0d70158b622113dd2b013c909fe41951b5d8f699258b7950afb121256e3df
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@types/node@npm:*, @types/node@npm:^16.4.0":
|
"@types/node@npm:*, @types/node@npm:^16.4.0":
|
||||||
version: 16.11.12
|
version: 16.11.12
|
||||||
resolution: "@types/node@npm:16.11.12"
|
resolution: "@types/node@npm:16.11.12"
|
||||||
@@ -527,6 +534,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"commander@npm:^8.3.0":
|
||||||
|
version: 8.3.0
|
||||||
|
resolution: "commander@npm:8.3.0"
|
||||||
|
checksum: 0f82321821fc27b83bd409510bb9deeebcfa799ff0bf5d102128b500b7af22872c0c92cb6a0ebc5a4cf19c6b550fba9cedfa7329d18c6442a625f851377bacf0
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"concat-map@npm:0.0.1":
|
"concat-map@npm:0.0.1":
|
||||||
version: 0.0.1
|
version: 0.0.1
|
||||||
resolution: "concat-map@npm:0.0.1"
|
resolution: "concat-map@npm:0.0.1"
|
||||||
@@ -685,11 +699,14 @@ __metadata:
|
|||||||
"@octokit/rest": ^18.7.0
|
"@octokit/rest": ^18.7.0
|
||||||
"@types/fs-extra": ^9.0.12
|
"@types/fs-extra": ^9.0.12
|
||||||
"@types/node": ^16.4.0
|
"@types/node": ^16.4.0
|
||||||
|
"@types/node-cron": ^3.0.0
|
||||||
"@types/rimraf": ^3.0.1
|
"@types/rimraf": ^3.0.1
|
||||||
"@vercel/ncc": ^0.29.0
|
"@vercel/ncc": ^0.29.0
|
||||||
|
commander: ^8.3.0
|
||||||
dotenv: ^10.0.0
|
dotenv: ^10.0.0
|
||||||
fs-extra: ^10.0.0
|
fs-extra: ^10.0.0
|
||||||
nanoid: ^3.1.23
|
nanoid: ^3.1.23
|
||||||
|
node-cron: ^3.0.0
|
||||||
ora: ^5.4.1
|
ora: ^5.4.1
|
||||||
rimraf: ^3.0.2
|
rimraf: ^3.0.2
|
||||||
simple-git: ^2.41.1
|
simple-git: ^2.41.1
|
||||||
@@ -1031,6 +1048,22 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"moment-timezone@npm:^0.5.31":
|
||||||
|
version: 0.5.34
|
||||||
|
resolution: "moment-timezone@npm:0.5.34"
|
||||||
|
dependencies:
|
||||||
|
moment: ">= 2.9.0"
|
||||||
|
checksum: 12a1d3d52e4ba509cf1fa36bbda59d898a08fa80ab35f6c358747e93aec1f07e617cec647eaf2e8acf5f9132e581d4704d34a9edffa9a80c5cd04bf23b277595
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"moment@npm:>= 2.9.0":
|
||||||
|
version: 2.29.1
|
||||||
|
resolution: "moment@npm:2.29.1"
|
||||||
|
checksum: 1e14d5f422a2687996be11dd2d50c8de3bd577c4a4ca79ba5d02c397242a933e5b941655de6c8cb90ac18f01cc4127e55b4a12ae3c527a6c0a274e455979345e
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"ms@npm:2.1.2":
|
"ms@npm:2.1.2":
|
||||||
version: 2.1.2
|
version: 2.1.2
|
||||||
resolution: "ms@npm:2.1.2"
|
resolution: "ms@npm:2.1.2"
|
||||||
@@ -1061,6 +1094,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"node-cron@npm:^3.0.0":
|
||||||
|
version: 3.0.0
|
||||||
|
resolution: "node-cron@npm:3.0.0"
|
||||||
|
dependencies:
|
||||||
|
moment-timezone: ^0.5.31
|
||||||
|
checksum: acae07d835f74636ec2bf598ffadf6b89aa2eae036671bffad336371b28fb1eefb11870912c965bb351cd958d4b62f9fa9b28cf922b5f681459ea4878837ffcf
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"node-fetch@npm:^2.6.1":
|
"node-fetch@npm:^2.6.1":
|
||||||
version: 2.6.6
|
version: 2.6.6
|
||||||
resolution: "node-fetch@npm:2.6.6"
|
resolution: "node-fetch@npm:2.6.6"
|
||||||
|
|||||||
Reference in New Issue
Block a user