Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ General Public License v2 (GPLv2). See LICENSES directory or go to
([PR#2486](https://github.com/bit-team/backintime/pull/2486))
- Dependency(build): `pandoc` to convert markdown changelog into HTML
- Dependency(runtime-cli): `gocryptfs`
- CLI option '--usage' to 'show' subcommand to display the total physical disk usage of all backups in a profile

### Removed
- **Breaking**: EncFS support including existing EncFS profiles
Expand All @@ -42,9 +43,6 @@ General Public License v2 (GPLv2). See LICENSES directory or go to
## Fixed
- Prevent crash in case a plugin fails ([#2447](https://github.com/bit-team/backintime/issues/2447))

## Fixed
- Prevent Back In Time crash when a plugin fails ([#2447](https://github.com/bit-team/backintime/issues/2447))

## [1.6.1] (2026-02-10)

### Fixed
Expand Down
6 changes: 6 additions & 0 deletions common/cliarguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -682,6 +682,12 @@ def _create_cmd_show(self):
default=False,
help='show the last (youngest) backup only')

parser.add_argument(
'--usage',
action='store_true',
default=False,
help='display total physical disk usage of all backups')

self.parsers[name] = parser

def _create_cmd_unmount(self):
Expand Down
103 changes: 84 additions & 19 deletions common/clicommands.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"""Module about CLI commands"""
import sys
import argparse
import subprocess
from datetime import datetime
from time import sleep
import tools
Expand All @@ -29,6 +30,7 @@
from mount import MountManager
from applicationinstance import ApplicationInstance
from shutdownagent import ShutdownAgent
from storagesize import StorageSize, SizeUnit


def _deprecation_msg(cmd_flag: str, replacement: str) -> str:
Expand Down Expand Up @@ -511,9 +513,18 @@ def show_backups(args: argparse.Namespace):
mounted_path=mount_manager.path
)

if not backups:
logger.error(f'No backups in profile "{cfg.profileName()}"')
_umount(cfg)
sys.exit(bitbase.RETURN_ERR)

if args.last:
backups = backups[-1:]

if args.usage:
size_bytes = _compute_total_usage(cfg, backups)
print(_format_usage(size_bytes))

if args.path:
# Path
def _element(e):
Expand All @@ -530,10 +541,6 @@ def _element(e):

print(result)

if not backups:
logger.info(f'No backups in profile "{cfg.profileName()}"')
sys.exit(bitbase.RETURN_ERR)

sys.exit(bitbase.RETURN_OK)


Expand Down Expand Up @@ -579,19 +586,77 @@ def prune(args: argparse.Namespace):
sys.exit(bitbase.RETURN_OK)


def unmount(args):
"""Unmount all filesystems.

Args:
args: Previously parsed arguments

Raises:
SystemExit: 0
"""
cli.set_quiet(args)

cfg = _get_config(args)
def _du_local_total(paths: list) -> int:
if not paths:
return 0
try:
result = subprocess.run(
['du', '-sbc'] + [str(p) for p in paths],
capture_output=True, text=True, check=True
)
total_line = result.stdout.strip().split('\n')[-1]
return int(total_line.split()[0])
except subprocess.CalledProcessError as err:
logger.error(
f'Failed to compute local disk usage: {err.stderr.strip()}')
return -1
except (ValueError, IndexError):
logger.error('Failed to parse disk usage output')
return -1


def _du_remote_total(cfg, backups) -> int:
mode = cfg.snapshotsMode()
remote_paths = []

for sid_str, _ in backups:
sid_obj = snapshots.SID(sid_str, cfg)
if mode == 'ssh_encfs':
remote_path = sid_obj.path(use_mode=['ssh_encfs'])
else:
remote_path = sid_obj.path(use_mode=['ssh'])
remote_paths.append(remote_path)

mount_manager = MountManager.create(cfg)
with mount_manager.mounted():
sys.exit(bitbase.RETURN_OK)
ssh_cmd = cfg.sshCommand(
cmd=['du', '-sbc'] + remote_paths,
nice=False, ionice=False
)
try:
result = subprocess.run(
ssh_cmd, capture_output=True, text=True, check=True
)
total_line = result.stdout.strip().split('\n')[-1]
return int(total_line.split()[0])
except subprocess.CalledProcessError as err:
logger.error(
f'Failed to compute remote disk usage: {err.stderr.strip()}')
return -1
except (ValueError, IndexError):
logger.error('Failed to parse remote disk usage output')
return -1


def _compute_total_usage(cfg, backups):
mode = cfg.snapshotsMode()
if mode in ('ssh', 'ssh_encfs'):
return _du_remote_total(cfg, backups)
return _du_local_total([p for _, p in backups])


def _format_usage(size_bytes: int) -> str:
if size_bytes < 0:
return 'Total disk usage: ERROR (could not determine size)'

size = StorageSize(size_bytes)

if size >= StorageSize(1, SizeUnit.GIB):
value = size.value(SizeUnit.GIB, decimal_places=1)
return f'Total disk usage: {value:.1f} GiB'
elif size >= StorageSize(1, SizeUnit.MIB):
value = size.value(SizeUnit.MIB, decimal_places=1)
return f'Total disk usage: {value:.1f} MiB'
elif size_bytes >= 1024:
value = size_bytes / 1024
return f'Total disk usage: {value:.1f} KiB'
else:
return f'Total disk usage: {size_bytes} Byte'
28 changes: 28 additions & 0 deletions common/test/test_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,3 +231,31 @@ def test_local_backup_and_no_local_backup(self):
('restore', '--local-backup', '--no-local-backup'),
self.parser_agent
)


class ShowCommand(unittest.TestCase):
"""Tests about arguments related to the 'show' command."""

def setUp(self):
super().setUp()
self.parser_agent = cliarguments.ParserAgent(
app_name=bitbase.APP_NAME,
bin_name=bitbase.BINARY_NAME_CLI)

def test_simple(self):
sut = cliarguments.parse_arguments(['show'], self.parser_agent)
self.assertEqual(sut.command, 'show')
self.assertIs(sut.func, clicommands.show_backups)

def test_usage(self):
sut = cliarguments.parse_arguments(
['show', '--usage'], self.parser_agent)
self.assertEqual(sut.command, 'show')
self.assertTrue(sut.usage)

def test_usage_last(self):
sut = cliarguments.parse_arguments(
['show', '--usage', '--last'], self.parser_agent)
self.assertEqual(sut.command, 'show')
self.assertTrue(sut.usage)
self.assertTrue(sut.last)
22 changes: 22 additions & 0 deletions common/test/test_backintime.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,3 +251,25 @@ def test_diagnostics_arg(self):
bitbase.APP_NAME)
self.assertEqual(diagnostics["backintime"]["version"],
version.__version__)

def test_show_with_usage(self):
snapshot_root = '/tmp/snapshots/backintime/test-host/test-user/1'

subprocess.getoutput(f'rm -rf {snapshot_root}')
os.makedirs(os.path.join(snapshot_root, '20260101-010000-001', 'backup'))
os.makedirs(os.path.join(snapshot_root, '20260315-083000-002', 'backup'))

proc = subprocess.Popen(["./backintime",
"--config", "test/config",
"show", "--usage"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
output, error = proc.communicate()
output_str = output.decode()

self.assertEqual(proc.returncode, 0,
f'show --usage failed. stderr: {error.decode()}')

self.assertIn('Total disk usage:', output_str)
self.assertIn('20260101-010000-001', output_str)
self.assertIn('20260315-083000-002', output_str)
2 changes: 1 addition & 1 deletion common/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import re

# Version string regularyly used by the application and presented to users.
__version__ = '2.0.0-dev.5d7ff833'
__version__ = '2.0.0-dev.250edd74'

# Version string ends with lower case ``rc`` and optionally with a number.
# e.g. "1.6.0rc", "1.6.0-rc", "1.6.0-rc2"
Expand Down