@@ -600,6 +600,63 @@ impl AgentFS {
600600 Ok ( whiteouts)
601601 }
602602
603+ /// Create a snapshot of the current database state.
604+ ///
605+ /// This creates a point-in-time copy of the database by checkpointing the WAL
606+ /// and copying the database file. The snapshot is consistent because we
607+ /// checkpoint first to ensure all data is written to the main database file.
608+ ///
609+ /// # Arguments
610+ /// * `dest_path` - Path where the snapshot will be saved (e.g., "/path/to/snapshot.db")
611+ ///
612+ /// # Example
613+ /// ```no_run
614+ /// use agentfs_sdk::{AgentFS, AgentFSOptions};
615+ ///
616+ /// # async fn example() -> agentfs_sdk::error::Result<()> {
617+ /// let agent = AgentFS::open(AgentFSOptions::with_id("my-agent")).await?;
618+ /// agent.snapshot("/tmp/my-agent-snapshot.db").await?;
619+ /// # Ok(())
620+ /// # }
621+ /// ```
622+ pub async fn snapshot ( & self , dest_path : & str ) -> Result < ( ) > {
623+ let conn = self . pool . get_connection ( ) . await ?;
624+
625+ // Checkpoint the WAL to ensure all data is in the main database file
626+ // PRAGMA wal_checkpoint(TRUNCATE) writes all WAL content to the database
627+ // and truncates the WAL file
628+ let mut checkpoint_rows = conn. query ( "PRAGMA wal_checkpoint(TRUNCATE)" , ( ) ) . await ?;
629+ // Consume the result rows
630+ while let Some ( _) = checkpoint_rows. next ( ) . await ? { }
631+
632+ // Get the source database path by querying the database filename
633+ let mut rows = conn. query ( "PRAGMA database_list" , ( ) ) . await ?;
634+ let mut source_path: Option < String > = None ;
635+
636+ while let Some ( row) = rows. next ( ) . await ? {
637+ // database_list returns: seq, name, file
638+ // We want the 'file' column (index 2) for the 'main' database
639+ if let Ok ( Value :: Text ( name) ) = row. get_value ( 1 ) {
640+ if name == "main" {
641+ if let Ok ( Value :: Text ( file) ) = row. get_value ( 2 ) {
642+ if !file. is_empty ( ) {
643+ source_path = Some ( file. clone ( ) ) ;
644+ }
645+ }
646+ break ;
647+ }
648+ }
649+ }
650+
651+ let source_path = source_path
652+ . ok_or_else ( || Error :: Snapshot ( "Cannot snapshot in-memory database" . to_string ( ) ) ) ?;
653+
654+ // Copy the database file
655+ std:: fs:: copy ( & source_path, dest_path) ?;
656+
657+ Ok ( ( ) )
658+ }
659+
603660 /// Check if overlay is enabled for this filesystem
604661 ///
605662 /// Returns the base path if overlay is enabled, None otherwise.
@@ -910,4 +967,135 @@ mod tests {
910967 let _ = std:: fs:: remove_file ( agentfs_dir ( ) . join ( file_name) ) ;
911968 }
912969 }
970+
971+ #[ tokio:: test]
972+ async fn test_snapshot ( ) {
973+ let temp_dir = std:: env:: temp_dir ( ) ;
974+ let source_path = temp_dir. join ( "test_snapshot_source.db" ) ;
975+ let snapshot_path = temp_dir. join ( "test_snapshot_dest.db" ) ;
976+
977+ // Cleanup any existing files
978+ let _ = std:: fs:: remove_file ( & source_path) ;
979+ let _ = std:: fs:: remove_file ( & snapshot_path) ;
980+
981+ // Create source database and add some data
982+ {
983+ let agentfs = AgentFS :: open ( AgentFSOptions :: with_path ( source_path. to_str ( ) . unwrap ( ) ) )
984+ . await
985+ . unwrap ( ) ;
986+
987+ // Add KV data
988+ agentfs
989+ . kv
990+ . set ( "snapshot_key" , & "snapshot_value" )
991+ . await
992+ . unwrap ( ) ;
993+
994+ // Add filesystem data
995+ agentfs. fs . mkdir ( "/snapshot_dir" , 0 , 0 ) . await . unwrap ( ) ;
996+ let ( _, file) = agentfs
997+ . fs
998+ . create_file ( "/snapshot_dir/file.txt" , DEFAULT_FILE_MODE , 0 , 0 )
999+ . await
1000+ . unwrap ( ) ;
1001+ file. pwrite ( 0 , b"snapshot content" ) . await . unwrap ( ) ;
1002+
1003+ // Create snapshot
1004+ agentfs
1005+ . snapshot ( snapshot_path. to_str ( ) . unwrap ( ) )
1006+ . await
1007+ . unwrap ( ) ;
1008+ }
1009+
1010+ // Verify snapshot file exists
1011+ assert ! ( snapshot_path. exists( ) , "Snapshot file should exist" ) ;
1012+
1013+ // Open snapshot and verify data
1014+ {
1015+ let snapshot_agentfs =
1016+ AgentFS :: open ( AgentFSOptions :: with_path ( snapshot_path. to_str ( ) . unwrap ( ) ) )
1017+ . await
1018+ . unwrap ( ) ;
1019+
1020+ // Verify KV data
1021+ let value: Option < String > = snapshot_agentfs. kv . get ( "snapshot_key" ) . await . unwrap ( ) ;
1022+ assert_eq ! ( value, Some ( "snapshot_value" . to_string( ) ) ) ;
1023+
1024+ // Verify filesystem data
1025+ let stats = snapshot_agentfs. fs . stat ( "/snapshot_dir" ) . await . unwrap ( ) ;
1026+ assert ! ( stats. is_some( ) ) ;
1027+ assert ! ( stats. unwrap( ) . is_directory( ) ) ;
1028+
1029+ let content = snapshot_agentfs
1030+ . fs
1031+ . read_file ( "/snapshot_dir/file.txt" )
1032+ . await
1033+ . unwrap ( ) ;
1034+ assert_eq ! ( content, Some ( b"snapshot content" . to_vec( ) ) ) ;
1035+ }
1036+
1037+ // Cleanup
1038+ let _ = std:: fs:: remove_file ( & source_path) ;
1039+ let _ = std:: fs:: remove_file ( & snapshot_path) ;
1040+ // Also clean up WAL files
1041+ let _ = std:: fs:: remove_file ( temp_dir. join ( "test_snapshot_source.db-shm" ) ) ;
1042+ let _ = std:: fs:: remove_file ( temp_dir. join ( "test_snapshot_source.db-wal" ) ) ;
1043+ let _ = std:: fs:: remove_file ( temp_dir. join ( "test_snapshot_dest.db-shm" ) ) ;
1044+ let _ = std:: fs:: remove_file ( temp_dir. join ( "test_snapshot_dest.db-wal" ) ) ;
1045+ }
1046+
1047+ #[ tokio:: test]
1048+ async fn test_snapshot_is_independent ( ) {
1049+ let temp_dir = std:: env:: temp_dir ( ) ;
1050+ let source_path = temp_dir. join ( "test_snapshot_indep_source.db" ) ;
1051+ let snapshot_path = temp_dir. join ( "test_snapshot_indep_dest.db" ) ;
1052+
1053+ // Cleanup any existing files
1054+ let _ = std:: fs:: remove_file ( & source_path) ;
1055+ let _ = std:: fs:: remove_file ( & snapshot_path) ;
1056+
1057+ // Create source database
1058+ let agentfs = AgentFS :: open ( AgentFSOptions :: with_path ( source_path. to_str ( ) . unwrap ( ) ) )
1059+ . await
1060+ . unwrap ( ) ;
1061+
1062+ // Add initial data
1063+ agentfs. kv . set ( "key1" , & "value1" ) . await . unwrap ( ) ;
1064+
1065+ // Create snapshot
1066+ agentfs
1067+ . snapshot ( snapshot_path. to_str ( ) . unwrap ( ) )
1068+ . await
1069+ . unwrap ( ) ;
1070+
1071+ // Modify source after snapshot
1072+ agentfs. kv . set ( "key2" , & "value2" ) . await . unwrap ( ) ;
1073+
1074+ // Open snapshot and verify it doesn't have the new data
1075+ let snapshot_agentfs =
1076+ AgentFS :: open ( AgentFSOptions :: with_path ( snapshot_path. to_str ( ) . unwrap ( ) ) )
1077+ . await
1078+ . unwrap ( ) ;
1079+
1080+ // Snapshot should have key1
1081+ let value: Option < String > = snapshot_agentfs. kv . get ( "key1" ) . await . unwrap ( ) ;
1082+ assert_eq ! ( value, Some ( "value1" . to_string( ) ) ) ;
1083+
1084+ // Snapshot should NOT have key2 (added after snapshot)
1085+ let value: Option < String > = snapshot_agentfs. kv . get ( "key2" ) . await . unwrap ( ) ;
1086+ assert_eq ! (
1087+ value, None ,
1088+ "Snapshot should not contain data added after snapshot was created"
1089+ ) ;
1090+
1091+ // Cleanup
1092+ drop ( agentfs) ;
1093+ drop ( snapshot_agentfs) ;
1094+ let _ = std:: fs:: remove_file ( & source_path) ;
1095+ let _ = std:: fs:: remove_file ( & snapshot_path) ;
1096+ let _ = std:: fs:: remove_file ( temp_dir. join ( "test_snapshot_indep_source.db-shm" ) ) ;
1097+ let _ = std:: fs:: remove_file ( temp_dir. join ( "test_snapshot_indep_source.db-wal" ) ) ;
1098+ let _ = std:: fs:: remove_file ( temp_dir. join ( "test_snapshot_indep_dest.db-shm" ) ) ;
1099+ let _ = std:: fs:: remove_file ( temp_dir. join ( "test_snapshot_indep_dest.db-wal" ) ) ;
1100+ }
9131101}
0 commit comments