Skip to content

Commit 1acc4dd

Browse files
author
root
committed
Initial version
1 parent 70b4d70 commit 1acc4dd

12 files changed

+1910
-101
lines changed

.env.example

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Configures the pino log level: debug, info, warn, error
2+
LOG_LEVEL=info

.eslintrc.yml

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
env:
2+
browser: true
3+
es2021: true
4+
extends:
5+
- standard
6+
parserOptions:
7+
ecmaVersion: 12
8+
sourceType: module
9+
rules: {}

.gitignore

+2-100
Original file line numberDiff line numberDiff line change
@@ -1,104 +1,6 @@
1-
# Logs
2-
logs
3-
*.log
4-
npm-debug.log*
5-
yarn-debug.log*
6-
yarn-error.log*
7-
lerna-debug.log*
8-
9-
# Diagnostic reports (https://nodejs.org/api/report.html)
10-
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11-
12-
# Runtime data
13-
pids
14-
*.pid
15-
*.seed
16-
*.pid.lock
17-
18-
# Directory for instrumented libs generated by jscoverage/JSCover
19-
lib-cov
20-
21-
# Coverage directory used by tools like istanbul
22-
coverage
23-
*.lcov
24-
25-
# nyc test coverage
26-
.nyc_output
27-
28-
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29-
.grunt
30-
31-
# Bower dependency directory (https://bower.io/)
32-
bower_components
33-
34-
# node-waf configuration
35-
.lock-wscript
36-
37-
# Compiled binary addons (https://nodejs.org/api/addons.html)
38-
build/Release
39-
401
# Dependency directories
412
node_modules/
42-
jspm_packages/
43-
44-
# TypeScript v1 declaration files
45-
typings/
46-
47-
# TypeScript cache
48-
*.tsbuildinfo
49-
50-
# Optional npm cache directory
51-
.npm
52-
53-
# Optional eslint cache
54-
.eslintcache
553

56-
# Microbundle cache
57-
.rpt2_cache/
58-
.rts2_cache_cjs/
59-
.rts2_cache_es/
60-
.rts2_cache_umd/
61-
62-
# Optional REPL history
63-
.node_repl_history
64-
65-
# Output of 'npm pack'
66-
*.tgz
67-
68-
# Yarn Integrity file
69-
.yarn-integrity
70-
71-
# dotenv environment variables file
4+
# Project files
725
.env
73-
.env.test
74-
75-
# parcel-bundler cache (https://parceljs.org/)
76-
.cache
77-
78-
# Next.js build output
79-
.next
80-
81-
# Nuxt.js build / generate output
82-
.nuxt
83-
dist
84-
85-
# Gatsby files
86-
.cache/
87-
# Comment in the public line in if your project uses Gatsby and *not* Next.js
88-
# https://nextjs.org/blog/next-9-1#public-directory-support
89-
# public
90-
91-
# vuepress build output
92-
.vuepress/dist
93-
94-
# Serverless directories
95-
.serverless/
96-
97-
# FuseBox cache
98-
.fusebox/
99-
100-
# DynamoDB Local files
101-
.dynamodb/
102-
103-
# TernJS port file
104-
.tern-port
6+
config.yaml

README.md

+10-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,10 @@
1-
# btrfs-backups
1+
# btrfs-backups
2+
3+
Tool for creating and managing backups using BTRFS snapshots and btrfs-sxbackup script.
4+
5+
Requires external scripts to be available in `$PATH`:
6+
7+
- https://github.com/masc3d/btrfs-sxbackup
8+
9+
- https://github.com/MurzNN/btrfs-du/tree/patch-1 (original [btrfs-du](https://github.com/nachoparker/btrfs-du) with [PR #24](https://github.com/nachoparker/btrfs-du/pull/24))
10+

backup.js

+132
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
#!/usr/bin/env node
2+
3+
import * as fs from 'fs'
4+
import * as yaml from 'js-yaml'
5+
import { performance } from 'perf_hooks'
6+
import { serializeError } from 'serialize-error'
7+
import yargs from 'yargs'
8+
import { hideBin } from 'yargs/helpers'
9+
10+
import { logger } from './lib/logger.js'
11+
import { timingsToSpans } from './lib/utils.js'
12+
import * as sxbackup from './lib/btrfs-sxbackup.js'
13+
import * as btrfs from './lib/btrfs.js'
14+
15+
const configFile = 'config.yaml'
16+
17+
/* Loading the config file */
18+
let config
19+
try {
20+
const fileContents = fs.readFileSync(new URL(configFile, import.meta.url), 'utf8')
21+
config = yaml.load(fileContents)
22+
} catch (e) {
23+
console.log(e)
24+
logger.error(serializeError(e), 'Probjems with loading config file \'config.yaml\'')
25+
process.exit(1)
26+
}
27+
28+
/**
29+
* Does a single backup job.
30+
*
31+
* @param {string} job - job name, matching to config file key
32+
*/
33+
34+
const backupDo = function (job) {
35+
let op
36+
const timings = {}
37+
try {
38+
timings.start = performance.now()
39+
op = 'info'
40+
const info = sxbackup.info(job)
41+
timings[op] = performance.now()
42+
43+
op = 'run'
44+
sxbackup.run(info['Source URL'])
45+
timings[op] = performance.now()
46+
47+
op = 'check'
48+
const checkStat = btrfs.check(info['Destination URL'])
49+
timings[op] = performance.now()
50+
const stat = {
51+
...checkStat,
52+
timings: timingsToSpans(timings)
53+
}
54+
logger.info({
55+
function: 'backupDo',
56+
job,
57+
...stat
58+
}, `Backup job ${job} finished`)
59+
} catch (e) {
60+
logger.error({
61+
function: 'backupDo',
62+
job: job,
63+
stage: op,
64+
exception: serializeError(e)
65+
}, `Backup job ${job} failed on stage '${op}'`)
66+
}
67+
}
68+
69+
/**
70+
* Configures a backup job (creates new or updates config).
71+
*
72+
* @param {string} job - job name, matching to config file key
73+
*/
74+
75+
export const backupConfigure = function (job) {
76+
const action = sxbackup.info(job) === false
77+
? 'init'
78+
: 'update'
79+
sxbackup.configure(
80+
action,
81+
job,
82+
config.jobs[job].destination,
83+
job.retention?.source ?? config.retentionDefault.source,
84+
job.retention?.destination ?? config.retentionDefault.destination
85+
)
86+
logger.info({
87+
function: 'backupConfigure',
88+
source: job,
89+
destination: config.jobs[job].destination,
90+
sr: job.retention?.source ?? config.retentionDefault.source,
91+
dr: job.retention?.destination ?? config.retentionDefault.destination
92+
}, `Backup job ${job} configured`)
93+
}
94+
95+
/**
96+
* Launches the configure job for all configured backup jobs.
97+
*
98+
* @param {string} argv - cli arguments
99+
*/
100+
101+
const doConfigure = function (argv = {}) {
102+
logger.info('Update backup jobs started')
103+
for (const job in config.jobs) {
104+
backupConfigure(job)
105+
}
106+
logger.info('Update backup jobs finished')
107+
}
108+
109+
/**
110+
* Launches the backup job for all configured backup jobs.
111+
*
112+
* @param {string} argv - cli arguments
113+
*/
114+
115+
const doBackups = function (argv = {}) {
116+
logger.info('Backup process started')
117+
for (const job in config.jobs) {
118+
backupDo(job)
119+
}
120+
logger.info('Backup process finished')
121+
}
122+
123+
// eslint-disable-next-line no-unused-expressions
124+
yargs(hideBin(process.argv))
125+
.command('configure', 'Configure (create and update) backup jobs', () => {}, (argv) => {
126+
doConfigure(argv)
127+
})
128+
.command('backup', 'Execute backup jobs', () => {}, (argv) => {
129+
doBackups(argv)
130+
})
131+
.demandCommand(1)
132+
.argv

config.example.yaml

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Default retention rules using btrfs-sxbackup expression:
2+
# From https://github.com/masc3d/btrfs-sxbackup/blob/master/README.rst
3+
# > Expression defining which source snapshots to
4+
# > retain/cleanup. can be a static number (of backups) or
5+
# > more complex expression like "1d:4/d, 1w:daily,
6+
# > 2m:none" literally translating to: "1 day from now
7+
# > keep 4 backups a day, 1 week from now keep daily
8+
# > backups, 2 months from now keep none"
9+
retentionDefault:
10+
source: "3"
11+
destination: "1d:1/d, 1w:daily, 2w:daily, 3w:daily, 4w:daily 5w:none"
12+
13+
# List of jobs to make backup process
14+
# key - source btrfs volume mount path
15+
# destination - destination btrfs volume mount path
16+
jobs:
17+
'/var/lib/mysql':
18+
destination: '/var/backups/mysql'
19+
20+
'/var/lib/postgresql':
21+
destination: '/var/backups/postgresql'
22+
23+
'/srv':
24+
destination: '/var/backups/srv'
25+
26+
'/home':
27+
destination: '/var/backups/home'

lib/btrfs-sxbackup.js

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { execSync } from 'child_process'
2+
3+
/**
4+
* Executes btrfs-sxbackup info action on given url.
5+
*
6+
* @param {string} url - backup identifier (source or destination)
7+
*/
8+
9+
export const info = function (url) {
10+
const output = execSync(`btrfs-sxbackup info ${url}`).toString()
11+
if (output.search('ERROR') !== -1) {
12+
// throw new Error(output)
13+
return false
14+
}
15+
const outputParsed = output.matchAll(/^\s{3}(?<key>.+?)\s\s+(?<value>.+(?:\n {26}\w.+$)*)$/mg)
16+
// console.log(output.toString())
17+
// console.log(...outputParsed)
18+
const result = {}
19+
for (const item of outputParsed) {
20+
result[item.groups.key] = item.groups.value.replace(/^ {26}/gm, '')
21+
}
22+
return result
23+
}
24+
25+
/**
26+
* Executes btrfs-sxbackup run action on given url.
27+
*
28+
* @param {string} url - backup identifier (source or destination)
29+
*/
30+
31+
export const run = function (url) {
32+
const output = execSync(`btrfs-sxbackup run ${url}`)
33+
34+
const outputParsed = output.toString().match(/(?<name>[^\s]+) created successfully/)
35+
36+
if (!outputParsed.groups.name) {
37+
throw new Error('Missing success result')
38+
}
39+
40+
return outputParsed.groups.name
41+
}
42+
43+
/**
44+
* Executes btrfs-sxbackup purge action on given url.
45+
*
46+
* @param {string} url - backup identifier (source or destination)
47+
*/
48+
49+
export const purge = function (url) {
50+
execSync(`btrfs-sxbackup purge ${url}`)
51+
}
52+
53+
/**
54+
* Configuring the backup job.
55+
*
56+
* @param {string} action - init | update
57+
* @param {string} source - source location
58+
* @param {string} destination - destination location
59+
* @param {string} sr - source retention rules
60+
* @param {string} dr - destination retention rules
61+
*/
62+
export const configure = function (action, source, destination, sr, dr) {
63+
if (['init', 'update'].indexOf(action) === -1) {
64+
throw new Error('action argument must be only "init" or "update"')
65+
}
66+
const cmd = `btrfs-sxbackup ${action} -sr "${sr}" -dr "${dr}" ${source} ${destination}`
67+
// console.log(cmd)
68+
try {
69+
execSync(cmd)
70+
} catch (e) {
71+
throw new Error('Execution failed: ' + e.output.toString())
72+
}
73+
}

0 commit comments

Comments
 (0)