-
-
Save ibash/d20e320c5248cc61eaa1cb1248be4bb3 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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