Repository: borgbackup/borg

Issue:
lock kept by declining repo path change?
From IRC (Manwe):
When repository location changes answering N (just by pressing enter) leaves the repo locked:

```
$ Warning: The repository at location ... Do you want to continue? [yN]
$ Repository access aborted
$ Now trying again with correct path
$ Failed to create/acquire the lock 
```

I came across this while running scripted backup job (that uses and creates about thousand borg repos).
I made a change in the script after initial backup and it accidentally introduces /./ in to the repo path. That of course activated the repo changed question.

the repo should not be locked by declining to change repo path. this with borg 0.30.0.

Steps to reproduce: 

```
$ mkdir /tmp/demo; touch /tmp/demo/file.txt
$ export BORG_CACHE_DIR=/tmp/bcache ; borg init -v /tmp/brepo ; borg create -v --stats /tmp/brepo::$(date +%s) /tmp/demo
$ borg create -v --stats /tmp/./brepo::$(date +%s) /tmp/demo
$ #(answer N)
$ borg create -v --stats /tmp/brepo::$(date +%s) /tmp/demo
$ # locked
```



Discussion:
related or even duplicate to #562 and #285.

Quickiy testing and looking at the cache.py class Cache. It seems that  cleanup is left for `__exit__`. 
I created a small simulation with now real locking, just to see what gets run after "raise"

I could not get my class to run `__exit__` -> without tracing the borg cache call path it looks like when ever there's any problem in Cache that will cause borg to abort and exit the lock will stay in place.

With raise -> fails

```
# python3.4 locktest.py
OPEN
Traceback (most recent call last):
  File "locktest.py", line 60, in main
    with Cache("Repo") as cache:
  File "locktest.py", line 37, in __init__
    raise self.RepositoryAccessAborted()
RepositoryAccessAborted
OPEN
Traceback (most recent call last):
  File "locktest.py", line 66, in main
    cache = Cache("Repo")
  File "locktest.py", line 37, in __init__
    raise self.RepositoryAccessAborted()
RepositoryAccessAborted
```

Without raise exception -> OK using with statement

```
$ python3.4 locktest.py
OPEN
HERE
ON EXIT
ON CLOSE
OPEN
```

```

def main():
  try:
    with Cache("Repo") as cache:
      print("HERE")
  except Exception as e:
      traceback.print_exc(file=sys.stdout)

  try:
    cache = Cache("Repo")
  except Exception as e:
    traceback.print_exc(file=sys.stdout)
    print(e)
```

Okay.. 
the Problem is error inside `__init__`.
If I change my test program so that `__init__` does not raise an Error, but another function in cache class does, then `__exit__` on close work
(which makes sense, object was not initalized because of an error -> there is no object on which to call `__exit__` ).

Yeah, guess that's like a context manager works. If the block (context) is not entered at all because it blows up before, the block (context) is also not exited.

Please note that the thing that the Cache contextmanager has to deal with is the **cache** lock.
The **repository** lock is not its business, that has to be dealt with by a not yet existing Repository contextmanager.

Yeah. I had not looked at that. But for this bug adding self.close() before raise will not leave lock behind. Altough pressing ctrl+c on prompt will leave the lock (because close() does not get called).


Resolving patch:
diff --git a/borg/cache.py b/borg/cache.py
--- a/borg/cache.py
+++ b/borg/cache.py
@@ -59,24 +59,28 @@ def __init__(self, repository, key, manifest, path=None, sync=True, do_files=Fal
                     raise self.CacheInitAbortedError()
             self.create()
         self.open(lock_wait=lock_wait)
-        # Warn user before sending data to a relocated repository
-        if self.previous_location and self.previous_location != repository._location.canonical_path():
-            msg = ("Warning: The repository at location {} was previously located at {}".format(repository._location.canonical_path(), self.previous_location) +
-                   "\n" +
-                   "Do you want to continue? [yN] ")
-            if not yes(msg, false_msg="Aborting.", default_notty=False,
-                       env_var_override='BORG_RELOCATED_REPO_ACCESS_IS_OK'):
-                raise self.RepositoryAccessAborted()
-
-        if sync and self.manifest.id != self.manifest_id:
-            # If repository is older than the cache something fishy is going on
-            if self.timestamp and self.timestamp > manifest.timestamp:
-                raise self.RepositoryReplay()
-            # Make sure an encrypted repository has not been swapped for an unencrypted repository
-            if self.key_type is not None and self.key_type != str(key.TYPE):
-                raise self.EncryptionMethodMismatch()
-            self.sync()
-            self.commit()
+        try:
+            # Warn user before sending data to a relocated repository
+            if self.previous_location and self.previous_location != repository._location.canonical_path():
+                msg = ("Warning: The repository at location {} was previously located at {}".format(repository._location.canonical_path(), self.previous_location) +
+                       "\n" +
+                       "Do you want to continue? [yN] ")
+                if not yes(msg, false_msg="Aborting.", default_notty=False,
+                           env_var_override='BORG_RELOCATED_REPO_ACCESS_IS_OK'):
+                    raise self.RepositoryAccessAborted()
+
+            if sync and self.manifest.id != self.manifest_id:
+                # If repository is older than the cache something fishy is going on
+                if self.timestamp and self.timestamp > manifest.timestamp:
+                    raise self.RepositoryReplay()
+                # Make sure an encrypted repository has not been swapped for an unencrypted repository
+                if self.key_type is not None and self.key_type != str(key.TYPE):
+                    raise self.EncryptionMethodMismatch()
+                self.sync()
+                self.commit()
+        except:
+            self.close()
+            raise
 
     def __enter__(self):
         return self
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -22,6 +22,12 @@
 # Note: 0.4.7 is also OK, but has no Python 3.2 support any more.
 install_requires=['msgpack-python==0.4.6', ]
 
+extras_require = {
+    # llfuse 0.40 (tested, proven, ok), needs FUSE version >= 2.8.0
+    # llfuse 0.41 (tested shortly, looks ok), needs FUSE version >= 2.8.0
+    # llfuse 0.42 (tested, does not work, incompatible api changes)
+    'fuse': ['llfuse<0.42', ],
+}
 
 from setuptools import setup, Extension
 from setuptools.command.sdist import sdist
@@ -260,4 +266,5 @@ def run(self):
     ext_modules=ext_modules,
     setup_requires=['setuptools_scm>=1.7'],
     install_requires=install_requires,
+    extras_require=extras_require,
 )


Test patch: