8
8
* @author Nazar Mokrynskyi <[email protected] >
9
9
* @copyright Copyright (c) 2014-2015, Nazar Mokrynskyi
10
10
* @license http://opensource.org/licenses/MIT
11
- * @version 0.3
11
+ * @version 0.4
12
12
*/
13
13
class Just_backup_btrfs {
14
14
/**
@@ -31,16 +31,14 @@ function backup () {
31
31
$ this ->do_backup ($ local_config );
32
32
}
33
33
}
34
+ /**
35
+ * @param array $config
36
+ */
34
37
protected function do_backup ($ config ) {
35
38
$ source = $ config ['source_mounted_volume ' ];
36
39
$ destination = $ config ['destination_within_partition ' ];
37
40
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 );
44
42
if (!$ history_db ) {
45
43
return ;
46
44
}
@@ -53,6 +51,7 @@ protected function do_backup ($config) {
53
51
$ date = time ();
54
52
$ snapshot = date ($ config ['date_format ' ], $ date );
55
53
shell_exec ("$ this ->binary subvolume snapshot -r \"$ source \" \"$ destination/ $ snapshot \"" );
54
+ shell_exec ("sync " ); // To actually write snapshot to disk
56
55
if (!file_exists ("$ destination/ $ snapshot " )) {
57
56
echo "Snapshot creation for $ source failed \n" ;
58
57
return ;
@@ -85,21 +84,28 @@ protected function do_backup ($config) {
85
84
}
86
85
echo "Snapshot $ snapshot for $ source created successfully \n" ;
87
86
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 );
89
90
$ history_db ->close ();
90
91
}
91
92
/**
92
- * @param string $source
93
93
* @param string $destination
94
94
*
95
95
* @return false|SQLite3
96
96
*/
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
+
98
103
$ history_db = new SQLite3 ("$ destination/history.db " );
99
104
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" ;
101
106
return false ;
102
107
}
108
+
103
109
$ history_db ->exec (
104
110
"CREATE TABLE IF NOT EXISTS `history` (
105
111
`snapshot_name` TEXT,
@@ -113,6 +119,162 @@ protected function init_database ($source, $destination) {
113
119
);
114
120
return $ history_db ;
115
121
}
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
+ }
116
278
/**
117
279
* @param SQLite3 $history_db
118
280
* @param array $keep_snapshots
@@ -172,14 +334,13 @@ protected function keep_or_not ($history_db, $keep, $interval) {
172
334
}
173
335
/**
174
336
* @param SQLite3 $history_db
175
- * @param string $source
176
337
* @param string $destination
177
338
*/
178
- protected function cleanup ($ history_db , $ source , $ destination ) {
339
+ protected function cleanup ($ history_db , $ destination ) {
179
340
foreach ($ this ->snapshots_for_removal ($ history_db ) as $ snapshot_for_removal ) {
180
341
shell_exec ("$ this ->binary subvolume delete \"$ destination/ $ snapshot_for_removal \"" );
181
342
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" ;
183
344
continue ;
184
345
}
185
346
$ snapshot_for_removal_escaped = $ history_db ->escapeString ($ snapshot_for_removal );
@@ -188,10 +349,10 @@ protected function cleanup ($history_db, $source, $destination) {
188
349
WHERE `snapshot_name` = ' $ snapshot_for_removal_escaped' "
189
350
)
190
351
) {
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" ;
192
353
continue ;
193
354
}
194
- echo "Old snapshot $ snapshot_for_removal for $ source removed successfully \n" ;
355
+ echo "Old snapshot $ snapshot_for_removal removed successfully from $ destination \n" ;
195
356
}
196
357
}
197
358
/**
0 commit comments