Bläddra i källkod

Merge pull request #4316 from ThomasWaldmann/init-make-parentdirs

borg init --make-parent-dirs parent1/parent2/repo_dir, fixes #4235
TW 6 år sedan
förälder
incheckning
9a3fbdd751
4 ändrade filer med 48 tillägg och 10 borttagningar
  1. 6 2
      src/borg/archiver.py
  2. 14 6
      src/borg/remote.py
  3. 13 2
      src/borg/repository.py
  4. 15 0
      src/borg/testsuite/archiver.py

+ 6 - 2
src/borg/archiver.py

@@ -124,15 +124,17 @@ def with_repository(fake=False, invert_fake=False, create=False, lock=True,
             location = args.location  # note: 'location' must be always present in args
             append_only = getattr(args, 'append_only', False)
             storage_quota = getattr(args, 'storage_quota', None)
+            make_parent_dirs = getattr(args, 'make_parent_dirs', False)
             if argument(args, fake) ^ invert_fake:
                 return method(self, args, repository=None, **kwargs)
             elif location.proto == 'ssh':
                 repository = RemoteRepository(location, create=create, exclusive=argument(args, exclusive),
-                                              lock_wait=self.lock_wait, lock=lock, append_only=append_only, args=args)
+                                              lock_wait=self.lock_wait, lock=lock, append_only=append_only,
+                                              make_parent_dirs=make_parent_dirs, args=args)
             else:
                 repository = Repository(location.path, create=create, exclusive=argument(args, exclusive),
                                         lock_wait=self.lock_wait, lock=lock, append_only=append_only,
-                                        storage_quota=storage_quota)
+                                        storage_quota=storage_quota, make_parent_dirs=make_parent_dirs)
             with repository:
                 if manifest or cache:
                     kwargs['manifest'], kwargs['key'] = Manifest.load(repository, compatibility)
@@ -2773,6 +2775,8 @@ class Archiver:
         subparser.add_argument('--storage-quota', metavar='QUOTA', dest='storage_quota', default=None,
                                type=parse_storage_quota,
                                help='Set storage quota of the new repository (e.g. 5G, 1.5T). Default: no quota.')
+        subparser.add_argument('--make-parent-dirs', dest='make_parent_dirs', action='store_true',
+                               help='create the parent directories of the repository directory, if they are missing.')
 
         check_epilog = process_epilog("""
         The check command verifies the consistency of a repository and the corresponding archives.

+ 14 - 6
src/borg/remote.py

@@ -333,7 +333,8 @@ class RepositoryServer:  # pragma: no cover
             path = path[3:]
         return os.path.realpath(path)
 
-    def open(self, path, create=False, lock_wait=None, lock=True, exclusive=None, append_only=False):
+    def open(self, path, create=False, lock_wait=None, lock=True, exclusive=None, append_only=False,
+             make_parent_dirs=False):
         logging.debug('Resolving repository path %r', path)
         path = self._resolve_path(path)
         logging.debug('Resolved repository path to %r', path)
@@ -362,7 +363,8 @@ class RepositoryServer:  # pragma: no cover
         self.repository = Repository(path, create, lock_wait=lock_wait, lock=lock,
                                      append_only=append_only,
                                      storage_quota=self.storage_quota,
-                                     exclusive=exclusive)
+                                     exclusive=exclusive,
+                                     make_parent_dirs=make_parent_dirs)
         self.repository.__enter__()  # clean exit handled by serve() method
         return self.repository.id
 
@@ -523,7 +525,8 @@ class RemoteRepository:
     # If compatibility with 1.0.x is not longer needed, replace all checks of this with True and simplify the code
     dictFormat = False  # outside of __init__ for testing of legacy free protocol
 
-    def __init__(self, location, create=False, exclusive=False, lock_wait=None, lock=True, append_only=False, args=None):
+    def __init__(self, location, create=False, exclusive=False, lock_wait=None, lock=True, append_only=False,
+                 make_parent_dirs=False, args=None):
         self.location = self._location = location
         self.preload_ids = []
         self.msgid = 0
@@ -576,7 +579,8 @@ class RemoteRepository:
 
             def do_open():
                 self.id = self.open(path=self.location.path, create=create, lock_wait=lock_wait,
-                                    lock=lock, exclusive=exclusive, append_only=append_only)
+                                    lock=lock, exclusive=exclusive, append_only=append_only,
+                                    make_parent_dirs=make_parent_dirs)
 
             if self.dictFormat:
                 do_open()
@@ -739,6 +743,8 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+.
                     raise PathNotAllowed('(unknown)')
                 else:
                     raise PathNotAllowed(args[0].decode())
+            elif error == 'ParentPathDoesNotExist':
+                raise Repository.ParentPathDoesNotExist(args[0].decode())
             elif error == 'ObjectNotFound':
                 if old_server:
                     raise Repository.ObjectNotFound('(not available)', self.location.orig)
@@ -884,8 +890,10 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+.
         self.ignore_responses |= set(waiting_for)  # we lose order here
 
     @api(since=parse_version('1.0.0'),
-         append_only={'since': parse_version('1.0.7'), 'previously': False})
-    def open(self, path, create=False, lock_wait=None, lock=True, exclusive=False, append_only=False):
+         append_only={'since': parse_version('1.0.7'), 'previously': False},
+         make_parent_dirs={'since': parse_version('1.1.9'), 'previously': False})
+    def open(self, path, create=False, lock_wait=None, lock=True, exclusive=False, append_only=False,
+             make_parent_dirs=False):
         """actual remoting is done via self.call in the @api decorator"""
 
     @api(since=parse_version('1.0.0'))

+ 13 - 2
src/borg/repository.py

@@ -120,6 +120,9 @@ class Repository:
     class PathAlreadyExists(Error):
         """There is already something at {}."""
 
+    class ParentPathDoesNotExist(Error):
+        """The parent path of the repo directory [{}] does not exist."""
+
     class InvalidRepository(Error):
         """{} is not a valid repository. Check repo config."""
 
@@ -147,7 +150,8 @@ class Repository:
         """The storage quota ({}) has been exceeded ({}). Try deleting some archives."""
 
     def __init__(self, path, create=False, exclusive=False, lock_wait=None, lock=True,
-                 append_only=False, storage_quota=None, check_segment_magic=True):
+                 append_only=False, storage_quota=None, check_segment_magic=True,
+                 make_parent_dirs=False):
         self.path = os.path.abspath(path)
         self._location = Location('file://%s' % self.path)
         self.io = None  # type: LoggedIO
@@ -168,6 +172,7 @@ class Repository:
         self.storage_quota_use = 0
         self.transaction_doomed = None
         self.check_segment_magic = check_segment_magic
+        self.make_parent_dirs = make_parent_dirs
 
     def __del__(self):
         if self.lock:
@@ -250,8 +255,14 @@ class Repository:
         """Create a new empty repository at `path`
         """
         self.check_can_create_repository(path)
+        if self.make_parent_dirs:
+            parent_path = os.path.join(path, os.pardir)
+            os.makedirs(parent_path, exist_ok=True)
         if not os.path.exists(path):
-            os.mkdir(path)
+            try:
+                os.mkdir(path)
+            except FileNotFoundError as err:
+                raise self.ParentPathDoesNotExist(path) from err
         with open(os.path.join(path, 'README'), 'w') as fd:
             fd.write(REPOSITORY_README)
         os.mkdir(os.path.join(path, 'data'))

+ 15 - 0
src/borg/testsuite/archiver.py

@@ -438,6 +438,17 @@ class ArchiverTestCase(ArchiverTestCaseBase):
         # the interesting parts of info_output2 and info_output should be same
         self.assert_equal(filter(info_output), filter(info_output2))
 
+    def test_init_parent_dirs(self):
+        parent_path = os.path.join(self.tmpdir, 'parent1', 'parent2')
+        repository_path = os.path.join(parent_path, 'repository')
+        repository_location = self.prefix + repository_path
+        with pytest.raises(Repository.ParentPathDoesNotExist):
+            # normal borg init does NOT create missing parent dirs
+            self.cmd('init', '--encryption=none', repository_location)
+        # but if told so, it does:
+        self.cmd('init', '--encryption=none', '--make-parent-dirs', repository_location)
+        assert os.path.exists(parent_path)
+
     def test_unix_socket(self):
         self.cmd('init', '--encryption=repokey', self.repository_location)
         try:
@@ -2893,6 +2904,10 @@ class ArchiverTestCaseBinary(ArchiverTestCase):
     EXE = 'borg.exe'
     FORK_DEFAULT = True
 
+    @unittest.skip('does not raise Exception, but sets rc==2')
+    def test_init_parent_dirs(self):
+        pass
+
     @unittest.skip('patches objects')
     def test_init_interrupt(self):
         pass