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
### v0.0.1 - MVP
### v0.0.1 - Simple CAS + insert/remove files manually
- [ ] `bsv init [<destination>] [--repo <local-repo-path>]` command to initialize bsv.
- [ ] `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 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.
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.
- [x] Simple CAS implementation (it's OK if it's naïve).
- [x] Content-based chunking to store files.
- [x] `bsv init` command to initialize bsv.
- [ ] `bsv info` print useful information bsv configuration.
- [ ] `bsv log` show the history of snapshots.
- [ ] `bsv show <digest>` show the object `digest`.
- [ ] `bsv ls <bsv-path>` list files in a bsv directory.
- [ ] `bsv mkdir <bsv-path>` create a directory in bsv directly.
- [ ] `bsv add <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 snapshot` capture a snapshot, i.e. to ensure that mapped files in the repositories match what is on the filesystem.
- [ ] `bsv add [-r] <fs-path> <bsv-path>` copy files from the filesystem to bsv.
- [ ] `bsv get [-r] <bsv-path> <fs-path>` copy files from bsv to 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`.
- [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 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).
- [ ] Support for symlinks.
### Later
- [ ] `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"]
dependencies = [
"fastcdc",
"rich",
"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 argparse import ArgumentParser
from os import getlogin
from pathlib import Path
import platform
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):
parser.add_argument(
"--name", "-n",
help = "Name of the repository. Default to system hostname.",
)
parser.add_argument(
"--interactive", "-i",
default = False,
action = "store_true",
help = "Prompt the user for configuration choices.",
)
parser.add_argument(
"destination",
"--local-repository", "-l",
type = Path,
default = default_local_repository_path(),
nargs = "?",
dest = "local_repository_path",
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)
def init(
repository_path: Path | None,
destination: Path | None = None,
name: str | None = None,
config_path: Path,
device_name: str,
local_repository_path: Path,
interactive: bool = False,
) -> int:
"""Initialize a new bsv repository.
@ -52,60 +57,62 @@ def init(
from datetime import datetime as DateTime
import tomlkit
if name is None:
name = platform.node()
from bsv.cli import get_console, get_error_console, prompt, prompt_confirmation
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:
# TODO: Choose a sensible system-dependent path.
destination = Path.cwd()
def make_device_name(value: str) -> str:
device_name = value.strip()
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:
name = input(f"Repository name: (default to {name})\n").strip() or name
destination = Path(input(f"Destination: (default to {destination})\n").strip()) or destination
if not destination.is_absolute():
destination = Path.cwd() / destination
if not name:
raise RuntimeError("repository name cannot be empty")
if not destination.parent.exists():
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")
config_path = prompt("Bsv configuration file", make_config_path, default=config_path)
device_name = prompt("Device name", make_device_name, default=device_name)
local_repository_path = prompt("Destination", make_local_repository_path, default=local_repository_path)
if not config_path.is_absolute():
config_path = config_path.resolve()
if not local_repository_path.is_absolute():
local_repository_path = local_repository_path.resolve()
try:
destination.mkdir(exist_ok=True)
except:
raise RuntimeError(f"failed to create destination directory {destination}")
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("name", name)
bsv_table.add(tomlkit.nl())
bsv_table.add(tomlkit.comment("Mapping between bsv tree and the actual filesystem."))
bsv_table.add("path_map", tomlkit.array())
cas_table = tomlkit.table()
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}")
check_config_path(config_path)
check_device_name(device_name)
check_local_repository_path(local_repository_path)
except ValueError as err:
get_error_console().print(err, style="bold red")
return 1
print("Bsv repository will be created with the following settings:", style="green")
print("")
print(f"\t[blue]:page_facing_up: Config path[/blue]: [bold yellow]{config_path}")
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}")
print("")
with stream:
tomlkit.dump(doc, stream)
if interactive:
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

36
src/bsv/main.py

@ -22,7 +22,9 @@ import sys
from textwrap import dedent
from bsv import __version__
from bsv.cli import get_error_console, init_consoles
from bsv.command import init_commands
from bsv.util import default_bsv_config_path
def make_parser(
@ -31,10 +33,20 @@ def make_parser(
) -> ArgumentParser:
parent_parser = ArgumentParser(add_help=False)
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,
dest = "config_path",
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(),
)
@ -68,16 +80,16 @@ def main(
)
arg_dict = vars(parser.parse_args(args or sys.argv[1:]))
repository_path: Path | None = arg_dict.pop("repository")
if repository_path is None and "BSV_REPOSITORY" in os.environ:
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
color = arg_dict.pop("color")
init_consoles(color=color)
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
from io import BytesIO
from pathlib import Path, PurePosixPath
import platform
import tomllib
from typing import TYPE_CHECKING, BinaryIO, Callable, Self
from typing import TYPE_CHECKING, Any, BinaryIO, Self
from fastcdc import fastcdc
import tomlkit
from bsv import __version__
from bsv.exception import ConfigError
@ -33,7 +31,7 @@ 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
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:
from bsv.tree_walker import TreeWalker
@ -45,8 +43,9 @@ DEFAULT_MAX_CHUNK_SIZE = 1 << 20
class Repository:
_path: Path
_name: str
_config_path: Path
_device_name: str
_local_repository_path: Path
_cas: SimpleCas
_min_chunk_size: int = DEFAULT_MIN_CHUNK_SIZE
@ -58,38 +57,36 @@ class Repository:
_context_depth: int = 0
def __init__(self, path: Path):
self._path = path
def __init__(self, config_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)
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(
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", []))
self._cas = make_cas(self._local_repository_path)
@property
def path(self) -> Path:
return self._path
@property
def config_file(self) -> Path:
return self.path / "bsv_config.toml"
def config_path(self) -> Path:
return self._config_path
@property
def name(self) -> str:
return self._name
def device_name(self) -> str:
return self._device_name
@property
def path_map(self) -> PathMap:
@ -257,9 +254,30 @@ class Repository:
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(
destination: Path,
name: str,
config_path: Path,
device_name: str,
local_repository_path: Path,
cas: str = "simple",
hash: str = "sha256",
min_chunk_size: int = DEFAULT_MIN_CHUNK_SIZE,
@ -269,55 +287,96 @@ def create_repository(
from datetime import datetime as DateTime
from os import getlogin
if not name:
raise RuntimeError("repository name cannot be empty")
if not destination.parent.exists():
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")
import tomlkit
check_config_path(config_path)
check_device_name(device_name)
check_local_repository_path(local_repository_path)
if config_path == default_bsv_config_path():
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:
destination.mkdir(exist_ok=True)
local_repository_path.mkdir(exist_ok=True)
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.add(tomlkit.comment("Name of the repository."))
bsv_table.add(tomlkit.comment("Ideally, this should be unique among all connected repositories."))
bsv_table.add("name", name)
bsv_table.add(tomlkit.comment("Name of the instance."))
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.comment("Mapping between bsv tree and the actual filesystem."))
bsv_table.add("path_map", tomlkit.array())
bsv_table.add("cas", cas)
bsv_table.add("hash", hash)
bsv_table.add(tomlkit.comment("Path to the local repository."))
bsv_table.add("local_repository", str(local_repository_path))
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("avg_chunk_size", avg_chunk_size)
bsv_table.add("max_chunk_size", max_chunk_size)
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)
bsv_table.add(tomlkit.nl())
bsv_table.add(tomlkit.comment("Mapping between bsv tree and the local filesystem."))
bsv_table.add("path_map", tomlkit.array())
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:
stream = config_path.open("w", encoding="utf-8")
bsv_stream = config_path.open("w", encoding="utf-8")
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:
if cas_name == "simple":
return SimpleCas(path, hash_factory)
raise ConfigError(f"unknown cas name {cas_name}")
type = get("type")
hash_factory = lambda: hashlib.new(get("hash"))
if type == "simple":
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 datetime import UTC, datetime as DateTime, timedelta as TimeDelta
import os
from pathlib import Path
import platform
import stat
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:
return (path / "bsv_config.toml").is_file()
return (path / "bsv_repository.config").is_file()
def object_type_from_path(path: Path) -> bytes:
@ -64,6 +66,45 @@ def object_type_from_mode(mode: int) -> bytes:
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):
name: str
digest_size: int

7
tests/test_repository.py

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

Loading…
Cancel
Save