Browse Source

support sftp: repositories via borgstore

Thomas Waldmann 9 tháng trước cách đây
mục cha
commit
7714b6542a

+ 12 - 0
src/borg/archiver/_common.py

@@ -46,6 +46,18 @@ def get_repository(
             args=args,
         )
 
+    elif location.proto in ("sftp", "file") and not v1_or_v2:  # stuff directly supported by borgstore
+        repository = Repository(
+            location,
+            create=create,
+            exclusive=exclusive,
+            lock_wait=lock_wait,
+            lock=lock,
+            append_only=append_only,
+            make_parent_dirs=make_parent_dirs,
+            storage_quota=storage_quota,
+        )
+
     else:
         RepoCls = LegacyRepository if v1_or_v2 else Repository
         repository = RepoCls(

+ 21 - 0
src/borg/helpers/parseformat.py

@@ -454,6 +454,19 @@ class Location:
         re.VERBOSE,
     )  # path
 
+    sftp_re = re.compile(
+        r"""
+        (?P<proto>sftp)://                                      # sftp://
+        """
+        + optional_user_re
+        + host_re
+        + r"""                 # user@  (optional), host name or address
+        (?::(?P<port>\d+))?                                     # :port (optional)
+        """
+        + abs_path_re,
+        re.VERBOSE,
+    )  # path
+
     socket_re = re.compile(
         r"""
         (?P<proto>socket)://                                    # socket://
@@ -518,6 +531,14 @@ class Location:
             return ("/." + p) if relative else p
 
         m = self.ssh_re.match(text)
+        if m:
+            self.proto = m.group("proto")
+            self.user = m.group("user")
+            self._host = m.group("host")
+            self.port = m.group("port") and int(m.group("port")) or None
+            self.path = normpath_special(m.group("path"))
+            return True
+        m = self.sftp_re.match(text)
         if m:
             self.proto = m.group("proto")
             self.user = m.group("user")

+ 16 - 8
src/borg/repository.py

@@ -81,7 +81,7 @@ class Repository:
 
     def __init__(
         self,
-        path,
+        path_or_location,
         create=False,
         exclusive=False,
         lock_wait=1.0,
@@ -91,8 +91,16 @@ class Repository:
         make_parent_dirs=False,
         send_log_cb=None,
     ):
-        self.path = os.path.abspath(path)
-        url = "file://%s" % self.path
+        if isinstance(path_or_location, Location):
+            location = path_or_location
+            if location.proto == "file":
+                url = f"file://{location.path}"  # frequently users give without file:// prefix
+            else:
+                url = location.processed  # location as given by user, processed placeholders
+        else:
+            url = "file://%s" % os.path.abspath(path_or_location)
+            location = Location(url)
+        self.location = location
         # use a Store with flat config storage and 2-levels-nested data storage
         self.store = Store(url, levels={"config/": [0], "data/": [2]})
         self._location = Location(url)
@@ -115,7 +123,7 @@ class Repository:
         self.exclusive = exclusive
 
     def __repr__(self):
-        return f"<{self.__class__.__name__} {self.path}>"
+        return f"<{self.__class__.__name__} {self.location}>"
 
     def __enter__(self):
         if self.do_create:
@@ -176,12 +184,12 @@ class Repository:
             self.lock = None
         readme = self.store.load("config/readme").decode()
         if readme != REPOSITORY_README:
-            raise self.InvalidRepository(self.path)
+            raise self.InvalidRepository(str(self.location))
         self.version = int(self.store.load("config/version").decode())
         if self.version not in self.acceptable_repo_versions:
             self.close()
             raise self.InvalidRepositoryConfig(
-                self.path, "repository version %d is not supported by this borg version" % self.version
+                str(self.location), "repository version %d is not supported by this borg version" % self.version
             )
         self.id = hex_to_bin(self.store.load("config/id").decode(), length=32)
         self.opened = True
@@ -338,7 +346,7 @@ class Repository:
                     raise IntegrityError(f"Object too small [id {id_hex}]: expected {meta_size}, got {len(meta)} bytes")
                 return hdr + meta
         except StoreObjectNotFound:
-            raise self.ObjectNotFound(id, self.path) from None
+            raise self.ObjectNotFound(id, str(self.location)) from None
 
     def get_many(self, ids, read_data=True, is_preloaded=False):
         for id_ in ids:
@@ -369,7 +377,7 @@ class Repository:
         try:
             self.store.delete(key)
         except StoreObjectNotFound:
-            raise self.ObjectNotFound(id, self.path) from None
+            raise self.ObjectNotFound(id, str(self.location)) from None
 
     def async_response(self, wait=True):
         """Get one async result (only applies to remote repositories).

+ 8 - 0
src/borg/testsuite/helpers.py

@@ -187,6 +187,14 @@ class TestLocationWithoutEnv:
             "host='2a02:0001:0002:0003:0004:0005:0006:0007', port=1234, path='/some/path')"
         )
 
+    def test_sftp(self, monkeypatch, keys_dir):
+        monkeypatch.delenv("BORG_REPO", raising=False)
+        assert (
+            repr(Location("sftp://user@host:1234/some/path"))
+            == "Location(proto='sftp', user='user', host='host', port=1234, path='/some/path')"
+        )
+        assert Location("sftp://user@host:1234/some/path").to_key_filename() == keys_dir + "host__some_path"
+
     def test_socket(self, monkeypatch, keys_dir):
         monkeypatch.delenv("BORG_REPO", raising=False)
         assert (

+ 1 - 1
src/borg/testsuite/repository.py

@@ -44,7 +44,7 @@ def reopen(repository, exclusive: Optional[bool] = True, create=False):
     if isinstance(repository, Repository):
         if repository.opened:
             raise RuntimeError("Repo must be closed before a reopen. Cannot support nested repository contexts.")
-        return Repository(repository.path, exclusive=exclusive, create=create)
+        return Repository(repository.location, exclusive=exclusive, create=create)
 
     if isinstance(repository, RemoteRepository):
         if repository.p is not None or repository.sock is not None: