mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 02:29:55 +02:00
feat(core): add configurable share access policy
This commit is contained in:
@@ -26,6 +26,13 @@ impl AccessPolicy {
|
|||||||
self.modes.write().await.insert(transfer_id, mode);
|
self.modes.write().await.insert(transfer_id, mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn allows_without_approval(&self, transfer_id: u64) -> bool {
|
||||||
|
matches!(
|
||||||
|
self.modes.read().await.get(&transfer_id),
|
||||||
|
Some(TransferAccessMode::Public)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) async fn remove_transfer(&self, transfer_id: u64) {
|
pub(crate) async fn remove_transfer(&self, transfer_id: u64) {
|
||||||
self.modes.write().await.remove(&transfer_id);
|
self.modes.write().await.remove(&transfer_id);
|
||||||
self.approved_sessions
|
self.approved_sessions
|
||||||
|
|||||||
@@ -153,9 +153,10 @@ pub struct ShareMetadataInput {
|
|||||||
pub transfer_id: u64,
|
pub transfer_id: u64,
|
||||||
pub transfer_name: Option<String>,
|
pub transfer_name: Option<String>,
|
||||||
pub sender_name: Option<String>,
|
pub sender_name: Option<String>,
|
||||||
|
pub access_mode: TransferAccessMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Enum)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, uniffi::Enum)]
|
||||||
pub enum TransferAccessMode {
|
pub enum TransferAccessMode {
|
||||||
Public,
|
Public,
|
||||||
ApprovalRequired,
|
ApprovalRequired,
|
||||||
@@ -173,6 +174,7 @@ pub struct StoredTransfer {
|
|||||||
pub ticket: Option<String>,
|
pub ticket: Option<String>,
|
||||||
pub file_count: u64,
|
pub file_count: u64,
|
||||||
pub total_size: u64,
|
pub total_size: u64,
|
||||||
|
pub access_mode: TransferAccessMode,
|
||||||
pub created_at: i64,
|
pub created_at: i64,
|
||||||
pub updated_at: i64,
|
pub updated_at: i64,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,9 +120,18 @@ impl ApprovalService {
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(true) => {
|
Ok(true) => {
|
||||||
|
if self
|
||||||
|
.access_policy
|
||||||
|
.allows_without_approval(request.transfer_id)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
self.allow_without_sender_decision(remote_endpoint_id, request)
|
||||||
|
.await
|
||||||
|
} else {
|
||||||
self.wait_for_sender_decision(remote_endpoint_id, request)
|
self.wait_for_sender_decision(remote_endpoint_id, request)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
}
|
||||||
Ok(false) => {
|
Ok(false) => {
|
||||||
self.deny(request.transfer_id, remote_endpoint_id, "unknown-transfer")
|
self.deny(request.transfer_id, remote_endpoint_id, "unknown-transfer")
|
||||||
.await
|
.await
|
||||||
@@ -135,6 +144,26 @@ impl ApprovalService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn allow_without_sender_decision(
|
||||||
|
&self,
|
||||||
|
remote_endpoint_id: String,
|
||||||
|
request: RequestTransfer,
|
||||||
|
) -> HandshakeResponse {
|
||||||
|
let token = Uuid::new_v4().to_string();
|
||||||
|
let expires_at = now_ms() + APPROVAL_TTL_MS;
|
||||||
|
self.event_hub.emit_transfer(
|
||||||
|
request.transfer_id,
|
||||||
|
"send",
|
||||||
|
"access",
|
||||||
|
"receiver-auto-approved",
|
||||||
|
json!({
|
||||||
|
"remote_endpoint_id": remote_endpoint_id,
|
||||||
|
"expires_at": expires_at,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
HandshakeResponse::Approved { token, expires_at }
|
||||||
|
}
|
||||||
|
|
||||||
async fn wait_for_sender_decision(
|
async fn wait_for_sender_decision(
|
||||||
&self,
|
&self,
|
||||||
remote_endpoint_id: String,
|
remote_endpoint_id: String,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ use sqlx::{
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
access_policy::mode_from_storage,
|
||||||
api::{CoreEvent, ReceiverRequest, StoredTransfer},
|
api::{CoreEvent, ReceiverRequest, StoredTransfer},
|
||||||
transfer_state::{ReceiverRequestStatus, TransferDirection, TransferStatus},
|
transfer_state::{ReceiverRequestStatus, TransferDirection, TransferStatus},
|
||||||
util::now_ms,
|
util::now_ms,
|
||||||
@@ -641,7 +642,7 @@ impl Repository {
|
|||||||
r#"
|
r#"
|
||||||
SELECT transfer_id, direction, status, transfer_name, content_hash, ticket,
|
SELECT transfer_id, direction, status, transfer_name, content_hash, ticket,
|
||||||
local_id, protocol_transfer_id, peer_id,
|
local_id, protocol_transfer_id, peer_id,
|
||||||
file_count, total_size, created_at, updated_at
|
file_count, total_size, access_mode, created_at, updated_at
|
||||||
FROM transfers
|
FROM transfers
|
||||||
ORDER BY updated_at DESC
|
ORDER BY updated_at DESC
|
||||||
"#,
|
"#,
|
||||||
@@ -716,6 +717,7 @@ fn row_to_transfer(row: sqlx::sqlite::SqliteRow) -> Result<StoredTransfer> {
|
|||||||
ticket: row.get("ticket"),
|
ticket: row.get("ticket"),
|
||||||
file_count: row.get::<i64, _>("file_count") as u64,
|
file_count: row.get::<i64, _>("file_count") as u64,
|
||||||
total_size: row.get::<i64, _>("total_size") as u64,
|
total_size: row.get::<i64, _>("total_size") as u64,
|
||||||
|
access_mode: mode_from_storage(&row.get::<String, _>("access_mode")),
|
||||||
created_at: row.get("created_at"),
|
created_at: row.get("created_at"),
|
||||||
updated_at: row.get("updated_at"),
|
updated_at: row.get("updated_at"),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -493,7 +493,7 @@ impl CoreInner {
|
|||||||
ticket: None,
|
ticket: None,
|
||||||
file_count: 0,
|
file_count: 0,
|
||||||
total_size: 0,
|
total_size: 0,
|
||||||
access_mode: mode_to_storage(&TransferAccessMode::ApprovalRequired),
|
access_mode: mode_to_storage(&metadata.access_mode),
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
let (cancel, mut cancelled) = oneshot::channel();
|
let (cancel, mut cancelled) = oneshot::channel();
|
||||||
@@ -538,6 +538,7 @@ impl CoreInner {
|
|||||||
sources: Vec<ShareSource>,
|
sources: Vec<ShareSource>,
|
||||||
metadata: ShareMetadataInput,
|
metadata: ShareMetadataInput,
|
||||||
) -> Result<ShareResult> {
|
) -> Result<ShareResult> {
|
||||||
|
let access_mode = metadata.access_mode.clone();
|
||||||
self.emit_transfer(
|
self.emit_transfer(
|
||||||
metadata.transfer_id,
|
metadata.transfer_id,
|
||||||
"send",
|
"send",
|
||||||
@@ -579,7 +580,7 @@ impl CoreInner {
|
|||||||
ticket: Some(&ticket),
|
ticket: Some(&ticket),
|
||||||
file_count: import.file_count,
|
file_count: import.file_count,
|
||||||
total_size: import.total_size,
|
total_size: import.total_size,
|
||||||
access_mode: mode_to_storage(&TransferAccessMode::ApprovalRequired),
|
access_mode: mode_to_storage(&access_mode),
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
self.hash_to_transfer
|
self.hash_to_transfer
|
||||||
@@ -587,7 +588,7 @@ impl CoreInner {
|
|||||||
.await
|
.await
|
||||||
.insert(content_hash, metadata.transfer_id);
|
.insert(content_hash, metadata.transfer_id);
|
||||||
self.access_policy
|
self.access_policy
|
||||||
.set_mode(metadata.transfer_id, TransferAccessMode::ApprovalRequired)
|
.set_mode(metadata.transfer_id, access_mode)
|
||||||
.await;
|
.await;
|
||||||
self.active_shares
|
self.active_shares
|
||||||
.lock()
|
.lock()
|
||||||
|
|||||||
@@ -6,7 +6,53 @@ use support::{
|
|||||||
receive_with_response, share_path, wait_for_receiver_request, CoreGuard, RecordingSink,
|
receive_with_response, share_path, wait_for_receiver_request, CoreGuard, RecordingSink,
|
||||||
TestNode,
|
TestNode,
|
||||||
};
|
};
|
||||||
use vnidrop::CoreLimits;
|
use vnidrop::{CoreLimits, ShareMetadataInput, ShareSource, SourceKind, TransferAccessMode};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn public_share_receives_without_sender_approval() {
|
||||||
|
let source_dir = tempfile::tempdir().unwrap();
|
||||||
|
let output_dir = tempfile::tempdir().unwrap();
|
||||||
|
let source_path = source_dir.path().join("public.txt");
|
||||||
|
std::fs::write(&source_path, b"public content").unwrap();
|
||||||
|
let sender = TestNode::new();
|
||||||
|
let receiver = TestNode::new();
|
||||||
|
let share = sender
|
||||||
|
.core
|
||||||
|
.share_files(
|
||||||
|
vec![ShareSource {
|
||||||
|
kind: SourceKind::Path,
|
||||||
|
value: source_path.to_string_lossy().into_owned(),
|
||||||
|
display_name: Some("public.txt".to_string()),
|
||||||
|
is_directory: false,
|
||||||
|
}],
|
||||||
|
ShareMetadataInput {
|
||||||
|
transfer_id: 30,
|
||||||
|
transfer_name: Some("Public file".to_string()),
|
||||||
|
sender_name: Some("Sender".to_string()),
|
||||||
|
access_mode: TransferAccessMode::Public,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
receiver
|
||||||
|
.core
|
||||||
|
.receive(
|
||||||
|
share.ticket,
|
||||||
|
output_dir.path().to_string_lossy().into_owned(),
|
||||||
|
Some("Receiver".to_string()),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
std::fs::read(output_dir.path().join("public.txt")).unwrap(),
|
||||||
|
b"public content"
|
||||||
|
);
|
||||||
|
assert!(sender
|
||||||
|
.core
|
||||||
|
.list_receiver_requests(share.transfer_id)
|
||||||
|
.unwrap()
|
||||||
|
.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn approval_required_denies_then_allows_receiver() {
|
fn approval_required_denies_then_allows_receiver() {
|
||||||
|
|||||||
@@ -6,6 +6,35 @@ use std::time::{Duration, Instant};
|
|||||||
use support::{share_path, CoreGuard, RecordingSink, TestNode};
|
use support::{share_path, CoreGuard, RecordingSink, TestNode};
|
||||||
use vnidrop::{CoreLimits, ShareMetadataInput, ShareSource, SourceKind, TransferAccessMode};
|
use vnidrop::{CoreLimits, ShareMetadataInput, ShareSource, SourceKind, TransferAccessMode};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn share_creation_persists_selected_access_mode_atomically() {
|
||||||
|
let source_dir = tempfile::tempdir().unwrap();
|
||||||
|
let source_path = source_dir.path().join("public.txt");
|
||||||
|
std::fs::write(&source_path, b"public share").unwrap();
|
||||||
|
let sender = TestNode::new();
|
||||||
|
|
||||||
|
sender
|
||||||
|
.core
|
||||||
|
.share_files(
|
||||||
|
vec![ShareSource {
|
||||||
|
kind: SourceKind::Path,
|
||||||
|
value: source_path.to_string_lossy().into_owned(),
|
||||||
|
display_name: Some("public.txt".to_string()),
|
||||||
|
is_directory: false,
|
||||||
|
}],
|
||||||
|
ShareMetadataInput {
|
||||||
|
transfer_id: 9,
|
||||||
|
transfer_name: Some("Public file".to_string()),
|
||||||
|
sender_name: Some("Sender".to_string()),
|
||||||
|
access_mode: TransferAccessMode::Public,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let transfer = sender.core.list_transfers().unwrap().remove(0);
|
||||||
|
assert_eq!(transfer.access_mode, TransferAccessMode::Public);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cancelling_share_updates_status_and_events() {
|
fn cancelling_share_updates_status_and_events() {
|
||||||
let source_dir = tempfile::tempdir().unwrap();
|
let source_dir = tempfile::tempdir().unwrap();
|
||||||
@@ -92,6 +121,7 @@ fn failed_import_leaves_durable_failed_transfer() {
|
|||||||
transfer_id,
|
transfer_id,
|
||||||
transfer_name: Some("missing".to_string()),
|
transfer_name: Some("missing".to_string()),
|
||||||
sender_name: None,
|
sender_name: None,
|
||||||
|
access_mode: TransferAccessMode::ApprovalRequired,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -128,6 +158,7 @@ fn duplicate_transfer_id_does_not_replace_active_share() {
|
|||||||
transfer_id: first.transfer_id,
|
transfer_id: first.transfer_id,
|
||||||
transfer_name: Some("second".to_string()),
|
transfer_name: Some("second".to_string()),
|
||||||
sender_name: None,
|
sender_name: None,
|
||||||
|
access_mode: TransferAccessMode::ApprovalRequired,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -188,6 +219,7 @@ fn source_limit_rejection_creates_no_transfer_state() {
|
|||||||
transfer_id: 24,
|
transfer_id: 24,
|
||||||
transfer_name: Some("too many".to_string()),
|
transfer_name: Some("too many".to_string()),
|
||||||
sender_name: None,
|
sender_name: None,
|
||||||
|
access_mode: TransferAccessMode::ApprovalRequired,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -217,6 +249,7 @@ fn cancellation_during_import_is_durable() {
|
|||||||
transfer_id: 25,
|
transfer_id: 25,
|
||||||
transfer_name: Some("large".to_string()),
|
transfer_name: Some("large".to_string()),
|
||||||
sender_name: None,
|
sender_name: None,
|
||||||
|
access_mode: TransferAccessMode::ApprovalRequired,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use std::{
|
|||||||
|
|
||||||
use vnidrop::{
|
use vnidrop::{
|
||||||
CoreEvent, CoreEventSink, CoreLimits, ReceiveOutputSink, ReceiverRequest, ShareMetadataInput,
|
CoreEvent, CoreEventSink, CoreLimits, ReceiveOutputSink, ReceiverRequest, ShareMetadataInput,
|
||||||
ShareResult, ShareSource, SourceKind, VnidropCore, VnidropError,
|
ShareResult, ShareSource, SourceKind, TransferAccessMode, VnidropCore, VnidropError,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
@@ -192,6 +192,7 @@ pub fn share_path(
|
|||||||
transfer_id,
|
transfer_id,
|
||||||
transfer_name: Some(display_name.to_string()),
|
transfer_name: Some(display_name.to_string()),
|
||||||
sender_name: Some("sender".to_string()),
|
sender_name: Some("sender".to_string()),
|
||||||
|
access_mode: TransferAccessMode::ApprovalRequired,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.expect("test share should be created")
|
.expect("test share should be created")
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ private class AndroidFileSystemService(
|
|||||||
file: PickedShareFile,
|
file: PickedShareFile,
|
||||||
transferName: String,
|
transferName: String,
|
||||||
senderName: String,
|
senderName: String,
|
||||||
|
accessPolicy: ShareAccessPolicy,
|
||||||
): Result<Share> = runCatching {
|
): Result<Share> = runCatching {
|
||||||
context.contentResolver.openFileDescriptor(Uri.parse(file.value), "r").use { descriptor ->
|
context.contentResolver.openFileDescriptor(Uri.parse(file.value), "r").use { descriptor ->
|
||||||
checkNotNull(descriptor) { "Could not open selected file descriptor" }
|
checkNotNull(descriptor) { "Could not open selected file descriptor" }
|
||||||
@@ -58,6 +59,7 @@ private class AndroidFileSystemService(
|
|||||||
displayName = file.displayName,
|
displayName = file.displayName,
|
||||||
transferName = transferName,
|
transferName = transferName,
|
||||||
senderName = senderName,
|
senderName = senderName,
|
||||||
|
accessPolicy = accessPolicy,
|
||||||
).getOrThrow()
|
).getOrThrow()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,16 +21,40 @@ data class CoreEventModel(
|
|||||||
val dataJson: String,
|
val dataJson: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
enum class ShareAccessPolicy {
|
||||||
|
RequireApproval,
|
||||||
|
AnyoneWithTransfer,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class TransferDirection {
|
||||||
|
Send,
|
||||||
|
Receive,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class TransferStatus {
|
||||||
|
Importing,
|
||||||
|
Sharing,
|
||||||
|
Receiving,
|
||||||
|
Done,
|
||||||
|
Failed,
|
||||||
|
Cancelled,
|
||||||
|
Stopped,
|
||||||
|
}
|
||||||
|
|
||||||
data class Transfer(
|
data class Transfer(
|
||||||
val localId: String,
|
val localId: String,
|
||||||
val transferId: ULong,
|
val transferId: ULong,
|
||||||
val direction: String,
|
val direction: TransferDirection,
|
||||||
val status: String,
|
val status: TransferStatus,
|
||||||
val peerId: String?,
|
val peerId: String?,
|
||||||
val transferName: String?,
|
val transferName: String?,
|
||||||
|
val contentHash: String?,
|
||||||
val fileCount: ULong,
|
val fileCount: ULong,
|
||||||
val totalSize: ULong,
|
val totalSize: ULong,
|
||||||
val ticket: String?,
|
val ticket: String?,
|
||||||
|
val accessPolicy: ShareAccessPolicy,
|
||||||
|
val createdAt: Long,
|
||||||
|
val updatedAt: Long,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class Share(
|
data class Share(
|
||||||
@@ -90,9 +114,21 @@ interface CoreGateway {
|
|||||||
|
|
||||||
suspend fun initialize(appDataDir: String): Result<Unit>
|
suspend fun initialize(appDataDir: String): Result<Unit>
|
||||||
fun shutdown()
|
fun shutdown()
|
||||||
suspend fun sharePath(path: String, transferName: String, senderName: String): Result<Share>
|
suspend fun sharePath(path: String, transferName: String, senderName: String, accessPolicy: ShareAccessPolicy): Result<Share>
|
||||||
suspend fun shareFileDescriptor(fd: Int, displayName: String, transferName: String, senderName: String): Result<Share>
|
suspend fun shareFileDescriptor(
|
||||||
suspend fun shareSecurityScopedFileUrl(fileUrl: String, displayName: String, transferName: String, senderName: String): Result<Share>
|
fd: Int,
|
||||||
|
displayName: String,
|
||||||
|
transferName: String,
|
||||||
|
senderName: String,
|
||||||
|
accessPolicy: ShareAccessPolicy,
|
||||||
|
): Result<Share>
|
||||||
|
suspend fun shareSecurityScopedFileUrl(
|
||||||
|
fileUrl: String,
|
||||||
|
displayName: String,
|
||||||
|
transferName: String,
|
||||||
|
senderName: String,
|
||||||
|
accessPolicy: ShareAccessPolicy,
|
||||||
|
): Result<Share>
|
||||||
suspend fun inspectTicket(ticket: String): Result<TicketInspectionModel>
|
suspend fun inspectTicket(ticket: String): Result<TicketInspectionModel>
|
||||||
suspend fun receive(ticket: String, outputDir: String, receiverName: String): Result<Unit>
|
suspend fun receive(ticket: String, outputDir: String, receiverName: String): Result<Unit>
|
||||||
suspend fun receiveWithOutputSink(ticket: String, outputSink: ReceiveOutputSink, receiverName: String): Result<Unit>
|
suspend fun receiveWithOutputSink(ticket: String, outputSink: ReceiveOutputSink, receiverName: String): Result<Unit>
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import uniffi.vnidrop.SourceKind
|
|||||||
import uniffi.vnidrop.StoredTransfer
|
import uniffi.vnidrop.StoredTransfer
|
||||||
import uniffi.vnidrop.TicketInspection
|
import uniffi.vnidrop.TicketInspection
|
||||||
import uniffi.vnidrop.TransferMetadata
|
import uniffi.vnidrop.TransferMetadata
|
||||||
|
import uniffi.vnidrop.TransferAccessMode
|
||||||
import uniffi.vnidrop.VnidropCore
|
import uniffi.vnidrop.VnidropCore
|
||||||
|
|
||||||
class CoreRepository(
|
class CoreRepository(
|
||||||
@@ -64,7 +65,12 @@ class CoreRepository(
|
|||||||
_state.value = CoreState()
|
_state.value = CoreState()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun sharePath(path: String, transferName: String, senderName: String): Result<Share> =
|
override suspend fun sharePath(
|
||||||
|
path: String,
|
||||||
|
transferName: String,
|
||||||
|
senderName: String,
|
||||||
|
accessPolicy: ShareAccessPolicy,
|
||||||
|
): Result<Share> =
|
||||||
shareSources(
|
shareSources(
|
||||||
sources = listOf(
|
sources = listOf(
|
||||||
ShareSource(
|
ShareSource(
|
||||||
@@ -76,6 +82,7 @@ class CoreRepository(
|
|||||||
),
|
),
|
||||||
transferName = transferName,
|
transferName = transferName,
|
||||||
senderName = senderName,
|
senderName = senderName,
|
||||||
|
accessPolicy = accessPolicy,
|
||||||
)
|
)
|
||||||
|
|
||||||
override suspend fun shareFileDescriptor(
|
override suspend fun shareFileDescriptor(
|
||||||
@@ -83,6 +90,7 @@ class CoreRepository(
|
|||||||
displayName: String,
|
displayName: String,
|
||||||
transferName: String,
|
transferName: String,
|
||||||
senderName: String,
|
senderName: String,
|
||||||
|
accessPolicy: ShareAccessPolicy,
|
||||||
): Result<Share> =
|
): Result<Share> =
|
||||||
shareSources(
|
shareSources(
|
||||||
sources = listOf(
|
sources = listOf(
|
||||||
@@ -95,6 +103,7 @@ class CoreRepository(
|
|||||||
),
|
),
|
||||||
transferName = transferName,
|
transferName = transferName,
|
||||||
senderName = senderName,
|
senderName = senderName,
|
||||||
|
accessPolicy = accessPolicy,
|
||||||
)
|
)
|
||||||
|
|
||||||
override suspend fun shareSecurityScopedFileUrl(
|
override suspend fun shareSecurityScopedFileUrl(
|
||||||
@@ -102,6 +111,7 @@ class CoreRepository(
|
|||||||
displayName: String,
|
displayName: String,
|
||||||
transferName: String,
|
transferName: String,
|
||||||
senderName: String,
|
senderName: String,
|
||||||
|
accessPolicy: ShareAccessPolicy,
|
||||||
): Result<Share> =
|
): Result<Share> =
|
||||||
shareSources(
|
shareSources(
|
||||||
sources = listOf(
|
sources = listOf(
|
||||||
@@ -114,6 +124,7 @@ class CoreRepository(
|
|||||||
),
|
),
|
||||||
transferName = transferName,
|
transferName = transferName,
|
||||||
senderName = senderName,
|
senderName = senderName,
|
||||||
|
accessPolicy = accessPolicy,
|
||||||
)
|
)
|
||||||
|
|
||||||
override suspend fun inspectTicket(ticket: String): Result<TicketInspectionModel> = runCore {
|
override suspend fun inspectTicket(ticket: String): Result<TicketInspectionModel> = runCore {
|
||||||
@@ -170,6 +181,7 @@ class CoreRepository(
|
|||||||
sources: List<ShareSource>,
|
sources: List<ShareSource>,
|
||||||
transferName: String,
|
transferName: String,
|
||||||
senderName: String,
|
senderName: String,
|
||||||
|
accessPolicy: ShareAccessPolicy,
|
||||||
): Result<Share> = runCore {
|
): Result<Share> = runCore {
|
||||||
withPlatformPathAccess(sources) {
|
withPlatformPathAccess(sources) {
|
||||||
requireCore().shareFiles(
|
requireCore().shareFiles(
|
||||||
@@ -178,6 +190,7 @@ class CoreRepository(
|
|||||||
transferId = nextTransferId(),
|
transferId = nextTransferId(),
|
||||||
transferName = transferName.ifBlank { null },
|
transferName = transferName.ifBlank { null },
|
||||||
senderName = senderName.ifBlank { null },
|
senderName = senderName.ifBlank { null },
|
||||||
|
accessMode = accessPolicy.toNative(),
|
||||||
),
|
),
|
||||||
).toModel()
|
).toModel()
|
||||||
}.also { share ->
|
}.also { share ->
|
||||||
@@ -243,15 +256,46 @@ private fun CoreEvent.toModel(): CoreEventModel = CoreEventModel(
|
|||||||
private fun StoredTransfer.toModel(): Transfer = Transfer(
|
private fun StoredTransfer.toModel(): Transfer = Transfer(
|
||||||
localId = localId,
|
localId = localId,
|
||||||
transferId = transferId,
|
transferId = transferId,
|
||||||
direction = direction,
|
direction = direction.toTransferDirection(),
|
||||||
status = status,
|
status = status.toTransferStatus(),
|
||||||
peerId = peerId,
|
peerId = peerId,
|
||||||
transferName = transferName,
|
transferName = transferName,
|
||||||
|
contentHash = contentHash,
|
||||||
fileCount = fileCount,
|
fileCount = fileCount,
|
||||||
totalSize = totalSize,
|
totalSize = totalSize,
|
||||||
ticket = ticket,
|
ticket = ticket,
|
||||||
|
accessPolicy = accessMode.toModel(),
|
||||||
|
createdAt = createdAt,
|
||||||
|
updatedAt = updatedAt,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private fun ShareAccessPolicy.toNative(): TransferAccessMode = when (this) {
|
||||||
|
ShareAccessPolicy.RequireApproval -> TransferAccessMode.APPROVAL_REQUIRED
|
||||||
|
ShareAccessPolicy.AnyoneWithTransfer -> TransferAccessMode.PUBLIC
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun TransferAccessMode.toModel(): ShareAccessPolicy = when (this) {
|
||||||
|
TransferAccessMode.APPROVAL_REQUIRED -> ShareAccessPolicy.RequireApproval
|
||||||
|
TransferAccessMode.PUBLIC -> ShareAccessPolicy.AnyoneWithTransfer
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.toTransferDirection(): TransferDirection = when (this) {
|
||||||
|
"send" -> TransferDirection.Send
|
||||||
|
"receive" -> TransferDirection.Receive
|
||||||
|
else -> error("Unknown transfer direction: $this")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.toTransferStatus(): TransferStatus = when (this) {
|
||||||
|
"importing" -> TransferStatus.Importing
|
||||||
|
"sharing" -> TransferStatus.Sharing
|
||||||
|
"receiving" -> TransferStatus.Receiving
|
||||||
|
"done" -> TransferStatus.Done
|
||||||
|
"failed" -> TransferStatus.Failed
|
||||||
|
"cancelled" -> TransferStatus.Cancelled
|
||||||
|
"stopped" -> TransferStatus.Stopped
|
||||||
|
else -> error("Unknown transfer status: $this")
|
||||||
|
}
|
||||||
|
|
||||||
private fun ShareResult.toModel(): Share = Share(
|
private fun ShareResult.toModel(): Share = Share(
|
||||||
transferId = transferId,
|
transferId = transferId,
|
||||||
ticket = ticket,
|
ticket = ticket,
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ interface FileSystemService {
|
|||||||
file: PickedShareFile,
|
file: PickedShareFile,
|
||||||
transferName: String,
|
transferName: String,
|
||||||
senderName: String,
|
senderName: String,
|
||||||
|
accessPolicy: ShareAccessPolicy,
|
||||||
): Result<Share>
|
): Result<Share>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,11 +47,13 @@ private class IosFileSystemService : FileSystemService {
|
|||||||
file: PickedShareFile,
|
file: PickedShareFile,
|
||||||
transferName: String,
|
transferName: String,
|
||||||
senderName: String,
|
senderName: String,
|
||||||
|
accessPolicy: ShareAccessPolicy,
|
||||||
): Result<Share> = repository.shareSecurityScopedFileUrl(
|
): Result<Share> = repository.shareSecurityScopedFileUrl(
|
||||||
file.value,
|
file.value,
|
||||||
file.displayName,
|
file.displayName,
|
||||||
transferName,
|
transferName,
|
||||||
senderName,
|
senderName,
|
||||||
|
accessPolicy,
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun validateSecurityScopedUrl(value: String): FolderAccessStatus {
|
private fun validateSecurityScopedUrl(value: String): FolderAccessStatus {
|
||||||
|
|||||||
@@ -33,5 +33,6 @@ private class JvmFileSystemService : FileSystemService {
|
|||||||
file: PickedShareFile,
|
file: PickedShareFile,
|
||||||
transferName: String,
|
transferName: String,
|
||||||
senderName: String,
|
senderName: String,
|
||||||
): Result<Share> = repository.sharePath(file.value, transferName, senderName)
|
accessPolicy: ShareAccessPolicy,
|
||||||
|
): Result<Share> = repository.sharePath(file.value, transferName, senderName, accessPolicy)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user