bsv init command.
This commit is contained in:
67
README.md
67
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.
|
||||
|
||||
### 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 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 tag <name> [<snapshot>] [-m <message>]` set/update a tag (an alias to a specific snapshot).
|
||||
- [ ] `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 ?
|
||||
|
||||
@@ -13,6 +13,7 @@ classifiers = [
|
||||
dynamic = ["version"]
|
||||
dependencies = [
|
||||
"fastcdc",
|
||||
"rich",
|
||||
"tomlkit",
|
||||
]
|
||||
|
||||
|
||||
126
src/bsv/cli.py
Normal file
126
src/bsv/cli.py
Normal file
@@ -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"
|
||||
@@ -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
|
||||
|
||||
if destination is None:
|
||||
# TODO: Choose a sensible system-dependent path.
|
||||
destination = Path.cwd()
|
||||
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
|
||||
|
||||
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
|
||||
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 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")
|
||||
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}")
|
||||
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
|
||||
|
||||
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())
|
||||
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("")
|
||||
|
||||
cas_table = tomlkit.table()
|
||||
cas_table.add("type", "simple")
|
||||
cas_table.add("hash", "sha256")
|
||||
if interactive:
|
||||
if not prompt_confirmation("Create repository ?"):
|
||||
return 1
|
||||
|
||||
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:
|
||||
tomlkit.dump(doc, stream)
|
||||
create_repository(
|
||||
config_path = config_path,
|
||||
device_name = device_name,
|
||||
local_repository_path = local_repository_path,
|
||||
)
|
||||
|
||||
return 0
|
||||
|
||||
@@ -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",
|
||||
type = Path,
|
||||
"--color",
|
||||
default = "auto",
|
||||
choices = ("always", "auto", "never"),
|
||||
help = dedent("""
|
||||
Bsv repository path. Overides default paths and BSV_REPOSITORY environment variable.
|
||||
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 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
|
||||
|
||||
@@ -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._cas = make_cas(self._local_repository_path)
|
||||
|
||||
self._path_map = PathMap.from_obj(bsv.get("path_map", []))
|
||||
|
||||
@property
|
||||
def path(self) -> Path:
|
||||
return self._path
|
||||
def config_path(self) -> Path:
|
||||
return self._config_path
|
||||
|
||||
@property
|
||||
def config_file(self) -> Path:
|
||||
return self.path / "bsv_config.toml"
|
||||
|
||||
@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 stream:
|
||||
tomlkit.dump(doc, stream)
|
||||
with bsv_stream:
|
||||
tomlkit.dump(bsv_config, bsv_stream)
|
||||
|
||||
return Repository(destination)
|
||||
with cas_stream:
|
||||
tomlkit.dump(cas_config, cas_stream)
|
||||
|
||||
repo = Repository(config_path)
|
||||
|
||||
return repo
|
||||
|
||||
|
||||
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}")
|
||||
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
|
||||
|
||||
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}")
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user