Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ jobs:
run: |
sudo apt-get update
sudo apt-get install -y rclone openssh-server curl
- name: Install macOS packages
if: runner.os == 'macOS'
run: |
brew update
brew install rclone
- name: Configure OpenSSH SFTP server (test only)
if: runner.os == 'Linux'
run: |
Expand Down Expand Up @@ -136,15 +141,15 @@ jobs:
run: |
python -m pip install --upgrade pip setuptools
pip install -r requirements.d/dev.txt
- name: Install borgstore (with all extras)
- name: Install borgstore
if: runner.os == 'Linux'
run: pip install -ve ".[s3,sftp]"
- name: Install borgstore (no extras)
run: pip install -ve ".[s3,sftp,rest,rclone]"
- name: Install borgstore
if: runner.os == 'Windows'
run: pip install -ve .
- name: Install borgstore (no extras)
run: pip install -ve ".[rest]"
- name: Install borgstore
if: runner.os == 'macOS'
run: pip install -ve .
run: pip install -ve ".[rest,rclone]"
- name: run tox envs (Linux)
if: runner.os == 'Linux'
run: tox -e all_extras
Expand Down
19 changes: 19 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,25 @@
Changelog
=========

Version 0.4.0 (2026-03-15)
--------------------------

New features:

- REST (http/https) backend, REST server, #18

Fixes:

- fix permissions check, #139
- posixfs/sftp: do not raise if base_path can not be deleted, #133
- list: do not yield invalid names, #130
- posixfs, s3, sftp: URL-unquote, #129

Other changes:

- add "rclone" and "rest" extras, "requests" is now an optional requirement


Version 0.3.1 (2026-02-09)
--------------------------

Expand Down
8 changes: 8 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,10 @@ Install without the extras:
pip install borgstore
pip install "borgstore[none]" # same thing (simplifies automation)

Install with the ``rest:`` backend (more dependencies)::

pip install "borgstore[rest]"

Install with the ``sftp:`` backend (more dependencies)::

pip install "borgstore[sftp]"
Expand All @@ -361,6 +365,10 @@ Install with the ``s3:`` backend (more dependencies)::

pip install "borgstore[s3]"

Install with the ``rclone:`` backend (more dependencies)::

pip install "borgstore[rclone]"

Please note that ``rclone:`` also supports SFTP and S3 remotes.

Want a demo?
Expand Down
7 changes: 6 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,15 @@ license = "BSD-3-Clause"
license-files = ["LICENSE.rst"]
requires-python = ">=3.10"
dependencies = [
"requests >= 2.25.1",
]

[project.optional-dependencies]
rest = [
"requests >= 2.25.1",
]
rclone = [
"requests >= 2.25.1",
]
sftp = [
"paramiko >= 1.9.1", # 1.9.1+ supports multiple IdentityKey entries in .ssh/config
]
Expand Down
11 changes: 10 additions & 1 deletion src/borgstore/backends/rclone.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@

import os
import re
import requests
import subprocess
import json
import secrets
from typing import Iterator
import time
import socket

try:
import requests
except ImportError:
requests = None

from ._base import BackendBase, ItemInfo, validate_name
from .errors import (
BackendError,
Expand Down Expand Up @@ -47,6 +51,11 @@ def get_rclone_backend(url):
if not url.startswith("rclone:"):
return None

if requests is None:
raise BackendDoesNotExist(
"The rclone backend requires dependencies. Install them with: 'pip install borgstore[rclone]'"
)

try:
# Check rclone is on the path
info = json.loads(subprocess.check_output([RCLONE, "rc", "--loopback", "core/version"]))
Expand Down
15 changes: 13 additions & 2 deletions src/borgstore/backends/rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@
from http import HTTPStatus as HTTP
from urllib.parse import unquote

import requests
from requests.auth import HTTPBasicAuth
try:
import requests
from requests.auth import HTTPBasicAuth
except ImportError:
requests = HTTPBasicAuth = None

from ._base import BackendBase, ItemInfo, validate_name
from .errors import (
Expand All @@ -26,6 +29,14 @@
def get_rest_backend(base_url: str):
# http(s)://username:password@hostname:port/ or http(s)://hostname:port/ + auth from env
# note: path component must be "/" (no sub-path allowed, as it would silently prepend to all item names)
if not base_url.startswith(("http:", "https:")):
return None

if requests is None:
raise BackendDoesNotExist(
"The REST backend requires dependencies. Install them with: 'pip install borgstore[rest]'"
)

http_regex = r"""
(?P<scheme>http|https)://
((?P<username>[^:]+):(?P<password>[^@]+)@)?
Expand Down
13 changes: 11 additions & 2 deletions tests/test_backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@

from . import key, list_names

try:
import requests
except ImportError:
requests = None

from borgstore.backends._base import ItemInfo
from borgstore.backends.errors import (
BackendAlreadyExists,
Expand Down Expand Up @@ -121,8 +126,9 @@ def check_s3_available():


sftp_is_available = check_sftp_available()
rclone_is_available = check_rclone_available()
rclone_is_available = requests is not None and check_rclone_available()
s3_is_available = check_s3_available()
rest_is_available = requests is not None


@pytest.fixture(scope="function")
Expand Down Expand Up @@ -181,7 +187,9 @@ def rest_backend_created(tmp_path):
def pytest_generate_tests(metafunc):
# Generates tests for misc. storages
if "tested_backends" in metafunc.fixturenames:
tested_backends = ["posixfs_backend_created", "rest_backend_created"]
tested_backends = ["posixfs_backend_created"]
if rest_is_available:
tested_backends += ["rest_backend_created"]
if sftp_is_available:
tested_backends += ["sftp_backend_created"]
if rclone_is_available:
Expand Down Expand Up @@ -255,6 +263,7 @@ def test_sftp_url(url, username, hostname, port, path):
assert backend.base_path == path


@pytest.mark.skipif(not rest_is_available, reason="REST is not available (requests missing)")
@pytest.mark.parametrize(
"url,base_url,username,password",
[
Expand Down
6 changes: 5 additions & 1 deletion tests/test_server_rest.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import threading
import pytest
import requests

try:
import requests
except ImportError:
pytest.skip("requests is not installed", allow_module_level=True)

from borgstore.constants import DEL_SUFFIX
from borgstore.server.rest import BorgStoreRESTServer
Expand Down
5 changes: 4 additions & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
envlist = py{310,311,312,313,314},flake8,mypy

[testenv]
deps = pytest
deps =
pytest
requests
commands = pytest -v -rs tests
pass_env =
BORGSTORE_TEST_*_URL
Expand Down Expand Up @@ -46,6 +48,7 @@ deps =
pytest
boto3
paramiko
requests
commands = pytest -v -rs tests
pass_env =
BORGSTORE_TEST_*_URL
Loading