Inital commit with basic commands.

This commit is contained in:
2023-11-05 02:31:32 +01:00
commit bdbd65ae28
12 changed files with 1151 additions and 0 deletions

18
src/bsv/__init__.py Normal file
View File

@@ -0,0 +1,18 @@
# 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 bsv._version import __version__, __version_tuple__

21
src/bsv/__main__.py Normal file
View File

@@ -0,0 +1,21 @@
# 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 bsv.main import main
exit(main())

View File

@@ -0,0 +1,64 @@
# 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 argparse import ArgumentParser
from dataclasses import dataclass
from textwrap import dedent
from typing import Callable
@dataclass
class Command:
init_parser: Callable[[ArgumentParser], None]
function: Callable[..., int]
commands: dict[str, Command] = {}
def register_command(
name: str,
init_parser: Callable[[ArgumentParser], None],
function: Callable[..., int],
):
commands[name] = Command(init_parser, function)
def command(init_parser: Callable[[ArgumentParser], None]=lambda p: None):
def decorator(fn: Callable[..., int]):
register_command(fn.__name__, init_parser, fn)
return fn
return decorator
def init_commands(parser: ArgumentParser, parent_parsers: list[ArgumentParser]=[]):
import bsv.command.info
import bsv.command.init
subparsers = parser.add_subparsers(
metavar = "COMMAND",
help = "Sub-command to run.",
required = True,
)
for name, command in commands.items():
subparser = subparsers.add_parser(
name,
parents = parent_parsers,
help = dedent(command.function.__doc__ or "").strip()
)
subparser.set_defaults(
command = command.function,
)
command.init_parser(subparser)

59
src/bsv/command/info.py Normal file
View File

@@ -0,0 +1,59 @@
# 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 argparse import ArgumentParser
from pathlib import Path
from bsv import __version__
from bsv.command import command
from bsv.repository import Repository
def init_parser(parser: ArgumentParser):
parser.add_argument(
"--verbose", "-v",
action = "count",
default = 0,
help = "Verbosity level. Specify several times to increase verbosity.",
dest = "verbosity",
)
@command(init_parser)
def info(repository_path: Path | None, verbosity: int=0) -> int:
"""Print informations about bsv: config file used, known repository, file mapping...
"""
print(f"bsv v{__version__}")
if repository_path is None:
print("Repository path not found. Bsv is likely not setup on this device.")
return 0
else:
print(f"Repository path: {repository_path}")
repo = Repository(repository_path)
print(f"Repository name: {repo.name}")
if repo.path_map:
print("Path map: (bsv path <-> filesystem path)")
for pair in sorted(repo.path_map):
print(f" {pair.bsv} <-> {pair.fs}")
else:
print("Path map is empty.")
return 0

105
src/bsv/command/init.py Normal file
View File

@@ -0,0 +1,105 @@
# 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 argparse import ArgumentParser
from os import getlogin
from pathlib import Path
import platform
from bsv.command import command
def init_parser(parser: ArgumentParser):
parser.add_argument(
"--name", "-d",
help = "Name of the repository. Default to system hostname.",
)
parser.add_argument(
"--interactive", "-i",
action = "store_true",
help = "Prompt the user for configuration choices.",
)
parser.add_argument(
"destination",
type = Path,
nargs = "?",
help = "Path to a non-existing or empty folder where bsv data will be stored.",
)
@command(init_parser)
def init(
repository_path: Path | None,
destination: Path | None = None,
name: str | None = None,
interactive: bool = False,
) -> int:
"""Initialize a new bsv repository.
"""
from datetime import datetime as DateTime
import tomlkit
if name is None:
name = platform.node()
if destination is None:
# TODO: Choose a sensible system-dependent path.
destination = Path.cwd()
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")
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())
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)
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)
return 0

83
src/bsv/main.py Normal file
View File

@@ -0,0 +1,83 @@
# 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 argparse import ArgumentParser
import os.path
from pathlib import Path
import sys
from textwrap import dedent
from bsv import __version__
from bsv.command import init_commands
def make_parser(
prog: str = "",
exit_on_error: bool = True,
) -> ArgumentParser:
parent_parser = ArgumentParser(add_help=False)
parent_parser.add_argument(
"--repository",
type = Path,
help = dedent("""
Bsv repository path. Overides default paths and BSV_REPOSITORY environment variable.
""").strip(),
)
parser = ArgumentParser(
prog = prog or os.path.basename(sys.argv[0]),
description = dedent("""
bsv - Backup, Synchronization, Versioning.
""").strip(),
parents = [parent_parser],
exit_on_error = exit_on_error,
)
parser.add_argument(
"--version",
action = "version",
version = f"bsv version {__version__}",
)
init_commands(parser, [parent_parser])
return parser
def main(
args: list[str] | None = None,
prog: str = "",
exit_on_error: bool = True,
) -> int:
parser = make_parser(
prog = prog or os.path.basename(sys.argv[0]),
exit_on_error = exit_on_error,
)
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
command = arg_dict.pop("command")
return command(repository_path=repository_path, **arg_dict)

86
src/bsv/repository.py Normal file
View File

@@ -0,0 +1,86 @@
# 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 pathlib import Path, PurePosixPath
import platform
import tomllib
from typing import Any
from bsv import __version__
class Repository:
_path: Path
_name: str
_path_map: list[PathPair]
# _remotes: list[object]
def __init__(self, path: Path):
self._path = path
with self.config_file.open("rb") as stream:
config = tomllib.load(stream)
bsv = config.get("bsv", {})
self._name = bsv.get("name") or platform.node()
self._path_map = [
PathPair.from_obj(pair)
for pair in bsv.get("path_map", [])
]
@property
def path(self) -> Path:
return self._path
@property
def config_file(self) -> Path:
return self.path / "bsv_config.toml"
@property
def name(self) -> str:
return self._name
@property
def path_map(self) -> list[PathPair]:
return list(self._path_map)
class PathPair:
bsv: PurePosixPath
fs: Path
def __init__(self, bsv: PurePosixPath, fs: Path):
self.bsv = bsv
self.fs = fs
@classmethod
def from_obj(cls, obj: dict[str, Any]) -> PathPair:
bsv = PurePosixPath(obj["bsv"])
fs = Path(obj["fs"])
if not bsv.is_absolute() or not fs.is_absolute():
raise ValueError("paths in path_map must be absolute")
return cls(
bsv = obj["bsv"],
fs = obj["fs"],
)
def __lt__(self, rhs: PathPair) -> bool:
return self.bsv < rhs.bsv

View File

@@ -0,0 +1,16 @@
# 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