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 DocumentId.encodeFolder(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 DocumentId.encodeFolder(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 @@ -14,7 +14,9 @@ import android.util.Log
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
import java.util.concurrent.Executors

Expand Down Expand Up @@ -81,12 +83,7 @@ class InternxtDocumentsProvider : DocumentsProvider() {

val api = apiClient(op = "queryDocument")
val row = if (api == null) null else try {
when (decoded?.kind) {
DocumentId.Kind.FOLDER -> api.getFolder(uuid)?.let { DocumentRowBuilder.folderRow(it) }
DocumentId.Kind.FILE -> api.getFile(uuid)?.let { DocumentRowBuilder.fileRow(it) }
null -> api.getFolder(uuid)?.let { DocumentRowBuilder.folderRow(it) }
?: api.getFile(uuid)?.let { DocumentRowBuilder.fileRow(it) }
}
fetchDocumentRow(api, decoded?.kind, uuid)
} catch (e: InternxtApiException) {
Log.w(TAG, "queryDocument id=$id failed: ${e.javaClass.simpleName}: ${e.message}")
null
Expand All @@ -96,6 +93,17 @@ class InternxtDocumentsProvider : DocumentsProvider() {
return cursor
}

private fun fetchDocumentRow(
api: InternxtApiClient,
kind: DocumentId.Kind?,
uuid: String,
): Map<String, Any?>? = when (kind) {
DocumentId.Kind.FOLDER -> api.getFolder(uuid)?.let(DocumentRowBuilder::folderRow)
DocumentId.Kind.FILE -> api.getFile(uuid)?.let(DocumentRowBuilder::fileRow)
null -> api.getFolder(uuid)?.let(DocumentRowBuilder::folderRow)
?: api.getFile(uuid)?.let(DocumentRowBuilder::fileRow)
}

override fun queryChildDocuments(
parentDocumentId: String?,
projection: Array<String>?,
Expand Down Expand Up @@ -200,6 +208,131 @@ class InternxtDocumentsProvider : DocumentsProvider() {
row.forEach { (column, value) -> builder.add(column, value) }
}

override fun renameDocument(documentId: String, displayName: String): String? =
mutate("renameDocument", documentId) { api, kind, uuid ->
val parent = parentUuidOf(api, kind, uuid)
when (kind) {
DocumentId.Kind.FILE -> api.renameFile(uuid, displayName)
DocumentId.Kind.FOLDER -> api.renameFolder(uuid, displayName)
}
notifyEncodedParent(parent) { encoded ->
patchRowDisplayName(encoded, documentId, displayName)
}
null
}

override fun moveDocument(
sourceDocumentId: String,
sourceParentDocumentId: String?,
targetParentDocumentId: String,
): String? = mutate("moveDocument", sourceDocumentId) { api, kind, uuid ->
val targetUuid = rawUuid(targetParentDocumentId)
when (kind) {
DocumentId.Kind.FILE -> api.moveFile(uuid, targetUuid)
DocumentId.Kind.FOLDER -> api.moveFolder(uuid, targetUuid)
}
sourceParentDocumentId?.let {
removeRow(it, sourceDocumentId)
notifyChildren(it)
}
invalidateChildren(targetParentDocumentId)
null
}

override fun deleteDocument(documentId: String) {
mutate("deleteDocument", documentId) { api, kind, uuid ->
val parent = parentUuidOf(api, kind, uuid)
api.sendToTrash(listOf(TrashItem(uuid, trashTypeOf(kind))))
notifyEncodedParent(parent) { encoded ->
removeRow(encoded, documentId)
}
}
}

private inline fun <R> mutate(
op: String,
documentId: String,
block: (api: InternxtApiClient, kind: DocumentId.Kind, uuid: String) -> R,
): R {
val api = apiClient(op) ?: throw FileNotFoundException("No auth")
val kind = resolveKind(api, documentId) ?: throw FileNotFoundException("Not found: $documentId")
return try {
block(api, kind, rawUuid(documentId))
} catch (e: InternxtApiException) {
Log.w(TAG, "$op $documentId failed: ${e.javaClass.simpleName}: ${e.message}")
throw FileNotFoundException(e.message)
}
}

private fun parentUuidOf(api: InternxtApiClient, kind: DocumentId.Kind, uuid: String): String? =
when (kind) {
DocumentId.Kind.FILE -> api.getFile(uuid)?.folderUuid
DocumentId.Kind.FOLDER -> api.getFolder(uuid)?.parentUuid
}

private fun trashTypeOf(kind: DocumentId.Kind): TrashItem.Type = when (kind) {
DocumentId.Kind.FILE -> TrashItem.Type.FILE
DocumentId.Kind.FOLDER -> TrashItem.Type.FOLDER
}

private inline fun notifyEncodedParent(rawParentUuid: String?, mutateCache: (encodedParent: String) -> Unit) {
rawParentUuid?.let {
val encoded = DocumentId.encodeFolder(it)
mutateCache(encoded)
notifyChildren(encoded)
}
}

private fun rawUuid(documentId: String): String =
DocumentId.decode(documentId)?.uuid ?: documentId

private fun resolveKind(api: InternxtApiClient, documentId: String): DocumentId.Kind? {
DocumentId.decode(documentId)?.kind?.let { return it }
return api.getFolder(documentId)?.let { DocumentId.Kind.FOLDER }
?: api.getFile(documentId)?.let { DocumentId.Kind.FILE }
}

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

private fun invalidateChildren(parentDocumentId: String) {
folderLoads.remove(parentDocumentId)
notifyChildren(parentDocumentId)
}

private fun patchRowDisplayName(parentDocumentId: String, documentId: String, displayName: String) {
updateRows(parentDocumentId) { rows ->
val idx = rows.indexOfFirst { it[Document.COLUMN_DOCUMENT_ID] == documentId }
if (idx >= 0) rows[idx] = rows[idx] + (Document.COLUMN_DISPLAY_NAME to displayName)
}
}

private fun removeRow(parentDocumentId: String, documentId: String) {
updateRows(parentDocumentId) { rows ->
rows.removeAll { it[Document.COLUMN_DOCUMENT_ID] == documentId }
}
}

private inline fun updateRows(
parentDocumentId: String,
action: (MutableList<Map<String, Any?>>) -> Unit,
) {
val load = folderLoads[parentDocumentId] ?: return
synchronized(load) { action(load.rows) }
}

override fun refresh(uri: Uri, args: Bundle?, cancellationSignal: CancellationSignal?): Boolean {
val documentId = try { DocumentsContract.getDocumentId(uri) } catch (_: Exception) { null }
Log.d(TAG, "refresh uri=$uri documentId=$documentId")
if (documentId == null) return false
invalidateChildren(documentId)
return true
}

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 @@ -240,7 +240,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