barecat 0.3.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- barecat/__init__.py +108 -0
- barecat/__main__.py +5 -0
- barecat/_api.py +143 -0
- barecat/_version.py +24 -0
- barecat/cli/__init__.py +0 -0
- barecat/cli/commands.py +540 -0
- barecat/cli/completions.py +27 -0
- barecat/cli/impl.py +609 -0
- barecat/cli/legacy.py +361 -0
- barecat/cli/main.py +1355 -0
- barecat/cli/shell.py +827 -0
- barecat/completions/__init__.py +1 -0
- barecat/completions/barecat.bash +246 -0
- barecat/completions/barecat.zsh +268 -0
- barecat/core/__init__.py +0 -0
- barecat/core/barecat.py +1209 -0
- barecat/core/index.py +1983 -0
- barecat/core/paths.py +61 -0
- barecat/core/sharder.py +328 -0
- barecat/core/types.py +340 -0
- barecat/exceptions.py +92 -0
- barecat/formats/__init__.py +0 -0
- barecat/formats/archive_formats.py +234 -0
- barecat/formats/from_tar_stream.py +53 -0
- barecat/formats/to_tar_stream.py +54 -0
- barecat/io/__init__.py +5 -0
- barecat/io/codecs.py +622 -0
- barecat/io/copyfile.py +550 -0
- barecat/io/fileobj.py +556 -0
- barecat/io/open.py +41 -0
- barecat/maintenance/__init__.py +0 -0
- barecat/maintenance/complete_database.py +79 -0
- barecat/maintenance/defrag.py +541 -0
- barecat/maintenance/merge.py +613 -0
- barecat/maintenance/reshard.py +153 -0
- barecat/maintenance/rsync.py +1282 -0
- barecat/maintenance/upgrade_database.py +200 -0
- barecat/maintenance/upgrade_database2.py +74 -0
- barecat/man/barecat.1 +284 -0
- barecat/sql/indexes.sql +6 -0
- barecat/sql/schema.sql +55 -0
- barecat/sql/triggers.sql +172 -0
- barecat/tui/__init__.py +0 -0
- barecat/tui/browse.py +448 -0
- barecat/tui/ncdu.py +318 -0
- barecat/util/__init__.py +0 -0
- barecat/util/consumed_threadpool.py +119 -0
- barecat/util/glob_helper.py +584 -0
- barecat/util/glob_to_regex.py +353 -0
- barecat/util/list_files.py +12 -0
- barecat/util/misc.py +174 -0
- barecat/util/physical_order.py +77 -0
- barecat/util/progbar.py +40 -0
- barecat/util/threading.py +59 -0
- barecat-0.3.2.dist-info/METADATA +276 -0
- barecat-0.3.2.dist-info/RECORD +60 -0
- barecat-0.3.2.dist-info/WHEEL +5 -0
- barecat-0.3.2.dist-info/entry_points.txt +15 -0
- barecat-0.3.2.dist-info/licenses/LICENSE +21 -0
- barecat-0.3.2.dist-info/top_level.txt +1 -0
barecat/cli/impl.py
ADDED
|
@@ -0,0 +1,609 @@
|
|
|
1
|
+
import glob
|
|
2
|
+
import itertools
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import os.path as osp
|
|
6
|
+
import stat
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
|
|
10
|
+
from ..util import misc
|
|
11
|
+
from ..util import physical_order as barecat_physical_order
|
|
12
|
+
from ..formats.archive_formats import (
|
|
13
|
+
get_archive_writer,
|
|
14
|
+
iter_archive,
|
|
15
|
+
iter_archive_nocontent,
|
|
16
|
+
)
|
|
17
|
+
from ..util.consumed_threadpool import ConsumedThreadPool
|
|
18
|
+
from ..core.types import BarecatDirInfo, BarecatFileInfo, Order
|
|
19
|
+
from ..core import barecat as barecat_
|
|
20
|
+
from ..core.sharder import Sharder
|
|
21
|
+
from ..core.paths import resolve_index_path
|
|
22
|
+
from ..util.progbar import progressbar
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# =============================================================================
|
|
26
|
+
# Main entry points
|
|
27
|
+
# =============================================================================
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def create(
|
|
31
|
+
filesys_and_store_path_pairs,
|
|
32
|
+
target_path,
|
|
33
|
+
shard_size_limit,
|
|
34
|
+
overwrite=False,
|
|
35
|
+
exist_ok=True,
|
|
36
|
+
workers=8,
|
|
37
|
+
):
|
|
38
|
+
if workers is None:
|
|
39
|
+
create_without_workers(
|
|
40
|
+
filesys_and_store_path_pairs, target_path, shard_size_limit, overwrite, exist_ok
|
|
41
|
+
)
|
|
42
|
+
else:
|
|
43
|
+
create_with_workers(
|
|
44
|
+
filesys_and_store_path_pairs,
|
|
45
|
+
target_path,
|
|
46
|
+
shard_size_limit,
|
|
47
|
+
overwrite,
|
|
48
|
+
exist_ok,
|
|
49
|
+
workers,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def merge(
|
|
54
|
+
source_paths,
|
|
55
|
+
target_path,
|
|
56
|
+
shard_size_limit=None,
|
|
57
|
+
overwrite=False,
|
|
58
|
+
ignore_duplicates=False,
|
|
59
|
+
as_subdirs=False,
|
|
60
|
+
prefix=None,
|
|
61
|
+
pattern=None,
|
|
62
|
+
filter_rules=None,
|
|
63
|
+
):
|
|
64
|
+
with barecat_.Barecat(
|
|
65
|
+
target_path, shard_size_limit=shard_size_limit, readonly=False, overwrite=overwrite
|
|
66
|
+
) as writer:
|
|
67
|
+
for source_path in source_paths:
|
|
68
|
+
# Build prefix for this archive
|
|
69
|
+
parts = []
|
|
70
|
+
if prefix:
|
|
71
|
+
parts.append(prefix)
|
|
72
|
+
if as_subdirs:
|
|
73
|
+
parts.append(get_subdir_name(source_path))
|
|
74
|
+
path_prefix = '/'.join(parts)
|
|
75
|
+
|
|
76
|
+
print(
|
|
77
|
+
f'Merging files from {source_path}'
|
|
78
|
+
+ (f' -> {path_prefix}/' if path_prefix else '')
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
if is_traditional_archive(source_path):
|
|
82
|
+
for file_or_dir_info, fileobj in iter_archive(source_path):
|
|
83
|
+
if not _should_include(file_or_dir_info.path, pattern, filter_rules):
|
|
84
|
+
continue
|
|
85
|
+
if path_prefix:
|
|
86
|
+
file_or_dir_info.path = f'{path_prefix}/{file_or_dir_info.path}'
|
|
87
|
+
writer.add(
|
|
88
|
+
file_or_dir_info,
|
|
89
|
+
fileobj=fileobj,
|
|
90
|
+
dir_exist_ok=True,
|
|
91
|
+
file_exist_ok=ignore_duplicates,
|
|
92
|
+
)
|
|
93
|
+
else:
|
|
94
|
+
writer.merge_from_other_barecat(
|
|
95
|
+
source_path,
|
|
96
|
+
ignore_duplicates=ignore_duplicates,
|
|
97
|
+
prefix=path_prefix or None,
|
|
98
|
+
pattern=pattern,
|
|
99
|
+
filter_rules=filter_rules,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def merge_symlink(
|
|
104
|
+
source_paths,
|
|
105
|
+
target_path,
|
|
106
|
+
overwrite=False,
|
|
107
|
+
ignore_duplicates=False,
|
|
108
|
+
as_subdirs=False,
|
|
109
|
+
prefix=None,
|
|
110
|
+
pattern=None,
|
|
111
|
+
filter_rules=None,
|
|
112
|
+
):
|
|
113
|
+
if pattern or filter_rules:
|
|
114
|
+
print('Error: --pattern/--include/--exclude not supported with --symlink', file=sys.stderr)
|
|
115
|
+
sys.exit(1)
|
|
116
|
+
|
|
117
|
+
# New format: target_path IS the index file
|
|
118
|
+
if overwrite and osp.exists(target_path):
|
|
119
|
+
os.remove(target_path)
|
|
120
|
+
|
|
121
|
+
with barecat_.Index(target_path, readonly=False) as index_writer:
|
|
122
|
+
c = index_writer.cursor
|
|
123
|
+
c.execute('COMMIT')
|
|
124
|
+
c.execute('PRAGMA synchronous=OFF')
|
|
125
|
+
c.execute('PRAGMA journal_mode=OFF')
|
|
126
|
+
|
|
127
|
+
i_out_shard = 0
|
|
128
|
+
for source_path in source_paths:
|
|
129
|
+
# Build prefix for this archive
|
|
130
|
+
parts = []
|
|
131
|
+
if prefix:
|
|
132
|
+
parts.append(prefix)
|
|
133
|
+
if as_subdirs:
|
|
134
|
+
parts.append(get_subdir_name(source_path))
|
|
135
|
+
path_prefix = '/'.join(parts) or None
|
|
136
|
+
|
|
137
|
+
print(
|
|
138
|
+
f'Merging files from {source_path}'
|
|
139
|
+
+ (f' -> {path_prefix}/' if path_prefix else '')
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
index_writer.merge_from_other_barecat(
|
|
143
|
+
resolve_index_path(source_path),
|
|
144
|
+
ignore_duplicates=ignore_duplicates,
|
|
145
|
+
prefix=path_prefix,
|
|
146
|
+
)
|
|
147
|
+
for shard_path in sorted(glob.glob(f'{source_path}-shard-*')):
|
|
148
|
+
os.symlink(
|
|
149
|
+
osp.relpath(shard_path, start=osp.dirname(target_path)),
|
|
150
|
+
f'{target_path}-shard-{i_out_shard:05d}',
|
|
151
|
+
)
|
|
152
|
+
i_out_shard += 1
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def extract(barecat_path, target_directory):
|
|
156
|
+
import warnings
|
|
157
|
+
|
|
158
|
+
warnings.warn(
|
|
159
|
+
"barecat.extract() is deprecated. Use 'barecat extract' CLI or "
|
|
160
|
+
'Barecat class directly instead.',
|
|
161
|
+
DeprecationWarning,
|
|
162
|
+
stacklevel=2,
|
|
163
|
+
)
|
|
164
|
+
from ..io.copyfile import copy
|
|
165
|
+
|
|
166
|
+
with barecat_.Barecat(barecat_path) as reader:
|
|
167
|
+
for path_in_archive in progressbar(reader, desc='Extracting files', unit=' files'):
|
|
168
|
+
target_path = osp.join(target_directory, path_in_archive)
|
|
169
|
+
os.makedirs(osp.dirname(target_path), exist_ok=True)
|
|
170
|
+
with open(target_path, 'wb') as output_file:
|
|
171
|
+
copy(reader.open(path_in_archive, 'rb'), output_file)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def archive2barecat(src_path, target_path, shard_size_limit, overwrite=False):
|
|
175
|
+
with barecat_.Barecat(
|
|
176
|
+
target_path, shard_size_limit=shard_size_limit, readonly=False, overwrite=overwrite
|
|
177
|
+
) as writer:
|
|
178
|
+
for file_or_dir_info, fileobj in iter_archive(src_path):
|
|
179
|
+
writer.add(file_or_dir_info, fileobj=fileobj, dir_exist_ok=True)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def barecat2archive(src_path, target_path, root_dir=None):
|
|
183
|
+
with barecat_.Barecat(src_path, readonly=True) as bc:
|
|
184
|
+
with get_archive_writer(target_path) as target_archive:
|
|
185
|
+
infos = bc.index.iter_all_infos(order=Order.PATH)
|
|
186
|
+
num_total = bc.index.num_files + bc.index.num_dirs
|
|
187
|
+
for entry in progressbar(infos, total=num_total, desc='Writing', unit=' entries'):
|
|
188
|
+
# Skip root directory
|
|
189
|
+
if not entry.path:
|
|
190
|
+
continue
|
|
191
|
+
original_path = entry.path
|
|
192
|
+
# Prepend root_dir if specified
|
|
193
|
+
if root_dir:
|
|
194
|
+
entry.path = osp.join(root_dir, entry.path)
|
|
195
|
+
if isinstance(entry, BarecatDirInfo):
|
|
196
|
+
target_archive.add(entry)
|
|
197
|
+
else:
|
|
198
|
+
with bc.open(original_path, 'rb') as file_in_barecat:
|
|
199
|
+
target_archive.add(entry, fileobj=file_in_barecat)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def wrap_archive(src_path, target_path, overwrite=False):
|
|
203
|
+
# New format: target_path IS the index file
|
|
204
|
+
index_path = target_path
|
|
205
|
+
shard_path = f'{target_path}-shard-00000'
|
|
206
|
+
|
|
207
|
+
# Check for compression regardless of extension
|
|
208
|
+
compression = is_compressed_file(src_path)
|
|
209
|
+
if compression:
|
|
210
|
+
raise ValueError(
|
|
211
|
+
f'Cannot wrap compressed file ({compression}). '
|
|
212
|
+
'Wrap only works with uncompressed .tar or .zip files.'
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# Check ZIP-specific issues (compression, encryption)
|
|
216
|
+
if src_path.lower().endswith('.zip'):
|
|
217
|
+
zip_error = check_zip_wrappable(src_path)
|
|
218
|
+
if zip_error:
|
|
219
|
+
raise ValueError(zip_error)
|
|
220
|
+
|
|
221
|
+
if overwrite:
|
|
222
|
+
if osp.exists(index_path):
|
|
223
|
+
os.remove(index_path)
|
|
224
|
+
if osp.lexists(shard_path):
|
|
225
|
+
os.remove(shard_path)
|
|
226
|
+
|
|
227
|
+
with barecat_.Index(index_path, readonly=False) as index:
|
|
228
|
+
for file_or_dir_info in iter_archive_nocontent(src_path):
|
|
229
|
+
index.add(file_or_dir_info)
|
|
230
|
+
|
|
231
|
+
os.symlink(osp.abspath(src_path), shard_path)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def stdin_tar2barecat(tar_format, target_path, shard_size_limit, overwrite=False):
|
|
235
|
+
"""Read tar from stdin and write to barecat."""
|
|
236
|
+
import tarfile
|
|
237
|
+
|
|
238
|
+
# Map format to tarfile mode
|
|
239
|
+
mode_map = {
|
|
240
|
+
'tar': 'r|',
|
|
241
|
+
'tar.gz': 'r|gz',
|
|
242
|
+
'tar.bz2': 'r|bz2',
|
|
243
|
+
'tar.xz': 'r|xz',
|
|
244
|
+
}
|
|
245
|
+
mode = mode_map[tar_format]
|
|
246
|
+
|
|
247
|
+
with barecat_.Barecat(
|
|
248
|
+
target_path, shard_size_limit=shard_size_limit, readonly=False, overwrite=overwrite
|
|
249
|
+
) as writer:
|
|
250
|
+
with tarfile.open(fileobj=sys.stdin.buffer, mode=mode) as tar:
|
|
251
|
+
for member in tar:
|
|
252
|
+
if member.isdir():
|
|
253
|
+
dinfo = BarecatDirInfo(
|
|
254
|
+
path=member.name,
|
|
255
|
+
mode=member.mode,
|
|
256
|
+
uid=member.uid,
|
|
257
|
+
gid=member.gid,
|
|
258
|
+
mtime_ns=int(member.mtime * 1_000_000_000),
|
|
259
|
+
)
|
|
260
|
+
writer.add(dinfo, dir_exist_ok=True)
|
|
261
|
+
elif member.isfile():
|
|
262
|
+
finfo = BarecatFileInfo(
|
|
263
|
+
path=member.name,
|
|
264
|
+
size=member.size,
|
|
265
|
+
mode=member.mode,
|
|
266
|
+
uid=member.uid,
|
|
267
|
+
gid=member.gid,
|
|
268
|
+
mtime_ns=int(member.mtime * 1_000_000_000),
|
|
269
|
+
)
|
|
270
|
+
with tar.extractfile(member) as file_in_tar:
|
|
271
|
+
writer.add(finfo, fileobj=file_in_tar, dir_exist_ok=True)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def barecat2stdout_tar(src_path, tar_format, root_dir=None):
|
|
275
|
+
"""Read barecat and write tar to stdout."""
|
|
276
|
+
import tarfile
|
|
277
|
+
|
|
278
|
+
# Map format to tarfile mode
|
|
279
|
+
mode_map = {
|
|
280
|
+
'tar': 'w|',
|
|
281
|
+
'tar.gz': 'w|gz',
|
|
282
|
+
'tar.bz2': 'w|bz2',
|
|
283
|
+
'tar.xz': 'w|xz',
|
|
284
|
+
}
|
|
285
|
+
mode = mode_map[tar_format]
|
|
286
|
+
|
|
287
|
+
with barecat_.Barecat(src_path, readonly=True) as bc:
|
|
288
|
+
with tarfile.open(fileobj=sys.stdout.buffer, mode=mode) as tar:
|
|
289
|
+
for entry in bc.index.iter_all_infos(order=Order.PATH):
|
|
290
|
+
# Skip root directory (empty path)
|
|
291
|
+
if not entry.path:
|
|
292
|
+
continue
|
|
293
|
+
original_path = entry.path
|
|
294
|
+
tar_path = osp.join(root_dir, entry.path) if root_dir else entry.path
|
|
295
|
+
if isinstance(entry, BarecatDirInfo):
|
|
296
|
+
tinfo = tarfile.TarInfo(name=tar_path)
|
|
297
|
+
tinfo.type = tarfile.DIRTYPE
|
|
298
|
+
tinfo.mode = entry.mode if entry.mode else 0o755
|
|
299
|
+
tinfo.uid = entry.uid if entry.uid else 0
|
|
300
|
+
tinfo.gid = entry.gid if entry.gid else 0
|
|
301
|
+
tinfo.mtime = entry.mtime_ns // 1_000_000_000 if entry.mtime_ns else 0
|
|
302
|
+
tar.addfile(tinfo)
|
|
303
|
+
else:
|
|
304
|
+
tinfo = tarfile.TarInfo(name=tar_path)
|
|
305
|
+
tinfo.size = entry.size
|
|
306
|
+
tinfo.mode = entry.mode if entry.mode else 0o644
|
|
307
|
+
tinfo.uid = entry.uid if entry.uid else 0
|
|
308
|
+
tinfo.gid = entry.gid if entry.gid else 0
|
|
309
|
+
tinfo.mtime = entry.mtime_ns // 1_000_000_000 if entry.mtime_ns else 0
|
|
310
|
+
with bc.open(original_path, 'rb') as file_in_barecat:
|
|
311
|
+
tar.addfile(tinfo, file_in_barecat)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def print_ncdu_json(path):
|
|
315
|
+
timestamp = time.time()
|
|
316
|
+
import importlib.metadata
|
|
317
|
+
|
|
318
|
+
progver = importlib.metadata.version('barecat')
|
|
319
|
+
progver = '.'.join(progver.split('.')[:3])
|
|
320
|
+
|
|
321
|
+
print(f'[1,1,{{"progname":"barecat","progver":"{progver}","timestamp":{timestamp}}},')
|
|
322
|
+
with barecat_.Index(path) as index_reader:
|
|
323
|
+
_print_ncdu_json(index_reader, '')
|
|
324
|
+
print(']')
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
# =============================================================================
|
|
328
|
+
# Higher-level create wrappers
|
|
329
|
+
# =============================================================================
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def create_from_stdin_paths(
|
|
333
|
+
target_path, shard_size_limit, zero_terminated=False, overwrite=False, workers=None
|
|
334
|
+
):
|
|
335
|
+
iterator = generate_from_stdin(zero_terminated)
|
|
336
|
+
create(iterator, target_path, shard_size_limit, overwrite, workers)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def create_recursive(
|
|
340
|
+
target_path, shard_size_limit, roots, overwrite, strip_root, workers=None, physical_order=False
|
|
341
|
+
):
|
|
342
|
+
iterator = generate_from_walks(roots, strip_root)
|
|
343
|
+
|
|
344
|
+
if physical_order:
|
|
345
|
+
iterator = iter(
|
|
346
|
+
sorted(iterator, key=lambda x: barecat_physical_order.get_physical_offset(x[0]))
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
iterator = progressbar(iterator, desc='Packing files', unit=' files')
|
|
350
|
+
create(iterator, target_path, shard_size_limit, overwrite, workers)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def generate_from_stdin(zero_terminated=False):
|
|
354
|
+
if zero_terminated:
|
|
355
|
+
input_paths = iterate_zero_terminated(sys.stdin.buffer)
|
|
356
|
+
else:
|
|
357
|
+
input_paths = (line.rstrip('\n') for line in sys.stdin)
|
|
358
|
+
|
|
359
|
+
for input_path in progressbar(input_paths, desc='Packing files', unit=' files'):
|
|
360
|
+
yield input_path, input_path
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def generate_from_walks(roots, strip_root):
|
|
364
|
+
for root in roots:
|
|
365
|
+
if not strip_root:
|
|
366
|
+
yield root, osp.basename(root)
|
|
367
|
+
|
|
368
|
+
for dirpath, subdirnames, filenames in os.walk(root):
|
|
369
|
+
for entryname in itertools.chain(filenames, subdirnames):
|
|
370
|
+
full_path = osp.join(dirpath, entryname)
|
|
371
|
+
relpath = osp.relpath(full_path, start=root)
|
|
372
|
+
if not strip_root:
|
|
373
|
+
store_path = osp.join(osp.basename(root), relpath)
|
|
374
|
+
else:
|
|
375
|
+
store_path = relpath
|
|
376
|
+
yield full_path, store_path
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
# =============================================================================
|
|
380
|
+
# Implementation helpers
|
|
381
|
+
# =============================================================================
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def create_without_workers(
|
|
385
|
+
filesys_and_store_path_pairs, target_path, shard_size_limit, overwrite=False, exist_ok=True
|
|
386
|
+
):
|
|
387
|
+
with barecat_.Barecat(
|
|
388
|
+
target_path,
|
|
389
|
+
shard_size_limit=shard_size_limit,
|
|
390
|
+
readonly=False,
|
|
391
|
+
overwrite=overwrite,
|
|
392
|
+
exist_ok=exist_ok,
|
|
393
|
+
append_only=False,
|
|
394
|
+
) as writer:
|
|
395
|
+
for filesys_path, store_path in filesys_and_store_path_pairs:
|
|
396
|
+
writer.add_by_path(filesys_path, store_path)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def create_with_workers(
|
|
400
|
+
filesys_and_store_path_pairs,
|
|
401
|
+
target_path,
|
|
402
|
+
shard_size_limit,
|
|
403
|
+
overwrite=False,
|
|
404
|
+
exist_ok=True,
|
|
405
|
+
workers=8,
|
|
406
|
+
):
|
|
407
|
+
if misc.exists(target_path):
|
|
408
|
+
if not exist_ok:
|
|
409
|
+
raise FileExistsError(target_path)
|
|
410
|
+
if overwrite:
|
|
411
|
+
misc.remove(target_path)
|
|
412
|
+
|
|
413
|
+
with (
|
|
414
|
+
Sharder(
|
|
415
|
+
target_path,
|
|
416
|
+
shard_size_limit=shard_size_limit,
|
|
417
|
+
readonly=False,
|
|
418
|
+
append_only=False,
|
|
419
|
+
threadsafe=True,
|
|
420
|
+
allow_writing_symlinked_shard=False,
|
|
421
|
+
) as sharder,
|
|
422
|
+
ConsumedThreadPool(
|
|
423
|
+
index_writer_main, main_args=(target_path,), max_workers=workers
|
|
424
|
+
) as ctp,
|
|
425
|
+
):
|
|
426
|
+
for filesys_path, store_path in filesys_and_store_path_pairs:
|
|
427
|
+
statresult = os.stat(filesys_path)
|
|
428
|
+
|
|
429
|
+
if stat.S_ISDIR(statresult.st_mode):
|
|
430
|
+
dinfo = BarecatDirInfo(path=store_path)
|
|
431
|
+
dinfo.fill_from_statresult(statresult)
|
|
432
|
+
ctp.submit(userdata=dinfo)
|
|
433
|
+
else:
|
|
434
|
+
finfo = BarecatFileInfo(path=store_path)
|
|
435
|
+
finfo.fill_from_statresult(statresult)
|
|
436
|
+
finfo.shard, finfo.offset = sharder.reserve(finfo.size)
|
|
437
|
+
ctp.submit(
|
|
438
|
+
sharder.add_by_path,
|
|
439
|
+
userdata=finfo,
|
|
440
|
+
args=(filesys_path, finfo.shard, finfo.offset, finfo.size),
|
|
441
|
+
kwargs=dict(raise_if_cannot_fit=True),
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def index_writer_main(target_path, future_iter):
|
|
446
|
+
with barecat_.Index(target_path, readonly=False) as index_writer:
|
|
447
|
+
for future in future_iter:
|
|
448
|
+
info = future.userdata
|
|
449
|
+
if isinstance(info, BarecatDirInfo):
|
|
450
|
+
index_writer.add_dir(info)
|
|
451
|
+
continue
|
|
452
|
+
|
|
453
|
+
shard_real, offset_real, size_real, crc32c = future.result()
|
|
454
|
+
info.shard = shard_real
|
|
455
|
+
info.offset = offset_real
|
|
456
|
+
info.crc32c = crc32c
|
|
457
|
+
|
|
458
|
+
if info.size != size_real:
|
|
459
|
+
raise ValueError('Size mismatch!')
|
|
460
|
+
index_writer.add_file(info)
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
# =============================================================================
|
|
464
|
+
# Archive format helpers
|
|
465
|
+
# =============================================================================
|
|
466
|
+
|
|
467
|
+
ARCHIVE_EXTENSIONS = ('.tar', '.tar.gz', '.tgz', '.tar.bz2', '.tbz2', '.tar.xz', '.txz', '.zip')
|
|
468
|
+
ALL_ARCHIVE_EXTENSIONS = ARCHIVE_EXTENSIONS + ('.barecat',)
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def is_traditional_archive(path):
|
|
472
|
+
"""Check if path looks like a tar/zip archive based on extension."""
|
|
473
|
+
return path.lower().endswith(ARCHIVE_EXTENSIONS)
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def get_subdir_name(archive_path):
|
|
477
|
+
"""Extract subdir name from archive path by stripping one archive extension."""
|
|
478
|
+
name = osp.basename(archive_path)
|
|
479
|
+
lower = name.lower()
|
|
480
|
+
for ext in ALL_ARCHIVE_EXTENSIONS:
|
|
481
|
+
if lower.endswith(ext):
|
|
482
|
+
return lower.removesuffix(ext)
|
|
483
|
+
return name
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def _should_include(path, pattern, filter_rules):
|
|
487
|
+
"""Check if path should be included based on pattern or filter rules."""
|
|
488
|
+
import re
|
|
489
|
+
from ..util.glob_to_regex import glob_to_regex
|
|
490
|
+
|
|
491
|
+
if pattern is not None:
|
|
492
|
+
recursive = '**' in pattern
|
|
493
|
+
regex = glob_to_regex(pattern, recursive=recursive, include_hidden=True)
|
|
494
|
+
return bool(re.match(regex, path))
|
|
495
|
+
|
|
496
|
+
if filter_rules:
|
|
497
|
+
# rsync-style first-match-wins
|
|
498
|
+
for sign, pat in filter_rules:
|
|
499
|
+
recursive = '**' in pat
|
|
500
|
+
regex = glob_to_regex(pat, recursive=recursive, include_hidden=True)
|
|
501
|
+
if re.match(regex, path):
|
|
502
|
+
return sign == '+'
|
|
503
|
+
return True # default include
|
|
504
|
+
|
|
505
|
+
return True
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def write_index(dictionary, target_path):
|
|
509
|
+
import warnings
|
|
510
|
+
|
|
511
|
+
warnings.warn(
|
|
512
|
+
"barecat.write_index() is deprecated. Use 'barecat.Index' class directly instead.",
|
|
513
|
+
DeprecationWarning,
|
|
514
|
+
stacklevel=2,
|
|
515
|
+
)
|
|
516
|
+
with barecat_.Index(target_path, readonly=False) as index_writer:
|
|
517
|
+
for path, (shard, offset, size) in dictionary.items():
|
|
518
|
+
index_writer.add_file(
|
|
519
|
+
BarecatFileInfo(path=path, shard=shard, offset=offset, size=size)
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def read_index(path):
|
|
524
|
+
import warnings
|
|
525
|
+
|
|
526
|
+
warnings.warn(
|
|
527
|
+
"barecat.read_index() is deprecated. Use 'barecat.Index' class directly instead.",
|
|
528
|
+
DeprecationWarning,
|
|
529
|
+
stacklevel=2,
|
|
530
|
+
)
|
|
531
|
+
with barecat_.Index(path) as reader:
|
|
532
|
+
return {
|
|
533
|
+
f.path: (f.shard, f.offset, f.size)
|
|
534
|
+
for f in reader.iter_all_fileinfos()
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def iterate_zero_terminated(fileobj):
|
|
539
|
+
partial_path = b''
|
|
540
|
+
while chunk := fileobj.read(4096):
|
|
541
|
+
parts = chunk.split(b'\x00')
|
|
542
|
+
parts[0] = partial_path + parts[0]
|
|
543
|
+
partial_path = parts.pop()
|
|
544
|
+
|
|
545
|
+
for input_path in parts:
|
|
546
|
+
input_path = input_path.decode()
|
|
547
|
+
yield input_path
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def is_compressed_file(path):
|
|
551
|
+
"""Check if file is compressed by reading magic bytes."""
|
|
552
|
+
with open(path, 'rb') as f:
|
|
553
|
+
magic = f.read(6)
|
|
554
|
+
# gzip: 1f 8b, bzip2: 42 5a 68 (BZh), xz: fd 37 7a 58 5a 00
|
|
555
|
+
if magic[:2] == b'\x1f\x8b':
|
|
556
|
+
return 'gzip'
|
|
557
|
+
if magic[:3] == b'BZh':
|
|
558
|
+
return 'bzip2'
|
|
559
|
+
if magic[:6] == b'\xfd7zXZ\x00':
|
|
560
|
+
return 'xz'
|
|
561
|
+
return None
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def check_zip_wrappable(path):
|
|
565
|
+
"""Check if ZIP file can be wrapped. Returns error message or None if OK."""
|
|
566
|
+
import zipfile
|
|
567
|
+
|
|
568
|
+
with zipfile.ZipFile(path, 'r') as zf:
|
|
569
|
+
# Check for multi-disk/split archives
|
|
570
|
+
if zf.namelist() and hasattr(zf, 'fp') and zf.fp:
|
|
571
|
+
# Read end of central directory to check disk number
|
|
572
|
+
pass # zipfile would fail to open split archives anyway
|
|
573
|
+
|
|
574
|
+
for info in zf.infolist():
|
|
575
|
+
if info.compress_type != zipfile.ZIP_STORED:
|
|
576
|
+
return (
|
|
577
|
+
'ZIP has compressed entries. '
|
|
578
|
+
"Use 'zip -0' to create uncompressed ZIP, or convert without --wrap."
|
|
579
|
+
)
|
|
580
|
+
# Check for encryption (bit 0 of flag_bits)
|
|
581
|
+
if info.flag_bits & 0x1:
|
|
582
|
+
return 'ZIP has encrypted entries. Cannot wrap encrypted ZIP files.'
|
|
583
|
+
return None
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def _print_ncdu_json(index_reader, dirpath):
|
|
587
|
+
basename = '/' if dirpath == '' else osp.basename(dirpath)
|
|
588
|
+
|
|
589
|
+
print('[', json.dumps(dict(name=basename, asize=4096, ino=0)), end='')
|
|
590
|
+
infos = index_reader.listdir_infos(dirpath)
|
|
591
|
+
file_infos = [f for f in infos if isinstance(f, BarecatFileInfo)]
|
|
592
|
+
subdir_infos = [d for d in infos if isinstance(d, BarecatDirInfo)]
|
|
593
|
+
del infos
|
|
594
|
+
|
|
595
|
+
if file_infos:
|
|
596
|
+
filedump = json.dumps(
|
|
597
|
+
[
|
|
598
|
+
dict(name=osp.basename(fi.path), asize=fi.size, dsize=fi.size, ino=0)
|
|
599
|
+
for fi in file_infos
|
|
600
|
+
]
|
|
601
|
+
)
|
|
602
|
+
print(',', filedump[1:-1], end='')
|
|
603
|
+
del file_infos
|
|
604
|
+
|
|
605
|
+
for subdir in subdir_infos:
|
|
606
|
+
print(',')
|
|
607
|
+
_print_ncdu_json(index_reader, subdir.path)
|
|
608
|
+
|
|
609
|
+
print(']', end='')
|