Skip to main content

citadel_sdk/
fs.rs

1//! Remote Encrypted Virtual Filesystem (RE-VFS)
2//!
3//! This module provides high-level operations for interacting with the Remote Encrypted
4//! Virtual Filesystem in the Citadel Protocol. RE-VFS enables secure storage and
5//! retrieval of files with end-to-end encryption.
6//!
7//! # Features
8//! - Secure file storage and retrieval
9//! - Configurable security levels
10//! - Automatic encryption handling
11//! - Virtual path management
12//! - File deletion support
13//! - Take operations for atomic reads
14//!
15//! # Example
16//! ```rust
17//! use citadel_sdk::prelude::*;
18//! use citadel_sdk::fs;
19//!
20//! async fn store_file<R: Ratchet>(remote: &impl TargetLockedRemote<R>) -> Result<(), NetworkError> {
21//!     // Write a file to RE-VFS
22//!     fs::write(remote, "/local/file.txt", "/virtual/file.txt").await?;
23//!
24//!     // Read the file back
25//!     let local_path = fs::read(remote, "/virtual/file.txt").await?;
26//!
27//!     // Delete the file
28//!     fs::delete(remote, "/virtual/file.txt").await?;
29//!
30//!     Ok(())
31//! }
32//! ```
33//!
34//! # Important Notes
35//! - All operations are end-to-end encrypted
36//! - Security levels are configurable per operation
37//! - Take operations atomically read and delete
38//! - Virtual paths are independent of local paths
39//!
40//! # Related Components
41//! - [`TargetLockedRemote`]: Remote connection interface
42//! - [`SecurityLevel`]: Encryption strength configuration
43//! - [`ObjectSource`]: File source abstraction
44//! - [`NetworkError`]: Error handling
45//!
46//! [`TargetLockedRemote`]: crate::prelude::TargetLockedRemote
47//! [`SecurityLevel`]: crate::prelude::SecurityLevel
48//! [`ObjectSource`]: crate::prelude::ObjectSource
49//! [`NetworkError`]: crate::prelude::NetworkError
50//!
51
52use crate::prelude::{ObjectSource, ProtocolRemoteTargetExt, TargetLockedRemote};
53
54use citadel_proto::prelude::NetworkError;
55use citadel_proto::prelude::*;
56use citadel_types::crypto::SecurityLevel;
57use std::path::PathBuf;
58
59/// Writes a file or BytesSource to the Remote Encrypted Virtual Filesystem
60pub async fn write<T: ObjectSource, P: Into<PathBuf> + Send, R: Ratchet>(
61    remote: &impl TargetLockedRemote<R>,
62    source: T,
63    virtual_path: P,
64) -> Result<(), NetworkError> {
65    write_with_security_level(remote, source, Default::default(), virtual_path).await
66}
67
68/// Writes a file or BytesSource to the Remote Encrypted Virtual Filesystem with a custom security level.
69pub async fn write_with_security_level<T: ObjectSource, P: Into<PathBuf> + Send, R: Ratchet>(
70    remote: &impl TargetLockedRemote<R>,
71    source: T,
72    security_level: SecurityLevel,
73    virtual_path: P,
74) -> Result<(), NetworkError> {
75    remote
76        .remote_encrypted_virtual_filesystem_push(source, virtual_path, security_level)
77        .await
78}
79
80/// Reads a file from the Remote Encrypted Virtual Filesystem
81pub async fn read<P: Into<PathBuf> + Send, R: Ratchet>(
82    remote: &impl TargetLockedRemote<R>,
83    virtual_path: P,
84) -> Result<PathBuf, NetworkError> {
85    read_with_security_level(remote, Default::default(), virtual_path).await
86}
87
88/// Reads a file from the Remote Encrypted Virtual Filesystem with a custom transport security level
89pub async fn read_with_security_level<P: Into<PathBuf> + Send, R: Ratchet>(
90    remote: &impl TargetLockedRemote<R>,
91    transfer_security_level: SecurityLevel,
92    virtual_path: P,
93) -> Result<PathBuf, NetworkError> {
94    remote
95        .remote_encrypted_virtual_filesystem_pull(virtual_path, transfer_security_level, false)
96        .await
97}
98
99/// Takes a file from the Remote Encrypted Virtual Filesystem
100pub async fn take<P: Into<PathBuf> + Send, R: Ratchet>(
101    remote: &impl TargetLockedRemote<R>,
102    virtual_path: P,
103) -> Result<PathBuf, NetworkError> {
104    remote
105        .remote_encrypted_virtual_filesystem_pull(virtual_path, Default::default(), true)
106        .await
107}
108
109/// Takes a file from the Remote Encrypted Virtual Filesystem with a custom security level.
110pub async fn take_with_security_level<P: Into<PathBuf> + Send, R: Ratchet>(
111    remote: &impl TargetLockedRemote<R>,
112    transfer_security_level: SecurityLevel,
113    virtual_path: P,
114) -> Result<PathBuf, NetworkError> {
115    remote
116        .remote_encrypted_virtual_filesystem_pull(virtual_path, transfer_security_level, true)
117        .await
118}
119
120/// Deletes a file from the Remote Encrypted Virtual Filesystem
121pub async fn delete<P: Into<PathBuf> + Send, R: Ratchet>(
122    remote: &impl TargetLockedRemote<R>,
123    virtual_path: P,
124) -> Result<(), NetworkError> {
125    remote
126        .remote_encrypted_virtual_filesystem_delete(virtual_path)
127        .await
128}
129
130#[cfg(all(test, not(target_family = "wasm")))]
131mod tests {
132    use crate::prefabs::client::single_connection::SingleClientServerConnectionKernel;
133    use crate::prefabs::server::accept_file_transfer_kernel::AcceptFileTransferKernel;
134
135    use crate::prefabs::client::peer_connection::FileTransferHandleRx;
136    #[cfg(feature = "localhost-testing")]
137    use crate::prefabs::client::peer_connection::PeerConnectionKernel;
138    use crate::prefabs::client::DefaultServerConnectionSettingsBuilder;
139    use crate::prelude::*;
140    #[cfg(feature = "localhost-testing")]
141    use crate::test_common::wait_for_peers;
142    use citadel_io::tokio;
143    use futures::StreamExt;
144    use rstest::rstest;
145    use std::net::SocketAddr;
146    use std::path::PathBuf;
147    use std::sync::atomic::{AtomicBool, Ordering};
148    use std::time::Duration;
149    use uuid::Uuid;
150
151    pub fn server_info<'a, R: Ratchet>() -> (NodeFuture<'a, AcceptFileTransferKernel<R>>, SocketAddr)
152    {
153        crate::test_common::server_test_node(AcceptFileTransferKernel::<R>::default(), |_| {})
154    }
155
156    #[rstest]
157    #[case(
158        EncryptionAlgorithm::AES_GCM_256,
159        KemAlgorithm::MlKem,
160        SigAlgorithm::None
161    )]
162    #[case(
163        EncryptionAlgorithm::MlKemHybrid,
164        KemAlgorithm::MlKem,
165        SigAlgorithm::MlDsa65
166    )]
167    #[timeout(Duration::from_secs(90))]
168    #[citadel_io::tokio::test]
169    async fn test_c2s_file_transfer_revfs(
170        #[case] enx: EncryptionAlgorithm,
171        #[case] kem: KemAlgorithm,
172        #[case] sig: SigAlgorithm,
173        #[values(SecurityLevel::Standard, SecurityLevel::Reinforced)] security_level: SecurityLevel,
174    ) {
175        citadel_logging::setup_log();
176        let client_success = &AtomicBool::new(false);
177        let (server, server_addr) = server_info::<StackedRatchet>();
178        let uuid = Uuid::new_v4();
179
180        let source_dir = PathBuf::from("../resources/TheBridge.pdf");
181
182        let session_security_settings = SessionSecuritySettingsBuilder::default()
183            .with_crypto_params(enx + kem + sig)
184            .with_security_level(security_level)
185            .build()
186            .unwrap();
187
188        let server_connection_settings =
189            DefaultServerConnectionSettingsBuilder::transient_with_id(server_addr, uuid)
190                .disable_udp()
191                .with_session_security_settings(session_security_settings)
192                .build()
193                .unwrap();
194
195        let client_kernel = SingleClientServerConnectionKernel::new(
196            server_connection_settings,
197            |connection| async move {
198                log::trace!(target: "citadel", "***CLIENT LOGIN SUCCESS :: File transfer next ***");
199
200                // Test list_sessions() - verify we can list active sessions with correct data
201                let sessions = connection.remote.list_sessions().await?;
202                log::info!(target: "citadel", "***CLIENT list_sessions() returned {} sessions***", sessions.sessions.len());
203                assert!(
204                    !sessions.sessions.is_empty(),
205                    "Should have at least one active session"
206                );
207                // Find our session by CID
208                let our_session = sessions.sessions.iter().find(|s| s.cid == connection.cid);
209                assert!(our_session.is_some(), "Should find our session in the list");
210                let our_session = our_session.unwrap();
211                assert!(
212                    !our_session.connections.is_empty(),
213                    "Session should have at least one connection"
214                );
215                // For C2S, peer_cid should be None
216                let c2s_conn = our_session
217                    .connections
218                    .iter()
219                    .find(|c| c.peer_cid.is_none());
220                assert!(
221                    c2s_conn.is_some(),
222                    "Should have a C2S connection (peer_cid = None)"
223                );
224                let c2s_conn = c2s_conn.unwrap();
225                assert!(c2s_conn.connected, "C2S connection should be active");
226                log::info!(target: "citadel", "***CLIENT list_sessions() verification PASSED***");
227
228                let virtual_path = PathBuf::from("/home/john.doe/TheBridge.pdf");
229                // write to file to the RE-VFS
230                crate::fs::write_with_security_level(
231                    &connection.remote,
232                    source_dir.clone(),
233                    security_level,
234                    &virtual_path,
235                )
236                .await?;
237                log::info!(target: "citadel", "***CLIENT FILE TRANSFER SUCCESS***");
238                // now, pull it
239                let save_dir = crate::fs::read(&connection.remote, virtual_path).await?;
240                // now, compare bytes
241                log::info!(target: "citadel", "***CLIENT REVFS PULL SUCCESS");
242                let original_bytes = citadel_io::tokio::fs::read(&source_dir).await.unwrap();
243                let revfs_pulled_bytes = citadel_io::tokio::fs::read(&save_dir).await.unwrap();
244                assert_eq!(original_bytes, revfs_pulled_bytes);
245                log::info!(target: "citadel", "***CLIENT REVFS PULL COMPARE SUCCESS");
246                client_success.store(true, Ordering::Relaxed);
247                connection.shutdown_kernel().await
248            },
249        );
250
251        let client = DefaultNodeBuilder::default()
252            .with_stun_servers(crate::test_common::LOCALHOST_STUN_SERVERS)
253            .build(client_kernel)
254            .unwrap();
255
256        let result = citadel_io::tokio::select! {
257            res0 = client => res0.map(|_| ()),
258            res1 = server => res1.map(|_| ())
259        };
260
261        result.unwrap();
262
263        assert!(client_success.load(Ordering::Relaxed));
264    }
265
266    #[rstest]
267    #[case(
268        EncryptionAlgorithm::AES_GCM_256,
269        KemAlgorithm::MlKem,
270        SigAlgorithm::None
271    )]
272    #[timeout(std::time::Duration::from_secs(90))]
273    #[citadel_io::tokio::test]
274    async fn test_c2s_file_transfer_revfs_take(
275        #[case] enx: EncryptionAlgorithm,
276        #[case] kem: KemAlgorithm,
277        #[case] sig: SigAlgorithm,
278        #[values(SecurityLevel::Standard)] security_level: SecurityLevel,
279    ) {
280        citadel_logging::setup_log();
281        let client_success = &AtomicBool::new(false);
282        let (server, server_addr) = server_info::<StackedRatchet>();
283        let uuid = Uuid::new_v4();
284
285        let source_dir = PathBuf::from("../resources/TheBridge.pdf");
286
287        let session_security_settings = SessionSecuritySettingsBuilder::default()
288            .with_crypto_params(enx + kem + sig)
289            .with_security_level(security_level)
290            .build()
291            .unwrap();
292
293        let server_connection_settings =
294            DefaultServerConnectionSettingsBuilder::transient_with_id(server_addr, uuid)
295                .disable_udp()
296                .with_session_security_settings(session_security_settings)
297                .build()
298                .unwrap();
299
300        let client_kernel = SingleClientServerConnectionKernel::new(
301            server_connection_settings,
302            |connection| async move {
303                log::trace!(target: "citadel", "***CLIENT LOGIN SUCCESS :: File transfer next ***");
304                let virtual_path = PathBuf::from("/home/john.doe/TheBridge.pdf");
305                // write to file to the RE-VFS
306                crate::fs::write_with_security_level(
307                    &connection.remote,
308                    source_dir.clone(),
309                    security_level,
310                    &virtual_path,
311                )
312                .await?;
313                log::trace!(target: "citadel", "***CLIENT FILE TRANSFER SUCCESS***");
314                // now, pull it
315                let save_dir = crate::fs::take(&connection.remote, &virtual_path).await?;
316                // now, compare bytes
317                log::trace!(target: "citadel", "***CLIENT REVFS PULL SUCCESS");
318                let original_bytes = citadel_io::tokio::fs::read(&source_dir).await.unwrap();
319                let revfs_pulled_bytes = citadel_io::tokio::fs::read(&save_dir).await.unwrap();
320                assert_eq!(original_bytes, revfs_pulled_bytes);
321                log::trace!(target: "citadel", "***CLIENT REVFS PULL COMPARE SUCCESS");
322                // prove we can no longer read from this virtual file
323                assert!(crate::fs::read(&connection.remote, &virtual_path)
324                    .await
325                    .is_err());
326                client_success.store(true, Ordering::Relaxed);
327                connection.shutdown_kernel().await
328            },
329        );
330
331        let client = DefaultNodeBuilder::default()
332            .with_stun_servers(crate::test_common::LOCALHOST_STUN_SERVERS)
333            .build(client_kernel)
334            .unwrap();
335
336        let result = citadel_io::tokio::select! {
337            res0 = client => res0.map(|_| ()),
338            res1 = server => res1.map(|_| ())
339        };
340
341        result.unwrap();
342
343        assert!(client_success.load(Ordering::Relaxed));
344    }
345
346    #[rstest]
347    #[case(
348        EncryptionAlgorithm::AES_GCM_256,
349        KemAlgorithm::MlKem,
350        SigAlgorithm::None
351    )]
352    #[timeout(std::time::Duration::from_secs(90))]
353    #[citadel_io::tokio::test]
354    async fn test_c2s_file_transfer_revfs_delete(
355        #[case] enx: EncryptionAlgorithm,
356        #[case] kem: KemAlgorithm,
357        #[case] sig: SigAlgorithm,
358        #[values(SecurityLevel::Standard)] security_level: SecurityLevel,
359    ) {
360        citadel_logging::setup_log();
361        let client_success = &AtomicBool::new(false);
362        let (server, server_addr) = server_info::<StackedRatchet>();
363        let uuid = Uuid::new_v4();
364
365        let source_dir = PathBuf::from("../resources/TheBridge.pdf");
366
367        let session_security_settings = SessionSecuritySettingsBuilder::default()
368            .with_crypto_params(enx + kem + sig)
369            .with_security_level(security_level)
370            .build()
371            .unwrap();
372
373        let server_connection_settings =
374            DefaultServerConnectionSettingsBuilder::transient_with_id(server_addr, uuid)
375                .disable_udp()
376                .with_session_security_settings(session_security_settings)
377                .build()
378                .unwrap();
379
380        let client_kernel = SingleClientServerConnectionKernel::new(
381            server_connection_settings,
382            |connection| async move {
383                log::trace!(target: "citadel", "***CLIENT LOGIN SUCCESS :: File transfer next ***");
384                let virtual_path = PathBuf::from("/home/john.doe/TheBridge.pdf");
385                // write to file to the RE-VFS
386                crate::fs::write_with_security_level(
387                    &connection.remote,
388                    source_dir.clone(),
389                    security_level,
390                    &virtual_path,
391                )
392                .await?;
393                log::trace!(target: "citadel", "***CLIENT FILE TRANSFER SUCCESS***");
394                // now, pull it
395                let save_dir = crate::fs::read(&connection.remote, &virtual_path).await?;
396                // now, compare bytes
397                log::trace!(target: "citadel", "***CLIENT REVFS PULL SUCCESS");
398                let original_bytes = citadel_io::tokio::fs::read(&source_dir).await.unwrap();
399                let revfs_pulled_bytes = citadel_io::tokio::fs::read(&save_dir).await.unwrap();
400                assert_eq!(original_bytes, revfs_pulled_bytes);
401                log::trace!(target: "citadel", "***CLIENT REVFS PULL COMPARE SUCCESS");
402                crate::fs::delete(&connection.remote, &virtual_path).await?;
403                // prove we can no longer read from this virtual file since it was just deleted
404                assert!(crate::fs::read(&connection.remote, &virtual_path)
405                    .await
406                    .is_err());
407                client_success.store(true, Ordering::Relaxed);
408                connection.shutdown_kernel().await
409            },
410        );
411
412        let client = DefaultNodeBuilder::default()
413            .with_stun_servers(crate::test_common::LOCALHOST_STUN_SERVERS)
414            .build(client_kernel)
415            .unwrap();
416
417        let result = citadel_io::tokio::select! {
418            res0 = client => res0.map(|_| ()),
419            res1 = server => res1.map(|_| ())
420        };
421
422        result.unwrap();
423
424        assert!(client_success.load(Ordering::Relaxed));
425    }
426
427    #[rstest]
428    #[case(SecrecyMode::BestEffort)]
429    #[timeout(Duration::from_secs(60))]
430    #[cfg(feature = "localhost-testing")]
431    #[citadel_io::tokio::test(flavor = "multi_thread")]
432    async fn test_p2p_file_transfer_revfs(
433        #[case] secrecy_mode: SecrecyMode,
434        #[values(KemAlgorithm::MlKem)] kem: KemAlgorithm,
435        #[values(EncryptionAlgorithm::AES_GCM_256)] enx: EncryptionAlgorithm,
436    ) {
437        citadel_logging::setup_log();
438        crate::test_common::TestBarrier::setup(2);
439        let client0_success = &AtomicBool::new(false);
440        let client1_success = &AtomicBool::new(false);
441
442        let (server, server_addr) = crate::test_common::server_info::<StackedRatchet>();
443
444        let uuid0 = Uuid::new_v4();
445        let uuid1 = Uuid::new_v4();
446        let session_security = SessionSecuritySettingsBuilder::default()
447            .with_secrecy_mode(secrecy_mode)
448            .with_crypto_params(kem + enx)
449            .build()
450            .unwrap();
451
452        let security_level = SecurityLevel::Standard;
453
454        let source_dir = &PathBuf::from("../resources/TheBridge.pdf");
455
456        let server_connection_settings =
457            DefaultServerConnectionSettingsBuilder::transient_with_id(server_addr, uuid0)
458                .disable_udp()
459                .with_session_security_settings(session_security)
460                .build()
461                .unwrap();
462
463        let peer_conn_0 = PeerConnectionSetupAggregator::default()
464            .with_peer_custom(uuid1)
465            .ensure_registered()
466            .with_session_security_settings(session_security)
467            .enable_udp()
468            .add();
469
470        // TODO: SinglePeerConnectionKernel
471        let client_kernel0 = PeerConnectionKernel::new(
472            server_connection_settings,
473            peer_conn_0,
474            move |mut connection, remote_outer| async move {
475                wait_for_peers().await;
476                let mut connection = connection.recv().await.unwrap()?;
477                let cid = connection.channel.get_session_cid();
478                wait_for_peers().await;
479                // The other peer will send the file first
480                log::info!(target: "citadel", "***CLIENT A {cid} LOGIN SUCCESS :: File transfer next ***");
481                let remote = connection.remote.clone();
482                let handle_orig = connection.incoming_object_transfer_handles.take().unwrap();
483                let _file_transfer_task = accept_all(handle_orig);
484
485                let virtual_path = PathBuf::from("/home/john.doe/TheBridge.pdf");
486                // write the file to the RE-VFS
487                crate::fs::write_with_security_level(
488                    &remote,
489                    source_dir.clone(),
490                    security_level,
491                    &virtual_path,
492                )
493                .await?;
494                log::info!(target: "citadel", "***CLIENT A {cid} FILE TRANSFER SUCCESS***");
495                tokio::time::sleep(Duration::from_secs(1)).await;
496                wait_for_peers().await;
497                // now, pull it
498                let save_dir = crate::fs::read(&remote, virtual_path).await?;
499                // now, compare bytes
500                log::info!(target: "citadel", "***CLIENT A {cid} REVFS PULL SUCCESS");
501                let original_bytes = tokio::fs::read(&source_dir).await.unwrap();
502                let revfs_pulled_bytes = tokio::fs::read(&save_dir).await.unwrap();
503                assert_eq!(original_bytes, revfs_pulled_bytes);
504                log::info!(target: "citadel", "***CLIENT A {cid} REVFS PULL COMPARE SUCCESS");
505                wait_for_peers().await;
506                client0_success.store(true, Ordering::Relaxed);
507                remote_outer.shutdown_kernel().await
508            },
509        );
510
511        let server_connection_settings =
512            DefaultServerConnectionSettingsBuilder::transient_with_id(server_addr, uuid1)
513                .disable_udp()
514                .with_session_security_settings(session_security)
515                .build()
516                .unwrap();
517
518        let peer_conn_1 = PeerConnectionSetupAggregator::default()
519            .with_peer_custom(uuid0)
520            .ensure_registered()
521            .with_session_security_settings(session_security)
522            .enable_udp()
523            .add();
524
525        let client_kernel1 = PeerConnectionKernel::new(
526            server_connection_settings,
527            peer_conn_1,
528            move |mut connection, remote_outer| async move {
529                wait_for_peers().await;
530                let mut connection = connection.recv().await.unwrap()?;
531                let cid = connection.channel.get_session_cid();
532                wait_for_peers().await;
533                let remote = connection.remote.clone();
534                let handle_orig = connection.incoming_object_transfer_handles.take().unwrap();
535                let _file_transfer_task = accept_all(handle_orig);
536                log::info!(target: "citadel", "***CLIENT B {cid} LOGIN SUCCESS :: File transfer next ***");
537                let virtual_path = PathBuf::from("/home/john.doe/TheBridge.pdf");
538                // write the file to the RE-VFS
539                crate::fs::write_with_security_level(
540                    &remote,
541                    source_dir.clone(),
542                    security_level,
543                    &virtual_path,
544                )
545                .await?;
546                log::info!(target: "citadel", "***CLIENT B {cid} FILE TRANSFER SUCCESS***");
547                // Wait some time for the file to synchronize
548                tokio::time::sleep(Duration::from_secs(1)).await;
549                wait_for_peers().await;
550                // now, pull it
551                let save_dir = crate::fs::read(&remote, virtual_path).await?;
552                // now, compare bytes
553                log::info!(target: "citadel", "***CLIENT B {cid} REVFS PULL SUCCESS");
554                let original_bytes = citadel_io::tokio::fs::read(&source_dir).await.unwrap();
555                let revfs_pulled_bytes = citadel_io::tokio::fs::read(&save_dir).await.unwrap();
556                assert_eq!(original_bytes, revfs_pulled_bytes);
557                log::info!(target: "citadel", "***CLIENT B {cid} REVFS PULL COMPARE SUCCESS");
558                wait_for_peers().await;
559                client1_success.store(true, Ordering::Relaxed);
560                remote_outer.shutdown_kernel().await
561            },
562        );
563
564        let client0 = DefaultNodeBuilder::default()
565            .with_stun_servers(crate::test_common::LOCALHOST_STUN_SERVERS)
566            .build(client_kernel0)
567            .unwrap();
568        let client1 = DefaultNodeBuilder::default()
569            .with_stun_servers(crate::test_common::LOCALHOST_STUN_SERVERS)
570            .build(client_kernel1)
571            .unwrap();
572        let clients = futures::future::try_join(client0, client1);
573
574        let task = async move {
575            citadel_io::tokio::select! {
576                server_res = server => Err(citadel_io::error!(citadel_io::ErrorCode::PrefabServerEndedPrematurely, citadel_io::Dbg(server_res.map(|_| ())))),
577                client_res = clients => client_res.map(|_| ())
578            }
579        };
580
581        let _ = citadel_io::time::timeout(Duration::from_secs(120), task)
582            .await
583            .unwrap();
584
585        assert!(client0_success.load(Ordering::Relaxed));
586        assert!(client1_success.load(Ordering::Relaxed));
587    }
588
589    #[allow(dead_code)]
590    fn accept_all(mut rx: FileTransferHandleRx) -> citadel_io::tokio::task::JoinHandle<()> {
591        citadel_io::tokio::task::spawn(async move {
592            while let Some(mut handle) = rx.recv().await {
593                if let Err(err) = handle.accept() {
594                    log::error!(target: "citadel", "Failed to accept file transfer: {err:?}");
595                    continue;
596                }
597
598                // Wait for this transfer to complete before accepting the next one
599                exhaust_file_transfer_async(handle).await;
600            }
601        })
602    }
603
604    #[allow(dead_code)]
605    async fn exhaust_file_transfer_async(mut handle: ObjectTransferHandler) {
606        while let Some(evt) = handle.next().await {
607            log::info!(target: "citadel", "File Transfer Event: {evt:?}");
608            if let ObjectTransferStatus::Fail(err) = &evt {
609                log::error!(target: "citadel", "File Transfer Failed: {err:?}");
610                break;
611            } else if let ObjectTransferStatus::TransferComplete = &evt {
612                break;
613            } else if let ObjectTransferStatus::ReceptionComplete = &evt {
614                break;
615            }
616        }
617    }
618}