|
|
|
@ -19,16 +19,17 @@ from dataclasses import dataclass |
|
|
|
from datetime import datetime as DateTime |
|
|
|
import hashlib |
|
|
|
from io import BytesIO |
|
|
|
from pathlib import Path |
|
|
|
from pathlib import Path, PurePosixPath |
|
|
|
import platform |
|
|
|
import tomllib |
|
|
|
from typing import TYPE_CHECKING, BinaryIO, Callable, Type |
|
|
|
from typing import TYPE_CHECKING, BinaryIO, Callable, Self, Type |
|
|
|
|
|
|
|
from fastcdc import fastcdc |
|
|
|
import tomlkit |
|
|
|
|
|
|
|
from bsv import __version__ |
|
|
|
from bsv.exception import ConfigError |
|
|
|
from bsv.object import ObjectInfo |
|
|
|
from bsv.path_map import PathMap |
|
|
|
from bsv.simple_cas import SimpleCas |
|
|
|
from bsv.simple_cas.cas import Digest, SimpleCas |
|
|
|
@ -94,34 +95,95 @@ class Repository: |
|
|
|
def path_map(self) -> PathMap: |
|
|
|
return self._path_map.clone() |
|
|
|
|
|
|
|
def get_blob(self, digest: Digest) -> Blob: |
|
|
|
def get_blob(self, digest: Digest) -> BlobObject: |
|
|
|
with self: |
|
|
|
return self._read(digest, object_type=b"blob", cls=Blob) # type: ignore |
|
|
|
|
|
|
|
def add_blob(self, stream: BinaryIO, *, dry_run: bool=False) -> Digest: |
|
|
|
obj, blob = self._read(digest, object_type=b"blob") |
|
|
|
return BlobObject( |
|
|
|
digest = obj.digest, |
|
|
|
object_type = obj.object_type, |
|
|
|
size = obj.size, |
|
|
|
blob = blob, |
|
|
|
) |
|
|
|
|
|
|
|
def add_blob(self, stream: BinaryIO, *, dry_run: bool=False) -> BlobObject: |
|
|
|
with self: |
|
|
|
return self._write(b"blob", stream, dry_run=dry_run) |
|
|
|
|
|
|
|
def get_tree(self, digest: Digest) -> Tree: |
|
|
|
def get_symlink(self, digest: Digest) -> SymlinkObject: |
|
|
|
with self: |
|
|
|
return Tree.from_bytes(self, self._cas.read(digest, object_type=b"tree").data) |
|
|
|
|
|
|
|
def add_tree(self, tree: Tree, *, dry_run: bool=False) -> Digest: |
|
|
|
obj = self._cas.read(digest, object_type=b"slnk") |
|
|
|
return SymlinkObject( |
|
|
|
digest = obj.digest, |
|
|
|
object_type = obj.object_type, |
|
|
|
size = obj.size, |
|
|
|
symlink = Symlink.from_bytes(self, obj.data), |
|
|
|
) |
|
|
|
|
|
|
|
def add_symlink(self, symlink: Symlink, *, dry_run: bool=False) -> SymlinkObject: |
|
|
|
with self: |
|
|
|
return self._cas.write(b"tree", tree.to_bytes(), dry_run=dry_run) |
|
|
|
data = symlink.to_bytes() |
|
|
|
return SymlinkObject( |
|
|
|
digest = self._cas.write(b"slnk", data, dry_run=dry_run), |
|
|
|
object_type = b"slnk", |
|
|
|
size = len(data), |
|
|
|
symlink = symlink, |
|
|
|
) |
|
|
|
|
|
|
|
def add_symlink_from_fs_target(self, fs_symlink: Path, fs_target: Path, *, dry_run: bool=False) -> SymlinkObject: |
|
|
|
assert fs_symlink.is_absolute() |
|
|
|
return self.add_symlink( |
|
|
|
Symlink( |
|
|
|
repo = self, |
|
|
|
is_absolute = fs_target.is_absolute(), |
|
|
|
target = self._path_map.relative_bsv_path(fs_target, relative_to=fs_symlink), |
|
|
|
), |
|
|
|
dry_run = dry_run, |
|
|
|
) |
|
|
|
|
|
|
|
def add_tree_from_path(self, path: Path, *, dry_run: bool=False) -> Digest: |
|
|
|
def get_tree(self, digest: Digest) -> TreeObject: |
|
|
|
with self: |
|
|
|
obj = self._cas.read(digest, object_type=b"tree") |
|
|
|
return TreeObject( |
|
|
|
digest = obj.digest, |
|
|
|
object_type = obj.object_type, |
|
|
|
size = obj.size, |
|
|
|
tree = Tree.from_bytes(self, obj.data), |
|
|
|
) |
|
|
|
|
|
|
|
def add_tree(self, tree: Tree, *, dry_run: bool=False) -> TreeObject: |
|
|
|
with self: |
|
|
|
data = tree.to_bytes() |
|
|
|
return TreeObject( |
|
|
|
digest = self._cas.write(b"tree", data, dry_run=dry_run), |
|
|
|
object_type = b"tree", |
|
|
|
size = len(data), |
|
|
|
tree = tree, |
|
|
|
) |
|
|
|
|
|
|
|
def add_tree_from_path(self, path: Path, *, dry_run: bool=False) -> TreeObject: |
|
|
|
from bsv.tree_walker import TreeWalker |
|
|
|
walker = TreeWalker(self, dry_run=dry_run) |
|
|
|
return walker.add_tree(path) |
|
|
|
|
|
|
|
def get_snapshot(self, digest: Digest) -> Snapshot: |
|
|
|
def get_snapshot(self, digest: Digest) -> SnapshotObject: |
|
|
|
with self: |
|
|
|
return Snapshot.from_bytes(self, self._cas.read(digest, object_type=b"snap").data) |
|
|
|
|
|
|
|
def add_snapshot(self, snapshot: Snapshot, *, dry_run: bool=False) -> Digest: |
|
|
|
obj = self._cas.read(digest, object_type=b"snap") |
|
|
|
return SnapshotObject( |
|
|
|
digest = obj.digest, |
|
|
|
object_type = obj.object_type, |
|
|
|
size = obj.size, |
|
|
|
snapshot = Snapshot.from_bytes(self, obj.data), |
|
|
|
) |
|
|
|
|
|
|
|
def add_snapshot(self, snapshot: Snapshot, *, dry_run: bool=False) -> SnapshotObject: |
|
|
|
with self: |
|
|
|
return self._cas.write(b"snap", snapshot.to_bytes(), dry_run=dry_run) |
|
|
|
data = snapshot.to_bytes() |
|
|
|
return SnapshotObject( |
|
|
|
digest = self._cas.write(b"snap", data, dry_run=dry_run), |
|
|
|
object_type = b"snap", |
|
|
|
size = len(data), |
|
|
|
snapshot = snapshot, |
|
|
|
) |
|
|
|
|
|
|
|
# def take_snapshot( |
|
|
|
# self, |
|
|
|
@ -151,14 +213,15 @@ class Repository: |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _read(self, digest: Digest, object_type: bytes, cls: Type[ChunkedObject]) -> ChunkedObject: |
|
|
|
def _read(self, digest: Digest, object_type: bytes) -> tuple[ObjectInfo, Blob]: |
|
|
|
obj = self._cas.read(digest, object_type=object_type) |
|
|
|
stream = BytesIO(obj.data) |
|
|
|
return cls.from_stream(self, stream, digest_size=self._cas._digest_size) |
|
|
|
return obj, Blob.from_stream(self, stream, digest_size=self._cas._digest_size) |
|
|
|
|
|
|
|
def _write(self, object_type: bytes, stream: BinaryIO, *, dry_run: bool=False) -> Digest: |
|
|
|
def _write(self, object_type: bytes, stream: BinaryIO, *, dry_run: bool=False) -> BlobObject: |
|
|
|
out = BytesIO() |
|
|
|
size = 0 |
|
|
|
chunks = [] |
|
|
|
for chunk in fastcdc( |
|
|
|
stream, |
|
|
|
min_size = self._min_chunk_size, |
|
|
|
@ -168,9 +231,19 @@ class Repository: |
|
|
|
): |
|
|
|
size += chunk.length |
|
|
|
digest = self._cas.write(b"chnk", chunk.data, dry_run=dry_run) |
|
|
|
chunks.append(Chunk(digest, chunk.length)) |
|
|
|
out.write(digest.digest) |
|
|
|
out.write(chunk.length.to_bytes(4)) |
|
|
|
return self._cas.write(object_type, size.to_bytes(8) + out.getvalue()) |
|
|
|
return BlobObject( |
|
|
|
digest = self._cas.write(object_type, size.to_bytes(8) + out.getvalue()), |
|
|
|
object_type = object_type, |
|
|
|
size = 8 + len(out.getvalue()), |
|
|
|
blob = Blob( |
|
|
|
repo = self, |
|
|
|
size = size, |
|
|
|
chunks = chunks, |
|
|
|
) |
|
|
|
) |
|
|
|
|
|
|
|
def __enter__(self): |
|
|
|
if self._context_depth == 0: |
|
|
|
@ -256,7 +329,7 @@ class ChunkedObject: |
|
|
|
chunks: list[Chunk] |
|
|
|
|
|
|
|
@classmethod |
|
|
|
def from_stream(cls, repo: Repository, stream: BinaryIO, digest_size: int) -> ChunkedObject: |
|
|
|
def from_stream(cls, repo: Repository, stream: BinaryIO, digest_size: int) -> Self: |
|
|
|
self = cls( |
|
|
|
repo = repo, |
|
|
|
size = int.from_bytes(read_exact(stream, 8)), |
|
|
|
@ -276,7 +349,7 @@ class Chunk: |
|
|
|
size: int |
|
|
|
|
|
|
|
@classmethod |
|
|
|
def from_stream(cls, stream: BinaryIO, digest_size: int) -> Chunk | None: |
|
|
|
def from_stream(cls, stream: BinaryIO, digest_size: int) -> Self | None: |
|
|
|
digest_bytes = read_exact_or_eof(stream, digest_size) |
|
|
|
if digest_bytes is None: |
|
|
|
return None |
|
|
|
@ -324,23 +397,65 @@ class ChunkedObjectReader: |
|
|
|
class Blob(ChunkedObject): |
|
|
|
pass |
|
|
|
|
|
|
|
@dataclass(frozen=True, order=True, slots=True) |
|
|
|
class BlobObject(ObjectInfo): |
|
|
|
blob: Blob |
|
|
|
|
|
|
|
|
|
|
|
@dataclass(slots=True) |
|
|
|
class Symlink: |
|
|
|
repo: Repository |
|
|
|
is_absolute: bool |
|
|
|
target: PurePosixPath |
|
|
|
|
|
|
|
@classmethod |
|
|
|
def from_stream(cls, repo: Repository, stream: BinaryIO) -> Self: |
|
|
|
return cls( |
|
|
|
repo = repo, |
|
|
|
is_absolute = bool(read_exact(stream, 1)), |
|
|
|
target = PurePosixPath(stream.read().decode("utf-8")), |
|
|
|
) |
|
|
|
|
|
|
|
@classmethod |
|
|
|
def from_bytes(cls, repo: Repository, bytes: bytes) -> Self: |
|
|
|
stream = BytesIO(bytes) |
|
|
|
return cls.from_stream(repo, stream) |
|
|
|
|
|
|
|
def write(self, stream: BinaryIO): |
|
|
|
stream.write(self.is_absolute.to_bytes(1)) |
|
|
|
stream.write(self.target.as_posix().encode("utf-8")) |
|
|
|
|
|
|
|
def to_bytes(self) -> bytes: |
|
|
|
stream = BytesIO() |
|
|
|
self.write(stream) |
|
|
|
return stream.getvalue() |
|
|
|
|
|
|
|
@dataclass(frozen=True, order=True, slots=True) |
|
|
|
class SymlinkObject(ObjectInfo): |
|
|
|
symlink: Symlink |
|
|
|
|
|
|
|
|
|
|
|
@dataclass |
|
|
|
class Tree: |
|
|
|
repo: Repository |
|
|
|
items: list[TreeItem] |
|
|
|
|
|
|
|
@property |
|
|
|
def total_size(self) -> int: |
|
|
|
return sum( |
|
|
|
item.size |
|
|
|
for item in self.items |
|
|
|
) |
|
|
|
|
|
|
|
@classmethod |
|
|
|
def from_stream(cls, repo: Repository, stream: BinaryIO) -> Tree: |
|
|
|
def from_stream(cls, repo: Repository, stream: BinaryIO) -> Self: |
|
|
|
tree = Tree(repo, []) |
|
|
|
while (item := TreeItem.from_stream(stream, repo._cas._digest_size)) is not None: |
|
|
|
tree.items.append(item) |
|
|
|
return tree |
|
|
|
|
|
|
|
@classmethod |
|
|
|
def from_bytes(cls, repo: Repository, data: bytes) -> Tree: |
|
|
|
def from_bytes(cls, repo: Repository, data: bytes) -> Self: |
|
|
|
stream = BytesIO(data) |
|
|
|
return cls.from_stream(repo, stream) |
|
|
|
|
|
|
|
@ -354,6 +469,14 @@ class Tree: |
|
|
|
self.write(stream) |
|
|
|
return stream.getvalue() |
|
|
|
|
|
|
|
@dataclass(frozen=True, order=True, slots=True) |
|
|
|
class TreeObject(ObjectInfo): |
|
|
|
tree: Tree |
|
|
|
|
|
|
|
@property |
|
|
|
def total_size(self) -> int: |
|
|
|
return self.size + self.tree.total_size |
|
|
|
|
|
|
|
|
|
|
|
@dataclass |
|
|
|
class TreeItem: |
|
|
|
@ -390,7 +513,7 @@ class TreeItem: |
|
|
|
self.modification_timestamp_us = timestamp_us_from_time(time) |
|
|
|
|
|
|
|
@classmethod |
|
|
|
def from_stream(cls, stream: BinaryIO, digest_size: int) -> TreeItem | None: |
|
|
|
def from_stream(cls, stream: BinaryIO, digest_size: int) -> Self | None: |
|
|
|
digest_bytes = read_exact_or_eof(stream, digest_size) |
|
|
|
if digest_bytes is None: |
|
|
|
return None |
|
|
|
@ -435,7 +558,7 @@ class Snapshot: |
|
|
|
self.timestamp_us = timestamp_us_from_time(time) |
|
|
|
|
|
|
|
@classmethod |
|
|
|
def from_stream(cls, repo: Repository, stream: BinaryIO) -> Snapshot: |
|
|
|
def from_stream(cls, repo: Repository, stream: BinaryIO) -> Self: |
|
|
|
return Snapshot( |
|
|
|
repo = repo, |
|
|
|
tree_digest = Digest(read_exact(stream, repo._cas._digest_size)), |
|
|
|
@ -448,7 +571,7 @@ class Snapshot: |
|
|
|
) |
|
|
|
|
|
|
|
@classmethod |
|
|
|
def from_bytes(cls, repo: Repository, data: bytes) -> Snapshot: |
|
|
|
def from_bytes(cls, repo: Repository, data: bytes) -> Self: |
|
|
|
stream = BytesIO(data) |
|
|
|
return cls.from_stream(repo, stream) |
|
|
|
|
|
|
|
@ -467,3 +590,7 @@ class Snapshot: |
|
|
|
stream = BytesIO() |
|
|
|
self.write(stream) |
|
|
|
return stream.getvalue() |
|
|
|
|
|
|
|
@dataclass(frozen=True, order=True, slots=True) |
|
|
|
class SnapshotObject(ObjectInfo): |
|
|
|
snapshot: Snapshot |
|
|
|
|