Repository: borgbackup/borg

Issue:
FileNotFoundError: [Errno 2] No such file or directory: '/mnt/sshfs/borg-cache/REPOID/txn.active/files'
## Is this a BUG / ISSUE report or a QUESTION?

Bug

## System information. For client/server mode post info for both machines.

#### Your borg version (borg -V).

borg 1.1.17

#### Operating system (distribution) and version.

Debian 11


## Describe the problem you're observing.

I was doing a backup and the storage (mounted using sshfs and keeping both the repo and the borg cache) was unresponsive, so I had to restart the server.
After the reboot I restarted a borg prune but I am getting this error:
```
borgbackup version 1.1.17
Killed stale lock borg.server@345051709351.4131-0.
Removed stale exclusive roster lock for host borg.server@345051709351 pid 4131 thread 0.
Removed stale exclusive roster lock for host borg.server@345051709351 pid 4131 thread 0.
Local Exception
Traceback (most recent call last):
  File "borg/archiver.py", line 4703, in main
  File "borg/archiver.py", line 4635, in run
  File "borg/archiver.py", line 177, in wrapper
  File "borg/archiver.py", line 1652, in do_prune
  File "borg/cache.py", line 387, in __new__
  File "borg/cache.py", line 381, in local
  File "borg/cache.py", line 464, in __init__
  File "borg/cache.py", line 512, in open
  File "borg/cache.py", line 624, in rollback
  File "shutil.py", line 248, in copy
  File "shutil.py", line 120, in copyfile
FileNotFoundError: [Errno 2] No such file or directory: '/mnt/sshfs/borg-cache/a0a213e2d7c13a3d80933520b7298d9ddf27ac25c0fbbd9c55c62acce94f8811/txn.active/files'

Platform: Linux borg 5.10.0-11-amd64 #1 SMP Debian 5.10.92-1 (2022-01-18) x86_64
Linux: debian 11.2
Borg: 1.1.17  Python: CPython 3.7.11 msgpack: 0.5.6.+borg1
PID: 572  CWD: /root
sys.argv: ['borg', 'prune', '-v', '--stats', '--show-rc', '--show-version', '--list', '--keep-within=188d']
SSH_ORIGINAL_COMMAND: None

terminating with error status, rc 2
```

Since I had a files.STORAGE inside the txn.active directory I did a:
```
ln -s files.STORAGE files
```
and that was enough to make borg prune run again.

So borg is expecting the "files" file inside the txn.active directory even when using BORG_FILES_CACHE_SUFFIX.

A similar issue happens with borg create.

The problem is that borg should not expect to have the "files" file when not needed, i.e. https://github.com/borgbackup/borg/pull/5777 should probably also be applied to 1.1 branch.


Discussion:
Guess there are 2 things to check:
- whether the BORG_FILES_CACHE_SUFFIX is implemented correctly.
- whether #5777 needs backporting

`borg prune` is a command not needing the files cache, that is correct, but it is a repo-writing command and it does need "the cache" in general (which also includes the `chunks` index, which is used for refcounting).
I also got a similar error with the create I scheduled after the prune. Here I had BORG_FILES_CACHE_SUFFIX=borg :

```
borgbackup version 1.1.17
Killed stale lock borg.server@345051709351.610-0.
Removed stale exclusive roster lock for host borg.server@345051709351 pid 610 thread 0.
Removed stale exclusive roster lock for host borg.server@345051709351 pid 610 thread 0.
Creating archive at "/mnt/sshfs/borg-repo::borg-2022-02-23"
Local Exception
Traceback (most recent call last):
  File "borg/archiver.py", line 4703, in main
  File "borg/archiver.py", line 4635, in run
  File "borg/archiver.py", line 177, in wrapper
  File "borg/archiver.py", line 591, in do_create
  File "borg/cache.py", line 387, in __new__
  File "borg/cache.py", line 381, in local
  File "borg/cache.py", line 464, in __init__
  File "borg/cache.py", line 512, in open
  File "borg/cache.py", line 624, in rollback
  File "shutil.py", line 248, in copy
  File "shutil.py", line 120, in copyfile
FileNotFoundError: [Errno 2] No such file or directory: '/mnt/sshfs/borg-cache/a0a213e2d7c13a3d80933520b7298d9ddf27ac25c0fbbd9c55c62acce94f8811/txn.active/files.borg'

Platform: Linux borg 5.10.0-11-amd64 #1 SMP Debian 5.10.92-1 (2022-01-18) x86_64
Linux: debian 11.2
Borg: 1.1.17  Python: CPython 3.7.11 msgpack: 0.5.6.+borg1
PID: 961  CWD: /
sys.argv: ['borg', 'create', '--exclude-from', '/root/borg-script/excludes.conf', '--exclude-caches', '-v', '--stats', '--show-rc', '--show-version', '--noatime', '--nobsdflags', '--noxattrs', '--noacls', '--numeric-owner', '-C', 'zstd,10', '::borg-2022-02-23', 'etc', 'root/borg-archivi', 'root/borg-script']
SSH_ORIGINAL_COMMAND: None

terminating with error status, rc 2
```

Is there any more info I can provide?
I think that is enough info for now.

If you want a quick solution, guess not using the SUFFIX would work.
Adding @luke-jr, author of the initial BORG_FILES_CACHE_SUFFIX implementation.
It happened again today with the prune (triggered by the storage being unresponsive). Doing this:

`touch /mnt/sshfs/borg-cache/a0a213e2d7c13a3d80933520b7298d9ddf27ac25c0fbbd9c55c62acce94f8811/txn.active/files`

is just enough to make it properly run again.
Same problem here, if borg fails in a previous backup and the CACHE_SUFFIX is different, then borg won't run.

In this case, as `borg list` doesn't use cache and works, but `borg info` does not, I wrote this code inside my bash script and my backups are working again:

```
borg list "${REPO_PATH}"
if [ $? -eq 0 ]; then
	INFO_FILE=/var/tmp/borg_info_$(basename ${REPO_PATH})_${ARCHIVE}_${DATETIME}
	borg info ${REPO_PATH} 2> ${INFO_FILE}
	if [ $? -eq 2 ]; then
		txnFolder="$(dirname $(grep "${BORG_CACHE_DIR}/.*" ${INFO_FILE}  -o))"
		mv "$txnFolder" "${txnFolder}_${DATETIME}"
	fi
	rm ${INFO_FILE}
	
	borg create [...]
fi
```

This is not a fix, but a workaround.
Hello,
Do you have any update on this issue? 
Regards
No, all updates (if any) can be read right here.
@luke-jr do you have time / motivation to look at this issue?
cache:624:
```
            shutil.copy(os.path.join(txn_dir, files_cache_name()), self.path)
```

```
def files_cache_name():
    suffix = os.environ.get('BORG_FILES_CACHE_SUFFIX', '')
    return 'files.' + suffix if suffix else 'files'
```

So, the code is correct, but `BORG_FILES_CACHE_SUFFIX` env var was not set and thus was not used in a consistent way.

Besides inconsistent use, it could be also forgetting the `export ` in `export BORG_FILES_CACHE_SUFFIX=...`.

@FabioPedretti this reply refers to your original bug report, top post.
https://github.com/borgbackup/borg/issues/6353#issuecomment-1050016043 and @santiagobiali post are a related, but slightly different issue:

switching to suffix B while a `txn.active` directory exists (due to some crash / interruption) that needs to get rolled back that was created with suffix A (both A and B can also be empty). 
Guess the solution is that we need to discover the files cache name inside the txn.active directory and use precisely that when rolling back, ignoring whatever is set via BORG_FILES_CACHE_SUFFIX. So that can be considered a bug.

This also removes the suffix consistency requirement for the user between "borg call before crash" and "next borg call after crash".

Resolving patch:
diff --git a/src/borg/cache.py b/src/borg/cache.py
--- a/src/borg/cache.py
+++ b/src/borg/cache.py
@@ -234,6 +234,10 @@ def files_cache_name():
     return 'files.' + suffix if suffix else 'files'
 
 
+def discover_files_cache_name(path):
+    return [fn for fn in os.listdir(path) if fn == 'files' or fn.startswith('files.')][0]
+
+
 class CacheConfig:
     def __init__(self, repository, path=None, lock_wait=None):
         self.repository = repository
@@ -650,7 +654,7 @@ def rollback(self):
         if os.path.exists(txn_dir):
             shutil.copy(os.path.join(txn_dir, 'config'), self.path)
             shutil.copy(os.path.join(txn_dir, 'chunks'), self.path)
-            shutil.copy(os.path.join(txn_dir, files_cache_name()), self.path)
+            shutil.copy(os.path.join(txn_dir, discover_files_cache_name(txn_dir)), self.path)
             os.rename(txn_dir, os.path.join(self.path, 'txn.tmp'))
             if os.path.exists(os.path.join(self.path, 'txn.tmp')):
                 shutil.rmtree(os.path.join(self.path, 'txn.tmp'))


Test patch: