@@ -38,6 +38,7 @@ pub use transfer::{BackupProvider, get_backup};
3838// Name of the database file in the backup.
3939const DBFILE_BACKUP_NAME : & str = "dc_database_backup.sqlite" ;
4040pub ( crate ) const BLOBS_BACKUP_NAME : & str = "blobs_backup" ;
41+ pub ( crate ) const PRIVITTY_BACKUP_NAME : & str = ".privitty" ;
4142
4243/// Import/export command.
4344#[ derive( Debug , Display , Copy , Clone , PartialEq , Eq , FromPrimitive , ToPrimitive ) ]
@@ -322,6 +323,15 @@ async fn import_backup_stream_inner<R: tokio::io::AsyncRead + Unpin>(
322323 let backup_file = ProgressReader :: new ( backup_file, context. clone ( ) , file_size) ;
323324 let mut archive = Archive :: new ( backup_file) ;
324325
326+ let context_dir = context
327+ . get_blobdir ( )
328+ . parent ( )
329+ . context ( "Context dir not found" ) ;
330+ let context_dir = match context_dir {
331+ Ok ( dir) => dir. to_path_buf ( ) ,
332+ Err ( e) => return ( Err ( e) . context ( "Failed to get context dir" ) , ) ,
333+ } ;
334+
325335 let mut entries = match archive. entries ( ) {
326336 Ok ( entries) => entries,
327337 Err ( e) => return ( Err ( e) . context ( "Failed to get archive entries" ) , ) ,
@@ -338,6 +348,38 @@ async fn import_backup_stream_inner<R: tokio::io::AsyncRead + Unpin>(
338348 Ok ( path) => path. to_path_buf ( ) ,
339349 Err ( e) => break Err ( e) . context ( "Failed to get entry path" ) ,
340350 } ;
351+
352+ // Handle .privitty folder separately - extract to context_dir instead of blobdir
353+ // Check if the path starts with .privitty component (skipping leading CurDir components)
354+ // This handles paths like ".privitty/file.txt", "./.privitty/file.txt", etc.
355+ let is_privitty_entry = path
356+ . components ( )
357+ . skip_while ( |c| matches ! ( c, std:: path:: Component :: CurDir ) )
358+ . next ( )
359+ . and_then ( |c| {
360+ if let std:: path:: Component :: Normal ( name) = c {
361+ name. to_str ( )
362+ } else {
363+ None
364+ }
365+ } )
366+ . map ( |name| name == PRIVITTY_BACKUP_NAME )
367+ . unwrap_or ( false ) ;
368+
369+ if is_privitty_entry {
370+ // Ensure target directory exists
371+ if let Err ( e) = fs:: create_dir_all ( & context_dir) . await {
372+ warn ! ( context, "Failed to create context dir {}: {:#}" , context_dir. display( ) , e) ;
373+ }
374+ if let Err ( e) = f. unpack_in ( & context_dir) . await {
375+ warn ! ( context, "Failed to unpack .privitty entry {} to {}: {:#}" , path. display( ) , context_dir. display( ) , e) ;
376+ // Continue processing other entries even if .privitty extraction fails
377+ } else {
378+ info ! ( context, "Successfully unpacked .privitty entry {} to {}" , path. display( ) , context_dir. display( ) ) ;
379+ }
380+ continue ;
381+ }
382+
341383 if let Err ( e) = f. unpack_in ( context. get_blobdir ( ) ) . await {
342384 break Err ( e) . context ( "Failed to unpack file" ) ;
343385 }
@@ -465,6 +507,14 @@ async fn export_backup(context: &Context, dir: &Path, passphrase: String) -> Res
465507 for blob in blobdir. iter ( ) {
466508 file_size += blob. to_abs_path ( ) . metadata ( ) ?. len ( )
467509 }
510+
511+ // Add size of .privitty folder if it exists
512+ let context_dir = context
513+ . get_blobdir ( )
514+ . parent ( )
515+ . context ( "Context dir not found" ) ?;
516+ let privitty_dir = context_dir. join ( PRIVITTY_BACKUP_NAME ) ;
517+ file_size += calculate_dir_size ( & privitty_dir) . await ?;
468518
469519 export_backup_stream ( context, & temp_db_path, blobdir, file, file_size)
470520 . await
@@ -545,6 +595,73 @@ where
545595 }
546596}
547597
598+ /// Calculates the total size of all files in a directory recursively.
599+ pub ( crate ) async fn calculate_dir_size ( dir : & Path ) -> Result < u64 > {
600+ let mut total_size = 0u64 ;
601+ if !dir. exists ( ) {
602+ return Ok ( 0 ) ;
603+ }
604+ // Use iterative approach with a stack to avoid recursion boxing issues
605+ let mut dirs_to_process = vec ! [ dir. to_path_buf( ) ] ;
606+
607+ while let Some ( current_dir) = dirs_to_process. pop ( ) {
608+ let mut entries = tokio:: fs:: read_dir ( & current_dir) . await ?;
609+ while let Ok ( Some ( entry) ) = entries. next_entry ( ) . await {
610+ let path = entry. path ( ) ;
611+ let metadata = entry. metadata ( ) . await ?;
612+ if metadata. is_file ( ) {
613+ total_size += metadata. len ( ) ;
614+ } else if metadata. is_dir ( ) {
615+ dirs_to_process. push ( path) ;
616+ }
617+ }
618+ }
619+ Ok ( total_size)
620+ }
621+
622+ /// Adds a directory to the tar archive recursively.
623+ async fn append_dir_to_tar < W > (
624+ builder : & mut tokio_tar:: Builder < W > ,
625+ source_dir : & Path ,
626+ archive_prefix : & Path ,
627+ ) -> Result < ( ) >
628+ where
629+ W : tokio:: io:: AsyncWrite + Unpin + Send ,
630+ {
631+ if !source_dir. exists ( ) {
632+ return Ok ( ( ) ) ;
633+ }
634+ // Use iterative approach with a stack to avoid recursion issues
635+ let mut dirs_to_process: Vec < ( PathBuf , PathBuf ) > = vec ! [ ( source_dir. to_path_buf( ) , archive_prefix. to_path_buf( ) ) ] ;
636+
637+ while let Some ( ( current_dir, current_archive_prefix) ) = dirs_to_process. pop ( ) {
638+ let mut entries = tokio:: fs:: read_dir ( & current_dir) . await ?;
639+ while let Ok ( Some ( entry) ) = entries. next_entry ( ) . await {
640+ let path = entry. path ( ) ;
641+ let metadata = entry. metadata ( ) . await ?;
642+
643+ if metadata. is_file ( ) {
644+ let file_name = path
645+ . file_name ( )
646+ . and_then ( |n| n. to_str ( ) )
647+ . ok_or_else ( || format_err ! ( "invalid file name" ) ) ?;
648+ let archive_path = current_archive_prefix. join ( file_name) ;
649+ let mut file = File :: open ( & path) . await ?;
650+ builder. append_file ( archive_path, & mut file) . await ?;
651+ } else if metadata. is_dir ( ) {
652+ let dir_name = path
653+ . file_name ( )
654+ . and_then ( |n| n. to_str ( ) )
655+ . ok_or_else ( || format_err ! ( "invalid directory name" ) ) ?;
656+ let archive_path = current_archive_prefix. join ( dir_name) ;
657+ // Add subdirectory to stack for processing
658+ dirs_to_process. push ( ( path, archive_path) ) ;
659+ }
660+ }
661+ }
662+ Ok ( ( ) )
663+ }
664+
548665/// Exports the database and blobs into a stream.
549666pub ( crate ) async fn export_backup_stream < ' a , W > (
550667 context : & ' a Context ,
@@ -569,6 +686,26 @@ where
569686 builder. append_file ( path_in_archive, & mut file) . await ?;
570687 }
571688
689+ // Add .privitty folder if it exists
690+ let context_dir = context
691+ . get_blobdir ( )
692+ . parent ( )
693+ . context ( "Context dir not found" ) ?;
694+ let privitty_dir = context_dir. join ( PRIVITTY_BACKUP_NAME ) ;
695+ if privitty_dir. exists ( ) {
696+ info ! ( context, "Adding .privitty folder to backup from {}" , privitty_dir. display( ) ) ;
697+ append_dir_to_tar (
698+ & mut builder,
699+ & privitty_dir,
700+ & PathBuf :: from ( PRIVITTY_BACKUP_NAME ) ,
701+ )
702+ . await
703+ . context ( "Failed to add .privitty folder to backup" ) ?;
704+ info ! ( context, "Successfully added .privitty folder to backup" ) ;
705+ } else {
706+ info ! ( context, ".privitty folder does not exist at {}, skipping" , privitty_dir. display( ) ) ;
707+ }
708+
572709 builder. finish ( ) . await ?;
573710 Ok ( ( ) )
574711}
0 commit comments