Skip to content

Commit b8034fc

Browse files
Merge pull request #9692 from mr-raj12/pack-files-step3-pack-id
repoobj, repository: add chunk_id to header, introduce packs/ namespace
2 parents 4d9369f + d8564b9 commit b8034fc

5 files changed

Lines changed: 60 additions & 54 deletions

File tree

src/borg/archiver/_common.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -127,9 +127,9 @@ def wrapper(self, args, **kwargs):
127127
)
128128

129129
with repository:
130-
if repository.version not in (3,):
130+
if repository.version not in (4,):
131131
raise Error(
132-
f"This borg version only accepts version 3 repos for -r/--repo, "
132+
f"This borg version only accepts version 4 repos for -r/--repo, "
133133
f"but not version {repository.version}. "
134134
f"You can use 'borg transfer' to copy archives from old to new repos."
135135
)
@@ -194,10 +194,10 @@ def wrapper(self, args, **kwargs):
194194
)
195195

196196
with repository:
197-
acceptable_versions = (1,) if v1_legacy else (3,)
197+
acceptable_versions = (1,) if v1_legacy else (4,)
198198
if repository.version not in acceptable_versions:
199199
raise Error(
200-
f"This borg version only accepts version {' or '.join(acceptable_versions)} "
200+
f"This borg version only accepts version {' or '.join(str(v) for v in acceptable_versions)} "
201201
f"repos for --other-repo."
202202
)
203203
kwargs["other_repository"] = repository

src/borg/repoobj.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,14 @@
1313
OBJ_MAGIC = b"BORG_OBJ"
1414
OBJ_VERSION = 0x01
1515

16+
# Fixed header size per blob: OBJ_MAGIC(8) + version(1) + chunk_id(32) + meta_size(4) + data_size(4)
17+
REPOOBJ_HEADER_SIZE = 49
18+
1619

1720
class RepoObj:
18-
# Object header: magic (8b), format version (1b), meta size (4b), data size (4b).
19-
obj_header = Struct("<8sBII")
20-
ObjHeader = namedtuple("ObjHeader", "magic version meta_size data_size")
21+
# Object header: magic (8b), format version (1b), chunk_id (32b), meta size (4b), data size (4b).
22+
obj_header = Struct("<8sB32sII")
23+
ObjHeader = namedtuple("ObjHeader", "magic version chunk_id meta_size data_size")
2124

2225
@classmethod
2326
def extract_crypted_data(cls, data: bytes) -> bytes:
@@ -72,7 +75,7 @@ def format(
7275
data_encrypted = self.key.encrypt(id, data_compressed)
7376
meta_packed = msgpack.packb(meta)
7477
meta_encrypted = self.key.encrypt(id, meta_packed)
75-
hdr = self.ObjHeader(OBJ_MAGIC, OBJ_VERSION, len(meta_encrypted), len(data_encrypted))
78+
hdr = self.ObjHeader(OBJ_MAGIC, OBJ_VERSION, id, len(meta_encrypted), len(data_encrypted))
7679
hdr_packed = self.obj_header.pack(*hdr)
7780
return hdr_packed + meta_encrypted + data_encrypted
7881

src/borg/repository.py

Lines changed: 39 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -115,15 +115,13 @@ def __init__(
115115
location = Location(url)
116116
self._location = location
117117
self.url = url
118-
# lots of stuff in data: use 2 levels by default (data/00/00/ .. data/ff/ff/ dirs)!
119-
data_levels = int(os.environ.get("BORG_STORE_DATA_LEVELS", "2"))
120118
ns_config = {
121119
"archives/": {"levels": [0]},
122120
"cache/": {"levels": [0]},
123121
"config/": {"levels": [0]},
124-
"data/": {"levels": [data_levels]},
125122
"keys/": {"levels": [0]},
126123
"locks/": {"levels": [0]},
124+
"packs/": {"levels": [1]},
127125
}
128126
# Get permissions from parameter or environment variable
129127
permissions = permissions if permissions is not None else os.environ.get("BORG_REPO_PERMISSIONS", "all")
@@ -136,19 +134,19 @@ def __init__(
136134
"archives": "lrw",
137135
"cache": "lrwWD", # WD for chunks.<HASH>, last-key-checked, ...
138136
"config": "lrW", # W for manifest
139-
"data": "lrw",
140137
"keys": "lr",
141138
"locks": "lrwD", # borg needs to create/delete a shared lock here
139+
"packs": "lrw",
142140
}
143141
elif permissions == "write-only": # mostly no reading
144142
permissions = {
145143
"": "l",
146144
"archives": "lw",
147145
"cache": "lrwWD", # read allowed, e.g. for chunks.<HASH> cache
148146
"config": "lrW", # W for manifest
149-
"data": "lw", # no r!
150147
"keys": "lr",
151148
"locks": "lrwD", # borg needs to create/delete a shared lock here
149+
"packs": "lw", # no r!
152150
}
153151
elif permissions == "read-only": # mostly r/o
154152
permissions = {"": "lr", "locks": "lrwD"}
@@ -171,7 +169,7 @@ def __init__(
171169
self._send_log = send_log_cb or (lambda: None)
172170
self.do_create = create
173171
self.created = False
174-
self.acceptable_repo_versions = (3,)
172+
self.acceptable_repo_versions = (4,)
175173
self.opened = False
176174
self.lock = None
177175
self.do_lock = lock
@@ -209,10 +207,10 @@ def create(self):
209207
self.store.open()
210208
try:
211209
self.store.store("config/readme", REPOSITORY_README.encode())
212-
self.version = 3
210+
self.version = 4
213211
self.store.store("config/version", str(self.version).encode())
214212
self.store.store("config/id", bin_to_hex(os.urandom(32)).encode())
215-
# we know repo/data/ still does not have any chunks stored in it,
213+
# we know repo/packs/ still does not have any chunks stored in it,
216214
# but for some stores, there might be a lot of empty directories and
217215
# listing them all might be rather slow, so we better cache an empty
218216
# ChunkIndex from here so that the first repo operation does not have
@@ -327,22 +325,21 @@ def log_error(msg):
327325
def check_object(obj):
328326
"""Check if obj looks valid."""
329327
hdr_size = RepoObj.obj_header.size
330-
obj_size = len(obj)
331-
if obj_size >= hdr_size:
332-
hdr = RepoObj.ObjHeader(*RepoObj.obj_header.unpack(obj[:hdr_size]))
333-
if hdr.magic != OBJ_MAGIC:
334-
log_error("invalid object magic.")
335-
elif hdr.version != OBJ_VERSION:
336-
log_error(f"unsupported object version: {hdr.version}.")
337-
else:
338-
meta = obj[hdr_size : hdr_size + hdr.meta_size]
339-
if hdr.meta_size != len(meta):
340-
log_error("metadata size mismatch.")
341-
data = obj[hdr_size + hdr.meta_size : hdr_size + hdr.meta_size + hdr.data_size]
342-
if hdr.data_size != len(data):
343-
log_error("data size mismatch.")
344-
else:
328+
if len(obj) < hdr_size:
345329
log_error("too small.")
330+
return
331+
hdr = RepoObj.ObjHeader(*RepoObj.obj_header.unpack(obj[:hdr_size]))
332+
if hdr.magic != OBJ_MAGIC:
333+
log_error("invalid object magic.")
334+
elif hdr.version != OBJ_VERSION:
335+
log_error(f"unsupported object version: {hdr.version}.")
336+
else:
337+
meta = obj[hdr_size : hdr_size + hdr.meta_size]
338+
if hdr.meta_size != len(meta):
339+
log_error("metadata size mismatch.")
340+
data = obj[hdr_size + hdr.meta_size : hdr_size + hdr.meta_size + hdr.data_size]
341+
if hdr.data_size != len(data):
342+
log_error("data size mismatch.")
346343

347344
# TODO: progress indicator, ...
348345
partial = bool(max_duration)
@@ -376,11 +373,11 @@ def check_object(obj):
376373
# As we don't do garbage collection here, this is not a problem.
377374
# We also don't know the plaintext size, so we set it to 0.
378375
init_entry = ChunkIndexEntry(flags=ChunkIndex.F_USED, size=0)
379-
infos = self.store.list("data")
376+
infos = self.store.list("packs")
380377
try:
381378
for info in infos:
382379
self._lock_refresh()
383-
key = "data/%s" % info.name
380+
key = "packs/%s" % info.name
384381
if key <= last_key_checked: # needs sorted keys
385382
continue
386383
try:
@@ -412,8 +409,9 @@ def check_object(obj):
412409
# add all existing objects to the index.
413410
# borg check: the index may have corrupted objects (we did not delete them)
414411
# borg check --repair: the index will only have non-corrupted objects.
415-
id = hex_to_bin(info.name)
416-
chunks[id] = init_entry
412+
pack_id = hex_to_bin(info.name)
413+
chunk_id = pack_id # N=1: chunk_id == pack_id
414+
chunks[chunk_id] = init_entry
417415
now = time.monotonic()
418416
if now > t_last_checkpoint + 300: # checkpoint every 5 mins
419417
t_last_checkpoint = now
@@ -437,7 +435,7 @@ def check_object(obj):
437435
self, chunks, incremental=False, clear=True, force_write=True, delete_other=True
438436
)
439437
except StoreObjectNotFound:
440-
# it can be that there is no "data/" at all, then it crashes when iterating infos.
438+
# it can be that there is no "packs/" at all, then it crashes when iterating infos.
441439
pass
442440
logger.info(f"Checked {objs_checked} repository objects, {objs_errors} errors.")
443441
if objs_errors == 0:
@@ -456,33 +454,35 @@ def list(self, limit=None, marker=None):
456454
"""
457455
collect = True if marker is None else False
458456
result = []
459-
infos = self.store.list("data") # generator yielding ItemInfos
457+
infos = self.store.list("packs") # generator yielding ItemInfos
460458
while True:
461459
self._lock_refresh()
462460
try:
463461
info = next(infos)
464462
except StoreObjectNotFound:
465-
break # can happen e.g. if "data" does not exist, pointless to continue in that case
463+
break # can happen e.g. if "packs" does not exist, pointless to continue in that case
466464
except StopIteration:
467465
break
468466
else:
469-
id = hex_to_bin(info.name)
467+
pack_id = hex_to_bin(info.name)
468+
chunk_id = pack_id # N=1: chunk_id == pack_id
470469
if collect:
471-
result.append((id, info.size))
470+
chunk_size = info.size # only correct for N=1
471+
result.append((chunk_id, chunk_size))
472472
if len(result) == limit:
473473
break
474-
elif id == marker:
474+
elif chunk_id == marker:
475475
collect = True
476476
# note: do not collect the marker id
477477
return result
478478

479479
def get(self, id, read_data=True, raise_missing=True):
480480
self._lock_refresh()
481+
pack_id = id # N=1: pack_id == chunk_id
481482
id_hex = bin_to_hex(id)
482-
key = "data/" + id_hex
483+
key = "packs/" + bin_to_hex(pack_id)
483484
try:
484485
if read_data:
485-
# read everything
486486
return self.store.load(key)
487487
else:
488488
# RepoObj layout supports separately encrypted metadata and data.
@@ -523,7 +523,8 @@ def put(self, id, data, wait=True):
523523
if data_size > MAX_DATA_SIZE:
524524
raise IntegrityError(f"More than allowed put data [{data_size} > {MAX_DATA_SIZE}]")
525525

526-
key = "data/" + bin_to_hex(id)
526+
pack_id = id # N=1: pack_id == chunk_id
527+
key = "packs/" + bin_to_hex(pack_id)
527528
self.store.store(key, data)
528529

529530
def delete(self, id, wait=True):
@@ -533,7 +534,8 @@ def delete(self, id, wait=True):
533534
deal with async results / exceptions later.
534535
"""
535536
self._lock_refresh()
536-
key = "data/" + bin_to_hex(id)
537+
pack_id = id # N=1: pack_id == chunk_id
538+
key = "packs/" + bin_to_hex(pack_id)
537539
try:
538540
self.store.delete(key)
539541
except StoreObjectNotFound:

src/borg/testsuite/archiver/check_cmd_test.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ def test_corrupted_manifest(archivers, request):
225225
archive, repository = open_archive(archiver.repository_path, "archive1")
226226
with repository:
227227
manifest = repository.get_manifest()
228-
corrupted_manifest = manifest[:123] + b"corrupted!" + manifest[123:]
228+
corrupted_manifest = manifest[:250] + b"corrupted!" + manifest[250:]
229229
repository.put_manifest(corrupted_manifest)
230230
cmd(archiver, "check", exit_code=1)
231231
output = cmd(archiver, "check", "-v", "--repair", exit_code=0)
@@ -273,7 +273,7 @@ def test_manifest_rebuild_corrupted_chunk(archivers, request):
273273
archive, repository = open_archive(archiver.repository_path, "archive1")
274274
with repository:
275275
manifest = repository.get_manifest()
276-
corrupted_manifest = manifest[:123] + b"corrupted!" + manifest[123:]
276+
corrupted_manifest = manifest[:250] + b"corrupted!" + manifest[250:]
277277
repository.put_manifest(corrupted_manifest)
278278
chunk = repository.get(archive.id)
279279
corrupted_chunk = chunk + b"corrupted!"
@@ -312,7 +312,7 @@ def test_spoofed_archive(archivers, request):
312312
with repository:
313313
# attacker would corrupt or delete the manifest to trigger a rebuild of it:
314314
manifest = repository.get_manifest()
315-
corrupted_manifest = manifest[:123] + b"corrupted!" + manifest[123:]
315+
corrupted_manifest = manifest[:250] + b"corrupted!" + manifest[250:]
316316
repository.put_manifest(corrupted_manifest)
317317
archive_dict = {
318318
"command_line": "",
@@ -351,8 +351,9 @@ def test_extra_chunks(archivers, request):
351351
check_cmd_setup(archiver)
352352
cmd(archiver, "check", exit_code=0)
353353
with Repository(archiver.repository_location, exclusive=True) as repository:
354-
chunk = fchunk(b"xxxx")
355-
repository.put(b"01234567890123456789012345678901", chunk)
354+
key = b"01234567890123456789012345678901"
355+
chunk = fchunk(b"xxxx", chunk_id=key)
356+
repository.put(key, chunk)
356357
cmd(archiver, "check", "-v", exit_code=0) # check does not deal with orphans anymore
357358

358359

src/borg/testsuite/repository_test.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,9 @@ def reopen(repository, exclusive: bool | None = True, create=False):
5353
)
5454

5555

56-
def fchunk(data, meta=b""):
56+
def fchunk(data, meta=b"", chunk_id=b"\x00" * 32):
5757
# Format chunk: create a raw chunk that has a valid RepoObj layout, but does not use encryption or compression.
58-
hdr = RepoObj.obj_header.pack(OBJ_MAGIC, OBJ_VERSION, len(meta), len(data))
58+
hdr = RepoObj.obj_header.pack(OBJ_MAGIC, OBJ_VERSION, chunk_id, len(meta), len(data))
5959
assert isinstance(data, bytes)
6060
chunk = hdr + meta + data
6161
return chunk
@@ -65,7 +65,7 @@ def pchunk(chunk):
6565
# Parse chunk: extract data and metadata from a raw chunk made by fchunk.
6666
hdr_size = RepoObj.obj_header.size
6767
hdr = chunk[:hdr_size]
68-
meta_size, data_size = RepoObj.obj_header.unpack(hdr)[2:4]
68+
meta_size, data_size = RepoObj.obj_header.unpack(hdr)[3:5]
6969
meta = chunk[hdr_size : hdr_size + meta_size]
7070
data = chunk[hdr_size + meta_size : hdr_size + meta_size + data_size]
7171
return data, meta
@@ -97,7 +97,7 @@ def test_basic_operations(repo_fixtures, request):
9797
def test_read_data(repo_fixtures, request):
9898
with get_repository_from_fixture(repo_fixtures, request) as repository:
9999
meta, data = b"meta", b"data"
100-
hdr = RepoObj.obj_header.pack(OBJ_MAGIC, OBJ_VERSION, len(meta), len(data))
100+
hdr = RepoObj.obj_header.pack(OBJ_MAGIC, OBJ_VERSION, H(0), len(meta), len(data))
101101
chunk_complete = hdr + meta + data
102102
chunk_short = hdr + meta
103103
repository.put(H(0), chunk_complete)

0 commit comments

Comments
 (0)