From 687818fbb5df6ee347874bc5f1c283ce31b75cae Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Mon, 13 Jul 2026 21:39:20 +0000 Subject: [PATCH] Improve local archive folder structure recognition. --- .../backup/v2/local/ArchiveFileSystem.kt | 18 +++++++-- .../backup/v2/local/ArchiveFileSystemTest.kt | 38 +++++++++++++++++++ 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/local/ArchiveFileSystem.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/local/ArchiveFileSystem.kt index 11cb93a15b..570ec1a287 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/local/ArchiveFileSystem.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/local/ArchiveFileSystem.kt @@ -104,11 +104,23 @@ class ArchiveFileSystem private constructor(private val context: Context, root: } /** - * Returns true if [dir] appears to be a SignalBackups directory based on its name and - * expected internal structure (presence of the "files" subdirectory). + * Returns true if [dir] appears to be a SignalBackups directory based on its expected internal + * structure. We can't rely on the directory being named [MAIN_DIRECTORY_NAME], since users may + * rename it or restore it into a differently-named folder (e.g. when transferring a backup + * between devices). A directory is therefore considered a backups directory if it either matches + * the expected name or directly contains the archive contents (a "files" subdirectory alongside + * at least one snapshot directory). */ private fun looksLikeSignalBackupsDirectory(dir: DocumentFile): Boolean { - return dir.name == MAIN_DIRECTORY_NAME && dir.findFile("files") != null + if (dir.findFile("files") == null) { + return false + } + + if (dir.name == MAIN_DIRECTORY_NAME) { + return true + } + + return dir.listFiles().any { it.isDirectory && it.name?.startsWith(BACKUP_DIRECTORY_PREFIX) == true } } /** diff --git a/app/src/test/java/org/thoughtcrime/securesms/backup/v2/local/ArchiveFileSystemTest.kt b/app/src/test/java/org/thoughtcrime/securesms/backup/v2/local/ArchiveFileSystemTest.kt index 104662816c..fbaf3e817f 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/backup/v2/local/ArchiveFileSystemTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/backup/v2/local/ArchiveFileSystemTest.kt @@ -98,6 +98,36 @@ class ArchiveFileSystemTest { assertThat(result).isNull() } + @Test + fun `openForRestore succeeds when the archive lives in a differently-named directory`() { + val renamed = temporaryFolder.newFolder("Signal") + buildArchiveContents(renamed) + + val result = ArchiveFileSystem.openForRestore(context, DocumentFile.fromFile(renamed)) + + assertThat(result).isNotNull() + } + + @Test + fun `openForRestore isRootedAtSignalBackups is true for a differently-named archive directory`() { + val renamed = temporaryFolder.newFolder("Signal") + buildArchiveContents(renamed) + + val result = ArchiveFileSystem.openForRestore(context, DocumentFile.fromFile(renamed))!! + + assertThat(result.isRootedAtSignalBackups).isTrue() + } + + @Test + fun `openForRestore returns null for a differently-named directory that only contains a files folder`() { + val renamed = temporaryFolder.newFolder("Signal") + renamed.resolve("files").mkdir() + + val result = ArchiveFileSystem.openForRestore(context, DocumentFile.fromFile(renamed)) + + assertThat(result).isNull() + } + /** * Creates the SignalBackups directory structure inside [parent] and returns the SignalBackups directory. */ @@ -106,4 +136,12 @@ class ArchiveFileSystemTest { signalBackups.resolve("files").mkdir() return signalBackups } + + /** + * Creates the raw archive contents (a "files" directory and a snapshot directory) directly inside [dir]. + */ + private fun buildArchiveContents(dir: java.io.File) { + dir.resolve("files").mkdir() + dir.resolve("${ArchiveFileSystem.BACKUP_DIRECTORY_PREFIX}-2026-01-01-00-00-00").mkdir() + } }