mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 10:29:58 +02:00
fix(ios): support sandboxed file transfers
This commit is contained in:
@@ -35,11 +35,13 @@ actual fun rememberShareFilePicker(
|
||||
return
|
||||
}
|
||||
|
||||
val picker = UIDocumentPickerViewController(forOpeningContentTypes = listOf(UTTypeItem), asCopy = false)
|
||||
// The composer outlives this callback, so Rust imports a sandbox copy instead of a short-lived provider URL.
|
||||
val picker = UIDocumentPickerViewController(forOpeningContentTypes = listOf(UTTypeItem), asCopy = true)
|
||||
picker.allowsMultipleSelection = true
|
||||
val delegate = DocumentPickerDelegate(
|
||||
onFilesPicked = onFilesPicked,
|
||||
onError = onError,
|
||||
useFileSystemPaths = true,
|
||||
)
|
||||
retainedPickerDelegate = delegate
|
||||
picker.delegate = delegate
|
||||
@@ -54,7 +56,7 @@ actual fun rememberShareFilePicker(
|
||||
onError("Could not find an iOS view controller for the folder picker")
|
||||
return
|
||||
}
|
||||
val picker = UIDocumentPickerViewController(forOpeningContentTypes = listOf(UTTypeFolder), asCopy = false)
|
||||
val picker = UIDocumentPickerViewController(forOpeningContentTypes = listOf(UTTypeFolder), asCopy = true)
|
||||
val delegate = DocumentPickerDelegate(
|
||||
onFilesPicked = { folders ->
|
||||
val folder = folders.firstOrNull() ?: return@DocumentPickerDelegate
|
||||
@@ -66,6 +68,7 @@ actual fun rememberShareFilePicker(
|
||||
},
|
||||
onError = onError,
|
||||
forceDirectory = true,
|
||||
useFileSystemPaths = true,
|
||||
)
|
||||
retainedPickerDelegate = delegate
|
||||
picker.delegate = delegate
|
||||
@@ -115,6 +118,7 @@ private class DocumentPickerDelegate(
|
||||
private val onFilesPicked: (List<PickedShareFile>) -> Unit,
|
||||
private val onError: (String) -> Unit,
|
||||
private val forceDirectory: Boolean = false,
|
||||
private val useFileSystemPaths: Boolean = false,
|
||||
) : NSObject(), UIDocumentPickerDelegateProtocol {
|
||||
override fun documentPicker(controller: UIDocumentPickerViewController, didPickDocumentsAtURLs: List<*>) {
|
||||
val files = didPickDocumentsAtURLs.mapNotNull { raw ->
|
||||
@@ -132,10 +136,11 @@ private class DocumentPickerDelegate(
|
||||
if (didStartAccess) url.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
PickedShareFile(
|
||||
url.absoluteString ?: url.path.orEmpty(),
|
||||
if (useFileSystemPaths) url.path.orEmpty() else url.absoluteString ?: url.path.orEmpty(),
|
||||
displayName,
|
||||
sizeBytes,
|
||||
nativeFileIcon(url),
|
||||
isTemporaryCopy = useFileSystemPaths,
|
||||
isDirectory = forceDirectory,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,18 +2,25 @@ package com.vnidrop.app.core
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import platform.Foundation.NSFileManager
|
||||
import platform.Foundation.NSDocumentDirectory
|
||||
import platform.Foundation.NSSearchPathForDirectoriesInDomains
|
||||
import platform.Foundation.NSURL
|
||||
import platform.Foundation.NSUserDomainMask
|
||||
import platform.UIKit.UIApplication
|
||||
import uniffi.vnidrop.ReceiveOutputSink
|
||||
import uniffi.vnidrop.SourceKind
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
@Composable
|
||||
actual fun rememberFileSystemService(): FileSystemService =
|
||||
remember { IosFileSystemService() }
|
||||
|
||||
private class IosFileSystemService : FileSystemService {
|
||||
// App-owned Documents remains durable across launches; raw external picker URLs do not.
|
||||
override val supportsCustomReceiveFolders: Boolean = false
|
||||
|
||||
override fun defaultReceiveFolder(): ReceiveFolder {
|
||||
val path = NSSearchPathForDirectoriesInDomains(
|
||||
NSDocumentDirectory,
|
||||
@@ -43,6 +50,37 @@ private class IosFileSystemService : FileSystemService {
|
||||
|
||||
override fun createReceiveOutputSink(folder: ReceiveFolder): ReceiveOutputSink? = null
|
||||
|
||||
override suspend fun discardPickedFiles(files: List<PickedShareFile>) {
|
||||
files.asSequence()
|
||||
.filter(PickedShareFile::isTemporaryCopy)
|
||||
.map(PickedShareFile::value)
|
||||
.distinct()
|
||||
.forEach { path -> NSFileManager.defaultManager.removeItemAtPath(path, null) }
|
||||
}
|
||||
|
||||
override fun canRevealReceiveFolder(folder: ReceiveFolder): Boolean =
|
||||
folder.kind == ReceiveFolderKind.FileSystemPath &&
|
||||
folder.value.trimEnd('/') == defaultReceiveFolder().value.trimEnd('/')
|
||||
|
||||
override suspend fun revealReceiveFolder(folder: ReceiveFolder): Result<Unit> {
|
||||
if (!canRevealReceiveFolder(folder)) {
|
||||
return Result.failure(IllegalArgumentException("The receive folder is not VniDrop Documents"))
|
||||
}
|
||||
// Files can reveal app-owned Documents after the sharing keys in Info.plist are enabled.
|
||||
val url = NSURL.URLWithString("shareddocuments://${folder.value}")
|
||||
?: return Result.failure(IllegalStateException("The Files location URL is unavailable"))
|
||||
val opened = suspendCancellableCoroutine { continuation ->
|
||||
UIApplication.sharedApplication.openURL(url, emptyMap<Any?, Any>()) { success ->
|
||||
if (continuation.isActive) continuation.resume(success)
|
||||
}
|
||||
}
|
||||
return if (opened) {
|
||||
Result.success(Unit)
|
||||
} else {
|
||||
Result.failure(IllegalStateException("Could not open VniDrop Documents in Files"))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun sharePickedFiles(
|
||||
repository: CoreGateway,
|
||||
files: List<PickedShareFile>,
|
||||
@@ -51,15 +89,7 @@ private class IosFileSystemService : FileSystemService {
|
||||
accessPolicy: ShareAccessPolicy,
|
||||
): Result<Share> {
|
||||
require(files.isNotEmpty()) { "Select at least one file to share" }
|
||||
val sources = files.map { file ->
|
||||
uniffi.vnidrop.ShareSource(
|
||||
kind = uniffi.vnidrop.SourceKind.IOS_SECURITY_SCOPED_URL,
|
||||
value = file.value,
|
||||
displayName = file.displayName,
|
||||
isDirectory = file.isDirectory,
|
||||
)
|
||||
}
|
||||
return repository.shareSources(sources, transferName, senderName, accessPolicy)
|
||||
return repository.shareSources(files.map(PickedShareFile::toIosShareSource), transferName, senderName, accessPolicy)
|
||||
}
|
||||
|
||||
private fun validateSecurityScopedUrl(value: String): FolderAccessStatus {
|
||||
@@ -77,3 +107,10 @@ private class IosFileSystemService : FileSystemService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun PickedShareFile.toIosShareSource() = uniffi.vnidrop.ShareSource(
|
||||
kind = SourceKind.PATH,
|
||||
value = value,
|
||||
displayName = displayName,
|
||||
isDirectory = isDirectory,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user