#!/usr/bin/env python3 """Local backup capacity checks; no deletion, provider contact or app changes. Candidate limits need operating admission. Estimates reserve complete pipeline scratch, while RLIMIT_FSIZE bounds individual newly written files. This is not a filesystem quota or a reservation against unrelated writers. """ from __future__ import annotations import argparse import json import os import resource import shutil import stat from pathlib import Path FILE_LIMIT = 1024**3 STATE_BUDGET = 8 * 1024**3 MINIMUM_FREE = 8 * 1024**3 CONTROL_MARGIN = 16 * 1024**2 MINIMUM_INODES = 1024 class Refused(ValueError): pass def active_file_limit(): return resource.getrlimit(resource.RLIMIT_FSIZE)[0] def enforce_file_limit(maximum=FILE_LIMIT): soft, hard = resource.getrlimit(resource.RLIMIT_FSIZE) effective = min( [maximum] + [value for value in [soft, hard] if value != resource.RLIM_INFINITY] ) if effective <= 0: raise Refused("positive_file_limit_required") resource.setrlimit(resource.RLIMIT_FSIZE, (effective, effective)) return effective def disk_free(path): return shutil.disk_usage(path).free def free_inodes(path): return os.statvfs(path).f_favail def state_usage(root): info = root.lstat() if ( not stat.S_ISDIR(info.st_mode) or info.st_uid != os.geteuid() or info.st_mode & 0o077 or root.resolve() != root.absolute() ): raise Refused("private_state_directory_required") def onerror(error): raise error used = max(info.st_size, info.st_blocks * 512) for directory, directories, files in os.walk(root, followlinks=False, onerror=onerror): for name in directories + files: item = (Path(directory) / name).lstat() if ( not (stat.S_ISREG(item.st_mode) or stat.S_ISDIR(item.st_mode)) or item.st_uid != os.geteuid() or item.st_mode & 0o077 or (stat.S_ISREG(item.st_mode) and item.st_nlink != 1) ): raise Refused("unsafe_state_entry") used += max(item.st_size, item.st_blocks * 512) return used def reserve_plan( state, *, phase="capture", require_file_limit=True, maximum_file=FILE_LIMIT, state_budget=STATE_BUDGET, minimum_free=MINIMUM_FREE, ): if phase not in {"capture", "resume", "mirror"} or any( type(v) is not int or v <= 0 for v in [maximum_file, state_budget, minimum_free] ): raise Refused("explicit_positive_capacity_profile_required") # New capture: dump, content, tar, ciphertext, readback, retained local copy, # and temporary rewritten inventory. Sealed retry needs three new large # files; mirror needs only its fresh ciphertext readback. slots = {"capture": 7, "resume": 3, "mirror": 1}[phase] state_reserve = slots * maximum_file + CONTROL_MARGIN # The existing command wrapper uses one anonymous output sink outside state. disk_reserve = state_reserve + maximum_file used, available, inodes = state_usage(state), disk_free(state), free_inodes(state) limit = active_file_limit() bounded = limit != resource.RLIM_INFINITY and 0 < limit <= maximum_file reasons = [] if used + state_reserve > state_budget: reasons.append("local_retention_budget_required") if available < minimum_free + disk_reserve: reasons.append("disk_headroom_required") if inodes < MINIMUM_INODES: reasons.append("inode_headroom_required") if require_file_limit and not bounded: reasons.append("bounded_file_executor_required") return { "schema": "coulomb.backup-capacity.v1", "phase": phase, "status": "refused" if reasons else ("ready" if bounded else "reviewed_unbounded_executor"), "reasons": reasons, "state_bytes": used, "state_budget_bytes": state_budget, "state_reserve_bytes": state_reserve, "disk_reserve_bytes": disk_reserve, "free_bytes": available, "minimum_free_bytes": minimum_free, "free_inodes": inodes, "minimum_free_inodes": MINIMUM_INODES, "maximum_new_file_bytes": maximum_file, "active_file_limit_bytes": limit, "bounded_file_executor": bounded, "state_modified": False, "retained_data_deleted": False, "provider_contacted": False, "filesystem_blocks_reserved": False, "limits_owner_admitted": False, } def require_capacity(state, *, phase="capture"): report = reserve_plan(state, phase=phase) if report["status"] != "ready": raise Refused("backup_capacity_review_required") return report def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--state-dir", type=Path, required=True) parser.add_argument("--phase", choices=["capture", "resume", "mirror"], default="capture") args = parser.parse_args() print( json.dumps( reserve_plan(args.state_dir, phase=args.phase, require_file_limit=False), sort_keys=True ) ) if __name__ == "__main__": try: main() except (ValueError, OSError): raise SystemExit("Capacity review refused; inspect private state locally.")