@@ -795,18 +795,45 @@ impl EncryptedClient {
795795
796796 /// Put an encrypted object using FlatNamespace mode
797797 ///
798- /// This automatically updates the forest index.
798+ /// This automatically updates AND SAVES the forest index after each file.
799+ /// For bulk uploads, use `put_object_flat_deferred` + `flush_forest` instead.
799800 pub async fn put_object_flat (
800801 & self ,
801802 bucket : & str ,
802803 key : & str ,
803804 data : impl Into < Bytes > ,
804805 content_type : Option < & str > ,
806+ ) -> Result < PutObjectResult > {
807+ let result = self . put_object_flat_deferred ( bucket, key, data, content_type) . await ?;
808+ self . flush_forest ( bucket) . await ?;
809+ Ok ( result)
810+ }
811+
812+ /// Put an encrypted object using FlatNamespace mode WITHOUT saving forest
813+ ///
814+ /// Use this for bulk uploads. Call `flush_forest` after uploading all files
815+ /// to persist the forest index. This is much more efficient for many files.
816+ ///
817+ /// # Example
818+ /// ```ignore
819+ /// // Upload 100 files efficiently
820+ /// for file in files {
821+ /// client.put_object_flat_deferred(bucket, &file.path, file.data, None).await?;
822+ /// }
823+ /// // Save forest once at the end
824+ /// client.flush_forest(bucket).await?;
825+ /// ```
826+ pub async fn put_object_flat_deferred (
827+ & self ,
828+ bucket : & str ,
829+ key : & str ,
830+ data : impl Into < Bytes > ,
831+ content_type : Option < & str > ,
805832 ) -> Result < PutObjectResult > {
806833 let data = data. into ( ) ;
807834 let original_size = data. len ( ) as u64 ;
808835
809- // Load or create forest
836+ // Load or create forest (from cache if available)
810837 let mut forest = self . load_forest ( bucket) . await ?;
811838
812839 // Generate a DEK for this object
@@ -833,7 +860,7 @@ impl EncryptedClient {
833860 let encrypted_meta = EncryptedPrivateMetadata :: encrypt ( & private_meta, & dek)
834861 . map_err ( ClientError :: Encryption ) ?;
835862
836- // Add to forest index
863+ // Add to forest index (in memory only)
837864 let entry = ForestFileEntry :: from_metadata ( & private_meta, storage_key. clone ( ) ) ;
838865 forest. upsert_file ( entry) ;
839866
@@ -856,19 +883,57 @@ impl EncryptedClient {
856883 . with_metadata ( "x-fula-encrypted" , "true" )
857884 . with_metadata ( "x-fula-encryption" , & enc_metadata. to_string ( ) ) ;
858885
859- let result = self . inner . put_object_with_metadata (
860- bucket,
861- & storage_key,
862- Bytes :: from ( ciphertext) ,
863- Some ( metadata) ,
864- ) . await ?;
886+ // Upload with optional pinning
887+ let result = if let Some ( ref pinning) = self . pinning {
888+ self . inner . put_object_with_metadata_and_pinning (
889+ bucket,
890+ & storage_key,
891+ Bytes :: from ( ciphertext) ,
892+ Some ( metadata) ,
893+ & pinning. endpoint ,
894+ & pinning. token ,
895+ ) . await ?
896+ } else {
897+ self . inner . put_object_with_metadata (
898+ bucket,
899+ & storage_key,
900+ Bytes :: from ( ciphertext) ,
901+ Some ( metadata) ,
902+ ) . await ?
903+ } ;
865904
866- // Save updated forest
867- self . save_forest ( bucket, & forest) . await ?;
905+ // Update cache (but don't save to storage yet)
906+ {
907+ let mut cache = self . forest_cache . write ( ) . unwrap ( ) ;
908+ cache. insert ( bucket. to_string ( ) , forest) ;
909+ }
868910
869911 Ok ( result)
870912 }
871913
914+ /// Flush the forest index to storage
915+ ///
916+ /// Call this after bulk uploads using `put_object_flat_deferred`.
917+ /// This persists the in-memory forest index to encrypted storage.
918+ pub async fn flush_forest ( & self , bucket : & str ) -> Result < ( ) > {
919+ let forest = {
920+ let cache = self . forest_cache . read ( ) . unwrap ( ) ;
921+ cache. get ( bucket) . cloned ( )
922+ } ;
923+
924+ if let Some ( forest) = forest {
925+ self . save_forest ( bucket, & forest) . await ?;
926+ }
927+
928+ Ok ( ( ) )
929+ }
930+
931+ /// Check if there are unsaved forest changes
932+ pub fn has_pending_forest_changes ( & self , bucket : & str ) -> bool {
933+ let cache = self . forest_cache . read ( ) . unwrap ( ) ;
934+ cache. contains_key ( bucket)
935+ }
936+
872937 /// Get an object using FlatNamespace mode
873938 ///
874939 /// Uses the forest index to resolve original path → storage key.
0 commit comments