14 Commits
Author SHA1 Message Date
sugoidogo 02ff1b58f7 try to fix logging again
Main / release (push) Successful in 41s
2026-01-14 00:57:44 -08:00
sugoidogo 8eab92841f handle and log unknown items
Main / release (push) Successful in 41s
2026-01-14 00:43:01 -08:00
sugoidogo fb4e872990 fix output piping
Main / release (push) Successful in 42s
2026-01-14 00:30:11 -08:00
sugoidogo e3893f594e fix subcommand output
Main / release (push) Successful in 42s
2026-01-14 00:21:48 -08:00
sugoidogo 2457617bdc redirect subcommand output to file
Main / release (push) Successful in 41s
2026-01-14 00:17:04 -08:00
sugoidogo 8ae7880d6b log output to file
Main / release (push) Successful in 40s
2026-01-14 00:09:38 -08:00
sugoidogo 81ab7be40c reload systemd after install
Main / release (push) Successful in 41s
2026-01-13 23:51:00 -08:00
sugoidogo 422a36bbeb fix create-config
Main / release (push) Successful in 41s
2026-01-13 23:45:15 -08:00
sugoidogo e49b1d9a4b BREAKING CHANGE: rename service
Main / release (push) Successful in 42s
2026-01-13 23:18:41 -08:00
sugoidogo a8a23db940 add sucess output to each command
Main / release (push) Successful in 42s
2026-01-13 23:09:53 -08:00
sugoidogo c7443470a2 add logic to find self executable/script
Main / release (push) Successful in 40s
2026-01-13 22:57:00 -08:00
sugoidogo 1f2bc5e153 add CLICOLOR_FORCE env permission
Main / release (push) Successful in 42s
2026-01-13 22:36:10 -08:00
sugoidogo ae7d31f72d ensure output directory exists before calling tar
Main / release (push) Successful in 48s
2026-01-13 22:32:49 -08:00
sugoidogo 248203186f re-order install process to fail early 2025-12-13 07:13:41 -08:00
4 changed files with 60 additions and 12 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ for (const target of targets) {
sugoiSpawn('deno', 'compile',
'--target', target,
'--include', 'src/snapper-template.txt',
'--include', 'src/snapper@.service',
'--include', 'src/snapback@.service',
'--output', 'dist/snapback-'+target,
'--permission-set','src/main.ts'
)
+2 -1
View File
@@ -5,7 +5,8 @@
"permissions": {
"read": true,
"write": true,
"run": ["tar","chmod"]
"run": ["tar","chmod"],
"env": ["CLICOLOR_FORCE"]
}
}
}
+56 -9
View File
@@ -3,14 +3,34 @@
import { program } from "commander"
import { parseJSON, parseTOML, stringifyJSON } from "confbox"
import fs from "node:fs"
import path from "node:path"
import util from 'node:util'
import strftime from 'strftime'
import { spawnSync } from "node:child_process"
let config: { [key: string]: string } = {}
let index: { [key: string]: { [key: string]: string } } = {}
let logfd = 1
function logToFile() {
// Source - https://stackoverflow.com/a
// Posted by Clément Désiles, modified by community. See post 'Timeline' for change history
// Retrieved 2026-01-14, License - CC BY-SA 3.0
logfd = fs.openSync('/etc/snapback/log.txt','w')
const logFile = fs.createWriteStream('', { 'fd':logfd });
const logStdout = process.stdout;
console.log = function (...args) {
logFile.write(util.format.apply(null, args) + '\n');
logStdout.write(util.format.apply(null, args) + '\n');
}
console.error = console.log;
}
let config: { [key: string]: string|undefined } = {}
let index: { [key: string]: { [key: string]: string|undefined }|undefined } = {}
function sugoiSpawn(...command: string[]) {
const result = spawnSync(command.shift()!, command, { stdio: 'inherit' })
const result = spawnSync(command.shift()!, command, { stdio: ['ignore',logfd,logfd] })
if (result.error) throw result.error
if (result.status !== 0) process.exit(result.status)
return result
@@ -34,45 +54,72 @@ program.command('*').description('snapback ignores unknown commands')
program.command('install').description('installs snapback on your system')
.action(function () {
const template = fs.readFileSync(import.meta.dirname + '/snapper-template.txt')
const service = fs.readFileSync(import.meta.dirname + '/snapback@.service')
let lib_snapper = '/usr/libexec/snapper'
if (!fs.existsSync(lib_snapper)) lib_snapper = '/usr/lib/snapper'
console.debug(`${import.meta.url} will be moved to ${lib_snapper}/99-snapback`)
const template = fs.readFileSync(import.meta.dirname + '/snapper-template.txt')
const service = fs.readFileSync(import.meta.dirname + '/snapper@.service')
fs.writeFileSync('/etc/systemd/system/snapper@.service', service, 'utf8')
if (!fs.existsSync('/etc/snapper/config-templates')) fs.mkdirSync('/etc/snapper/config-templates')
fs.writeFileSync('/etc/snapper/config-templates/snapback', template, 'utf8')
fs.writeFileSync('/etc/systemd/system/snapback@.service', service, 'utf8')
fs.mkdirSync(lib_snapper + '/plugins', { recursive: true })
fs.renameSync(import.meta.url, lib_snapper + '/plugins/99-snapback')
let argv0 = process.argv0
if (['node','deno','bun'].includes(path.basename(argv0).replace('.exe',''))) {
argv0 = import.meta.url
}
fs.renameSync(argv0, lib_snapper + '/plugins/99-snapback')
sugoiSpawn('chmod', '+x', lib_snapper + '/plugins/99-snapback')
sugoiSpawn('systemctl','daemon-reload')
console.log('snapback installed')
})
program.command('create-snapshot-post <subvolume> <fstype> <number>')
.description('create a backup after a snapshot')
.action(function (subvolume, fstype, number) {
if (!config[subvolume]) return
logToFile()
if (!config[subvolume]) {console.log(subvolume + ' not configured for snapback'); return}
const destination = strftime(config[subvolume])
const dirname = path.dirname(destination)
try { fs.mkdirSync(dirname, { 'recursive': true }) } catch {}
sugoiSpawn('tar',
`--directory=${subvolume}/.snapshots/${number}/snapshot`,
'--create', '--file', destination,
'--auto-compress', '--dereference', '.'
)
if(!index[subvolume]) index[subvolume]={}
index[subvolume][number] = destination
console.log('created backup at '+destination)
})
program.command('delete-snapshot-post <subvolume> <fstype> <number>')
.description('delete a backup after deleting a snapshot')
.action(function (subvolume, fstype, number) {
logToFile()
if (!index[subvolume]) {
console.log('no backups indexed for ' + subvolume)
return
}
const destination = index[subvolume][number]
if (!destination) {
console.log('no backup indexed for ' + subvolume + ':' + number)
return
}
fs.unlinkSync(destination)
delete index[subvolume][number]
console.log('deleted backup at '+destination)
})
program.command('delete-config-post <subvolume> <fstype>')
.description('delete metadata of deleted subvolume, retaining backups')
.action(function (subvolume, fstype) {
logToFile()
delete index[subvolume]
if (!config[subvolume]) {
console.log(subvolume + ' not configured for snapback')
return
}
const destination = strftime(config[subvolume])
const dirname = path.dirname(destination)
console.log('existing backups remain at '+dirname)
})
program.parse()
@@ -1,5 +1,5 @@
[Service]
Type=exec
ExecStartPre=-btrfs subvolume create %I
ExecStartPre=-snapper --config %i create-config %I --template snapback
ExecStartPre=-snapper --config %i create-config --template snapback %I
ExecStart=snapper --config %i create --cleanup-algorithm number