Skip to content

Commit 22e12ff

Browse files
committed
wip
Signed-off-by: alperozturk96 <alper_ozturk@proton.me>
1 parent 0a10115 commit 22e12ff

7 files changed

Lines changed: 632 additions & 14 deletions

File tree

app/src/main/java/com/nextcloud/utils/extensions/FileDataStorageManagerExtensions.kt

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ fun FileDataStorageManager.getNonEncryptedSubfolders(id: Long, accountName: Stri
6464
suspend fun FileDataStorageManager.getCapabilitiesByAccountName(accountName: String): OCCapability =
6565
capabilityDao.getByAccountName(accountName).toOCCapability()
6666

67-
fun FileDataStorageManager.moveLocalFile(ocFile: OCFile?, targetPath: String, targetParentPath: String) {
67+
fun FileDataStorageManager.moveFiles(ocFile: OCFile?, targetPath: String, targetParentPath: String) {
6868
Log_OC.d(
6969
FileDataStorageManager.TAG,
7070
("moveLocalFile ==> ocFile: "
@@ -111,11 +111,23 @@ fun FileDataStorageManager.moveLocalFile(ocFile: OCFile?, targetPath: String, ta
111111
val defaultSavePath = FileStorageUtils.getSavePath(accountName)
112112

113113
val originalMediaPaths =
114-
fileDao.moveFileEntities(oldPath, targetPath, defaultSavePath, targetParent.getFileId(), accountName)
114+
fileDao.moveFilesInDb(oldPath, targetPath, defaultSavePath, targetParent.fileId, accountName)
115115

116+
moveLocalFiles(accountName, ocFile, defaultSavePath, targetPath)
117+
118+
for (originalMediaPath in originalMediaPaths) {
119+
deleteFileInMediaScan(originalMediaPath)
120+
val newMediaPath = defaultSavePath + targetPath + originalMediaPath.substring(
121+
(defaultSavePath + oldPath).length
122+
)
123+
FileDataStorageManager.triggerMediaScan(newMediaPath)
124+
}
125+
}
126+
127+
private fun moveLocalFiles(accountName: String, ocFile: OCFile, defaultSavePath: String, targetPath: String) {
116128
val localFile = File(FileStorageUtils.getDefaultSavePathFor(accountName, ocFile))
117129
if (!localFile.exists()) {
118-
Log_OC.d(FileDataStorageManager.TAG, "moveLocalFile: no local file to move at " + localFile.getAbsolutePath())
130+
Log_OC.d(FileDataStorageManager.TAG, "moveLocalFile: no local file to move at " + localFile.absolutePath)
119131
return
120132
}
121133

@@ -135,18 +147,9 @@ fun FileDataStorageManager.moveLocalFile(ocFile: OCFile?, targetPath: String, ta
135147
)
136148
return
137149
}
138-
139-
for (originalMediaPath in originalMediaPaths) {
140-
deleteFileInMediaScan(originalMediaPath)
141-
val newMediaPath = defaultSavePath + targetPath + originalMediaPath.substring(
142-
(defaultSavePath + oldPath).length
143-
)
144-
FileDataStorageManager.triggerMediaScan(newMediaPath)
145-
}
146150
}
147151

148-
149-
private fun FileDao.moveFileEntities(
152+
private fun FileDao.moveFilesInDb(
150153
oldPath: String,
151154
targetPath: String,
152155
defaultSavePath: String,

app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1110,7 +1110,7 @@ private boolean removeLocalFolder(File localFolder) {
11101110
* Updates database and file system for a file or folder that was moved to a different location.
11111111
*/
11121112
public void moveLocalFile(OCFile ocFile, String targetPath, String targetParentPath) {
1113-
FileDataStorageManagerExtensionsKt.moveLocalFile(this, ocFile, targetPath, targetParentPath);
1113+
FileDataStorageManagerExtensionsKt.moveFiles(this, ocFile, targetPath, targetParentPath);
11141114
}
11151115

11161116
public void copyLocalFile(OCFile ocFile, String targetPath) {
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*
2+
* Nextcloud - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
package com.owncloud.android.datamodel
8+
9+
import com.nextcloud.client.database.entity.FileEntity
10+
import io.mockk.every
11+
import io.mockk.slot
12+
import io.mockk.verify
13+
import org.junit.Assert.assertEquals
14+
import org.junit.Assert.assertNull
15+
import org.junit.Test
16+
17+
@Suppress("TooManyFunctions")
18+
class MoveFileEntitiesUpdateTest : MoveFilesTestBase() {
19+
20+
private val capturedEntities = slot<List<FileEntity>>()
21+
22+
private fun arrangeAndMove(
23+
entities: List<FileEntity>,
24+
targetPath: String = TARGET_PATH
25+
) {
26+
stubTargetParent()
27+
every { mockFileDao.getFolderWithDescendants("${entities.first().path}%", ACCOUNT_NAME) } returns entities
28+
every { mockFileDao.updateAll(capture(capturedEntities)) } returns Unit
29+
val file = OCFile(entities.first().path!!).apply { fileId = 1 }
30+
manager.moveLocalFile(file, targetPath, TARGET_PARENT_PATH)
31+
}
32+
33+
@Test
34+
fun testMoveLocalFileWhenValidFileShouldCallUpdateAllOnFileDao() {
35+
val entities = listOf(createFileEntity(path = OLD_PATH))
36+
arrangeAndMove(entities)
37+
38+
verify(exactly = 1) { mockFileDao.updateAll(any()) }
39+
}
40+
41+
@Test
42+
fun testMoveLocalFileWhenValidFileShouldUpdatePathToTargetPath() {
43+
val entities = listOf(createFileEntity(path = OLD_PATH))
44+
arrangeAndMove(entities)
45+
46+
assertEquals(TARGET_PATH, capturedEntities.captured.single().path)
47+
}
48+
49+
@Test
50+
fun testMoveLocalFileWhenNonEncryptedFileShouldUpdatePathDecryptedToNewPath() {
51+
val entities = listOf(createFileEntity(path = OLD_PATH, pathDecrypted = OLD_PATH, isEncrypted = 0))
52+
arrangeAndMove(entities)
53+
54+
assertEquals(TARGET_PATH, capturedEntities.captured.single().pathDecrypted)
55+
}
56+
57+
@Test
58+
fun testMoveLocalFileWhenEncryptedFileShouldNotUpdatePathDecrypted() {
59+
val encryptedDecryptedPath = "/documents/encrypted_name"
60+
val entities = listOf(
61+
createFileEntity(path = OLD_PATH, pathDecrypted = encryptedDecryptedPath, isEncrypted = 1)
62+
)
63+
arrangeAndMove(entities)
64+
65+
assertEquals(encryptedDecryptedPath, capturedEntities.captured.single().pathDecrypted)
66+
}
67+
68+
@Test
69+
fun testMoveLocalFileWhenFileHasStoragePathUnderSavePathShouldUpdateStoragePath() {
70+
val originalStorage = "$SAVE_PATH$OLD_PATH"
71+
val entities = listOf(createFileEntity(path = OLD_PATH, storagePath = originalStorage))
72+
arrangeAndMove(entities)
73+
74+
assertEquals("$SAVE_PATH$TARGET_PATH", capturedEntities.captured.single().storagePath)
75+
}
76+
77+
@Test
78+
fun testMoveLocalFileWhenFileHasStoragePathOutsideSavePathShouldKeepOriginalStoragePath() {
79+
val externalPath = "/sdcard/downloads/report.pdf"
80+
val entities = listOf(createFileEntity(path = OLD_PATH, storagePath = externalPath))
81+
arrangeAndMove(entities)
82+
83+
assertEquals(externalPath, capturedEntities.captured.single().storagePath)
84+
}
85+
86+
@Test
87+
fun testMoveLocalFileWhenFileHasNoStoragePathShouldKeepStoragePathNull() {
88+
val entities = listOf(createFileEntity(path = OLD_PATH, storagePath = null))
89+
arrangeAndMove(entities)
90+
91+
assertNull(capturedEntities.captured.single().storagePath)
92+
}
93+
94+
@Test
95+
fun testMoveLocalFileWhenMovingFileShouldUpdateParentIdToTargetParentId() {
96+
val targetParentId = 99L
97+
val entities = listOf(createFileEntity(path = OLD_PATH, parent = 10L))
98+
every { mockFileDao.getFolderWithDescendants("$OLD_PATH%", ACCOUNT_NAME) } returns entities
99+
every { mockFileDao.updateAll(capture(capturedEntities)) } returns Unit
100+
val parent = OCFile(TARGET_PARENT_PATH).apply {
101+
fileId = targetParentId
102+
mimeType = com.owncloud.android.utils.MimeType.DIRECTORY
103+
}
104+
every { manager.getFileByPath(TARGET_PARENT_PATH) } returns parent
105+
106+
manager.moveLocalFile(OCFile(OLD_PATH).apply { fileId = 1 }, TARGET_PATH, TARGET_PARENT_PATH)
107+
108+
assertEquals(targetParentId, capturedEntities.captured.single().parent)
109+
}
110+
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/*
2+
* Nextcloud - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
package com.owncloud.android.datamodel
8+
9+
import android.media.MediaScannerConnection
10+
import com.owncloud.android.utils.FileStorageUtils
11+
import io.mockk.every
12+
import io.mockk.verify
13+
import org.junit.After
14+
import org.junit.Before
15+
import org.junit.Test
16+
import java.io.File
17+
import java.nio.file.Files
18+
19+
@Suppress("TooManyFunctions")
20+
class MoveFilesFilesystemAndMediaTest : MoveFilesTestBase() {
21+
22+
private lateinit var tempDir: File
23+
24+
@Before
25+
fun setUpTempDir() {
26+
tempDir = Files.createTempDirectory("moveLocalFileTest").toFile()
27+
every { FileStorageUtils.getSavePath(any()) } returns tempDir.absolutePath
28+
every { FileStorageUtils.getDefaultSavePathFor(any(), any()) } answers {
29+
tempDir.absolutePath + secondArg<OCFile>().remotePath
30+
}
31+
}
32+
33+
@After
34+
fun tearDownTempDir() {
35+
tempDir.deleteRecursively()
36+
}
37+
38+
private fun doMove(
39+
oldPath: String = OLD_PATH,
40+
targetPath: String = TARGET_PATH,
41+
entities: List<com.nextcloud.client.database.entity.FileEntity> = emptyList()
42+
) {
43+
stubTargetParent()
44+
every { mockFileDao.getFolderWithDescendants("$oldPath%", ACCOUNT_NAME) } returns entities
45+
manager.moveLocalFile(OCFile(oldPath).apply { fileId = 1 }, targetPath, TARGET_PARENT_PATH)
46+
}
47+
48+
@Test
49+
fun testMoveLocalFileWhenNoLocalFilePresentShouldSkipRenameAndMediaScan() {
50+
doMove()
51+
52+
verify(exactly = 0) { manager.deleteFileInMediaScan(any()) }
53+
verify(exactly = 0) { MediaScannerConnection.scanFile(any(), any(), any(), any()) }
54+
}
55+
56+
@Test
57+
fun testMoveLocalFileWhenLocalFilePresentShouldRenameToTargetLocation() {
58+
val sourceFile = File("${tempDir.absolutePath}$OLD_PATH").also {
59+
it.parentFile?.mkdirs()
60+
it.createNewFile()
61+
}
62+
val targetFile = File("${tempDir.absolutePath}$TARGET_PATH")
63+
64+
doMove()
65+
66+
assert(!sourceFile.exists()) { "Source file should have been moved" }
67+
assert(targetFile.exists()) { "Target file should exist after rename" }
68+
}
69+
70+
@Test
71+
fun testMoveLocalFileWhenRenameFailsShouldNotTriggerMediaScan() {
72+
val oldStoragePath = "${tempDir.absolutePath}$OLD_PATH"
73+
// Source file is NOT created → renameTo returns false
74+
val mediaEntity = createFileEntity(
75+
path = OLD_PATH,
76+
storagePath = oldStoragePath,
77+
contentType = "image/jpeg"
78+
)
79+
doMove(entities = listOf(mediaEntity))
80+
81+
verify(exactly = 0) { manager.deleteFileInMediaScan(any()) }
82+
verify(exactly = 0) { MediaScannerConnection.scanFile(any(), any(), any(), any()) }
83+
}
84+
85+
@Test
86+
fun testMoveLocalFileWhenMediaFileIsMovedShouldDeleteFromMediaScanAtOriginalPath() {
87+
val oldStoragePath = "${tempDir.absolutePath}$OLD_PATH"
88+
File(oldStoragePath).also { it.parentFile?.mkdirs(); it.createNewFile() }
89+
val mediaEntity = createFileEntity(
90+
path = OLD_PATH,
91+
storagePath = oldStoragePath,
92+
contentType = "image/jpeg"
93+
)
94+
doMove(entities = listOf(mediaEntity))
95+
96+
verify(exactly = 1) { manager.deleteFileInMediaScan(oldStoragePath) }
97+
}
98+
99+
@Test
100+
fun testMoveLocalFileWhenMediaFileIsMovedShouldTriggerMediaScanAtNewStoragePath() {
101+
val savePath = tempDir.absolutePath
102+
val oldStoragePath = "$savePath$OLD_PATH"
103+
val expectedNewStoragePath = "$savePath$TARGET_PATH"
104+
File(oldStoragePath).also { it.parentFile?.mkdirs(); it.createNewFile() }
105+
val mediaEntity = createFileEntity(
106+
path = OLD_PATH,
107+
storagePath = oldStoragePath,
108+
contentType = "image/jpeg"
109+
)
110+
doMove(entities = listOf(mediaEntity))
111+
112+
verify {
113+
MediaScannerConnection.scanFile(
114+
any(),
115+
match { paths -> paths.any { it == expectedNewStoragePath } },
116+
any(),
117+
any()
118+
)
119+
}
120+
}
121+
122+
@Test
123+
fun testMoveLocalFileWhenNonMediaFileIsMovedShouldNotTriggerAnyMediaScan() {
124+
val oldStoragePath = "${tempDir.absolutePath}$OLD_PATH"
125+
File(oldStoragePath).also { it.parentFile?.mkdirs(); it.createNewFile() }
126+
val docEntity = createFileEntity(
127+
path = OLD_PATH,
128+
storagePath = oldStoragePath,
129+
contentType = "application/pdf"
130+
)
131+
doMove(entities = listOf(docEntity))
132+
133+
verify(exactly = 0) { manager.deleteFileInMediaScan(any()) }
134+
verify(exactly = 0) { MediaScannerConnection.scanFile(any(), any(), any(), any()) }
135+
}
136+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Nextcloud - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
package com.owncloud.android.datamodel
8+
9+
import com.owncloud.android.utils.MimeType
10+
import io.mockk.every
11+
import io.mockk.verify
12+
import org.junit.Test
13+
14+
@Suppress("TooManyFunctions")
15+
class MoveFilesGuardTest : MoveFilesTestBase() {
16+
17+
@Test
18+
fun testMoveLocalFileWhenFileIsNullShouldReturnEarlyWithoutInteractingWithDatabase() {
19+
manager.moveLocalFile(null, TARGET_PATH, TARGET_PARENT_PATH)
20+
21+
verify(exactly = 0) { mockFileDao.getFolderWithDescendants(any(), any()) }
22+
verify(exactly = 0) { mockFileDao.updateAll(any()) }
23+
}
24+
25+
@Test
26+
fun testMoveLocalFileWhenFileDoesNotExistShouldReturnEarlyWithoutInteractingWithDatabase() {
27+
val file = OCFile(OLD_PATH).apply { fileId = 0 }
28+
29+
manager.moveLocalFile(file, TARGET_PATH, TARGET_PARENT_PATH)
30+
31+
verify(exactly = 0) { mockFileDao.getFolderWithDescendants(any(), any()) }
32+
verify(exactly = 0) { mockFileDao.updateAll(any()) }
33+
}
34+
35+
@Test
36+
fun testMoveLocalFileWhenFileNameIsRootPathShouldReturnEarlyWithoutInteractingWithDatabase() {
37+
val rootFile = OCFile(OCFile.ROOT_PATH).apply {
38+
fileId = 1
39+
mimeType = MimeType.DIRECTORY
40+
}
41+
42+
manager.moveLocalFile(rootFile, TARGET_PATH, TARGET_PARENT_PATH)
43+
44+
verify(exactly = 0) { mockFileDao.getFolderWithDescendants(any(), any()) }
45+
verify(exactly = 0) { mockFileDao.updateAll(any()) }
46+
}
47+
48+
@Test
49+
fun testMoveLocalFileWhenSourceAndTargetPathsAreIdenticalShouldReturnEarlyWithoutInteractingWithDatabase() {
50+
val file = OCFile(OLD_PATH).apply { fileId = 1 }
51+
52+
manager.moveLocalFile(file, OLD_PATH, TARGET_PARENT_PATH)
53+
54+
verify(exactly = 0) { mockFileDao.getFolderWithDescendants(any(), any()) }
55+
verify(exactly = 0) { mockFileDao.updateAll(any()) }
56+
}
57+
58+
@Test
59+
fun testMoveLocalFileWhenTargetParentNotFoundShouldReturnEarlyWithoutInteractingWithDatabase() {
60+
val file = OCFile(OLD_PATH).apply { fileId = 1 }
61+
// getFileByPath returns null by default from base setUp
62+
63+
manager.moveLocalFile(file, TARGET_PATH, TARGET_PARENT_PATH)
64+
65+
verify(exactly = 0) { mockFileDao.getFolderWithDescendants(any(), any()) }
66+
verify(exactly = 0) { mockFileDao.updateAll(any()) }
67+
}
68+
69+
@Test
70+
fun testMoveLocalFileWhenTargetParentIsNotFolderShouldReturnEarlyWithoutInteractingWithDatabase() {
71+
val file = OCFile(OLD_PATH).apply { fileId = 1 }
72+
val notAFolder = OCFile(TARGET_PARENT_PATH).apply {
73+
fileId = 99
74+
mimeType = "application/pdf"
75+
}
76+
every { manager.getFileByPath(TARGET_PARENT_PATH) } returns notAFolder
77+
78+
manager.moveLocalFile(file, TARGET_PATH, TARGET_PARENT_PATH)
79+
80+
verify(exactly = 0) { mockFileDao.getFolderWithDescendants(any(), any()) }
81+
verify(exactly = 0) { mockFileDao.updateAll(any()) }
82+
}
83+
}

0 commit comments

Comments
 (0)