44using tar and xz compression.
55"""
66
7+ import datetime
78import itertools
9+ import json
810import logging
911import os
1012import shlex
1113import subprocess
1214import sys
1315import time
16+ from pathlib import Path
1417
15- from src .commands .command import Command
16- from src .config import BackupConfig
17- from src .utils import SizeCalculator
18+ from autotarcompress .commands .command import Command
19+ from autotarcompress .config import BackupConfig
20+ from autotarcompress .utils import SizeCalculator
1821
1922
2023class BackupCommand (Command ):
@@ -25,29 +28,33 @@ def __init__(self, config: BackupConfig):
2528 self .logger = logging .getLogger (__name__ )
2629
2730 def execute (self ) -> bool :
28- """Execute backup process"""
31+ """Execute backup process. """
2932 if not self .config .dirs_to_backup :
3033 self .logger .error ("No directories configured for backup" )
3134 return False
3235
3336 total_size = self ._calculate_total_size ()
34- self ._run_backup_process (total_size )
35- return True
37+ success = self ._run_backup_process (total_size )
38+
39+ # Save backup info if backup was successful
40+ if success :
41+ self ._save_backup_info (total_size )
42+
43+ return success
3644
3745 def _calculate_total_size (self ) -> int :
3846 calculator = SizeCalculator (self .config .dirs_to_backup , self .config .ignore_list )
3947 return calculator .calculate_total_size ()
4048
41- # HACK: use loading spinner as a workaround loading which tqdm won't work
42-
43- def _run_backup_process (self , total_size : int ) -> None :
49+ def _run_backup_process (self , total_size : int ) -> bool :
50+ """Run the backup process and return success status."""
4451 # Check is there any file exist with same name
4552 if os .path .exists (self .config .backup_path ):
4653 print (f"File already exist: { self .config .backup_path } " )
4754 if input ("Do you want to remove it? (y/n): " ).lower () == "y" :
4855 os .remove (self .config .backup_path )
4956 else :
50- return
57+ return False
5158
5259 exclude_options = " " .join ([f"--exclude={ path } " for path in self .config .ignore_list ])
5360
@@ -56,14 +63,18 @@ def _run_backup_process(self, total_size: int) -> None:
5663 # exclude_options += f" --exclude={self.config.backup_folder}"
5764
5865 dir_paths = [os .path .expanduser (path ) for path in self .config .dirs_to_backup ]
59-
66+
6067 # Properly quote directory paths to handle spaces and special characters
6168 quoted_paths = [shlex .quote (path ) for path in dir_paths ]
62-
69+
70+ # Get CPU count safely
71+ cpu_count = os .cpu_count () or 1
72+ threads = max (1 , cpu_count - 1 )
73+
6374 # HACK: h option is used to follow symlinks
6475 cmd = (
6576 f"tar -chf - --one-file-system { exclude_options } { ' ' .join (quoted_paths )} | "
66- f"xz --threads={ os . cpu_count () - 1 } > { self .config .backup_path } "
77+ f"xz --threads={ threads } > { self .config .backup_path } "
6778 )
6879 total_size_gb = total_size / 1024 ** 3
6980
@@ -72,12 +83,49 @@ def _run_backup_process(self, total_size: int) -> None:
7283
7384 try :
7485 # FIX: later spinner not working for now
75- # FAILED: not work as expected because of "| tar: Removing leading `/' from member names" outputs
86+ # FAILED: not work as expected because of
87+ # "| tar: Removing leading `/' from member names" outputs
7688 # self._show_spinner(subprocess.Popen(cmd, shell=True))
7789 subprocess .run (cmd , shell = True , check = True )
7890 self .logger .info ("Backup completed successfully" )
91+ return True
7992 except subprocess .CalledProcessError as e :
8093 self .logger .error (f"Backup failed: { e } " )
94+ return False
95+
96+ def _save_backup_info (self , total_size : int ) -> None :
97+ """Save backup information to last-backup-info.json."""
98+ try :
99+ backup_info = {
100+ "backup_file" : Path (self .config .backup_path ).name ,
101+ "backup_path" : str (self .config .backup_path ),
102+ "backup_date" : datetime .datetime .now ().isoformat (),
103+ "backup_size_bytes" : total_size ,
104+ "backup_size_human" : self ._format_size (total_size ),
105+ "directories_backed_up" : self .config .dirs_to_backup ,
106+ }
107+
108+ # Save the info file in the backup folder
109+ info_file_path = Path (self .config .backup_folder ) / "last-backup-info.json"
110+
111+ with open (info_file_path , "w" , encoding = "utf-8" ) as f :
112+ json .dump (backup_info , f , indent = 2 )
113+
114+ self .logger .info (f"Backup info saved to { info_file_path } " )
115+
116+ except Exception as e :
117+ self .logger .error (f"Failed to save backup info: { e } " )
118+
119+ def _format_size (self , size_bytes : int ) -> str :
120+ """Format size in bytes to human readable format."""
121+ BYTES_IN_KB = 1024.0
122+ size = float (size_bytes )
123+
124+ for unit in ["B" , "KB" , "MB" , "GB" , "TB" ]:
125+ if size < BYTES_IN_KB :
126+ return f"{ size :.2f} { unit } "
127+ size /= BYTES_IN_KB
128+ return f"{ size :.2f} PB"
81129
82130 def _show_spinner (self , process ) -> None :
83131 spinner = itertools .cycle (["/" , "-" , "\\ " , "|" ])
0 commit comments