Skip to content

Instantly share code, notes, and snippets.

@ibash

ibash/backup.js Secret

Created December 27, 2023 18:20
Show Gist options
  • Save ibash/d20e320c5248cc61eaa1cb1248be4bb3 to your computer and use it in GitHub Desktop.
Save ibash/d20e320c5248cc61eaa1cb1248be4bb3 to your computer and use it in GitHub Desktop.
const Excludes = require("./excludes");
const Conf = require("conf");
const shell = require("shelljs");
const MAINTENANCE_SCHEDULE = 3 * 24 * 60 * 60 * 1000;
module.exports = class Backup {
constructor() {
this.config = new Conf();
}
async run() {
console.log(`Restic Backup Started at ${new Date().toISOString()}`);
try {
const excludes = new Excludes();
excludes.build();
await this.unlock();
//await this.forceCloudFilesToSync()
await this.backup();
if (this.shouldMaintain()) {
console.log("Running Maintain...");
await this.forget();
await this.check();
await this.stats();
this.config.set("lastMaintain", new Date().getTime());
}
await this.execSafe(
"curl https://hc-ping.com/redacted"
);
} catch (error) {
console.error(error);
await this.execSafe(
"curl https://hc-ping.com/redacted/fail"
);
throw error;
}
}
shouldMaintain() {
const last = this.config.get("lastMaintain", 0);
const now = new Date().getTime();
return now - last > MAINTENANCE_SCHEDULE;
}
async unlock() {
return this.execSafe(`/opt/homebrew/bin/restic unlock`);
}
// If a file is synced to the cloud it can interrupt the backup with an error:
//
// error: read /Users/islam/Library/CloudStorage/Dropbox/some_file.txt: resource deadlock avoided/
//
// The solution is to force reading cloud files.
//
// ref: https://www.reddit.com/r/Arqbackup/comments/t9eggc/solution_to_resource_deadlock_avoided_error/
//
// TODO(ibash) shouldn't have to use this since we can just not backup cloud
// files...
async forceCloudFilesToSync() {
const cloudFolders = [
'"/Users/islam/Library/Mobile Documents"',
'/Users/islam/Library/CloudStorage/Dropbox'
]
for (const folder of cloudFolders) {
await this.execSafe(
`find ${folder} -type f -print0 | xargs -0 shasum`,
{silent: true}
)
}
}
async backup() {
return this.execSafe(
`
/opt/homebrew/bin/restic backup \
--one-file-system \
--cache-dir "/Users/islam/Library/Caches/restic" \
--cleanup-cache \
--host "Islams-MacBook-Pro.local" \
--exclude-file "built-excludes.txt" \
"${process.env.HOME}"
`
);
}
async forget() {
return this.execSafe(
`
/opt/homebrew/bin/restic forget \
--cache-dir "/Users/islam/Library/Caches/restic" \
--cleanup-cache \
--host "Islams-MacBook-Pro.local" \
--keep-tag "preformat" \
--keep-last 10 \
--keep-weekly 52 \
--keep-monthly 12 \
--keep-yearly 3 \
--prune
`
);
}
async check() {
return this.execSafe(`/opt/homebrew/bin/restic check`);
}
async stats() {
return this.execSafe(
`
/opt/homebrew/bin/restic stats \
--cache-dir "/Users/islam/Library/Caches/restic" \
--cleanup-cache
`
);
}
async execSafe(command, options) {
return new Promise((resolve, reject) => {
let isConnectionLost = false
const child = shell.exec(command, options, (code, stdout, stderr) => {
if (isConnectionLost) {
return
}
if (code !== 0) {
const error = new Error(
`command failed:
stdout:
${stdout}
stderr:
${stderr}`
);
reject(error);
return
}
resolve()
})
child.stderr.on('data', (data) => {
// unfortunately restic never recovers from this, so end early s.t. the
// backup can be retried again sooner
//
// ref: https://github.com/restic/restic/issues/353
if (data.includes('returned error, retrying after') && data.includes('connection lost')) {
isConnectionLost = true
reject(new Error('connection lost'))
}
})
})
const result = shell.exec.apply(shell, arguments);
if (result.code !== 0) {
throw new Error(
`command failed:
stdout:
${result.stdout}
stderr:
${result.stderr}`
);
}
return result;
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment