Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,30 @@ import java.time.format.DateTimeParseException

object DocumentRowBuilder {

private const val FOLDER_FLAGS = Document.FLAG_DIR_SUPPORTS_CREATE
private const val FILE_FLAGS = 0
private const val MUTATION_FLAGS =
Document.FLAG_SUPPORTS_RENAME or
Document.FLAG_SUPPORTS_DELETE or
Document.FLAG_SUPPORTS_MOVE

fun folderRow(folder: DriveFolder): Map<String, Any?> = folderRow(
uuid = folder.uuid,
displayName = folder.plainName,
lastModified = parseIsoToMillis(folder.updatedAt),
private const val FOLDER_FLAGS_BASIC = Document.FLAG_DIR_SUPPORTS_CREATE
private const val FOLDER_FLAGS = FOLDER_FLAGS_BASIC or MUTATION_FLAGS
private const val FILE_FLAGS = MUTATION_FLAGS

fun folderRow(folder: DriveFolder): Map<String, Any?> = mapOf(
Document.COLUMN_DOCUMENT_ID to folder.uuid,
Document.COLUMN_MIME_TYPE to Document.MIME_TYPE_DIR,
Document.COLUMN_DISPLAY_NAME to folder.plainName,
Document.COLUMN_LAST_MODIFIED to parseIsoToMillis(folder.updatedAt),
Document.COLUMN_FLAGS to FOLDER_FLAGS,
Document.COLUMN_SIZE to null,
)

fun folderRow(uuid: String, displayName: String, lastModified: Long? = null): Map<String, Any?> = mapOf(
Document.COLUMN_DOCUMENT_ID to uuid,
Document.COLUMN_MIME_TYPE to Document.MIME_TYPE_DIR,
Document.COLUMN_DISPLAY_NAME to displayName,
Document.COLUMN_LAST_MODIFIED to lastModified,
Document.COLUMN_FLAGS to FOLDER_FLAGS,
Document.COLUMN_FLAGS to FOLDER_FLAGS_BASIC,
Document.COLUMN_SIZE to null,
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,17 @@
import com.internxt.cloud.R
import com.internxt.cloud.documents.api.InternxtApiClient
import com.internxt.cloud.documents.api.InternxtApiException
import com.internxt.cloud.documents.api.model.TrashItem
import com.internxt.cloud.documents.auth.InternxtAuthManager
import java.io.FileNotFoundException
import java.util.concurrent.ConcurrentHashMap

class InternxtDocumentsProvider : DocumentsProvider() {

private lateinit var authManager: InternxtAuthManager
private val itemKinds = ConcurrentHashMap<String, ItemKind>()

private enum class ItemKind { FILE, FOLDER }

override fun onCreate(): Boolean {
authManager = InternxtAuthManager.create(context!!.applicationContext)
Expand Down Expand Up @@ -57,8 +63,13 @@

val api = apiClient(op = "queryDocument")
val row = if (api == null) null else try {
api.getFolder(id)?.let { DocumentRowBuilder.folderRow(it) }
?: api.getFile(id)?.let { DocumentRowBuilder.fileRow(it) }
api.getFolder(id)?.let {
itemKinds[id] = ItemKind.FOLDER
DocumentRowBuilder.folderRow(it)
} ?: api.getFile(id)?.let {
itemKinds[id] = ItemKind.FILE
DocumentRowBuilder.fileRow(it)
}
} catch (e: InternxtApiException) {
Log.w(TAG, "queryDocument id=$id failed: ${e.javaClass.simpleName}: ${e.message}")
null
Expand Down Expand Up @@ -86,9 +97,11 @@

try {
paginate({ offset, size -> api.listFolderFolders(parent, offset, size) }) {
itemKinds[it.uuid] = ItemKind.FOLDER
cursor.addDocumentRow(DocumentRowBuilder.folderRow(it))
}
paginate({ offset, size -> api.listFolderFiles(parent, offset, size) }) {
itemKinds[it.uuid] = ItemKind.FILE
cursor.addDocumentRow(DocumentRowBuilder.fileRow(it))
}
Log.d(TAG, "queryChildDocuments parent=$parent rows=${cursor.count}")
Expand Down Expand Up @@ -125,6 +138,81 @@
row.forEach { (column, value) -> builder.add(column, value) }
}

override fun renameDocument(documentId: String, displayName: String): String? {
val api = apiClient(op = "renameDocument") ?: throw FileNotFoundException("No auth")

Check failure on line 142 in android/app/src/main/java/com/internxt/cloud/documents/InternxtDocumentsProvider.kt

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "No auth" 3 times.

See more on https://sonarcloud.io/project/issues?id=internxt_drive-mobile&issues=AZ3e2NSry0CUDAcuJ5E1&open=AZ3e2NSry0CUDAcuJ5E1&pullRequest=440
val kind = resolveKind(api, documentId) ?: throw FileNotFoundException("Not found: $documentId")
val parentUuid: String? = try {
when (kind) {
ItemKind.FILE -> {
val parent = api.getFile(documentId)?.folderUuid
api.renameFile(documentId, displayName)
parent
}
ItemKind.FOLDER -> {
val parent = api.getFolder(documentId)?.parentUuid
api.renameFolder(documentId, displayName)
parent
}
}
} catch (e: InternxtApiException) {
Log.w(TAG, "renameDocument $documentId failed: ${e.javaClass.simpleName}: ${e.message}")
throw FileNotFoundException(e.message)
}
parentUuid?.let { notifyChildren(it) }
return null
}

override fun moveDocument(
sourceDocumentId: String,
sourceParentDocumentId: String?,
targetParentDocumentId: String
): String? {
val api = apiClient(op = "moveDocument") ?: throw FileNotFoundException("No auth")
val kind = resolveKind(api, sourceDocumentId) ?: throw FileNotFoundException("Not found: $sourceDocumentId")
try {
when (kind) {
ItemKind.FILE -> api.moveFile(sourceDocumentId, targetParentDocumentId)
ItemKind.FOLDER -> api.moveFolder(sourceDocumentId, targetParentDocumentId)
}
} catch (e: InternxtApiException) {
Log.w(TAG, "moveDocument $sourceDocumentId failed: ${e.javaClass.simpleName}: ${e.message}")
throw FileNotFoundException(e.message)
}
sourceParentDocumentId?.let { notifyChildren(it) }
notifyChildren(targetParentDocumentId)
return null
}

override fun deleteDocument(documentId: String) {
val api = apiClient(op = "deleteDocument") ?: throw FileNotFoundException("No auth")
val kind = resolveKind(api, documentId) ?: throw FileNotFoundException("Not found: $documentId")
val parentUuid: String? = when (kind) {
ItemKind.FILE -> api.getFile(documentId)?.folderUuid
ItemKind.FOLDER -> api.getFolder(documentId)?.parentUuid
}
val trashType = if (kind == ItemKind.FILE) TrashItem.Type.FILE else TrashItem.Type.FOLDER
try {
api.sendToTrash(listOf(TrashItem(documentId, trashType)))
} catch (e: InternxtApiException) {
Log.w(TAG, "deleteDocument $documentId failed: ${e.javaClass.simpleName}: ${e.message}")
throw FileNotFoundException(e.message)
}
itemKinds.remove(documentId)
parentUuid?.let { notifyChildren(it) }
}

private fun resolveKind(api: InternxtApiClient, uuid: String): ItemKind? =
itemKinds[uuid]
?: api.getFolder(uuid)?.let { itemKinds[uuid] = ItemKind.FOLDER; ItemKind.FOLDER }
?: api.getFile(uuid)?.let { itemKinds[uuid] = ItemKind.FILE; ItemKind.FILE }

private fun notifyChildren(parentUuid: String) {
context?.contentResolver?.notifyChange(
DocumentsContract.buildChildDocumentsUri(AUTHORITY, parentUuid),
null
)
}

override fun openDocument(
documentId: String?,
mode: String?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,20 +68,20 @@ class InternxtApiClient(
return parseFolder(executeApiRequest(req))
}

fun renameFile(fileUuid: String, newName: String): DriveFile {
val payload = JSONObject().put("name", newName)
val req = driveRequest(driveUrl("files/$fileUuid"))
.patch(payload.toString().toRequestBody(JSON))
fun renameFile(fileUuid: String, newName: String) {
val payload = JSONObject().put("plainName", newName)
val req = driveRequest(driveUrl("files/$fileUuid/meta"))
.put(payload.toString().toRequestBody(JSON))
.build()
return parseFile(executeApiRequest(req))
executeApiRequest(req)
}

fun renameFolder(folderUuid: String, newName: String): DriveFolder {
val payload = JSONObject().put("name", newName)
val req = driveRequest(driveUrl("folders/$folderUuid"))
fun renameFolder(folderUuid: String, newName: String) {
val payload = JSONObject().put("plainName", newName)
val req = driveRequest(driveUrl("folders/$folderUuid/meta"))
.put(payload.toString().toRequestBody(JSON))
.build()
return parseFolder(executeApiRequest(req))
executeApiRequest(req)
}

fun moveFile(fileUuid: String, destinationFolderUuid: String): DriveFile {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ class DocumentRowBuilderTest {
assertEquals(Document.MIME_TYPE_DIR, row[Document.COLUMN_MIME_TYPE])
assertEquals("Documents", row[Document.COLUMN_DISPLAY_NAME])
assertEquals(1768089600000L, row[Document.COLUMN_LAST_MODIFIED])
assertEquals(Document.FLAG_DIR_SUPPORTS_CREATE, row[Document.COLUMN_FLAGS])
val expectedFolderFlags = Document.FLAG_DIR_SUPPORTS_CREATE or
Document.FLAG_SUPPORTS_RENAME or
Document.FLAG_SUPPORTS_DELETE or
Document.FLAG_SUPPORTS_MOVE
assertEquals(expectedFolderFlags, row[Document.COLUMN_FLAGS])
assertNull(row[Document.COLUMN_SIZE])
}

Expand All @@ -50,7 +54,10 @@ class DocumentRowBuilderTest {
assertEquals("application/pdf", row[Document.COLUMN_MIME_TYPE])
assertEquals("report.pdf", row[Document.COLUMN_DISPLAY_NAME])
assertEquals(1768089600000L, row[Document.COLUMN_LAST_MODIFIED])
assertEquals(0, row[Document.COLUMN_FLAGS])
val expectedFileFlags = Document.FLAG_SUPPORTS_RENAME or
Document.FLAG_SUPPORTS_DELETE or
Document.FLAG_SUPPORTS_MOVE
assertEquals(expectedFileFlags, row[Document.COLUMN_FLAGS])
assertEquals(102400L, row[Document.COLUMN_SIZE])
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.internxt.cloud.documents.api

import com.internxt.cloud.documents.api.model.TrashItem
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import okhttp3.mockwebserver.SocketPolicy
Expand Down Expand Up @@ -74,7 +75,7 @@

assertEquals(1, files.size)
val file = files[0]
assertEquals("file-uuid-1", file.uuid)

Check failure on line 78 in android/app/src/test/java/com/internxt/cloud/documents/api/InternxtApiClientTest.kt

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "file-uuid-1" 7 times.

See more on https://sonarcloud.io/project/issues?id=internxt_drive-mobile&issues=AZ3e2NYUy0CUDAcuJ5E2&open=AZ3e2NYUy0CUDAcuJ5E2&pullRequest=440
assertEquals("report.pdf", file.plainName)
assertEquals("pdf", file.type)
assertEquals(102400L, file.size)
Expand Down Expand Up @@ -181,7 +182,7 @@

assertEquals(1, folders.size)
val folder = folders[0]
assertEquals("folder-uuid-1", folder.uuid)

Check failure on line 185 in android/app/src/test/java/com/internxt/cloud/documents/api/InternxtApiClientTest.kt

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "folder-uuid-1" 7 times.

See more on https://sonarcloud.io/project/issues?id=internxt_drive-mobile&issues=AZ3e2NYUy0CUDAcuJ5E3&open=AZ3e2NYUy0CUDAcuJ5E3&pullRequest=440
assertEquals("Documents", folder.plainName)
assertEquals(PARENT_UUID, folder.parentUuid)
assertEquals(BUCKET_ID, folder.bucket)
Expand Down Expand Up @@ -306,6 +307,76 @@
assertNull(client.getFile("missing-uuid"))
}

@Test
fun renameFilePutsPlainNameToMetaEndpoint() {
enqueueJson("")

client.renameFile("file-uuid-1", "renamed.pdf")

val recorded = server.takeRequest()
assertEquals("PUT", recorded.method)
assertEquals("/files/file-uuid-1/meta", recorded.path)
assertEquals("renamed.pdf", JSONObject(recorded.body.readUtf8()).getString("plainName"))
}

@Test
fun renameFolderPutsPlainNameToMetaEndpoint() {
enqueueJson("")

client.renameFolder("folder-uuid-1", "Renamed")

val recorded = server.takeRequest()
assertEquals("PUT", recorded.method)
assertEquals("/folders/folder-uuid-1/meta", recorded.path)
assertEquals("Renamed", JSONObject(recorded.body.readUtf8()).getString("plainName"))
}

@Test
fun moveFilePatchesDestinationPayload() {
enqueueJson("""{"uuid":"file-uuid-1","folderUuid":"$PARENT_UUID"}""")

client.moveFile("file-uuid-1", PARENT_UUID)

val recorded = server.takeRequest()
assertEquals("PATCH", recorded.method)
assertEquals("/files/file-uuid-1", recorded.path)
assertEquals(PARENT_UUID, JSONObject(recorded.body.readUtf8()).getString("destinationFolder"))
}

@Test
fun moveFolderPatchesDestinationPayload() {
enqueueJson("""{"uuid":"folder-uuid-1","parentUuid":"$PARENT_UUID"}""")

client.moveFolder("folder-uuid-1", PARENT_UUID)

val recorded = server.takeRequest()
assertEquals("PATCH", recorded.method)
assertEquals("/folders/folder-uuid-1", recorded.path)
assertEquals(PARENT_UUID, JSONObject(recorded.body.readUtf8()).getString("destinationFolder"))
}

@Test
fun sendToTrashPostsItemsPayload() {
enqueueJson("")

client.sendToTrash(
listOf(
TrashItem("file-uuid-1", TrashItem.Type.FILE),
TrashItem("folder-uuid-1", TrashItem.Type.FOLDER),
)
)

val recorded = server.takeRequest()
assertEquals("POST", recorded.method)
assertEquals("/storage/trash/add", recorded.path)
val items = JSONObject(recorded.body.readUtf8()).getJSONArray("items")
assertEquals(2, items.length())
assertEquals("file-uuid-1", items.getJSONObject(0).getString("uuid"))
assertEquals("file", items.getJSONObject(0).getString("type"))
assertEquals("folder-uuid-1", items.getJSONObject(1).getString("uuid"))
assertEquals("folder", items.getJSONObject(1).getString("type"))
}

@Test
fun serverErrorSurfacesAsApiError() {
enqueueJson("""{"error":"boom"}""", code = 500)
Expand Down
2 changes: 1 addition & 1 deletion src/store/slices/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ export const signOutThunk = createAsyncThunk<
const reason = payload.reason;
authService.signout(reason).catch(errorService.reportError);
drive.clear().catch(errorService.reportError);
clearCredentials().catch(errorService.reportError);
await clearCredentials().catch(errorService.reportError);
dispatch(uiActions.resetState());
dispatch(authActions.resetState());
dispatch(driveActions.resetState());
Expand Down
Loading