Skip to content

Commit be737e1

Browse files
committed
* Real backups to other partition/drive added, not just snapshots
* Some changes in output messages
1 parent 2213cc9 commit be737e1

File tree

3 files changed

+204
-19
lines changed

3 files changed

+204
-19
lines changed

changelog.md

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
# 0.x (Unreleased yet)
22
* Code moved into class
33
* Fix for Ubuntu 15.10 (binaries moved from `/usr/sbin` to `/bin`, now both cases are supported)
4-
* Refactoring into multiple methods.
5-
* Fix for `0` as count of keeping snapshots.
4+
* Refactoring into multiple methods
5+
* Fix for `0` as count of keeping snapshots
6+
* Real backups to other partition/drive added, not just snapshots
7+
* Some changes in output messages
68

79
# 0.1 (Nov 20, 2014)
810
* initial release

just-backup-btrfs.php

+177-16
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
* @author Nazar Mokrynskyi <[email protected]>
99
* @copyright Copyright (c) 2014-2015, Nazar Mokrynskyi
1010
* @license http://opensource.org/licenses/MIT
11-
* @version 0.3
11+
* @version 0.4
1212
*/
1313
class Just_backup_btrfs {
1414
/**
@@ -31,16 +31,14 @@ function backup () {
3131
$this->do_backup($local_config);
3232
}
3333
}
34+
/**
35+
* @param array $config
36+
*/
3437
protected function do_backup ($config) {
3538
$source = $config['source_mounted_volume'];
3639
$destination = $config['destination_within_partition'];
3740

38-
if (!file_exists($destination) && !mkdir($destination, 0755, true)) {
39-
echo "Creating backup destination $destination failed, skip backing up $source, check permissions\n";
40-
return;
41-
}
42-
43-
$history_db = $this->init_database($source, $destination);
41+
$history_db = $this->init_database($destination);
4442
if (!$history_db) {
4543
return;
4644
}
@@ -53,6 +51,7 @@ protected function do_backup ($config) {
5351
$date = time();
5452
$snapshot = date($config['date_format'], $date);
5553
shell_exec("$this->binary subvolume snapshot -r \"$source\" \"$destination/$snapshot\"");
54+
shell_exec("sync"); // To actually write snapshot to disk
5655
if (!file_exists("$destination/$snapshot")) {
5756
echo "Snapshot creation for $source failed\n";
5857
return;
@@ -85,21 +84,28 @@ protected function do_backup ($config) {
8584
}
8685
echo "Snapshot $snapshot for $source created successfully\n";
8786

88-
$this->cleanup($history_db, $source, $destination);
87+
$this->do_backup_external($config, $history_db, $snapshot, $comment, $date);
88+
89+
$this->cleanup($history_db, $destination);
8990
$history_db->close();
9091
}
9192
/**
92-
* @param string $source
9393
* @param string $destination
9494
*
9595
* @return false|SQLite3
9696
*/
97-
protected function init_database ($source, $destination) {
97+
protected function init_database ($destination) {
98+
if (!file_exists($destination) && !mkdir($destination, 0755, true)) {
99+
echo "Creating backup destination $destination failed, check permissions\n";
100+
return false;
101+
}
102+
98103
$history_db = new SQLite3("$destination/history.db");
99104
if (!$history_db) {
100-
echo "Opening database $destination/history.db failed, skip backing up $source, check permissions\n";
105+
echo "Opening database $destination/history.db failed, check permissions\n";
101106
return false;
102107
}
108+
103109
$history_db->exec(
104110
"CREATE TABLE IF NOT EXISTS `history` (
105111
`snapshot_name` TEXT,
@@ -113,6 +119,162 @@ protected function init_database ($source, $destination) {
113119
);
114120
return $history_db;
115121
}
122+
/**
123+
* @param array $config
124+
* @param SQLite3 $history_db
125+
* @param string $snapshot
126+
* @param string $comment
127+
* @param int $date
128+
*/
129+
protected function do_backup_external ($config, $history_db, $snapshot, $comment, $date) {
130+
if (!isset($config['destination_other_partition']) || !$config['destination_other_partition']) {
131+
return;
132+
}
133+
$source = $config['source_mounted_volume'];
134+
$destination = $config['destination_within_partition'];
135+
$destination_external = $config['destination_other_partition'];
136+
$history_db_external = $this->init_database($destination_external);
137+
if (!$history_db_external) {
138+
return;
139+
}
140+
list($keep_year, $keep_month, $keep_day, $keep_hour) = $this->how_long_to_keep($history_db_external, $config['keep_snapshots']);
141+
if (!$keep_hour) {
142+
return;
143+
}
144+
/**
145+
* Next block is because of BTRFS limitations - we can't receive incremental snapshots diff into path which is mounted as subvolume, not root of the partition.
146+
* To overcome this we determine partition which was mounted, subvolume path inside partition, mount partition root to temporary path and determine full path of our destination inside this new mount point.
147+
* This new mount point will be stored as $destination_external_fixed, mount point will be stored in $target_mount_point variables
148+
*/
149+
$mount_point_options = $this->determine_mount_point($destination_external);
150+
if (!$mount_point_options) {
151+
echo "Can't find where and how $destination_external is mounted, probably it is not on BTRFS partition?\n";
152+
echo "Creating backup $snapshot of $source to $destination_external failed\n";
153+
return;
154+
}
155+
list($partition, $mount_point, $mount_options) = $mount_point_options;
156+
157+
/**
158+
* Set fixed destination as path inside subvolume, just remove mount point from the whole destination path
159+
*/
160+
$destination_external_fixed = str_replace($mount_point, '', $destination_external);
161+
/**
162+
* Now detect whether partition subvolume was mounted, $m[1] will contain subvolume path inside partition
163+
*/
164+
if (preg_match("#$partition\[(.+)\]#", exec("findmnt $mount_point"), $m)) {
165+
$destination_external_fixed = $m[1].$destination_external_fixed;
166+
}
167+
168+
$target_mount_point = '/tmp/'.uniqid('just_backup_btrfs_');
169+
$destination_external_fixed = $target_mount_point.$destination_external_fixed;
170+
mkdir($target_mount_point);
171+
shell_exec("mount -o subvol=/,$mount_options /$partition $target_mount_point");
172+
unset($mount_point_options, $partition, $mount_point, $mount_options);
173+
174+
if (!isset($destination_external_fixed, $target_mount_point)) {
175+
echo "Can't find where and how $destination_external is mounted, probably it is not on BTRFS partition?\n";
176+
echo "Creating backup $snapshot of $source to $destination_external failed\n";
177+
return;
178+
}
179+
$common_snapshot = $this->get_last_common_snapshot($history_db, $history_db_external);
180+
if ($common_snapshot) {
181+
shell_exec(
182+
"$this->binary send -p \"$destination/$common_snapshot\" \"$destination/$snapshot\" | $this->binary receive $destination_external_fixed"
183+
);
184+
$type = 'incremental backup';
185+
} else {
186+
shell_exec("$this->binary send \"$destination/$snapshot\" | $this->binary receive $destination_external_fixed");
187+
$type = 'backup';
188+
}
189+
if (!file_exists("$destination_external/$snapshot")) {
190+
echo "Creating $type $snapshot of $source to $destination_external failed\n";
191+
} else {
192+
$snapshot_escaped = $history_db->escapeString($snapshot);
193+
if (!$history_db_external->exec(
194+
"INSERT INTO `history` (
195+
`snapshot_name`,
196+
`comment`,
197+
`date`,
198+
`keep_hour`,
199+
`keep_day`,
200+
`keep_month`,
201+
`keep_year`
202+
) VALUES (
203+
'$snapshot_escaped',
204+
'$comment',
205+
'$date',
206+
'$keep_hour',
207+
'$keep_day',
208+
'$keep_month',
209+
'$keep_year'
210+
)"
211+
)
212+
) {
213+
echo "Creating $type $snapshot of $source to $destination_external finished successfully, but not added to history because of database error\n";
214+
} else {
215+
$this->cleanup($history_db_external, $destination_external);
216+
echo "Creating $type $snapshot of $source to $destination_external finished successfully\n";
217+
}
218+
}
219+
shell_exec("umount $target_mount_point");
220+
rmdir($target_mount_point);
221+
}
222+
protected function determine_mount_point ($destination_external) {
223+
$mount_point_options = [];
224+
foreach (explode("\n", shell_exec('mount')) as $mount_string) {
225+
/**
226+
* Choose only BTRFS filesystems
227+
*/
228+
preg_match("#^(.+) on (.+) type btrfs \((.+)\)$#", $mount_string, $m);
229+
/**
230+
* If our destination is inside current mount point - this is what we need
231+
*/
232+
if (isset($m[2]) && strpos($destination_external, $m[2]) === 0) {
233+
/**
234+
* Partition in form of /dev/sdXY
235+
*/
236+
$partition = $m[1];
237+
/**
238+
* Mount point
239+
*/
240+
$mount_point = $m[2];
241+
/**
242+
* Mount options
243+
*/
244+
$mount_options = $m[3];
245+
if (!isset($mount_point_options[1]) || strlen($mount_point_options[1]) < strlen($mount_point)) {
246+
$mount_point_options = [$partition, $mount_point, $mount_options];
247+
}
248+
}
249+
}
250+
return $mount_point_options ?: false;
251+
}
252+
/**
253+
* @param SQLite3 $history_db
254+
* @param SQLite3 $history_db_external
255+
*
256+
* @return bool
257+
*/
258+
protected function get_last_common_snapshot ($history_db, $history_db_external) {
259+
$snapshots = $history_db_external->query(
260+
"SELECT `snapshot_name`
261+
FROM `history`"
262+
);
263+
while ($snapshot = $snapshots->fetchArray(SQLITE3_ASSOC)['snapshot_name']) {
264+
$snapshot_escaped = $history_db->escapeString($snapshot);
265+
$snapshot_found = $history_db
266+
->query(
267+
"SELECT `snapshot_name`
268+
FROM `history`
269+
WHERE `snapshot_name` = '$snapshot_escaped'"
270+
)
271+
->fetchArray();
272+
if ($snapshot_found) {
273+
return $snapshot;
274+
}
275+
}
276+
return false;
277+
}
116278
/**
117279
* @param SQLite3 $history_db
118280
* @param array $keep_snapshots
@@ -172,14 +334,13 @@ protected function keep_or_not ($history_db, $keep, $interval) {
172334
}
173335
/**
174336
* @param SQLite3 $history_db
175-
* @param string $source
176337
* @param string $destination
177338
*/
178-
protected function cleanup ($history_db, $source, $destination) {
339+
protected function cleanup ($history_db, $destination) {
179340
foreach ($this->snapshots_for_removal($history_db) as $snapshot_for_removal) {
180341
shell_exec("$this->binary subvolume delete \"$destination/$snapshot_for_removal\"");
181342
if (file_exists("$destination/$snapshot_for_removal")) {
182-
echo "Removing old snapshot $snapshot_for_removal for $source failed\n";
343+
echo "Removing old snapshot $snapshot_for_removal from $destination failed\n";
183344
continue;
184345
}
185346
$snapshot_for_removal_escaped = $history_db->escapeString($snapshot_for_removal);
@@ -188,10 +349,10 @@ protected function cleanup ($history_db, $source, $destination) {
188349
WHERE `snapshot_name` = '$snapshot_for_removal_escaped'"
189350
)
190351
) {
191-
echo "Old snapshot $snapshot_for_removal for $source removed successfully, but not removed from history because of database error\n";
352+
echo "Old snapshot $snapshot_for_removal removed successfully from $destination, but not removed from history because of database error\n";
192353
continue;
193354
}
194-
echo "Old snapshot $snapshot_for_removal for $source removed successfully\n";
355+
echo "Old snapshot $snapshot_for_removal removed successfully from $destination\n";
195356
}
196357
}
197358
/**

readme.md

+23-1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,24 @@ sudo ./just-backup-btrfs.php
3636

3737
or mark as executable, rename it to `just-backup-btrfs` (since file can't contain dots in that place) and put into `/etc/cron.daily` to make backups every day.
3838

39+
Output will be like this:
40+
```
41+
nazar-pc@nazar-pc ~> sudo /just-backup-btrfs.php
42+
Just backup btrfs started...
43+
Snapshot 2015-05-17_07:31:13 for / created successfully
44+
At subvol /backup/root/2015-05-17_07:31:13
45+
Creating incremental backup 2015-05-17_07:31:13 of / to /backup_hdd/root finished successfully
46+
Snapshot 2015-05-17_07:31:13 for /home created successfully
47+
At subvol /backup/home/2015-05-17_07:31:13
48+
At subvol 2015-05-17_07:31:13
49+
Creating backup 2015-05-17_07:31:13 of /home to /backup_hdd/home finished successfully
50+
Snapshot 2015-05-17_07:36:37 for /web created successfully
51+
At subvol /backup/web/2015-05-17_07:36:37
52+
At subvol 2015-05-17_07:36:37
53+
Creating backup 2015-05-17_07:36:37 of /web to /backup_hdd/web finished successfully
54+
Just backup btrfs finished!
55+
```
56+
3957
Also you can call it with cron or in some other way:)
4058

4159
### What it actually does?
@@ -52,6 +70,7 @@ Configuration options are especially made self-explanatory:
5270
{
5371
"source_mounted_volume" : "/",
5472
"destination_within_partition" : "/backup/root",
73+
"destination_other_partition" : false,
5574
"date_format" : "Y-m-d_H:i:s",
5675
"keep_snapshots" : {
5776
"hour" : 60,
@@ -63,6 +82,7 @@ Configuration options are especially made self-explanatory:
6382
{
6483
"source_mounted_volume" : "/home",
6584
"destination_within_partition" : "/backup/home",
85+
"destination_other_partition" : "/backup_external/home",
6686
"date_format" : "Y-m-d_H:i:s",
6787
"keep_snapshots" : {
6888
"hour" : 120,
@@ -73,7 +93,9 @@ Configuration options are especially made self-explanatory:
7393
}
7494
]
7595
```
76-
The only thing that may not be obvious here - you can use `-1` as value for `keep_snapshots` elements to allow storing of all created snapshots.
96+
Here you can use `-1` as value for `keep_snapshots` elements to allow storing of all created snapshots.
97+
Also `destination_other_partition` might be `false` or path on some other BTRFS partition (even on other drive) to create backups, not just snapshots.
98+
Most options should be obvious
7799

78100
Save this config as `/etc/just-backup-btrfs.json` and customize as you like.
79101

0 commit comments

Comments
 (0)