1
0
Fork 0
mirror of https://github.com/borgbackup/borg.git synced 2025-01-01 12:45:34 +00:00

Merge pull request #7615 from ThomasWaldmann/serve-socket2

implement unix domain socket support
This commit is contained in:
TW 2023-06-06 23:21:15 +02:00 committed by GitHub
commit cad57e70c3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 364 additions and 119 deletions

View file

@ -150,6 +150,10 @@ Directories and files:
`XDG env var`_ ``XDG_DATA_HOME`` is set, then ``$XDG_DATA_HOME/borg`` is being used instead.
This directory contains all borg data directories, see the FAQ
for a security advisory about the data in this directory: :ref:`home_data_borg`
BORG_RUNTIME_DIR
Defaults to ``$BORG_BASE_DIR/.cache/borg``. If ``BORG_BASE_DIR`` is not explicitly set while
`XDG env var`_ ``XDG_RUNTIME_DIR`` is set, then ``$XDG_RUNTIME_DIR/borg`` is being used instead.
This directory contains borg runtime files, like e.g. the socket file.
BORG_SECURITY_DIR
Defaults to ``$BORG_DATA_DIR/security``.
This directory contains security relevant data.

View file

@ -29,7 +29,7 @@
def get_repository(location, *, create, exclusive, lock_wait, lock, append_only, make_parent_dirs, storage_quota, args):
if location.proto == "ssh":
if location.proto in ("ssh", "socket"):
repository = RemoteRepository(
location,
create=create,
@ -573,6 +573,16 @@ def define_common_options(add_common_option):
action=Highlander,
help="Use this command to connect to the 'borg serve' process (default: 'ssh')",
)
add_common_option(
"--socket",
metavar="PATH",
dest="use_socket",
default=False,
const=True,
nargs="?",
action=Highlander,
help="Use UNIX DOMAIN (IPC) socket at PATH for client/server communication with socket: protocol.",
)
add_common_option(
"-r",
"--repo",

View file

@ -19,6 +19,7 @@ def do_serve(self, args):
restrict_to_repositories=args.restrict_to_repositories,
append_only=args.append_only,
storage_quota=args.storage_quota,
use_socket=args.use_socket,
).serve()
return EXIT_SUCCESS
@ -27,7 +28,17 @@ def build_parser_serve(self, subparsers, common_parser, mid_common_parser):
serve_epilog = process_epilog(
"""
This command starts a repository server process. This command is usually not used manually.
This command starts a repository server process.
borg serve can currently support:
- Getting automatically started via ssh when the borg client uses a ssh://...
remote repository. In this mode, `borg serve` will live until that ssh connection
gets terminated.
- Getting started by some other means (not by the borg client) as a long-running socket
server to be used for borg clients using a socket://... repository (see the `--socket`
option if you do not want to use the default path for the socket and pid file).
"""
)
subparser = subparsers.add_parser(

View file

@ -11,7 +11,8 @@
from .checks import check_extension_modules, check_python
from .datastruct import StableDict, Buffer, EfficientCollectionQueue
from .errors import Error, ErrorWithTraceback, IntegrityError, DecompressionError
from .fs import ensure_dir, get_security_dir, get_keys_dir, get_base_dir, join_base_dir, get_cache_dir, get_config_dir
from .fs import ensure_dir, join_base_dir, get_socket_filename
from .fs import get_security_dir, get_keys_dir, get_base_dir, get_cache_dir, get_config_dir, get_runtime_dir
from .fs import dir_is_tagged, dir_is_cachedir, make_path_safe, scandir_inorder
from .fs import secure_erase, safe_unlink, dash_open, os_open, os_stat, umount
from .fs import O_, flags_root, flags_dir, flags_special_follow, flags_special, flags_base, flags_normal, flags_noatime

View file

@ -106,6 +106,22 @@ def get_data_dir(*, legacy=False):
return data_dir
def get_runtime_dir(*, legacy=False):
"""Determine where to store runtime files, like sockets, PID files, ..."""
assert legacy is False, "there is no legacy variant of the borg runtime dir"
runtime_dir = os.environ.get(
"BORG_RUNTIME_DIR", join_base_dir(".cache", "borg", legacy=legacy) or platformdirs.user_runtime_dir("borg")
)
# Create path if it doesn't exist yet
ensure_dir(runtime_dir)
return runtime_dir
def get_socket_filename():
return os.path.join(get_runtime_dir(), "borg.sock")
def get_cache_dir(*, legacy=False):
"""Determine where to repository keys and cache"""

View file

@ -390,7 +390,7 @@ class Location:
# path must not contain :: (it ends at :: or string end), but may contain single colons.
# to avoid ambiguities with other regexes, it must also not start with ":" nor with "//" nor with "ssh://".
local_path_re = r"""
(?!(:|//|ssh://)) # not starting with ":" or // or ssh://
(?!(:|//|ssh://|socket://)) # not starting with ":" or // or ssh:// or socket://
(?P<path>([^:]|(:(?!:)))+) # any chars, but no "::"
"""
@ -429,6 +429,14 @@ class Location:
re.VERBOSE,
) # path
socket_re = re.compile(
r"""
(?P<proto>socket):// # socket://
"""
+ abs_path_re,
re.VERBOSE,
) # path
file_re = re.compile(
r"""
(?P<proto>file):// # file://
@ -493,6 +501,11 @@ def normpath_special(p):
self.path = normpath_special(m.group("path"))
return True
m = self.file_re.match(text)
if m:
self.proto = m.group("proto")
self.path = normpath_special(m.group("path"))
return True
m = self.socket_re.match(text)
if m:
self.proto = m.group("proto")
self.path = normpath_special(m.group("path"))
@ -516,7 +529,7 @@ def __str__(self):
def to_key_filename(self):
name = re.sub(r"[^\w]", "_", self.path).strip("_")
if self.proto != "file":
if self.proto not in ("file", "socket"):
name = re.sub(r"[^\w]", "_", self.host) + "__" + name
if len(name) > 100:
# Limit file names to some reasonable length. Most file systems
@ -535,7 +548,7 @@ def host(self):
return self._host.lstrip("[").rstrip("]")
def canonical_path(self):
if self.proto == "file":
if self.proto in ("file", "socket"):
return self.path
else:
if self.path and self.path.startswith("~"):

View file

@ -28,6 +28,24 @@
* what is output on INFO level is additionally controlled by commandline
flags
Logging setup is a bit complicated in borg, as it needs to work under misc. conditions:
- purely local, not client/server (easy)
- client/server: RemoteRepository ("borg serve" process) writes log records into a global
queue, which is then sent to the client side by the main serve loop (via the RPC protocol,
either over ssh stdout, more directly via process stdout without ssh [used in the tests]
or via a socket. On the client side, the log records are fed into the clientside logging
system. When remote_repo.close() is called, server side must send all queued log records
via the RPC channel before returning the close() call's return value (as the client will
then shut down the connection).
- progress output is always given as json to the logger (including the plain text inside
the json), but then formatted by the logging system's formatter as either plain text or
json depending on the cli args given (--log-json?).
- tests: potentially running in parallel via pytest-xdist, capturing borg output into a
given stream.
- logging might be short-lived (e.g. when invoking a single borg command via the cli)
or long-lived (e.g. borg serve --socket or when running the tests)
- logging is global and exists only once per process.
"""
import inspect
@ -115,10 +133,14 @@ def remove_handlers(logger):
logger.removeHandler(handler)
def teardown_logging():
global configured
logging.shutdown()
configured = False
def flush_logging():
# make sure all log output is flushed,
# this is especially important for the "borg serve" RemoteRepository logging:
# all log output needs to be sent via the ssh / socket connection before closing it.
for logger_name in "borg.output.progress", "":
logger = logging.getLogger(logger_name)
for handler in logger.handlers:
handler.flush()
def setup_logging(

View file

@ -1,3 +1,4 @@
import atexit
import errno
import functools
import inspect
@ -7,6 +8,7 @@
import select
import shlex
import shutil
import socket
import struct
import sys
import tempfile
@ -27,6 +29,7 @@
from .helpers import format_file_size
from .helpers import safe_unlink
from .helpers import prepare_subprocess_env, ignore_sigint
from .helpers import get_socket_filename
from .logger import create_logger, borg_serve_log_queue
from .helpers import msgpack
from .repository import Repository
@ -136,7 +139,7 @@ class RepositoryServer: # pragma: no cover
"inject_exception",
)
def __init__(self, restrict_to_paths, restrict_to_repositories, append_only, storage_quota):
def __init__(self, restrict_to_paths, restrict_to_repositories, append_only, storage_quota, use_socket):
self.repository = None
self.restrict_to_paths = restrict_to_paths
self.restrict_to_repositories = restrict_to_repositories
@ -147,6 +150,12 @@ def __init__(self, restrict_to_paths, restrict_to_repositories, append_only, sto
self.append_only = append_only
self.storage_quota = storage_quota
self.client_version = None # we update this after client sends version information
if use_socket is False:
self.socket_path = None
elif use_socket is True: # --socket
self.socket_path = get_socket_filename()
else: # --socket=/some/path
self.socket_path = use_socket
def filter_args(self, f, kwargs):
"""Remove unknown named parameters from call, because client did (implicitly) say it's ok."""
@ -165,95 +174,133 @@ def send_queued_log(self):
os_write(self.stdout_fd, msg)
def serve(self):
self.stdin_fd = sys.stdin.fileno()
self.stdout_fd = sys.stdout.fileno()
os.set_blocking(self.stdin_fd, False)
os.set_blocking(self.stdout_fd, True)
unpacker = get_limited_unpacker("server")
shutdown_serve = False
while True:
# before processing any new RPCs, send out all pending log output
self.send_queued_log()
def inner_serve():
os.set_blocking(self.stdin_fd, False)
assert not os.get_blocking(self.stdin_fd)
os.set_blocking(self.stdout_fd, True)
assert os.get_blocking(self.stdout_fd)
if shutdown_serve:
# shutdown wanted! get out of here after sending all log output.
if self.repository is not None:
self.repository.close()
return
unpacker = get_limited_unpacker("server")
shutdown_serve = False
while True:
# before processing any new RPCs, send out all pending log output
self.send_queued_log()
# process new RPCs
r, w, es = select.select([self.stdin_fd], [], [], 10)
if r:
data = os.read(self.stdin_fd, BUFSIZE)
if not data:
if shutdown_serve:
# shutdown wanted! get out of here after sending all log output.
assert self.repository is None
return
# process new RPCs
r, w, es = select.select([self.stdin_fd], [], [], 10)
if r:
data = os.read(self.stdin_fd, BUFSIZE)
if not data:
shutdown_serve = True
continue
unpacker.feed(data)
for unpacked in unpacker:
if isinstance(unpacked, dict):
msgid = unpacked[MSGID]
method = unpacked[MSG]
args = unpacked[ARGS]
else:
if self.repository is not None:
self.repository.close()
raise UnexpectedRPCDataFormatFromClient(__version__)
try:
if method not in self.rpc_methods:
raise InvalidRPCMethod(method)
try:
f = getattr(self, method)
except AttributeError:
f = getattr(self.repository, method)
args = self.filter_args(f, args)
res = f(**args)
except BaseException as e:
ex_short = traceback.format_exception_only(e.__class__, e)
ex_full = traceback.format_exception(*sys.exc_info())
ex_trace = True
if isinstance(e, Error):
ex_short = [e.get_message()]
ex_trace = e.traceback
if isinstance(e, (Repository.DoesNotExist, Repository.AlreadyExists, PathNotAllowed)):
# These exceptions are reconstructed on the client end in RemoteRepository.call_many(),
# and will be handled just like locally raised exceptions. Suppress the remote traceback
# for these, except ErrorWithTraceback, which should always display a traceback.
pass
else:
logging.debug("\n".join(ex_full))
sys_info = sysinfo()
try:
msg = msgpack.packb(
{
MSGID: msgid,
"exception_class": e.__class__.__name__,
"exception_args": e.args,
"exception_full": ex_full,
"exception_short": ex_short,
"exception_trace": ex_trace,
"sysinfo": sys_info,
}
)
except TypeError:
msg = msgpack.packb(
{
MSGID: msgid,
"exception_class": e.__class__.__name__,
"exception_args": [
x if isinstance(x, (str, bytes, int)) else None for x in e.args
],
"exception_full": ex_full,
"exception_short": ex_short,
"exception_trace": ex_trace,
"sysinfo": sys_info,
}
)
os_write(self.stdout_fd, msg)
else:
os_write(self.stdout_fd, msgpack.packb({MSGID: msgid, RESULT: res}))
if es:
shutdown_serve = True
continue
unpacker.feed(data)
for unpacked in unpacker:
if isinstance(unpacked, dict):
msgid = unpacked[MSGID]
method = unpacked[MSG]
args = unpacked[ARGS]
else:
if self.repository is not None:
self.repository.close()
raise UnexpectedRPCDataFormatFromClient(__version__)
try:
if method not in self.rpc_methods:
raise InvalidRPCMethod(method)
try:
f = getattr(self, method)
except AttributeError:
f = getattr(self.repository, method)
args = self.filter_args(f, args)
res = f(**args)
except BaseException as e:
ex_short = traceback.format_exception_only(e.__class__, e)
ex_full = traceback.format_exception(*sys.exc_info())
ex_trace = True
if isinstance(e, Error):
ex_short = [e.get_message()]
ex_trace = e.traceback
if isinstance(e, (Repository.DoesNotExist, Repository.AlreadyExists, PathNotAllowed)):
# These exceptions are reconstructed on the client end in RemoteRepository.call_many(),
# and will be handled just like locally raised exceptions. Suppress the remote traceback
# for these, except ErrorWithTraceback, which should always display a traceback.
pass
else:
logging.debug("\n".join(ex_full))
sys_info = sysinfo()
try:
msg = msgpack.packb(
{
MSGID: msgid,
"exception_class": e.__class__.__name__,
"exception_args": e.args,
"exception_full": ex_full,
"exception_short": ex_short,
"exception_trace": ex_trace,
"sysinfo": sys_info,
}
)
except TypeError:
msg = msgpack.packb(
{
MSGID: msgid,
"exception_class": e.__class__.__name__,
"exception_args": [x if isinstance(x, (str, bytes, int)) else None for x in e.args],
"exception_full": ex_full,
"exception_short": ex_short,
"exception_trace": ex_trace,
"sysinfo": sys_info,
}
)
if self.socket_path: # server for socket:// connections
try:
# remove any left-over socket file
os.unlink(self.socket_path)
except OSError:
if os.path.exists(self.socket_path):
raise
sock_dir = os.path.dirname(self.socket_path)
os.makedirs(sock_dir, exist_ok=True)
if self.socket_path.endswith(".sock"):
pid_file = self.socket_path.replace(".sock", ".pid")
else:
pid_file = self.socket_path + ".pid"
pid = os.getpid()
with open(pid_file, "w") as f:
f.write(str(pid))
atexit.register(functools.partial(os.remove, pid_file))
atexit.register(functools.partial(os.remove, self.socket_path))
sock = socket.socket(family=socket.AF_UNIX, type=socket.SOCK_STREAM)
sock.bind(self.socket_path) # this creates the socket file in the fs
sock.listen(0) # no backlog
os.chmod(self.socket_path, mode=0o0770) # group members may use the socket, too.
print(f"borg serve: PID {pid}, listening on socket {self.socket_path} ...", file=sys.stderr)
os_write(self.stdout_fd, msg)
else:
os_write(self.stdout_fd, msgpack.packb({MSGID: msgid, RESULT: res}))
if es:
shutdown_serve = True
continue
while True:
connection, client_address = sock.accept()
print(f"Accepted a connection on socket {self.socket_path} ...", file=sys.stderr)
self.stdin_fd = connection.makefile("rb").fileno()
self.stdout_fd = connection.makefile("wb").fileno()
inner_serve()
print(f"Finished with connection on socket {self.socket_path} .", file=sys.stderr)
else: # server for one ssh:// connection
self.stdin_fd = sys.stdin.fileno()
self.stdout_fd = sys.stdout.fileno()
inner_serve()
def negotiate(self, client_data):
if isinstance(client_data, dict):
@ -318,7 +365,8 @@ def open(
def close(self):
if self.repository is not None:
self.repository.__exit__(None, None, None)
borg.logger.teardown_logging()
self.repository = None
borg.logger.flush_logging()
self.send_queued_log()
def inject_exception(self, kind):
@ -489,6 +537,7 @@ def __init__(
self.rx_bytes = 0
self.tx_bytes = 0
self.to_send = EfficientCollectionQueue(1024 * 1024, bytes)
self.stdin_fd = self.stdout_fd = self.stderr_fd = None
self.stderr_received = b"" # incomplete stderr line bytes received (no \n yet)
self.chunkid_to_msgids = {}
self.ignore_responses = set()
@ -499,27 +548,54 @@ def __init__(
self.upload_buffer_size_limit = args.upload_buffer * 1024 * 1024 if args and args.upload_buffer else 0
self.unpacker = get_limited_unpacker("client")
self.server_version = None # we update this after server sends its version
self.p = None
self.p = self.sock = None
self._args = args
testing = location.host == "__testsuite__"
# when testing, we invoke and talk to a borg process directly (no ssh).
# when not testing, we invoke the system-installed ssh binary to talk to a remote borg.
env = prepare_subprocess_env(system=not testing)
borg_cmd = self.borg_cmd(args, testing)
if not testing:
borg_cmd = self.ssh_cmd(location) + borg_cmd
logger.debug("SSH command line: %s", borg_cmd)
# we do not want the ssh getting killed by Ctrl-C/SIGINT because it is needed for clean shutdown of borg.
# borg's SIGINT handler tries to write a checkpoint and requires the remote repo connection.
self.p = Popen(borg_cmd, bufsize=0, stdin=PIPE, stdout=PIPE, stderr=PIPE, env=env, preexec_fn=ignore_sigint)
self.stdin_fd = self.p.stdin.fileno()
self.stdout_fd = self.p.stdout.fileno()
self.stderr_fd = self.p.stderr.fileno()
if self.location.proto == "ssh":
testing = location.host == "__testsuite__"
# when testing, we invoke and talk to a borg process directly (no ssh).
# when not testing, we invoke the system-installed ssh binary to talk to a remote borg.
env = prepare_subprocess_env(system=not testing)
borg_cmd = self.borg_cmd(args, testing)
if not testing:
borg_cmd = self.ssh_cmd(location) + borg_cmd
logger.debug("SSH command line: %s", borg_cmd)
# we do not want the ssh getting killed by Ctrl-C/SIGINT because it is needed for clean shutdown of borg.
# borg's SIGINT handler tries to write a checkpoint and requires the remote repo connection.
self.p = Popen(borg_cmd, bufsize=0, stdin=PIPE, stdout=PIPE, stderr=PIPE, env=env, preexec_fn=ignore_sigint)
self.stdin_fd = self.p.stdin.fileno()
self.stdout_fd = self.p.stdout.fileno()
self.stderr_fd = self.p.stderr.fileno()
self.r_fds = [self.stdout_fd, self.stderr_fd]
self.x_fds = [self.stdin_fd, self.stdout_fd, self.stderr_fd]
elif self.location.proto == "socket":
if args.use_socket is False or args.use_socket is True: # nothing or --socket
socket_path = get_socket_filename()
else: # --socket=/some/path
socket_path = args.use_socket
self.sock = socket.socket(family=socket.AF_UNIX, type=socket.SOCK_STREAM)
try:
self.sock.connect(socket_path) # note: socket_path length is rather limited.
except FileNotFoundError:
self.sock = None
raise Error(f"The socket file {socket_path} does not exist.")
except ConnectionRefusedError:
self.sock = None
raise Error(f"There is no borg serve running for the socket file {socket_path}.")
self.stdin_fd = self.sock.makefile("wb").fileno()
self.stdout_fd = self.sock.makefile("rb").fileno()
self.stderr_fd = None
self.r_fds = [self.stdout_fd]
self.x_fds = [self.stdin_fd, self.stdout_fd]
else:
raise Error(f"Unsupported protocol {location.proto}")
os.set_blocking(self.stdin_fd, False)
assert not os.get_blocking(self.stdin_fd)
os.set_blocking(self.stdout_fd, False)
os.set_blocking(self.stderr_fd, False)
self.r_fds = [self.stdout_fd, self.stderr_fd]
self.x_fds = [self.stdin_fd, self.stdout_fd, self.stderr_fd]
assert not os.get_blocking(self.stdout_fd)
if self.stderr_fd is not None:
os.set_blocking(self.stderr_fd, False)
assert not os.get_blocking(self.stderr_fd)
try:
try:
@ -551,7 +627,7 @@ def __init__(
def __del__(self):
if len(self.responses):
logging.debug("still %d cached responses left in RemoteRepository" % (len(self.responses),))
if self.p:
if self.p or self.sock:
self.close()
assert False, "cleanup happened in Repository.__del__"
@ -906,12 +982,21 @@ def break_lock(self):
"""actual remoting is done via self.call in the @api decorator"""
def close(self):
self.call("close", {}, wait=True)
if self.p or self.sock:
self.call("close", {}, wait=True)
if self.p:
self.p.stdin.close()
self.p.stdout.close()
self.p.wait()
self.p = None
if self.sock:
try:
self.sock.shutdown(socket.SHUT_RDWR)
except OSError as e:
if e.errno != errno.ENOTCONN:
raise
self.sock.close()
self.sock = None
def async_response(self, wait=True):
for resp in self.call_many("async_responses", calls=[], wait=True, async_wait=wait):

View file

@ -21,7 +21,7 @@
from ...helpers import Location
from ...helpers import EXIT_SUCCESS
from ...helpers import bin_to_hex
from ...logger import teardown_logging
from ...logger import flush_logging
from ...manifest import Manifest
from ...remote import RemoteRepository
from ...repository import Repository
@ -83,9 +83,9 @@ def exec_cmd(*args, archiver=None, fork=False, exe=None, input=b"", binary_outpu
output_text.flush()
return e.code, output.getvalue() if binary_output else output.getvalue().decode()
try:
ret = archiver.run(args)
ret = archiver.run(args) # calls setup_logging internally
finally:
teardown_logging() # usually done via atexit, but we do not exit here
flush_logging() # usually done via atexit, but we do not exit here
output_text.flush()
return ret, output.getvalue() if binary_output else output.getvalue().decode()
finally:

View file

@ -0,0 +1,50 @@
import os
import subprocess
import tempfile
import time
import pytest
import platformdirs
from . import exec_cmd
from ...platformflags import is_win32
from ...helpers import get_runtime_dir
def have_a_short_runtime_dir(mp):
# under pytest, we use BORG_BASE_DIR to keep stuff away from the user's normal borg dirs.
# this leads to a very long get_runtime_dir() path - too long for a socket file!
# thus, we override that again via BORG_RUNTIME_DIR to get a shorter path.
mp.setenv("BORG_RUNTIME_DIR", os.path.join(platformdirs.user_runtime_dir(), "pytest"))
@pytest.fixture
def serve_socket(monkeypatch):
have_a_short_runtime_dir(monkeypatch)
# use a random unique socket filename, so tests can run in parallel.
socket_file = tempfile.mktemp(suffix=".sock", prefix="borg-", dir=get_runtime_dir())
with subprocess.Popen(["borg", "serve", f"--socket={socket_file}"]) as p:
while not os.path.exists(socket_file):
time.sleep(0.01) # wait until socket server has started
yield socket_file
p.terminate()
@pytest.mark.skipif(is_win32, reason="hangs on win32")
def test_with_socket(serve_socket, tmpdir, monkeypatch):
have_a_short_runtime_dir(monkeypatch)
repo_path = str(tmpdir.join("repo"))
ret, output = exec_cmd(f"--socket={serve_socket}", f"--repo=socket://{repo_path}", "rcreate", "--encryption=none")
assert ret == 0
ret, output = exec_cmd(f"--socket={serve_socket}", f"--repo=socket://{repo_path}", "rinfo")
assert ret == 0
assert "Repository ID: " in output
monkeypatch.setenv("BORG_DELETE_I_KNOW_WHAT_I_AM_DOING", "YES")
ret, output = exec_cmd(f"--socket={serve_socket}", f"--repo=socket://{repo_path}", "rdelete")
assert ret == 0
@pytest.mark.skipif(is_win32, reason="hangs on win32")
def test_socket_permissions(serve_socket):
st = os.stat(serve_socket)
assert st.st_mode & 0o0777 == 0o0770 # user and group are permitted to use the socket

View file

@ -27,7 +27,7 @@
)
from ..helpers import make_path_safe, clean_lines
from ..helpers import interval
from ..helpers import get_base_dir, get_cache_dir, get_keys_dir, get_security_dir, get_config_dir
from ..helpers import get_base_dir, get_cache_dir, get_keys_dir, get_security_dir, get_config_dir, get_runtime_dir
from ..helpers import is_slow_msgpack
from ..helpers import msgpack
from ..helpers import yes, TRUISH, FALSISH, DEFAULTISH
@ -184,6 +184,14 @@ def test_ssh(self, monkeypatch, keys_dir):
== "Location(proto='ssh', user='user', host='2a02:0001:0002:0003:0004:0005:0006:0007', port=1234, path='/some/path')"
)
def test_socket(self, monkeypatch, keys_dir):
monkeypatch.delenv("BORG_REPO", raising=False)
assert (
repr(Location("socket:///repo/path"))
== "Location(proto='socket', user=None, host=None, port=None, path='/repo/path')"
)
assert Location("socket:///some/path").to_key_filename() == keys_dir + "some_path"
def test_file(self, monkeypatch, keys_dir):
monkeypatch.delenv("BORG_REPO", raising=False)
assert (
@ -275,6 +283,7 @@ def test_canonical_path(self, monkeypatch):
"file://some/path",
"host:some/path",
"host:~user/some/path",
"socket:///some/path",
"ssh://host/some/path",
"ssh://user@host:1234/some/path",
]
@ -752,6 +761,30 @@ def test_get_security_dir(monkeypatch):
assert get_security_dir() == "/var/tmp"
def test_get_runtime_dir(monkeypatch):
"""test that get_runtime_dir respects environment"""
monkeypatch.delenv("BORG_BASE_DIR", raising=False)
home_dir = os.path.expanduser("~")
if is_win32:
monkeypatch.delenv("BORG_RUNTIME_DIR", raising=False)
assert get_runtime_dir() == os.path.join(home_dir, "AppData", "Local", "Temp", "borg", "borg")
monkeypatch.setenv("BORG_RUNTIME_DIR", home_dir)
assert get_runtime_dir() == home_dir
elif is_darwin:
monkeypatch.delenv("BORG_RUNTIME_DIR", raising=False)
assert get_runtime_dir() == os.path.join(home_dir, "Library", "Caches", "TemporaryItems", "borg")
monkeypatch.setenv("BORG_RUNTIME_DIR", "/var/tmp")
assert get_runtime_dir() == "/var/tmp"
else:
monkeypatch.delenv("XDG_RUNTIME_DIR", raising=False)
monkeypatch.delenv("BORG_RUNTIME_DIR", raising=False)
assert get_runtime_dir() == os.path.join("/run/user", str(os.getuid()), "borg")
monkeypatch.setenv("XDG_RUNTIME_DIR", "/var/tmp/.cache")
assert get_runtime_dir() == os.path.join("/var/tmp/.cache", "borg")
monkeypatch.setenv("BORG_RUNTIME_DIR", "/var/tmp")
assert get_runtime_dir() == "/var/tmp"
def test_file_size():
"""test the size formatting routines"""
si_size_map = {