Browse Source

bsv init command.

master
Draklaw 2 years ago
parent
commit
d058cd0631
  1. 63
      README.md
  2. 1
      pyproject.toml
  3. 126
      src/bsv/cli.py
  4. 123
      src/bsv/command/init.py
  5. 36
      src/bsv/main.py
  6. 181
      src/bsv/repository.py
  7. 43
      src/bsv/util.py
  8. 7
      tests/test_repository.py

63
README.md

@ -22,31 +22,62 @@ This will automatically install the dependencies (including `pytest`). Happy hac
## Roadmap ## Roadmap
### v0.0.1 - MVP ### v0.0.1 - Simple CAS + insert/remove files manually
- [ ] `bsv init [<destination>] [--repo <local-repo-path>]` command to initialize bsv. Basic features. Naïve CAS implementation that store everything in a single big file with no option for removing objects. Supports a single "local" repository.
- [ ] `bsv repo [-v]` list all known repositories.
- [ ] `bsv repo create <name> <destination>` create a new repository. - [x] Simple CAS implementation (it's OK if it's naïve).
- [ ] `bsv repo add [<name>] <destination>` add an already existing repository. - [x] Content-based chunking to store files.
- [ ] `bsv repo remove <name>|<destination>` remove a repository. - [x] `bsv init` command to initialize bsv.
- [ ] `bsv map` list mappings between bsv paths and filesystem paths. - [ ] `bsv info` print useful information bsv configuration.
- [ ] `bsv map add <bsv-path> <fs-path>` add a mapping.
- [ ] `bsv map remove <bsv-path> <fs-path>` remove a mapping.
- [ ] `bsv log` show the history of snapshots. - [ ] `bsv log` show the history of snapshots.
- [ ] `bsv show <digest>` show the object `digest`. - [ ] `bsv show <digest>` show the object `digest`.
- [ ] `bsv ls <bsv-path>` list files in a bsv directory. - [ ] `bsv ls <bsv-path>` list files in a bsv directory.
- [ ] `bsv mkdir <bsv-path>` create a directory in bsv directly. - [ ] `bsv mkdir <bsv-path>` create a directory in bsv directly.
- [ ] `bsv add <fs-path> <bsv-path>` copy files from the filesystem to bsv. - [ ] `bsv add [-r] <fs-path> <bsv-path>` copy files from the filesystem to bsv.
- [ ] `bsv get <bsv-path> <fs-path>` copy files from bsv to the filesystem. - [ ] `bsv get [-r] <bsv-path> <fs-path>` copy files from bsv to the filesystem.
- [ ] `bsv snapshot` capture a snapshot, i.e. to ensure that mapped files in the repositories match what is on the filesystem. - [ ] `bsv rm [-r] <bsv-path>` create a directory in bsv directly.
### v0.0.2 - File map + snapshots
Add support for mapping files from BSV virtual file system to the actual file system. Add snapshot and restore commands.
- [ ] `bsv map` list mappings between bsv paths and filesystem paths.
- [ ] `bsv map add <bsv-path> <fs-path>` add a mapping.
- [ ] `bsv map remove <bsv-path> <fs-path>` remove a mapping.
- [ ] `bsv snapshot` capture a snapshot, i.e. ensure that mapped files in the repositories match what is on the filesystem.
- [ ] `bsv restore <snapshot> <fs-path>` update files on the filesystem to the version captured by `snapshot`. - [ ] `bsv restore <snapshot> <fs-path>` update files on the filesystem to the version captured by `snapshot`.
- [x] Simple CAS implementation (it's OK if it's naïve).
- [x] Content-based chunking to store files.
### Later ### v0.0.3 - Multiple repository
Support multiple repository. Repository can be configured to store only metadata (typically for the local repository) or everything.
- [ ] Support repositories that store only metadata.
- [ ] `bsv repo [-v]` list all known repositories.
- [ ] `bsv repo create <name> <destination>` create a new repository.
- [ ] `bsv repo add [<name>] <destination>` add an already existing repository.
- [ ] `bsv repo remove <name>|<destination>` remove a repository.
- [ ] `bsv fetch [<name>]` fetch latest metadata from known repositories. - [ ] `bsv fetch [<name>]` fetch latest metadata from known repositories.
- [ ] `bsv sync` similar to `snapshot` + `fetch` + `restore`: Fetch latest changes from the repositories and update the filesystem to match. In case of conflict (file changed both in the repositories and locally), performs a snapshot first to ensure all conflicting versions are backed'up, then use some conflict-resolution strategy and warn the user. - [ ] `bsv sync` similar to `snapshot` + `fetch` + `restore`: Fetch latest changes from the repositories and update the filesystem to match. In case of conflict (file changed both in the repositories and locally), performs a snapshot first to ensure all conflicting versions are backed'up, then use some conflict-resolution strategy and warn the user.
### v0.0.4 - Proper CAS
- [ ] Safe concurrent access (e.g. when several devices use a shared repository).
- [ ] Support removing objects.
- [ ] Garbage collection (remove unreferenced objects).
- [ ] Use garbage collection to keep metadata-only repository clean.
### v0.0.5 - Some extra features
- [ ] `bsv tag <name> [<snapshot>] [-m <message>]` set/update a tag (an alias to a specific snapshot). - [ ] `bsv tag <name> [<snapshot>] [-m <message>]` set/update a tag (an alias to a specific snapshot).
- [ ] Support for symlinks.
### Later
- [ ] `bsv watch` starts a daemon that watch changes in mapped directories and automatically create snapshots. - [ ] `bsv watch` starts a daemon that watch changes in mapped directories and automatically create snapshots.
- [ ] `bsv serve` starts an http server that expose an API + an interface to manipulate BSV. Allow to list files, explore history, download and upload files... - [ ] `bsv http` starts an http server that expose an API + an interface to manipulate BSV. Allow to list files, explore history, download and upload files...
- [ ] Bsv protocol + client/server
- [ ] Custom rules for repository to select what must be stored or not.
- [ ] Create sensible rules for backup (keep a lot of recent versions, less for older versions).
- [ ] Add object set support (a kind of object that simply store a collection of objects). Can be used as tag.
- [ ] Add mail object ?

1
pyproject.toml

@ -13,6 +13,7 @@ classifiers = [
dynamic = ["version"] dynamic = ["version"]
dependencies = [ dependencies = [
"fastcdc", "fastcdc",
"rich",
"tomlkit", "tomlkit",
] ]

126
src/bsv/cli.py

@ -0,0 +1,126 @@
# bsv - Backup, Synchronization, Versioning
# Copyright (C) 2023 Simon Boyé
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations
from typing import Any, Callable, TypeVar
from rich.console import Console
from rich.text import Text
_console: Console | None = None
def get_console() -> Console:
assert _console is not None
return _console
_error_console: Console | None = None
def get_error_console() -> Console:
assert _error_console is not None
return _error_console
def init_consoles(color: str="auto"):
global _console
global _error_console
assert _console is None
assert _error_console is None
kwargs: dict[str, Any] = {
"tab_size": 4,
}
match color:
case "always":
kwargs["force_terminal"] = True
case "auto":
pass
case "never":
kwargs["no_color"] = True
_console = Console(
**kwargs,
)
_error_console = Console(
stderr = True,
**kwargs,
)
PromptType = TypeVar("PromptType")
class NoDefaultType:
def __repr__(self):
return "NoDefault"
NoDefault = NoDefaultType()
def prompt(
prompt: str,
factory: Callable[[str], PromptType],
*,
console: Console | None = None,
default: PromptType | NoDefaultType = NoDefault,
show_default: bool = True,
) -> PromptType:
if console is None:
console = get_console()
prompt_text = Text(prompt, style="prompt")
prompt_text.end = ""
if show_default and default is not NoDefault:
prompt_text.append(" ")
prompt_text.append(f"({default})", style="prompt.default")
prompt_text.append(": ")
while True:
try:
value = console.input(prompt_text)
except KeyboardInterrupt:
console.print("")
raise
if not value and not isinstance(default, NoDefaultType):
return default
try:
return factory(value)
except ValueError as err:
console.print(err)
def prompt_confirmation(prompt: str, *, console: Console | None=None, default: bool=True) -> bool:
if console is None:
console = get_console()
prompt_text = Text(prompt, style="prompt")
prompt_text.end = ""
prompt_text.append(" ")
if default:
prompt_text.append("(Y/n)", style="prompt.default")
else:
prompt_text.append("(y/N)", style="prompt.default")
prompt_text.append(": ")
while True:
try:
value = console.input(prompt_text).strip().lower()
except KeyboardInterrupt:
console.print("")
raise
if not value and not isinstance(default, NoDefaultType):
return default
if value not in "yn":
console.print("Please answer 'y' or 'n'.")
else:
return value == "y"

123
src/bsv/command/init.py

@ -16,35 +16,40 @@
from __future__ import annotations from __future__ import annotations
from argparse import ArgumentParser from argparse import ArgumentParser
from os import getlogin
from pathlib import Path from pathlib import Path
import platform import platform
from bsv.command import command from bsv.command import command
from bsv.repository import check_config_path, check_device_name, check_local_repository_path, create_repository
from bsv.util import default_local_repository_path
def init_parser(parser: ArgumentParser): def init_parser(parser: ArgumentParser):
parser.add_argument(
"--name", "-n",
help = "Name of the repository. Default to system hostname.",
)
parser.add_argument( parser.add_argument(
"--interactive", "-i", "--interactive", "-i",
default = False,
action = "store_true", action = "store_true",
help = "Prompt the user for configuration choices.", help = "Prompt the user for configuration choices.",
) )
parser.add_argument( parser.add_argument(
"destination", "--local-repository", "-l",
type = Path, type = Path,
default = default_local_repository_path(),
nargs = "?", nargs = "?",
dest = "local_repository_path",
help = "Path to a non-existing or empty folder where bsv data will be stored.", help = "Path to a non-existing or empty folder where bsv data will be stored.",
) )
parser.add_argument(
"--device-name", "-n",
default = platform.node(),
help = "Name of the device. Default to system hostname.",
)
@command(init_parser) @command(init_parser)
def init( def init(
repository_path: Path | None, config_path: Path,
destination: Path | None = None, device_name: str,
name: str | None = None, local_repository_path: Path,
interactive: bool = False, interactive: bool = False,
) -> int: ) -> int:
"""Initialize a new bsv repository. """Initialize a new bsv repository.
@ -52,60 +57,62 @@ def init(
from datetime import datetime as DateTime from datetime import datetime as DateTime
import tomlkit import tomlkit
if name is None: from bsv.cli import get_console, get_error_console, prompt, prompt_confirmation
name = platform.node()
print = get_console().print
def make_config_path(value: str) -> Path:
path = Path(value.strip())
if not path.is_absolute():
path = path.resolve()
check_config_path(path)
return path
if destination is None: def make_device_name(value: str) -> str:
# TODO: Choose a sensible system-dependent path. device_name = value.strip()
destination = Path.cwd() check_device_name(device_name)
return device_name
def make_local_repository_path(value: str) -> Path:
path = Path(value)
if not path.is_absolute():
path = path.resolve()
check_local_repository_path(path)
return path
if interactive: if interactive:
name = input(f"Repository name: (default to {name})\n").strip() or name config_path = prompt("Bsv configuration file", make_config_path, default=config_path)
destination = Path(input(f"Destination: (default to {destination})\n").strip()) or destination device_name = prompt("Device name", make_device_name, default=device_name)
if not destination.is_absolute(): local_repository_path = prompt("Destination", make_local_repository_path, default=local_repository_path)
destination = Path.cwd() / destination
if not config_path.is_absolute():
if not name: config_path = config_path.resolve()
raise RuntimeError("repository name cannot be empty") if not local_repository_path.is_absolute():
if not destination.parent.exists(): local_repository_path = local_repository_path.resolve()
raise RuntimeError(f"destination directory {destination.parent} does not exists")
if destination.exists() and not destination.is_dir():
raise RuntimeError(f"destination {destination} exists but is not a directory")
if destination.exists() and len(list(destination.iterdir())):
raise RuntimeError(f"destination directory {destination} is not empty")
try: try:
destination.mkdir(exist_ok=True) check_config_path(config_path)
except: check_device_name(device_name)
raise RuntimeError(f"failed to create destination directory {destination}") check_local_repository_path(local_repository_path)
except ValueError as err:
bsv_table = tomlkit.table() get_error_console().print(err, style="bold red")
bsv_table.add(tomlkit.comment("Name of the repository.")) return 1
bsv_table.add(tomlkit.comment("Ideally, this should be unique among all connected repositories."))
bsv_table.add("name", name) print("Bsv repository will be created with the following settings:", style="green")
bsv_table.add(tomlkit.nl()) print("")
bsv_table.add(tomlkit.comment("Mapping between bsv tree and the actual filesystem.")) print(f"\t[blue]:page_facing_up: Config path[/blue]: [bold yellow]{config_path}")
bsv_table.add("path_map", tomlkit.array()) print(f"\t[blue]:computer: Device name[/blue]: [bold yellow]{device_name}")
print(f"\t[blue]:floppy_disk: Local repository[/blue]: [bold yellow]{local_repository_path}")
cas_table = tomlkit.table() print("")
cas_table.add("type", "simple")
cas_table.add("hash", "sha256")
doc = tomlkit.document()
doc.add(tomlkit.comment("bsv repository configuration"))
doc.add(tomlkit.comment(f"Created by {getlogin()} on {DateTime.now().isoformat()}."))
doc.add(tomlkit.nl())
doc.add("bsv", bsv_table)
doc.add(tomlkit.nl())
doc.add("cas", cas_table)
config_path = destination / "bsv_config.toml"
try:
stream = config_path.open("w", encoding="utf-8")
except:
raise RuntimeError("failed to open configuration file {config_path}")
with stream: if interactive:
tomlkit.dump(doc, stream) if not prompt_confirmation("Create repository ?"):
return 1
create_repository(
config_path = config_path,
device_name = device_name,
local_repository_path = local_repository_path,
)
return 0 return 0

36
src/bsv/main.py

@ -22,7 +22,9 @@ import sys
from textwrap import dedent from textwrap import dedent
from bsv import __version__ from bsv import __version__
from bsv.cli import get_error_console, init_consoles
from bsv.command import init_commands from bsv.command import init_commands
from bsv.util import default_bsv_config_path
def make_parser( def make_parser(
@ -31,10 +33,20 @@ def make_parser(
) -> ArgumentParser: ) -> ArgumentParser:
parent_parser = ArgumentParser(add_help=False) parent_parser = ArgumentParser(add_help=False)
parent_parser.add_argument( parent_parser.add_argument(
"--repository", "--color",
default = "auto",
choices = ("always", "auto", "never"),
help = dedent("""
Force or disable colors, or auto-detect terminal support.
""").strip(),
)
parent_parser.add_argument(
"--config",
default = default_bsv_config_path(),
type = Path, type = Path,
dest = "config_path",
help = dedent(""" help = dedent("""
Bsv repository path. Overides default paths and BSV_REPOSITORY environment variable. Bsv config path. Overrides default paths and BSV_CONFIG environment variable.
""").strip(), """).strip(),
) )
@ -68,16 +80,16 @@ def main(
) )
arg_dict = vars(parser.parse_args(args or sys.argv[1:])) arg_dict = vars(parser.parse_args(args or sys.argv[1:]))
repository_path: Path | None = arg_dict.pop("repository") color = arg_dict.pop("color")
if repository_path is None and "BSV_REPOSITORY" in os.environ: init_consoles(color=color)
repository_path = Path(os.environ["BSV_REPOSITORY"])
# else:
# for path in get_config_dirs():
# maybe_config_path = path / "config.toml"
# if maybe_config_path.is_file():
# config_path = maybe_config_path
# break
command = arg_dict.pop("command") command = arg_dict.pop("command")
return command(repository_path=repository_path, **arg_dict) try:
return command(**arg_dict)
except Exception as err:
get_error_console().print_exception()
except KeyboardInterrupt:
return 130
return 0

181
src/bsv/repository.py

@ -20,12 +20,10 @@ from datetime import datetime as DateTime
import hashlib import hashlib
from io import BytesIO from io import BytesIO
from pathlib import Path, PurePosixPath from pathlib import Path, PurePosixPath
import platform
import tomllib import tomllib
from typing import TYPE_CHECKING, BinaryIO, Callable, Self from typing import TYPE_CHECKING, Any, BinaryIO, Self
from fastcdc import fastcdc from fastcdc import fastcdc
import tomlkit
from bsv import __version__ from bsv import __version__
from bsv.exception import ConfigError from bsv.exception import ConfigError
@ -33,7 +31,7 @@ from bsv.object import ObjectInfo
from bsv.path_map import PathMap from bsv.path_map import PathMap
from bsv.simple_cas import SimpleCas from bsv.simple_cas import SimpleCas
from bsv.simple_cas.cas import Digest, SimpleCas from bsv.simple_cas.cas import Digest, SimpleCas
from bsv.util import Hash, read_exact, read_exact_or_eof, time_from_timestamp_us, timestamp_us_from_time from bsv.util import default_bsv_config_path, default_local_repository_path, read_exact, read_exact_or_eof, time_from_timestamp_us, timestamp_us_from_time
if TYPE_CHECKING: if TYPE_CHECKING:
from bsv.tree_walker import TreeWalker from bsv.tree_walker import TreeWalker
@ -45,8 +43,9 @@ DEFAULT_MAX_CHUNK_SIZE = 1 << 20
class Repository: class Repository:
_path: Path _config_path: Path
_name: str _device_name: str
_local_repository_path: Path
_cas: SimpleCas _cas: SimpleCas
_min_chunk_size: int = DEFAULT_MIN_CHUNK_SIZE _min_chunk_size: int = DEFAULT_MIN_CHUNK_SIZE
@ -58,38 +57,36 @@ class Repository:
_context_depth: int = 0 _context_depth: int = 0
def __init__(self, path: Path): def __init__(self, config_path: Path):
self._path = path self._config_path = config_path
with self.config_file.open("rb") as stream: with self._config_path.open("rb") as stream:
config = tomllib.load(stream) config = tomllib.load(stream)
bsv = config.get("bsv", {}) bsv = config.get("bsv", {})
def get(key: str) -> Any:
value = bsv.get(key)
if value is None:
raise ConfigError(f"invalid bsv configuration: missing bsv.{key} item")
return value
self._name = bsv.get("name") or platform.node() self._device_name = get("device_name")
self._local_repository_path = Path(get("local_repository"))
self._min_chunk_size = get("min_chunk_size")
self._avg_chunk_size = get("avg_chunk_size")
self._max_chunk_size = get("max_chunk_size")
self._path_map = PathMap.from_obj(get("path_map"))
self._cas = make_cas( self._cas = make_cas(self._local_repository_path)
bsv.get("cas"),
self._path,
lambda: hashlib.new(bsv.get("hash")), # type: ignore
)
self._min_chunk_size = bsv.get("min_chunk_size")
self._avg_chunk_size = bsv.get("avg_chunk_size")
self._max_chunk_size = bsv.get("max_chunk_size")
self._path_map = PathMap.from_obj(bsv.get("path_map", []))
@property
def path(self) -> Path:
return self._path
@property @property
def config_file(self) -> Path: def config_path(self) -> Path:
return self.path / "bsv_config.toml" return self._config_path
@property @property
def name(self) -> str: def device_name(self) -> str:
return self._name return self._device_name
@property @property
def path_map(self) -> PathMap: def path_map(self) -> PathMap:
@ -257,9 +254,30 @@ class Repository:
return self._cas.__exit__(exc_type, exc_value, traceback) return self._cas.__exit__(exc_type, exc_value, traceback)
def check_config_path(path: Path):
if path.exists():
raise ValueError(f"{path} already exists.")
if path != default_bsv_config_path() and not path.parent.is_dir():
raise ValueError(f"{path.parent} does not exist or is not a directory.")
def check_device_name(device_name: str):
if not device_name:
raise ValueError("Device name cannot be empty.")
if not device_name.isidentifier():
raise ValueError(f"{device_name} is not a valid device name.")
def check_local_repository_path(path: Path):
if path != default_local_repository_path() and not path.parent.exists():
raise ValueError(f"Directory {path.parent} does not exists.")
if path.exists() and not path.is_dir():
raise ValueError(f"{path} exists but is not a directory.")
if path.exists() and len(list(path.iterdir())):
raise ValueError(f"Local repository directory {path} is not empty.")
def create_repository( def create_repository(
destination: Path, config_path: Path,
name: str, device_name: str,
local_repository_path: Path,
cas: str = "simple", cas: str = "simple",
hash: str = "sha256", hash: str = "sha256",
min_chunk_size: int = DEFAULT_MIN_CHUNK_SIZE, min_chunk_size: int = DEFAULT_MIN_CHUNK_SIZE,
@ -269,55 +287,96 @@ def create_repository(
from datetime import datetime as DateTime from datetime import datetime as DateTime
from os import getlogin from os import getlogin
if not name: import tomlkit
raise RuntimeError("repository name cannot be empty")
if not destination.parent.exists(): check_config_path(config_path)
raise RuntimeError(f"destination directory {destination.parent} does not exists") check_device_name(device_name)
if destination.exists() and not destination.is_dir(): check_local_repository_path(local_repository_path)
raise RuntimeError(f"destination {destination} exists but is not a directory")
if destination.exists() and len(list(destination.iterdir())): if config_path == default_bsv_config_path():
raise RuntimeError(f"destination directory {destination} is not empty") try:
config_path.parent.mkdir(parents=True, exist_ok=True)
except:
raise RuntimeError(f"failed to create bsv config destination directory {config_path.parent}")
try: try:
destination.mkdir(exist_ok=True) local_repository_path.mkdir(exist_ok=True)
except: except:
raise RuntimeError(f"failed to create destination directory {destination}") raise RuntimeError(f"failed to create local repository directory {local_repository_path}")
bsv_table = tomlkit.table() bsv_table = tomlkit.table()
bsv_table.add(tomlkit.comment("Name of the repository."))
bsv_table.add(tomlkit.comment("Ideally, this should be unique among all connected repositories.")) bsv_table.add(tomlkit.comment("Name of the instance."))
bsv_table.add("name", name) bsv_table.add(tomlkit.comment("Ideally, this should be unique among all connected devices."))
bsv_table.add("device_name", device_name)
bsv_table.add(tomlkit.nl()) bsv_table.add(tomlkit.nl())
bsv_table.add(tomlkit.comment("Mapping between bsv tree and the actual filesystem.")) bsv_table.add(tomlkit.comment("Path to the local repository."))
bsv_table.add("path_map", tomlkit.array()) bsv_table.add("local_repository", str(local_repository_path))
bsv_table.add("cas", cas)
bsv_table.add("hash", hash) bsv_table.add(tomlkit.nl())
bsv_table.add(tomlkit.comment("Properties of the content-based chunking algorithm."))
bsv_table.add("min_chunk_size", min_chunk_size) bsv_table.add("min_chunk_size", min_chunk_size)
bsv_table.add("avg_chunk_size", avg_chunk_size) bsv_table.add("avg_chunk_size", avg_chunk_size)
bsv_table.add("max_chunk_size", max_chunk_size) bsv_table.add("max_chunk_size", max_chunk_size)
doc = tomlkit.document() bsv_table.add(tomlkit.nl())
doc.add(tomlkit.comment("bsv repository configuration")) bsv_table.add(tomlkit.comment("Mapping between bsv tree and the local filesystem."))
doc.add(tomlkit.comment(f"Created by {getlogin()} on {DateTime.now().isoformat()}.")) bsv_table.add("path_map", tomlkit.array())
doc.add(tomlkit.nl())
doc.add("bsv", bsv_table) bsv_config = tomlkit.document()
bsv_config.add(tomlkit.comment("bsv device configuration"))
bsv_config.add(tomlkit.comment(f"Created by {getlogin()} on {DateTime.now().isoformat()}."))
bsv_config.add(tomlkit.nl())
bsv_config.add("bsv", bsv_table)
cas_table = tomlkit.table()
cas_table.add("type", cas)
cas_table.add("hash", hash)
cas_config = tomlkit.document()
cas_config.add(tomlkit.comment(f"bsv local repository configuration for instance {config_path}."))
cas_config.add(tomlkit.comment(f"Created by {getlogin()} on {DateTime.now().isoformat()}."))
cas_config.add(tomlkit.nl())
cas_config.add("cas", cas_table)
config_path = destination / "bsv_config.toml"
try: try:
stream = config_path.open("w", encoding="utf-8") bsv_stream = config_path.open("w", encoding="utf-8")
except: except:
raise RuntimeError("failed to open configuration file {config_path}") raise RuntimeError(f"failed to open bsv configuration file {config_path}")
try:
cas_stream = (local_repository_path / "bsv_repository.config").open("w", encoding="utf-8")
except:
raise RuntimeError(f"failed to open local repository configuration file {config_path}")
with bsv_stream:
tomlkit.dump(bsv_config, bsv_stream)
with cas_stream:
tomlkit.dump(cas_config, cas_stream)
repo = Repository(config_path)
return repo
with stream:
tomlkit.dump(doc, stream)
return Repository(destination) def make_cas(cas_config_path: Path) -> SimpleCas:
with (cas_config_path / "bsv_repository.config").open("rb") as stream:
config = tomllib.load(stream)
cas = config.get("cas", {})
def get(key: str) -> Any:
value = cas.get(key)
if value is None:
raise ConfigError(f"invalid repository configuration: missing {key} item")
return value
def make_cas(cas_name: str, path: Path, hash_factory: Callable[[], Hash]) -> SimpleCas: type = get("type")
if cas_name == "simple": hash_factory = lambda: hashlib.new(get("hash"))
return SimpleCas(path, hash_factory) if type == "simple":
raise ConfigError(f"unknown cas name {cas_name}") return SimpleCas(cas_config_path, hash_factory) # type: ignore
raise ConfigError(f"unknown cas type {type}")

43
src/bsv/util.py

@ -17,7 +17,9 @@ from __future__ import annotations
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from datetime import UTC, datetime as DateTime, timedelta as TimeDelta from datetime import UTC, datetime as DateTime, timedelta as TimeDelta
import os
from pathlib import Path from pathlib import Path
import platform
import stat import stat
from typing import BinaryIO from typing import BinaryIO
@ -48,7 +50,7 @@ def read_exact_or_eof(stream: BinaryIO, num_bytes: int) -> bytes | None:
def is_bsv_repository(path: Path) -> bool: def is_bsv_repository(path: Path) -> bool:
return (path / "bsv_config.toml").is_file() return (path / "bsv_repository.config").is_file()
def object_type_from_path(path: Path) -> bytes: def object_type_from_path(path: Path) -> bytes:
@ -64,6 +66,45 @@ def object_type_from_mode(mode: int) -> bytes:
return b"" return b""
def default_bsv_config_path() -> Path:
path = Path(os.environ.get("BSV_CONFIG", ""))
if path and path.is_absolute() and path.is_file():
return path
for path in user_config_dirs():
if path.is_file():
return path
return user_config_home() / "bsv/config"
def default_local_repository_path() -> Path:
return user_data_home() / "bsv"
def user_data_home() -> Path:
if platform.system() in ("Windows", "Darwin", "Java"):
raise NotImplemented(f"{platform.system()} support not implemented yet")
else: # Assume Unix
path = Path(os.environ.get("XDG_DATA_HOME", ""))
if path and path.is_absolute():
return path
return Path.home() / ".local/share"
def user_config_home() -> Path:
if platform.system() in ("Windows", "Darwin", "Java"):
raise NotImplemented(f"{platform.system()} support not implemented yet")
else: # Assume Unix
path = Path(os.environ.get("XDG_CONFIG_HOME", ""))
if path and path.is_absolute():
return path
return Path.home() / ".config"
def user_config_dirs() -> list[Path]:
if platform.system() in ("Windows", "Darwin", "Java"):
raise NotImplemented(f"{platform.system()} support not implemented yet")
else: # Assume Unix
paths = list(filter(Path.is_absolute, map(Path, (os.environ.get("XDG_CONFIG_DIRS") or "/etc/xdg").split(":"))))
return [user_config_home()] + paths
class Hash(ABC): class Hash(ABC):
name: str name: str
digest_size: int digest_size: int

7
tests/test_repository.py

@ -38,8 +38,9 @@ def tmp_dir():
@pytest.fixture @pytest.fixture
def repo(tmp_dir): def repo(tmp_dir):
return create_repository( return create_repository(
tmp_dir / "bsv", tmp_dir / "bsv.config",
"test_repo", "test_repo",
tmp_dir / "bsv_repo",
) )
@ -154,7 +155,7 @@ def test_add_tree(tmp_dir: Path, repo: Repository):
}, },
"Another test with long name and spaces and a bang !": b"Should works.\n", "Another test with long name and spaces and a bang !": b"Should works.\n",
"bsv_repo": { "bsv_repo": {
"bsv_config.toml": b"[bsv]\n", "bsv_repository.config": b"[bsv]\n",
}, },
} }
structure1 = { structure1 = {
@ -168,7 +169,7 @@ def test_add_tree(tmp_dir: Path, repo: Repository):
"new_file": b"whatever", "new_file": b"whatever",
"Another test with long name and spaces and a bang !": b"Should works.\n", "Another test with long name and spaces and a bang !": b"Should works.\n",
"bsv_repo": { "bsv_repo": {
"bsv_config.toml": b"[bsv]\n", "bsv_repository.config": b"[bsv]\n",
}, },
} }

Loading…
Cancel
Save