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.
Files changed (60) hide show
  1. barecat/__init__.py +108 -0
  2. barecat/__main__.py +5 -0
  3. barecat/_api.py +143 -0
  4. barecat/_version.py +24 -0
  5. barecat/cli/__init__.py +0 -0
  6. barecat/cli/commands.py +540 -0
  7. barecat/cli/completions.py +27 -0
  8. barecat/cli/impl.py +609 -0
  9. barecat/cli/legacy.py +361 -0
  10. barecat/cli/main.py +1355 -0
  11. barecat/cli/shell.py +827 -0
  12. barecat/completions/__init__.py +1 -0
  13. barecat/completions/barecat.bash +246 -0
  14. barecat/completions/barecat.zsh +268 -0
  15. barecat/core/__init__.py +0 -0
  16. barecat/core/barecat.py +1209 -0
  17. barecat/core/index.py +1983 -0
  18. barecat/core/paths.py +61 -0
  19. barecat/core/sharder.py +328 -0
  20. barecat/core/types.py +340 -0
  21. barecat/exceptions.py +92 -0
  22. barecat/formats/__init__.py +0 -0
  23. barecat/formats/archive_formats.py +234 -0
  24. barecat/formats/from_tar_stream.py +53 -0
  25. barecat/formats/to_tar_stream.py +54 -0
  26. barecat/io/__init__.py +5 -0
  27. barecat/io/codecs.py +622 -0
  28. barecat/io/copyfile.py +550 -0
  29. barecat/io/fileobj.py +556 -0
  30. barecat/io/open.py +41 -0
  31. barecat/maintenance/__init__.py +0 -0
  32. barecat/maintenance/complete_database.py +79 -0
  33. barecat/maintenance/defrag.py +541 -0
  34. barecat/maintenance/merge.py +613 -0
  35. barecat/maintenance/reshard.py +153 -0
  36. barecat/maintenance/rsync.py +1282 -0
  37. barecat/maintenance/upgrade_database.py +200 -0
  38. barecat/maintenance/upgrade_database2.py +74 -0
  39. barecat/man/barecat.1 +284 -0
  40. barecat/sql/indexes.sql +6 -0
  41. barecat/sql/schema.sql +55 -0
  42. barecat/sql/triggers.sql +172 -0
  43. barecat/tui/__init__.py +0 -0
  44. barecat/tui/browse.py +448 -0
  45. barecat/tui/ncdu.py +318 -0
  46. barecat/util/__init__.py +0 -0
  47. barecat/util/consumed_threadpool.py +119 -0
  48. barecat/util/glob_helper.py +584 -0
  49. barecat/util/glob_to_regex.py +353 -0
  50. barecat/util/list_files.py +12 -0
  51. barecat/util/misc.py +174 -0
  52. barecat/util/physical_order.py +77 -0
  53. barecat/util/progbar.py +40 -0
  54. barecat/util/threading.py +59 -0
  55. barecat-0.3.2.dist-info/METADATA +276 -0
  56. barecat-0.3.2.dist-info/RECORD +60 -0
  57. barecat-0.3.2.dist-info/WHEEL +5 -0
  58. barecat-0.3.2.dist-info/entry_points.txt +15 -0
  59. barecat-0.3.2.dist-info/licenses/LICENSE +21 -0
  60. 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='')