1
|
|
|
import os |
2
|
|
|
import shutil |
3
|
|
|
|
4
|
|
|
from backuppc_clone.Config import Config |
5
|
|
|
from backuppc_clone.DataLayer import DataLayer |
6
|
|
|
from backuppc_clone.ProgressBar import ProgressBar |
7
|
|
|
from backuppc_clone.helper.BackupScanner import BackupScanner |
8
|
|
|
from backuppc_clone.misc import sizeof_fmt |
9
|
|
|
from backuppc_clone.style.BackupPcCloneStyle import BackupPcCloneStyle |
10
|
|
|
|
11
|
|
|
|
12
|
|
View Code Duplication |
class BackupClone: |
|
|
|
|
13
|
|
|
""" |
14
|
|
|
Clones a backup of a host |
15
|
|
|
""" |
16
|
|
|
|
17
|
|
|
# ------------------------------------------------------------------------------------------------------------------ |
18
|
|
|
def __init__(self, io: BackupPcCloneStyle): |
19
|
|
|
""" |
20
|
|
|
Object constructor. |
21
|
|
|
|
22
|
|
|
@param BackupPcCloneStyle io: The output style. |
23
|
|
|
""" |
24
|
|
|
|
25
|
|
|
self.__io: BackupPcCloneStyle = io |
26
|
|
|
""" |
27
|
|
|
The output style. |
28
|
|
|
""" |
29
|
|
|
|
30
|
|
|
self.__host: str = '' |
31
|
|
|
""" |
32
|
|
|
The host of the backup. |
33
|
|
|
""" |
34
|
|
|
|
35
|
|
|
self.__backup_no: int = 0 |
36
|
|
|
""" |
37
|
|
|
The number of the backup. |
38
|
|
|
""" |
39
|
|
|
|
40
|
|
|
# ------------------------------------------------------------------------------------------------------------------ |
41
|
|
|
def __scan_host_backup(self, csv_filename: str) -> None: |
42
|
|
|
""" |
43
|
|
|
Scans the backup of a host. |
44
|
|
|
|
45
|
|
|
@param str csv_filename: The name of the CSV file. |
46
|
|
|
""" |
47
|
|
|
self.__io.section('Original backup') |
48
|
|
|
|
49
|
|
|
scanner = BackupScanner(self.__io) |
50
|
|
|
scanner.scan_directory(self.__host, self.__backup_no, csv_filename) |
51
|
|
|
|
52
|
|
|
self.__io.writeln('') |
53
|
|
|
self.__io.writeln(' Files found: {}'.format(scanner.file_count)) |
54
|
|
|
self.__io.writeln(' Directories found: {}'.format(scanner.dir_count)) |
55
|
|
|
self.__io.writeln('') |
56
|
|
|
|
57
|
|
|
# ------------------------------------------------------------------------------------------------------------------ |
58
|
|
|
def __import_host_scan_csv(self, csv_filename: str) -> None: |
59
|
|
|
""" |
60
|
|
|
Imports to CSV file with entries of the original pool into the SQLite database. |
61
|
|
|
|
62
|
|
|
@param str csv_filename: The name of the CSV file. |
63
|
|
|
""" |
64
|
|
|
self.__io.log_very_verbose(' Importing <fso>{}</fso>'.format(csv_filename)) |
65
|
|
|
|
66
|
|
|
hst_id = DataLayer.instance.get_host_id(self.__host) |
67
|
|
|
bck_id = DataLayer.instance.get_bck_id(hst_id, int(self.__backup_no)) |
68
|
|
|
|
69
|
|
|
DataLayer.instance.backup_empty(bck_id) |
70
|
|
|
DataLayer.instance.import_csv('BKC_BACKUP_TREE', |
71
|
|
|
['bbt_seq', 'bbt_inode_original', 'bbt_dir', 'bbt_name'], |
72
|
|
|
csv_filename, |
73
|
|
|
False, |
74
|
|
|
{'bck_id': bck_id}) |
75
|
|
|
|
76
|
|
|
# ------------------------------------------------------------------------------------------------------------------ |
77
|
|
|
def __import_pre_scan_csv(self, csv_filename: str) -> None: |
78
|
|
|
""" |
79
|
|
|
Imports to CSV file with entries of the original pool into the SQLite database. |
80
|
|
|
|
81
|
|
|
@param str csv_filename: The name of the CSV file. |
82
|
|
|
""" |
83
|
|
|
self.__io.section('Using pre-scan') |
84
|
|
|
|
85
|
|
|
self.__import_host_scan_csv(csv_filename) |
86
|
|
|
|
87
|
|
|
hst_id = DataLayer.instance.get_host_id(self.__host) |
88
|
|
|
bck_id = DataLayer.instance.get_bck_id(hst_id, int(self.__backup_no)) |
89
|
|
|
|
90
|
|
|
stats = DataLayer.instance.backup_get_stats(bck_id) |
91
|
|
|
|
92
|
|
|
self.__io.writeln(' Files found: {}'.format(stats['#files'])) |
93
|
|
|
self.__io.writeln(' Directories found: {}'.format(stats['#dirs'])) |
94
|
|
|
self.__io.writeln('') |
95
|
|
|
|
96
|
|
|
# ------------------------------------------------------------------------------------------------------------------ |
97
|
|
|
def __copy_pool_file(self, dir_name: str, file_name: str, bpl_inode_original: int) -> int: |
98
|
|
|
""" |
99
|
|
|
Copies a pool file from the Original pool to the clone pool. Returns the size eof the file. |
100
|
|
|
|
101
|
|
|
@param str dir_name: The directory name relative to the top dir. |
102
|
|
|
@param str file_name: The file name. |
103
|
|
|
@param int bpl_inode_original: The inode of the original pool file. |
104
|
|
|
|
105
|
|
|
:rtype: int |
106
|
|
|
""" |
107
|
|
|
original_path = os.path.join(Config.instance.top_dir_original, dir_name, file_name) |
108
|
|
|
clone_dir = os.path.join(Config.instance.top_dir_clone, dir_name) |
109
|
|
|
clone_path = os.path.join(clone_dir, file_name) |
110
|
|
|
|
111
|
|
|
self.__io.log_very_verbose('Coping <fso>{}</fso> to <fso>{}</fso>'.format(original_path, clone_dir)) |
112
|
|
|
|
113
|
|
|
stats_original = os.stat(original_path) |
114
|
|
|
# BackupPC 3.x renames pool files with hash collisions. |
115
|
|
|
if stats_original.st_ino != bpl_inode_original: |
116
|
|
|
raise FileNotFoundError("Filename '{}' and inode {} do not match".format(original_path, bpl_inode_original)) |
117
|
|
|
|
118
|
|
|
if not os.path.exists(clone_dir): |
119
|
|
|
os.makedirs(clone_dir) |
120
|
|
|
|
121
|
|
|
shutil.copyfile(original_path, clone_path) |
122
|
|
|
|
123
|
|
|
stats_clone = os.stat(clone_path) |
124
|
|
|
os.chmod(clone_path, stats_original.st_mode) |
125
|
|
|
os.utime(clone_path, (stats_original.st_mtime, stats_original.st_mtime)) |
126
|
|
|
|
127
|
|
|
DataLayer.instance.pool_update_by_inode_original(stats_original.st_ino, |
128
|
|
|
stats_clone.st_ino, |
129
|
|
|
stats_original.st_size, |
130
|
|
|
stats_original.st_mtime) |
131
|
|
|
|
132
|
|
|
return stats_original.st_size |
133
|
|
|
|
134
|
|
|
# ------------------------------------------------------------------------------------------------------------------ |
135
|
|
|
def __update_clone_pool(self) -> None: |
136
|
|
|
""" |
137
|
|
|
Copies required pool files from the original pool to the clone pool. |
138
|
|
|
""" |
139
|
|
|
self.__io.section('Clone pool') |
140
|
|
|
self.__io.writeln(' Adding files ...') |
141
|
|
|
self.__io.writeln('') |
142
|
|
|
|
143
|
|
|
hst_id = DataLayer.instance.get_host_id(self.__host) |
144
|
|
|
bck_id = DataLayer.instance.get_bck_id(hst_id, self.__backup_no) |
145
|
|
|
|
146
|
|
|
file_count = DataLayer.instance.backup_prepare_required_clone_pool_files(bck_id) |
147
|
|
|
progress = ProgressBar(self.__io.output, file_count) |
148
|
|
|
|
149
|
|
|
total_size = 0 |
150
|
|
|
file_count = 0 |
151
|
|
|
for rows in DataLayer.instance.backup_yield_required_clone_pool_files(): |
152
|
|
|
for row in rows: |
153
|
|
|
total_size += self.__copy_pool_file(row['bpl_dir'], row['bpl_name'], row['bpl_inode_original']) |
154
|
|
|
file_count += 1 |
155
|
|
|
progress.advance() |
156
|
|
|
|
157
|
|
|
progress.finish() |
158
|
|
|
|
159
|
|
|
self.__io.writeln('') |
160
|
|
|
self.__io.writeln(' Number of files copied: {}'.format(file_count)) |
161
|
|
|
self.__io.writeln(' Total bytes copied : {} ({}B) '.format(sizeof_fmt(total_size), total_size)) |
162
|
|
|
self.__io.writeln('') |
163
|
|
|
|
164
|
|
|
# ------------------------------------------------------------------------------------------------------------------ |
165
|
|
|
def __clone_backup(self) -> None: |
166
|
|
|
""" |
167
|
|
|
Clones the backup. |
168
|
|
|
""" |
169
|
|
|
self.__io.section('Clone backup') |
170
|
|
|
self.__io.writeln(' Populating ...') |
171
|
|
|
self.__io.writeln('') |
172
|
|
|
|
173
|
|
|
hst_id = DataLayer.instance.get_host_id(self.__host) |
174
|
|
|
bck_id = DataLayer.instance.get_bck_id(hst_id, int(self.__backup_no)) |
175
|
|
|
DataLayer.instance.backup_set_in_progress(bck_id, 1) |
176
|
|
|
|
177
|
|
|
backup_dir_clone = Config.instance.backup_dir_clone(self.__host, self.__backup_no) |
178
|
|
|
if os.path.exists(backup_dir_clone): |
179
|
|
|
shutil.rmtree(backup_dir_clone) |
180
|
|
|
os.makedirs(backup_dir_clone) |
181
|
|
|
|
182
|
|
|
backup_dir_original = Config.instance.backup_dir_original(self.__host, self.__backup_no) |
183
|
|
|
top_dir_clone = Config.instance.top_dir_clone |
184
|
|
|
|
185
|
|
|
file_count = DataLayer.instance.backup_prepare_tree(bck_id) |
186
|
|
|
progress = ProgressBar(self.__io.output, file_count) |
187
|
|
|
|
188
|
|
|
file_count = 0 |
189
|
|
|
link_count = 0 |
190
|
|
|
dir_count = 0 |
191
|
|
|
for rows in DataLayer.instance.backup_yield_tree(): |
192
|
|
|
for row in rows: |
193
|
|
|
if row['bbt_dir'] is None: |
194
|
|
|
row['bbt_dir'] = '' |
195
|
|
|
|
196
|
|
|
target_clone = os.path.join(backup_dir_clone, row['bbt_dir'], row['bbt_name']) |
197
|
|
|
|
198
|
|
|
if row['bpl_inode_original']: |
199
|
|
|
# Entry is a file linked to the pool. |
200
|
|
|
source_clone = os.path.join(top_dir_clone, row['bpl_dir'], row['bpl_name']) |
201
|
|
|
self.__io.log_very_verbose( |
202
|
|
|
'Linking to <fso>{}</fso> from <fso>{}</fso>'.format(source_clone, target_clone)) |
203
|
|
|
os.link(source_clone, target_clone) |
204
|
|
|
link_count += 1 |
205
|
|
|
|
206
|
|
|
elif row['bbt_inode_original']: |
207
|
|
|
# Entry is a file not linked to the pool. |
208
|
|
|
source_original = os.path.join(backup_dir_original, row['bbt_dir'], row['bbt_name']) |
209
|
|
|
self.__io.log_very_verbose('Copying <fso>{}</fso> to <fso>{}</fso>'.format(source_original, |
210
|
|
|
target_clone)) |
211
|
|
|
shutil.copy2(source_original, target_clone) |
212
|
|
|
file_count += 1 |
213
|
|
|
else: |
214
|
|
|
# Entry is a directory |
215
|
|
|
os.mkdir(target_clone) |
216
|
|
|
dir_count += 1 |
217
|
|
|
|
218
|
|
|
progress.advance() |
219
|
|
|
|
220
|
|
|
progress.finish() |
221
|
|
|
|
222
|
|
|
DataLayer.instance.backup_set_in_progress(bck_id, 0) |
223
|
|
|
|
224
|
|
|
self.__io.writeln('') |
225
|
|
|
self.__io.writeln(' Number of files copied : {}'.format(file_count)) |
226
|
|
|
self.__io.writeln(' Number of hardlinks created : {}'.format(link_count)) |
227
|
|
|
self.__io.writeln(' Number of directories created: {}'.format(dir_count)) |
228
|
|
|
self.__io.writeln('') |
229
|
|
|
|
230
|
|
|
# ------------------------------------------------------------------------------------------------------------------ |
231
|
|
|
def clone_backup(self, host: str, backup_no: int) -> None: |
232
|
|
|
""" |
233
|
|
|
Clones a backup of a host. |
234
|
|
|
""" |
235
|
|
|
self.__host = host |
236
|
|
|
self.__backup_no = backup_no |
237
|
|
|
|
238
|
|
|
backup_dir_original = Config.instance.backup_dir_original(host, backup_no) |
239
|
|
|
pre_scan_csv_filename = os.path.join(backup_dir_original, 'backuppc-clone.csv') |
240
|
|
|
if os.path.isfile(pre_scan_csv_filename): |
241
|
|
|
self.__import_pre_scan_csv(pre_scan_csv_filename) |
242
|
|
|
else: |
243
|
|
|
csv_filename = os.path.join(Config.instance.tmp_dir_clone, 'backup-{}-{}.csv'.format(host, backup_no)) |
244
|
|
|
self.__scan_host_backup(csv_filename) |
245
|
|
|
self.__import_host_scan_csv(csv_filename) |
246
|
|
|
|
247
|
|
|
self.__update_clone_pool() |
248
|
|
|
self.__clone_backup() |
249
|
|
|
|
250
|
|
|
# ---------------------------------------------------------------------------------------------------------------------- |
251
|
|
|
|