Sceduled mode

This commit is contained in:
Morten Olsen
2021-12-18 13:47:57 +01:00
parent 91615e005e
commit ecf382b486
7 changed files with 161 additions and 92 deletions

View File

@@ -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

View File

@@ -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"

3
entry.sh Normal file
View File

@@ -0,0 +1,3 @@
#!/bin/sh
exec node /app/index.js $@

View File

@@ -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"
}, },

View File

@@ -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'); process.exit(-1);
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; program.parse(process.argv);
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);
}
};
run().catch(console.error);

89
src/run.ts Normal file
View 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;

View File

@@ -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"