11"""Backup command implementation for creating compressed archives.
22
3- This module contains the BackupCommand class that handles the creation of backup archives
4- using tar and xz compression.
3+ This module contains the BackupCommand class that handles the creation of
4+ backup archives using tar and xz compression.
55"""
66
77import datetime
@@ -41,15 +41,15 @@ def execute(self) -> bool:
4141
4242 """
4343 if not self .config .dirs_to_backup :
44- msg = "No directories configured for backup. Skipping backup."
45- print ( msg )
46- self . logger . error ( "No directories configured for backup. Skipping backup." )
44+ self . _print_and_log (
45+ "No directories configured for backup. Skipping backup." , level = "error"
46+ )
4747 return False
4848 total_size : int = self ._calculate_total_size ()
4949 if total_size == 0 :
50- msg = "Total backup size is 0 bytes. Nothing to back up."
51- print ( msg )
52- self . logger . warning ( "Total backup size is 0 bytes. Nothing to back up." )
50+ self . _print_and_log (
51+ "Total backup size is 0 bytes. Nothing to back up." , level = "warning"
52+ )
5353 return False
5454 success : bool = self ._run_backup_process (total_size )
5555 if success :
@@ -77,56 +77,64 @@ def _run_backup_process(self, total_size: int) -> bool:
7777
7878 """
7979 if os .path .exists (self .config .backup_path ):
80- print (f"File already exists: { self .config .backup_path } " )
81- self .logger .warning ("Backup file already exists: %s" , self .config .backup_path )
82- if input ("Do you want to remove it? (y/n): " ).lower () == "y" :
83- try :
84- os .remove (self .config .backup_path )
85- self .logger .info ("Removed existing backup file: %s" , self .config .backup_path )
86- except Exception as e :
87- print (f"Failed to remove existing backup: { e } " )
88- self .logger .error ("Failed to remove existing backup: %s" , e )
89- return False
90- else :
91- print ("Backup aborted by user." )
92- self .logger .info ("Backup aborted by user due to existing file." )
80+ self ._print_and_log (f"File already exists: { self .config .backup_path } " , level = "warning" )
81+ if not self ._prompt_overwrite ():
82+ self ._print_and_log ("Backup aborted by user due to existing file." , level = "info" )
83+ return False
84+ try :
85+ os .remove (self .config .backup_path )
86+ self .logger .info ("Removed existing backup file: %s" , self .config .backup_path )
87+ # NOTE: Broad except is used here to ensure any file removal error
88+ # is caught during backup overwrite prompt. This is a critical IO
89+ # operation.
90+ except (OSError , PermissionError ) as e :
91+ self ._print_and_log (f"Failed to remove existing backup: { e } " , level = "error" )
9392 return False
9493
95- exclude_options = " " .join (f"--exclude={ path } " for path in self .config .ignore_list )
94+ cmd = self ._build_tar_command ()
95+ total_size_gb = total_size / 1024 ** 3
9696
97- dir_paths = [os .path .expanduser (path ) for path in self .config .dirs_to_backup ]
97+ self .logger .info ("Starting backup to %s" , self .config .backup_path )
98+ self .logger .info ("Total size: %.2f GB" , total_size_gb )
9899
99- # Properly quote directory paths to handle spaces and special characters
100- quoted_paths = [shlex .quote (path ) for path in dir_paths ]
100+ try :
101+ subprocess .run (cmd , shell = True , check = True )
102+ self .logger .info ("Backup completed successfully" )
103+ self ._print_and_log ("Backup completed successfully." , level = "info" )
104+ return True
105+ except subprocess .CalledProcessError as e :
106+ self ._print_and_log (f"Backup failed: { e } " , level = "error" )
107+ return False
101108
102- # Get CPU count safely
109+ def _build_tar_command (self ) -> str :
110+ """Build the tar+xz command string for backup."""
111+ exclude_options = " " .join (f"--exclude={ path } " for path in self .config .ignore_list )
112+ dir_paths = [os .path .expanduser (path ) for path in self .config .dirs_to_backup ]
113+ quoted_paths = [shlex .quote (path ) for path in dir_paths ]
103114 cpu_count = os .cpu_count () or 1
104115 threads = max (1 , cpu_count - 1 )
105-
106- # HACK: h option is used to follow symlinks
107- cmd = (
116+ return (
108117 f"tar -chf - --one-file-system { exclude_options } "
109118 f"{ ' ' .join (quoted_paths )} | "
110119 f"xz --threads={ threads } > { self .config .backup_path } "
111120 )
112- total_size_gb = total_size / 1024 ** 3
113-
114- self .logger .info (f"Starting backup to { self .config .backup_path } " )
115- self .logger .info (f"Total size: { total_size_gb :.2f} GB" )
116121
117- try :
118- # FIX: later spinner not working for now
119- # FAILED: not work as expected because of
120- # "| tar: Removing leading `/' from member names" outputs
121- # self._show_spinner(subprocess.Popen(cmd, shell=True))
122- subprocess .run (cmd , shell = True , check = True )
123- self .logger .info ("Backup completed successfully" )
124- print ("Backup completed successfully." )
125- return True
126- except subprocess .CalledProcessError as e :
127- print (f"Backup failed: { e } " )
128- self .logger .error (f"Backup failed: { e } " )
129- return False
122+ def _prompt_overwrite (self ) -> bool :
123+ """Prompt user to overwrite existing backup file."""
124+ response = input ("Do you want to remove it? (y/n): " ).strip ().lower ()
125+ return response == "y"
126+
127+ def _print_and_log (self , message : str , level : str = "info" ) -> None :
128+ """Print and log a message at the specified level."""
129+ print (message )
130+ if level == "info" :
131+ self .logger .info (message )
132+ elif level == "warning" :
133+ self .logger .warning (message )
134+ elif level == "error" :
135+ self .logger .error (message )
136+ else :
137+ self .logger .debug (message )
130138
131139 def _save_backup_info (self , total_size : int ) -> None :
132140 """Save backup information to last-backup-info.json."""
@@ -146,10 +154,12 @@ def _save_backup_info(self, total_size: int) -> None:
146154 with open (info_file_path , "w" , encoding = "utf-8" ) as f :
147155 json .dump (backup_info , f , indent = 2 )
148156
149- self .logger .info (f "Backup info saved to { info_file_path } " )
157+ self .logger .info ("Backup info saved to %s" , info_file_path )
150158
151- except Exception as e :
152- self .logger .error (f"Failed to save backup info: { e } " )
159+ # NOTE: Broad except is used here to ensure any error during backup
160+ # info saving is logged, as this is a non-critical reporting step.
161+ except (OSError , PermissionError , ValueError ) as e :
162+ self .logger .error ("Failed to save backup info: %s" , e )
153163
154164 def _format_size (self , size_bytes : int ) -> str :
155165 """Format size in bytes to human readable format."""
0 commit comments