Skip to content
Merged
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
7 changes: 6 additions & 1 deletion docs/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ Compatibility notes:
- removed --rsh option (use the BORG_RSH environment variable)
- removed --upload-ratelimit and --upload-buffer options (they only ever
affected uploads to a borg 1.x repository via ssh://)
- removed --iec option (use the BORG_IEC environment variable)
- removed --iec option (use the BORG_UNITS environment variable)
- removed --debug-profile option (use the BORG_DEBUG_PROFILE environment variable)
- removed borg config command (only worked locally anyway)
- compact command now requires access to the borg key if the repo is encrypted
Expand Down Expand Up @@ -174,6 +174,11 @@ New features:
deleting the whole set would free). Chunk ids and sizes are read from the
per-archive references cache maintained by ``borg compact``, so unchanged archives
usually do not need to be opened.
- new ``BORG_UNITS`` environment variable, #5513.
It determines how sizes are formatted in human-readable output: ``si`` (default,
1kB = 1000B), ``iec`` (1KiB = 1024B) or ``raw`` (exact byte counts, e.g. for easy
parsing by monitoring scripts).
``BORG_IEC`` was removed, use ``BORG_UNITS=iec`` instead.

Version 2.0.0b22 (2026-07-22)
-----------------------------
Expand Down
19 changes: 15 additions & 4 deletions docs/usage/general/environment.rst.inc
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,21 @@ General:
BORG_REMOTE_PATH
When set, use the given path as borg executable on the remote (defaults to "borg" if unset).
This is the replacement for the removed ``--remote-path PATH`` command line option.
BORG_IEC
When set to ``yes``, format sizes using IEC units (1KiB = 1024B) instead of
decimal units (1kB = 1000B).
This is the replacement for the removed ``--iec`` command line option.
BORG_UNITS
Determines how borg formats sizes in its human-readable output:

- ``si`` (default): decimal units, e.g. ``1.23 MB`` (1kB = 1000B)
- ``iec``: binary units, e.g. ``1.18 MiB`` (1KiB = 1024B)
- ``raw``: exact byte counts, e.g. ``1234567 B``

Use ``raw`` if you want to parse sizes with scripts (e.g. for monitoring),
so you do not have to deal with scaled values and different units.
Alternatively, use a command's ``--json`` output or, for the commands
supporting ``--format``, the size related format keys - sizes are given
as byte counts there anyway.

``BORG_UNITS=iec`` is the replacement for the removed ``BORG_IEC`` environment
variable (and for the ``--iec`` command line option removed before that).
BORG_DEBUG_PROFILE
When set to a filename, write an execution profile in Borg format into that file
(see :ref:`debugging`). If the filename ends with ``.pyprof``, a Python-compatible
Expand Down
36 changes: 14 additions & 22 deletions src/borg/archive.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,14 @@
del _method


def format_store_stats(stats, iec=False):
def format_store_stats(stats):
"""Render a borgstore stats dict as one "Store <name>: <value>" line per entry."""

def format_value(key, value):
if key.endswith("_throughput"):
return f"{format_file_size(value, iec=iec)}/s"
return f"{format_file_size(value)}/s"
if key.endswith("_volume"):
return format_file_size(value, iec=iec)
return format_file_size(value)
if key.endswith("_time"):
return f"{value:.3f} seconds"
if key.endswith("_ratio"):
Expand All @@ -106,9 +106,8 @@ def format_value(key, value):


class Statistics:
def __init__(self, output_json=False, iec=False):
def __init__(self, output_json=False):
self.output_json = output_json
self.iec = iec
self.osize = self.usize = self.nfiles = 0
self.last_progress = 0 # timestamp when last progress was shown
self.files_stats = defaultdict(int)
Expand All @@ -124,7 +123,7 @@ def update(self, size, unique):
def __add__(self, other):
if not isinstance(other, Statistics):
raise TypeError("can only add Statistics objects")
stats = Statistics(self.output_json, self.iec)
stats = Statistics(self.output_json)
stats.osize = self.osize + other.osize
stats.usize = self.usize + other.usize
stats.nfiles = self.nfiles + other.nfiles
Expand Down Expand Up @@ -160,7 +159,7 @@ def __str__(self):
files_changed_while_reading=self.files_stats["C"],
)
if self.store_stats:
result += format_store_stats(self.store_stats, iec=self.iec) + "\n"
result += format_store_stats(self.store_stats) + "\n"
return result

def __repr__(self):
Expand All @@ -170,7 +169,7 @@ def __repr__(self):

def as_dict(self):
return {
"original_size": FileSize(self.osize, iec=self.iec),
"original_size": FileSize(self.osize),
"nfiles": self.nfiles,
"hashing_time": self.hashing_time,
"chunking_time": self.chunking_time,
Expand All @@ -190,11 +189,11 @@ def from_raw_dict(cls, **kw):

@property
def osize_fmt(self):
return format_file_size(self.osize, iec=self.iec)
return format_file_size(self.osize)

@property
def usize_fmt(self):
return format_file_size(self.usize, iec=self.iec)
return format_file_size(self.usize)

def show_progress(self, item=None, final=False, stream=None, dt=None):
now = time.monotonic()
Expand Down Expand Up @@ -518,7 +517,6 @@ def __init__(
start=None,
end=None,
log_json=False,
iec=False,
deleted=False,
):
name_is_id = isinstance(name, bytes)
Expand All @@ -534,8 +532,7 @@ def __init__(
self.repo_objs = manifest.repo_objs
self.repository = manifest.repository
self.cache = cache
self.stats = Statistics(output_json=log_json, iec=iec)
self.iec = iec
self.stats = Statistics(output_json=log_json)
self.show_progress = progress
self.name = name # overwritten later with name from archive metadata
self.name_in_manifest = name # can differ from .name later (if borg check fixed duplicate archive names)
Expand Down Expand Up @@ -738,7 +735,7 @@ def save(self, name=None, comment=None, timestamp=None, stats=None, additional_m
return metadata

def calc_stats(self, cache, want_unique=True):
stats = Statistics(iec=self.iec)
stats = Statistics()
stats.usize = 0 # this is expensive to compute
stats.nfiles = self.metadata.nfiles
stats.osize = self.metadata.size
Expand Down Expand Up @@ -1318,7 +1315,6 @@ def __init__(
show_progress,
sparse,
log_json,
iec,
file_status_printer=None,
files_changed="mtime" if is_win32 else "ctime",
):
Expand All @@ -1332,7 +1328,7 @@ def __init__(
self.files_changed = files_changed

self.hlm = HardLinkManager(id_type=tuple, info_type=(list, type(None))) # (dev, ino) -> chunks or None
self.stats = Statistics(output_json=log_json, iec=iec) # threading: done by cache (including progress)
self.stats = Statistics(output_json=log_json) # threading: done by cache (including progress)
self.cwd = os.getcwd()
self.chunker = get_chunker(*chunker_params, key=key, sparse=sparse)

Expand Down Expand Up @@ -1616,7 +1612,6 @@ def __init__(
chunker_params,
show_progress,
log_json,
iec,
file_status_printer=None,
):
self.cache = cache
Expand All @@ -1626,7 +1621,7 @@ def __init__(
self.show_progress = show_progress
self.print_file_status = file_status_printer or (lambda *args: None)

self.stats = Statistics(output_json=log_json, iec=iec) # threading: done by cache (including progress)
self.stats = Statistics(output_json=log_json) # threading: done by cache (including progress)
self.chunker = get_chunker(*chunker_params, key=key, sparse=False)
self.hlm = HardLinkManager(id_type=str, info_type=list) # normalized/safe path -> chunks

Expand Down Expand Up @@ -1829,7 +1824,6 @@ def check(
newer=None,
oldest=None,
newest=None,
iec=False,
):
"""Perform a set of checks on 'repository'

Expand All @@ -1841,7 +1835,6 @@ def check(
:param oldest/newest: only check archives older/newer than timedelta from oldest/newest archive timestamp
:param verify_data: integrity verification of data referenced by archives
:param format: format string used to describe an archive in the log output
:param iec: format sizes using IEC units (1KiB = 1024B)
"""
if not isinstance(repository, Repository):
logger.error("Checking legacy repositories is not supported.")
Expand All @@ -1850,7 +1843,6 @@ def check(
self.check_all = not any((first, last, match, older, newer, oldest, newest))
self.repair = repair
self.format = format
self.iec = iec
self.repository = repository
# A normal (non-repair) archives check trusts the in-repo index: the repository check verified
# each index object's sha256, and the index is the authoritative record of which chunks exist,
Expand Down Expand Up @@ -2210,7 +2202,7 @@ def valid_item(obj):
else:
archive_infos = self.manifest.archives.list(sort_by=sort_by)
num_archives = len(archive_infos)
formatter = ArchiveFormatter(self.format, self.repository, self.manifest, self.key, iec=self.iec)
formatter = ArchiveFormatter(self.format, self.repository, self.manifest, self.key)

pi = ProgressIndicatorPercent(
total=num_archives, msg="Checking archives %3.1f%%", step=0.1, msgid="check.rebuild_archives"
Expand Down
4 changes: 0 additions & 4 deletions src/borg/archiver/_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from ..helpers import Error
from ..helpers import SortBySpec, location_validator, Location, relative_time_marker_validator
from ..helpers import Highlander, octal_int
from ..helpers import use_iec_units
from ..helpers.argparsing import SUPPRESS, PositiveInt
from ..helpers.nanorst import rst_to_terminal
from ..manifest import Manifest, AI_HUMAN_SORT_KEYS
Expand Down Expand Up @@ -167,7 +166,6 @@ def wrapper(self, args, **kwargs):
progress=getattr(args, "progress", False),
cache_mode=getattr(args, "files_cache_mode", FILES_CACHE_MODE_DISABLED),
start_backup=getattr(self, "start_backup", None),
iec=use_iec_units(),
) as cache_:
return method(self, args, repository=repository, cache=cache_, **kwargs)
else:
Expand Down Expand Up @@ -238,7 +236,6 @@ def wrapper(self, args, **kwargs):
manifest_,
progress=False,
cache_mode=getattr(args, "files_cache_mode", FILES_CACHE_MODE_DISABLED),
iec=use_iec_units(),
) as cache_:
kwargs["other_cache"] = cache_
return method(self, args, **kwargs)
Expand All @@ -265,7 +262,6 @@ def wrapper(self, args, repository, manifest, **kwargs):
noxattrs=getattr(args, "noxattrs", False),
cache=kwargs.get("cache"),
log_json=args.log_json,
iec=use_iec_units(),
)
return method(self, args, repository=repository, manifest=manifest, archive=archive, **kwargs)

Expand Down
7 changes: 3 additions & 4 deletions src/borg/archiver/analyze_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from ..archive import Archive
from ..cache import get_archive_references, list_archive_reference_caches
from ..constants import * # NOQA
from ..helpers import bin_to_hex, Error, format_file_size, use_iec_units
from ..helpers import bin_to_hex, Error, format_file_size
from ..helpers import ProgressIndicatorPercent
from ..helpers.argparsing import ArgumentParser
from ..manifest import Manifest
Expand Down Expand Up @@ -38,7 +38,6 @@ def __init__(self, args, repository, manifest):
self.repository = repository
assert isinstance(repository, Repository)
self.manifest = manifest
self.iec = use_iec_units()
self.difference_by_path = defaultdict(int) # directory path -> count of chunks changed

def analyze(self):
Expand All @@ -63,7 +62,7 @@ def analyze(self):
logger.info("Finished archives analysis.")

def fmt(self, value):
return format_file_size(value, iec=self.iec)
return format_file_size(value)

def factor(self, stored, plaintext):
"""The compression factor stored/plaintext, or n/a if there is no plaintext size to relate to
Expand All @@ -89,7 +88,7 @@ def mark_references(self, archive_infos, update_flags):
)
for i, info in enumerate(archive_infos):
references = get_archive_references(
self.repository, self.manifest, info.id, cached=bin_to_hex(info.id) in cached_hex_ids, iec=self.iec
self.repository, self.manifest, info.id, cached=bin_to_hex(info.id) in cached_hex_ids
)
for id, ref in references.ids.items():
entry = index.get(id)
Expand Down
3 changes: 1 addition & 2 deletions src/borg/archiver/check_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from ..archive import ArchiveChecker
from ..constants import * # NOQA
from ..helpers import set_ec, EXIT_WARNING, CancelledByUser, CommandError, IntegrityError
from ..helpers import yes, ArchiveFormatter, use_iec_units
from ..helpers import yes, ArchiveFormatter
from ..helpers.argparsing import ArgumentParser

from ..logger import create_logger
Expand Down Expand Up @@ -80,7 +80,6 @@ def do_check(self, args, repository):
oldest=args.oldest,
newest=args.newest,
format=format,
iec=use_iec_units(),
):
set_ec(EXIT_WARNING)
return
Expand Down
27 changes: 10 additions & 17 deletions src/borg/archiver/compact_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
from ..hashindex import ChunkIndex
from ..helpers import set_ec, EXIT_ERROR, Error, sig_int, format_file_size, bin_to_hex, hex_to_bin, IntegrityError
from ..helpers import ProgressIndicatorPercent
from ..helpers import use_iec_units
from ..manifest import Manifest
from ..repository import Repository

Expand All @@ -24,7 +23,7 @@


class ArchiveGarbageCollector:
def __init__(self, repository, manifest, *, stats, iec, threshold, dry_run=False):
def __init__(self, repository, manifest, *, stats, threshold, dry_run=False):
self.repository = repository
assert isinstance(repository, Repository)
self.manifest = manifest
Expand All @@ -35,7 +34,6 @@ def __init__(self, repository, manifest, *, stats, iec, threshold, dry_run=False
self.archives_count = None # number of archives
self.stats = stats # compute repo space usage before/after - lists all repo objects, can be slow.
self.threshold = threshold # rewrite a mixed pack only when its wasted-bytes fraction reaches this percent
self.iec = iec # formats statistics using IEC units (1KiB = 1024B)
self.dry_run = dry_run
self.store_changed = False # True once compact_packs() has changed the store (and its index)

Expand Down Expand Up @@ -160,7 +158,6 @@ def analyze_archives(self) -> tuple[set, int, int, int]:
self.manifest,
info.id,
cached=bin_to_hex(info.id) in cached_hex_ids,
iec=self.iec,
store=not self.dry_run,
)
total_files += references.file_count # every fs object counts, not just regular files
Expand Down Expand Up @@ -200,22 +197,18 @@ def report_and_delete(self):
count = len(self.chunks)
logger.info(f"Overall statistics, considering all {self.archives_count} archives in this repository:")
logger.info(
f"Source data size was {format_file_size(self.total_size, precision=0, iec=self.iec)} "
f"Source data size was {format_file_size(self.total_size, precision=0)} "
f"in {self.total_files} files."
)
logger.info(f"Deduplicated size is {format_file_size(deduplicated_size, precision=0, iec=self.iec)}.")
logger.info(f"Deduplicated size is {format_file_size(deduplicated_size, precision=0)}.")
dedup_factor = deduplicated_size / self.total_size if self.total_size else 1.0
logger.info(f"Deduplication factor is {dedup_factor:.2f}.")
logger.info(
f"Repository size is {format_file_size(repo_size_after, precision=0, iec=self.iec)} "
f"in {count} objects."
)
logger.info(f"Repository size is {format_file_size(repo_size_after, precision=0)} " f"in {count} objects.")
comp_factor = repo_size_after / deduplicated_size if deduplicated_size else 1.0
logger.info(f"Compression factor is {comp_factor:.2f}.")
if not self.dry_run: # nothing was deleted on a dry run, so before == after
logger.info(
f"Compaction saved "
f"{format_file_size(repo_size_before - repo_size_after, precision=0, iec=self.iec)}."
f"Compaction saved " f"{format_file_size(repo_size_before - repo_size_after, precision=0)}."
)

def mark_soft_deleted_used(self):
Expand All @@ -228,7 +221,7 @@ def mark_soft_deleted_used(self):
for archive_info in self.manifest.archives.list(sort_by=["ts"], deleted=True):
name, hex_id = archive_info.name, bin_to_hex(archive_info.id)
try:
archive = Archive(self.manifest, archive_info.id, iec=self.iec, deleted=True)
archive = Archive(self.manifest, archive_info.id, deleted=True)
missing = sum(not self._mark_object_used(id) for id in self._archive_object_ids(archive))
if missing:
logger.warning(
Expand Down Expand Up @@ -306,7 +299,7 @@ def compact_packs(self):
unindexed = sum(total - pack_indexed[pid] for pid, total in pack_total.items() if total > pack_indexed[pid])
if unindexed:
logger.info(
f"{format_file_size(unindexed, iec=self.iec)} in pack files is not covered by the index; "
f"{format_file_size(unindexed)} in pack files is not covered by the index; "
'redundant copies are reclaimed on pack rewrite, the rest by "borg check --repair".'
)

Expand Down Expand Up @@ -357,7 +350,7 @@ def compact_packs(self):
if self.dry_run:
freed = sum(pack_reclaim.values())
logger.info(
f"Would free {format_file_size(freed, iec=self.iec)} by dropping {len(drop_packs)} "
f"Would free {format_file_size(freed)} by dropping {len(drop_packs)} "
f"packs, rewriting {len(rewrite_packs)} packs and merging {len(merge_packs)} tiny packs."
)
return repo_size_before, repo_size_before # dry run: report only, change nothing
Expand Down Expand Up @@ -385,7 +378,7 @@ def compact_packs(self):
# objects cut from rewritten packs. reclaimed counts the bytes freed.
deleted = sum(len(ids) for ids in forget.values()) + sum(len(ids) for ids in drop.values())
reclaimed = sum(pack_reclaim.values())
logger.info(f"Deleting {deleted} unused objects, freeing {format_file_size(reclaimed, iec=self.iec)}...")
logger.info(f"Deleting {deleted} unused objects, freeing {format_file_size(reclaimed)}...")
pi = ProgressIndicatorPercent(
total=len(drop_packs) + len(rewrite_packs) + (1 if merge_packs else 0),
msg="Compacting packs %3.1f%%",
Expand Down Expand Up @@ -437,7 +430,7 @@ def do_compact(self, args, repository, manifest):
# is nothing to do and make compaction a silent no-op.
repository.assert_writable()
ArchiveGarbageCollector(
repository, manifest, stats=args.stats, iec=use_iec_units(), threshold=args.threshold, dry_run=args.dry_run
repository, manifest, stats=args.stats, threshold=args.threshold, dry_run=args.dry_run
).garbage_collect()

def build_parser_compact(self, subparsers, common_parser, mid_common_parser):
Expand Down
Loading
Loading