From 6ffd1c0869c31a25a634f021e5c39b2ca89aa80f Mon Sep 17 00:00:00 2001 From: Abdourahamane Boinaidi Date: Fri, 6 Feb 2026 15:55:05 +0100 Subject: [PATCH 01/11] chore: Add FileUtilsForApiV2 --- .../database/utils/FileUtilsForApiV2.kt | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/utils/FileUtilsForApiV2.kt diff --git a/STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/utils/FileUtilsForApiV2.kt b/STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/utils/FileUtilsForApiV2.kt new file mode 100644 index 00000000..66d40bae --- /dev/null +++ b/STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/utils/FileUtilsForApiV2.kt @@ -0,0 +1,93 @@ +/* + * Infomaniak SwissTransfer - Multiplatform + * Copyright (C) 2024 Infomaniak Network SA + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.infomaniak.multiplatform_swisstransfer.database.utils + +import androidx.collection.ArraySet +import com.infomaniak.multiplatform_swisstransfer.common.interfaces.transfers.v2.File +import com.infomaniak.multiplatform_swisstransfer.database.models.transfers.v2.FileDB +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid + +object FileUtilsForApiV2 { + + fun getFileDbTree(transferId: String, files: List): Set { + val folderByPath = HashMap(files.size) + val out = ArraySet(files.size * 2) + + for (file in files) { + val parentId = ensureFolders(file.path, file.size, transferId, folderByPath, out) + out += FileDB(file, transferId, parentId) + } + return out + } + + private fun ensureFolders( + filePath: String, + fileSize: Long, + transferId: String, + folderByPath: MutableMap, + out: MutableSet, + ): String? { + val lastSlash = filePath.lastIndexOf('/') + if (lastSlash <= 0) return null // No parent folder (root file) + + var parentId: String? = null + val pathBuilder = StringBuilder(lastSlash) + + var currentIndex = 0 + while (currentIndex < lastSlash) { + val nextSlash = filePath.indexOf('/', currentIndex).takeIf { it != -1 } ?: lastSlash + + // Skip duplicated separators + if (nextSlash == currentIndex) { + currentIndex++ + continue + } + + if (pathBuilder.isNotEmpty()) pathBuilder.append('/') + pathBuilder.append(filePath, currentIndex, nextSlash) + + val folderPath = pathBuilder.toString() + val folder = folderByPath[folderPath] ?: generateFolderId(transferId).let { newId -> + FileDB( + id = newId, + path = folderPath, + size = 0L, + mimeType = null, + isFolder = true, + transferId = transferId, + folderId = parentId + ).also { + folderByPath[folderPath] = it + out += it + } + } + + folder.size += fileSize + + parentId = folder.id + currentIndex = nextSlash + 1 + } + return parentId + } + + @OptIn(ExperimentalUuidApi::class) + private fun generateFolderId(transferId: String): String { + return "$transferId:${Uuid.random()}" + } +} From 7f45332d71a19a40a2bd44aa1bcaff4b17561163 Mon Sep 17 00:00:00 2001 From: Abdourahamane Boinaidi Date: Fri, 6 Feb 2026 15:58:31 +0100 Subject: [PATCH 02/11] chore: Add tests for FileUtilsForApiV2 There are the same tests as v1 --- .../database/FileUtilsForApiV2Test.kt | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 STDatabase/src/commonTest/kotlin/com/infomaniak/multiplatform_swisstransfer/database/FileUtilsForApiV2Test.kt diff --git a/STDatabase/src/commonTest/kotlin/com/infomaniak/multiplatform_swisstransfer/database/FileUtilsForApiV2Test.kt b/STDatabase/src/commonTest/kotlin/com/infomaniak/multiplatform_swisstransfer/database/FileUtilsForApiV2Test.kt new file mode 100644 index 00000000..395c1b0f --- /dev/null +++ b/STDatabase/src/commonTest/kotlin/com/infomaniak/multiplatform_swisstransfer/database/FileUtilsForApiV2Test.kt @@ -0,0 +1,144 @@ +/* + * Infomaniak SwissTransfer - Multiplatform + * Copyright (C) 2024 Infomaniak Network SA + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.infomaniak.multiplatform_swisstransfer.database + +import com.infomaniak.multiplatform_swisstransfer.database.models.transfers.v2.FileDB +import com.infomaniak.multiplatform_swisstransfer.database.utils.FileUtilsForApiV2 +import kotlin.test.AfterTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import com.infomaniak.multiplatform_swisstransfer.common.interfaces.transfers.v2.File as FileInterfaceV2 + +class FileUtilsForApiV2Test { + + private val treeApiV2 = mutableListOf() + + private data class FileV2Mock( + override val id: String, + override val path: String, + override val size: Long, + override val mimeType: String? = null + ) : FileInterfaceV2 + + @AfterTest + fun clean() { + treeApiV2.clear() + } + + @Test + fun filesInRoot() { + val filesList = listOf( + FileV2Mock(id = "1", path = "file1.txt", size = 100L), + FileV2Mock(id = "2", path = "file2.txt", size = 200L), + ) + treeApiV2.addAll(FileUtilsForApiV2.getFileDbTree("transfer1", filesList)) + + val rootFiles = treeApiV2.filter { !it.isFolder } + assertEquals(2, rootFiles.size, "Should have 2 root files") + assertNotNull(rootFiles.find { it.path == "file1.txt" }, "file1.txt should exist") + assertNotNull(rootFiles.find { it.path == "file2.txt" }, "file2.txt should exist") + } + + @Test + fun filesInFolder() { + val filesList = listOf( + FileV2Mock(id = "1", path = "folder1/file3.txt", size = 100L), + FileV2Mock(id = "2", path = "folder1/file4.txt", size = 200L), + FileV2Mock(id = "3", path = "folder1/randomFolder/empty_file.txt", size = 50L), + FileV2Mock(id = "4", path = "folder2/file5.txt", size = 300L), + FileV2Mock(id = "5", path = "folder2/file6.txt", size = 400L), + ) + treeApiV2.addAll(FileUtilsForApiV2.getFileDbTree("transfer2", filesList)) + + val folder1 = treeApiV2.find { it.path == "folder1" && it.isFolder } + val folder2 = treeApiV2.find { it.path == "folder2" && it.isFolder } + val randomFolder = treeApiV2.find { it.path == "folder1/randomFolder" && it.isFolder } + + assertNotNull(folder1, "folder1 should exist") + assertNotNull(folder2, "folder2 should exist") + assertNotNull(randomFolder, "folder1/randomFolder should exist") + + assertEquals(350L, folder1.size, "folder1 size should be 350 (100 + 200 + 50)") + assertEquals(700L, folder2.size, "folder2 size should be 700 (300 + 400)") + assertEquals(50L, randomFolder.size, "randomFolder size should be 50") + + val folder1Children = treeApiV2.filter { it.folderId == folder1.id && !it.isFolder } + assertEquals(2, folder1Children.size, "folder1 should have 2 direct file children") + } + + @Test + fun fileContainedIn5NestedFolders() { + val path = "folder1/folder2/folder3/folder4/folder5" + val filesList = listOf( + FileV2Mock(id = "1", path = "$path/nested_file.txt", size = 1000L) + ) + treeApiV2.addAll(FileUtilsForApiV2.getFileDbTree("transfer3", filesList)) + + checkFolders("folder1/folder2/folder3/folder4/folder5", size = 1000L) + + val file = treeApiV2.find { it.path == "$path/nested_file.txt" } + assertNotNull(file, "nested_file.txt should exist") + } + + @Test + fun folderSizeAccumulatedCorrectly() { + val filesList = listOf( + FileV2Mock(id = "1", path = "docs/report.pdf", size = 500L), + FileV2Mock(id = "2", path = "docs/notes.txt", size = 300L), + FileV2Mock(id = "3", path = "docs/subfolder/image.png", size = 200L), + ) + treeApiV2.addAll(FileUtilsForApiV2.getFileDbTree("transfer4", filesList)) + + val docs = treeApiV2.find { it.path == "docs" && it.isFolder } + val subfolder = treeApiV2.find { it.path == "docs/subfolder" && it.isFolder } + + assertEquals(1000L, docs?.size, "docs size should be 1000 (500 + 300 + 200)") + assertEquals(200L, subfolder?.size, "subfolder size should be 200") + } + + @Test + fun folderHierarchyIsCorrect() { + val filesList = listOf( + FileV2Mock(id = "1", path = "a/b/c/file.txt", size = 100L), + ) + treeApiV2.addAll(FileUtilsForApiV2.getFileDbTree("transfer5", filesList)) + + val folderA = treeApiV2.find { it.path == "a" && it.isFolder } + val folderB = treeApiV2.find { it.path == "a/b" && it.isFolder } + val folderC = treeApiV2.find { it.path == "a/b/c" && it.isFolder } + + assertNotNull(folderA, "folder 'a' should exist") + assertNotNull(folderB, "folder 'a/b' should exist") + assertNotNull(folderC, "folder 'a/b/c' should exist") + + assertEquals(null, folderA.folderId, "folder 'a' should have no parent") + assertEquals(folderA.id, folderB.folderId, "folder 'a/b' should have 'a' as parent") + assertEquals(folderB.id, folderC.folderId, "folder 'a/b/c' should have 'a/b' as parent") + } + + private fun checkFolders(path: String, size: Long) { + val parts = path.split("/") + for (i in parts.indices) { + val partialPath = parts.take(i + 1).joinToString("/") + val folder = treeApiV2.find { it.path == partialPath && it.isFolder } + assertNotNull(folder, "$partialPath should exist as folder") + assertEquals(size, folder.size, "$partialPath should have correct accumulated size") + } + } +} From e94d7323a6542a056c6b67a80db619819f9c9d3c Mon Sep 17 00:00:00 2001 From: Louis CAD Date: Thu, 19 Feb 2026 16:48:57 +0100 Subject: [PATCH 03/11] refactor: Rename folderId to parentId in FileDB Co-authored-by: Gibran Chevalley --- .../1.json | 10 +++++----- .../database/dao/TransferDao.kt | 6 +++--- .../database/models/transfers/v2/FileDB.kt | 4 ++-- .../database/utils/FileUtilsForApiV2.kt | 2 +- .../database/FileUtilsForApiV2Test.kt | 8 ++++---- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/STDatabase/schemas/com.infomaniak.multiplatform_swisstransfer.database.AppDatabase/1.json b/STDatabase/schemas/com.infomaniak.multiplatform_swisstransfer.database.AppDatabase/1.json index 1706d3bc..dd1eb1f7 100644 --- a/STDatabase/schemas/com.infomaniak.multiplatform_swisstransfer.database.AppDatabase/1.json +++ b/STDatabase/schemas/com.infomaniak.multiplatform_swisstransfer.database.AppDatabase/1.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "53552e1c6a78205feeaf944f7da6005a", + "identityHash": "3802f1313df6c8c8e691e75e221bd42d", "entities": [ { "tableName": "TransferDB", @@ -92,7 +92,7 @@ }, { "tableName": "FileDB", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `path` TEXT NOT NULL, `size` INTEGER NOT NULL, `mimeType` TEXT, `isFolder` INTEGER NOT NULL, `thumbnailPath` TEXT, `transferId` TEXT NOT NULL, `folderId` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`transferId`) REFERENCES `TransferDB`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `path` TEXT NOT NULL, `size` INTEGER NOT NULL, `mimeType` TEXT, `isFolder` INTEGER NOT NULL, `thumbnailPath` TEXT, `transferId` TEXT NOT NULL, `parentId` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`transferId`) REFERENCES `TransferDB`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", "fields": [ { "fieldPath": "id", @@ -135,8 +135,8 @@ "notNull": true }, { - "fieldPath": "folderId", - "columnName": "folderId", + "fieldPath": "parentId", + "columnName": "parentId", "affinity": "TEXT" } ], @@ -174,7 +174,7 @@ ], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '53552e1c6a78205feeaf944f7da6005a')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3802f1313df6c8c8e691e75e221bd42d')" ] } } \ No newline at end of file diff --git a/STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/dao/TransferDao.kt b/STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/dao/TransferDao.kt index 942b7b65..3ef754d4 100644 --- a/STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/dao/TransferDao.kt +++ b/STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/dao/TransferDao.kt @@ -82,13 +82,13 @@ interface TransferDao { @Query("SELECT * FROM FileDB WHERE id=:fileId LIMIT 1") fun getFile(fileId: String): Flow - @Query("SELECT * FROM FileDB WHERE transferId=:transferId AND folderId IS NULL") + @Query("SELECT * FROM FileDB WHERE transferId=:transferId AND parentId IS NULL") suspend fun getTransferRootFiles(transferId: String): List - @Query("SELECT * FROM FileDB WHERE transferId=:transferId AND folderId=:folderId") + @Query("SELECT * FROM FileDB WHERE transferId=:transferId AND parentId=:folderId") fun getTransferFolderFiles(transferId: String, folderId: String?): Flow> - @Query("SELECT * FROM FileDB WHERE folderId=:folderId") + @Query("SELECT * FROM FileDB WHERE parentId=:folderId") fun getFilesByFolderId(folderId: String): Flow> //TODO[API-V2]: suspend fun getNotReadyTransfers(userId: Long): List diff --git a/STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/models/transfers/v2/FileDB.kt b/STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/models/transfers/v2/FileDB.kt index cd58c2ca..916f52c0 100644 --- a/STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/models/transfers/v2/FileDB.kt +++ b/STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/models/transfers/v2/FileDB.kt @@ -43,7 +43,7 @@ data class FileDB( override val thumbnailPath: String? = null, @ColumnInfo(index = true) val transferId: String, - val folderId: String? = null, + val parentId: String? = null, ) : File { @Ignore val children: List = emptyList() @@ -57,6 +57,6 @@ data class FileDB( mimeType = file.mimeType, isFolder = file.isFolder, transferId = transferId, - folderId = folderId + parentId = folderId ) } diff --git a/STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/utils/FileUtilsForApiV2.kt b/STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/utils/FileUtilsForApiV2.kt index 66d40bae..5c99d8fe 100644 --- a/STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/utils/FileUtilsForApiV2.kt +++ b/STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/utils/FileUtilsForApiV2.kt @@ -71,7 +71,7 @@ object FileUtilsForApiV2 { mimeType = null, isFolder = true, transferId = transferId, - folderId = parentId + parentId = parentId ).also { folderByPath[folderPath] = it out += it diff --git a/STDatabase/src/commonTest/kotlin/com/infomaniak/multiplatform_swisstransfer/database/FileUtilsForApiV2Test.kt b/STDatabase/src/commonTest/kotlin/com/infomaniak/multiplatform_swisstransfer/database/FileUtilsForApiV2Test.kt index 395c1b0f..b0413587 100644 --- a/STDatabase/src/commonTest/kotlin/com/infomaniak/multiplatform_swisstransfer/database/FileUtilsForApiV2Test.kt +++ b/STDatabase/src/commonTest/kotlin/com/infomaniak/multiplatform_swisstransfer/database/FileUtilsForApiV2Test.kt @@ -78,7 +78,7 @@ class FileUtilsForApiV2Test { assertEquals(700L, folder2.size, "folder2 size should be 700 (300 + 400)") assertEquals(50L, randomFolder.size, "randomFolder size should be 50") - val folder1Children = treeApiV2.filter { it.folderId == folder1.id && !it.isFolder } + val folder1Children = treeApiV2.filter { it.parentId == folder1.id && !it.isFolder } assertEquals(2, folder1Children.size, "folder1 should have 2 direct file children") } @@ -127,9 +127,9 @@ class FileUtilsForApiV2Test { assertNotNull(folderB, "folder 'a/b' should exist") assertNotNull(folderC, "folder 'a/b/c' should exist") - assertEquals(null, folderA.folderId, "folder 'a' should have no parent") - assertEquals(folderA.id, folderB.folderId, "folder 'a/b' should have 'a' as parent") - assertEquals(folderB.id, folderC.folderId, "folder 'a/b/c' should have 'a/b' as parent") + assertEquals(null, folderA.parentId, "folder 'a' should have no parent") + assertEquals(folderA.id, folderB.parentId, "folder 'a/b' should have 'a' as parent") + assertEquals(folderB.id, folderC.parentId, "folder 'a/b/c' should have 'a/b' as parent") } private fun checkFolders(path: String, size: Long) { From 2beb268c245a17c10c05f7ff79efe3e48a971bf8 Mon Sep 17 00:00:00 2001 From: Louis CAD Date: Thu, 19 Feb 2026 16:49:20 +0100 Subject: [PATCH 04/11] docs: Add KDoc to FileUtilsForApiV2.getFileDbTree Co-authored-by: Gibran Chevalley --- .../database/utils/FileUtilsForApiV2.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/utils/FileUtilsForApiV2.kt b/STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/utils/FileUtilsForApiV2.kt index 5c99d8fe..a28f2be4 100644 --- a/STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/utils/FileUtilsForApiV2.kt +++ b/STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/utils/FileUtilsForApiV2.kt @@ -25,6 +25,12 @@ import kotlin.uuid.Uuid object FileUtilsForApiV2 { + /** + * Takes a list of [files] that may be in a folder structure, according to their paths, + * and returns a set of [FileDB] objects that includes all these files, plus all the distinct + * folders, as [FileDB] instances with their [FileDB.isFolder] property set to `true`, + * and with their size reflecting the total size of their content. + */ fun getFileDbTree(transferId: String, files: List): Set { val folderByPath = HashMap(files.size) val out = ArraySet(files.size * 2) From 018d7024e3083dc575615794cb1f428f91d33144 Mon Sep 17 00:00:00 2001 From: Louis CAD Date: Thu, 19 Feb 2026 17:33:24 +0100 Subject: [PATCH 05/11] refactor: Split looping logic from the rest when traversing folders Co-authored-by: Gibran Chevalley --- .../database/utils/FileUtilsForApiV2.kt | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/utils/FileUtilsForApiV2.kt b/STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/utils/FileUtilsForApiV2.kt index a28f2be4..a395f325 100644 --- a/STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/utils/FileUtilsForApiV2.kt +++ b/STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/utils/FileUtilsForApiV2.kt @@ -53,22 +53,8 @@ object FileUtilsForApiV2 { if (lastSlash <= 0) return null // No parent folder (root file) var parentId: String? = null - val pathBuilder = StringBuilder(lastSlash) - var currentIndex = 0 - while (currentIndex < lastSlash) { - val nextSlash = filePath.indexOf('/', currentIndex).takeIf { it != -1 } ?: lastSlash - - // Skip duplicated separators - if (nextSlash == currentIndex) { - currentIndex++ - continue - } - - if (pathBuilder.isNotEmpty()) pathBuilder.append('/') - pathBuilder.append(filePath, currentIndex, nextSlash) - - val folderPath = pathBuilder.toString() + filePath.loopOverParentFolders { folderPath -> val folder = folderByPath[folderPath] ?: generateFolderId(transferId).let { newId -> FileDB( id = newId, @@ -87,11 +73,29 @@ object FileUtilsForApiV2 { folder.size += fileSize parentId = folder.id - currentIndex = nextSlash + 1 } + return parentId } + /** + * Will loop over parent folders based on an input path in the form of a string + * + * "abc/def/ghi/jkl.txt" will give "abc" then "abc/def" and then "abc/def/ghi" + */ + private inline fun String.loopOverParentFolders(action: (String) -> Unit) { + val directoryPath = this.substringBeforeLast('/', "") + if (directoryPath.isEmpty()) return + + var index = directoryPath.indexOf('/') + while (index != -1) { + action(directoryPath.substring(0, index)) + index = directoryPath.indexOf('/', index + 1) + } + + action(directoryPath) + } + @OptIn(ExperimentalUuidApi::class) private fun generateFolderId(transferId: String): String { return "$transferId:${Uuid.random()}" From 2ca1a41947e7664c7067d16662029e1fd35f3943 Mon Sep 17 00:00:00 2001 From: Louis CAD Date: Mon, 23 Feb 2026 15:32:58 +0100 Subject: [PATCH 06/11] chore: Use getOrPut --- .../database/utils/FileUtilsForApiV2.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/utils/FileUtilsForApiV2.kt b/STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/utils/FileUtilsForApiV2.kt index a395f325..dbec89b6 100644 --- a/STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/utils/FileUtilsForApiV2.kt +++ b/STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/utils/FileUtilsForApiV2.kt @@ -55,7 +55,8 @@ object FileUtilsForApiV2 { var parentId: String? = null filePath.loopOverParentFolders { folderPath -> - val folder = folderByPath[folderPath] ?: generateFolderId(transferId).let { newId -> + val folder: FileDB = folderByPath.getOrPut(folderPath) { + val newId = generateFolderId(transferId) FileDB( id = newId, path = folderPath, From cfc405ea4885c37b808d9938a880c9edf9a41b30 Mon Sep 17 00:00:00 2001 From: Louis CAD Date: Mon, 23 Feb 2026 15:33:18 +0100 Subject: [PATCH 07/11] docs: Make some KDoc easier to read --- .../database/utils/FileUtilsForApiV2.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/utils/FileUtilsForApiV2.kt b/STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/utils/FileUtilsForApiV2.kt index dbec89b6..3c2a223c 100644 --- a/STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/utils/FileUtilsForApiV2.kt +++ b/STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/utils/FileUtilsForApiV2.kt @@ -82,7 +82,10 @@ object FileUtilsForApiV2 { /** * Will loop over parent folders based on an input path in the form of a string * - * "abc/def/ghi/jkl.txt" will give "abc" then "abc/def" and then "abc/def/ghi" + * `"abc/def/ghi/jkl.txt"` will lead [action] to be called in order with this: + * - `"abc"` + * - `"abc/def"` + * - `"abc/def/ghi"` */ private inline fun String.loopOverParentFolders(action: (String) -> Unit) { val directoryPath = this.substringBeforeLast('/', "") From a16b3651762f6c176b087c84f6ccb371a5046eb5 Mon Sep 17 00:00:00 2001 From: Louis CAD Date: Mon, 23 Feb 2026 15:33:59 +0100 Subject: [PATCH 08/11] refactor: Simplify loopOverParentFolders with forEachSubstring --- .../database/StringExtensions.kt | 41 +++++++++++++++++++ .../database/utils/FileUtilsForApiV2.kt | 15 ++----- .../database/StringExtensionsTest.kt | 37 +++++++++++++++++ 3 files changed, 82 insertions(+), 11 deletions(-) create mode 100644 STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/StringExtensions.kt create mode 100644 STDatabase/src/commonTest/kotlin/com/infomaniak/multiplatform_swisstransfer/database/StringExtensionsTest.kt diff --git a/STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/StringExtensions.kt b/STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/StringExtensions.kt new file mode 100644 index 00000000..119bc86b --- /dev/null +++ b/STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/StringExtensions.kt @@ -0,0 +1,41 @@ +/* + * Infomaniak SwissTransfer - Multiplatform + * Copyright (C) 2026 Infomaniak Network SA + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.infomaniak.multiplatform_swisstransfer.database + +/** + * Example: + * + * For the input `"1/2/3/4/whatever"` and a [delimiter] set to `'/'`, [action] will be called with: + * - `"1"` + * - `"1/2"` + * - `"1/2/3"` + * - `"1/2/3/4"` + * - `"1/2/3/4/whatever"` + */ +internal inline fun String.forEachSubstring( + delimiter: Char, + action: (substring: String) -> Unit +) { + var lastOccurrenceIndex = 0 + do { + lastOccurrenceIndex = indexOf(delimiter, startIndex = lastOccurrenceIndex) + val found = lastOccurrenceIndex != -1 + action(if (found) substring(0, lastOccurrenceIndex) else this) + lastOccurrenceIndex += 1 // Delimiter length, to skip the previous occurrence. + } while (found) +} diff --git a/STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/utils/FileUtilsForApiV2.kt b/STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/utils/FileUtilsForApiV2.kt index 3c2a223c..2f7038f2 100644 --- a/STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/utils/FileUtilsForApiV2.kt +++ b/STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/utils/FileUtilsForApiV2.kt @@ -19,6 +19,7 @@ package com.infomaniak.multiplatform_swisstransfer.database.utils import androidx.collection.ArraySet import com.infomaniak.multiplatform_swisstransfer.common.interfaces.transfers.v2.File +import com.infomaniak.multiplatform_swisstransfer.database.forEachSubstring import com.infomaniak.multiplatform_swisstransfer.database.models.transfers.v2.FileDB import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid @@ -87,17 +88,9 @@ object FileUtilsForApiV2 { * - `"abc/def"` * - `"abc/def/ghi"` */ - private inline fun String.loopOverParentFolders(action: (String) -> Unit) { - val directoryPath = this.substringBeforeLast('/', "") - if (directoryPath.isEmpty()) return - - var index = directoryPath.indexOf('/') - while (index != -1) { - action(directoryPath.substring(0, index)) - index = directoryPath.indexOf('/', index + 1) - } - - action(directoryPath) + private inline fun String.loopOverParentFolders(action: (folderPath: String) -> Unit) { + val directoryPath = substringBeforeLast('/', "").ifEmpty { return } + directoryPath.forEachSubstring(delimiter = '/') { action(it) } } @OptIn(ExperimentalUuidApi::class) diff --git a/STDatabase/src/commonTest/kotlin/com/infomaniak/multiplatform_swisstransfer/database/StringExtensionsTest.kt b/STDatabase/src/commonTest/kotlin/com/infomaniak/multiplatform_swisstransfer/database/StringExtensionsTest.kt new file mode 100644 index 00000000..1cfe1815 --- /dev/null +++ b/STDatabase/src/commonTest/kotlin/com/infomaniak/multiplatform_swisstransfer/database/StringExtensionsTest.kt @@ -0,0 +1,37 @@ +/* + * Infomaniak SwissTransfer - Multiplatform + * Copyright (C) 2026 Infomaniak Network SA + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.infomaniak.multiplatform_swisstransfer.database + +import com.infomaniak.multiplatform_swisstransfer.database.extensions.shouldBe +import kotlin.test.Test + +class StringExtensionsTest { + + @Test + fun `test forEachSubstring`() { + val list = mutableListOf() + "1/2/3/4/whatever".forEachSubstring(delimiter = '/') { list += it } + list shouldBe listOf( + "1", + "1/2", + "1/2/3", + "1/2/3/4", + "1/2/3/4/whatever", + ) + } +} From 305eee7833a150d0bf746a5eecb1a2326c0044fe Mon Sep 17 00:00:00 2001 From: Louis CAD Date: Mon, 23 Feb 2026 15:42:15 +0100 Subject: [PATCH 09/11] chore: Add clarifying comment --- .../database/utils/FileUtilsForApiV2.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/utils/FileUtilsForApiV2.kt b/STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/utils/FileUtilsForApiV2.kt index 2f7038f2..42e9cb9d 100644 --- a/STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/utils/FileUtilsForApiV2.kt +++ b/STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/utils/FileUtilsForApiV2.kt @@ -33,7 +33,7 @@ object FileUtilsForApiV2 { * and with their size reflecting the total size of their content. */ fun getFileDbTree(transferId: String, files: List): Set { - val folderByPath = HashMap(files.size) + val folderByPath = HashMap(files.size) // The value is mutable, but not the key, so we're safe. val out = ArraySet(files.size * 2) for (file in files) { From 0ec6dd5bc2cf7fe9f2f384988c3a0ee5f97b7e8a Mon Sep 17 00:00:00 2001 From: Louis CAD Date: Mon, 23 Feb 2026 15:52:34 +0100 Subject: [PATCH 10/11] chore: Rename ensureFolders to updateFoldersAndGetParentId and add KDoc --- .../database/utils/FileUtilsForApiV2.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/utils/FileUtilsForApiV2.kt b/STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/utils/FileUtilsForApiV2.kt index 42e9cb9d..b711494d 100644 --- a/STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/utils/FileUtilsForApiV2.kt +++ b/STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/utils/FileUtilsForApiV2.kt @@ -37,13 +37,17 @@ object FileUtilsForApiV2 { val out = ArraySet(files.size * 2) for (file in files) { - val parentId = ensureFolders(file.path, file.size, transferId, folderByPath, out) + val parentId = updateFoldersAndGetParentId(file.path, file.size, transferId, folderByPath, out) out += FileDB(file, transferId, parentId) } return out } - private fun ensureFolders( + /** + * Gets or puts the parent folders that this [filePath] references in both [folderByPath] & [out], + * updates their size while doing so, then, returns the id of the nearest (deepest/immediate) parent folder. + */ + private fun updateFoldersAndGetParentId( filePath: String, fileSize: Long, transferId: String, From 218e918c7dd993add5d331858d00b4a444fe54d4 Mon Sep 17 00:00:00 2001 From: Louis CAD Date: Thu, 26 Feb 2026 10:13:14 +0100 Subject: [PATCH 11/11] chore: Pass lambda parameter directly Co-authored-by: Gibran Chevalley --- .../database/utils/FileUtilsForApiV2.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/utils/FileUtilsForApiV2.kt b/STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/utils/FileUtilsForApiV2.kt index b711494d..091727b3 100644 --- a/STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/utils/FileUtilsForApiV2.kt +++ b/STDatabase/src/commonMain/kotlin/com/infomaniak/multiplatform_swisstransfer/database/utils/FileUtilsForApiV2.kt @@ -94,7 +94,7 @@ object FileUtilsForApiV2 { */ private inline fun String.loopOverParentFolders(action: (folderPath: String) -> Unit) { val directoryPath = substringBeforeLast('/', "").ifEmpty { return } - directoryPath.forEachSubstring(delimiter = '/') { action(it) } + directoryPath.forEachSubstring(delimiter = '/', action) } @OptIn(ExperimentalUuidApi::class)