Files
7Z_Python_Script/old_01/compress_folder.py
T

330 lines
10 KiB
Python
Raw Normal View History

2026-09-13 19:48:16 +01:00
#!/usr/bin/env python3
"""
Folder Compressor using 7-Zip
Lists available folders, lets you select one, and compresses it
using 7-Zip with no compression (store) at fastest speed.
Real-time progress bar shown during compression.
"""
import os
import re
import sys
import shutil
import subprocess
import time
from pathlib import Path
# ── Terminal color helpers ────────────────────────────────────────────────────
SUPPORTS_COLOR = (
sys.stdout.isatty()
and (sys.platform != "win32" or os.environ.get("WT_SESSION") or os.environ.get("TERM"))
)
def _c(code: str, text: str) -> str:
return f"\033[{code}m{text}\033[0m" if SUPPORTS_COLOR else text
def green(t): return _c("32", t)
def yellow(t): return _c("33", t)
def cyan(t): return _c("36", t)
def bold(t): return _c("1", t)
def dim(t): return _c("2", t)
def red(t): return _c("31", t)
# ── Size / time formatters ────────────────────────────────────────────────────
def fmt_size(b: float) -> str:
for unit in ("B", "KB", "MB", "GB", "TB"):
if b < 1024:
return f"{b:.1f} {unit}"
b /= 1024
return f"{b:.1f} PB"
def fmt_time(seconds: float) -> str:
s = int(seconds)
if s < 60:
return f"{s}s"
m, s = divmod(s, 60)
if m < 60:
return f"{m}m {s:02d}s"
h, m = divmod(m, 60)
return f"{h}h {m:02d}m {s:02d}s"
# ── Folder size calculator ────────────────────────────────────────────────────
def folder_size(path: Path) -> int:
total = 0
for root, _, files in os.walk(path):
for f in files:
try:
total += os.path.getsize(os.path.join(root, f))
except OSError:
pass
return total
# ── 7-Zip locator ─────────────────────────────────────────────────────────────
def find_7zip() -> str | None:
for name in ("7z", "7za", "7zz", "7zip"):
path = shutil.which(name)
if path:
return path
if sys.platform == "win32":
for p in (
r"C:\Program Files\7-Zip\7z.exe",
r"C:\Program Files (x86)\7-Zip\7z.exe",
):
if os.path.isfile(p):
return p
return None
# ── Folder listing / selection ────────────────────────────────────────────────
def list_folders(base_dir: str) -> list[Path]:
base = Path(base_dir)
return sorted(
(e for e in base.iterdir() if e.is_dir()),
key=lambda p: p.name.lower(),
)
def prompt_folder_selection(folders: list[Path]) -> Path:
print("\nAvailable folders:\n")
for i, folder in enumerate(folders, start=1):
try:
sz = folder_size(folder)
size_label = dim(f" ({fmt_size(sz)})")
except PermissionError:
size_label = dim(" (no access)")
print(f" {dim(f'[{i:>3}]')} {folder.name}{size_label}")
print()
while True:
try:
raw = input("Enter the number of the folder to compress: ").strip()
idx = int(raw)
if 1 <= idx <= len(folders):
return folders[idx - 1]
print(f" Please enter a number between 1 and {len(folders)}.")
except ValueError:
print(" Invalid input — please enter a number.")
except (EOFError, KeyboardInterrupt):
print("\nAborted.")
sys.exit(0)
# ── Progress bar renderer ─────────────────────────────────────────────────────
BAR_WIDTH = 36
# Matches 7-Zip progress segments like "47% 3 + path/to/file.bin"
# or just "47%" when no file is on the same segment
_PROGRESS_RE = re.compile(r"(\d{1,3})%(?:\s+(\d+)\s*\+?\s*(.*))?")
# Split on ANY run of backspaces (\x08) or carriage returns (\r).
# - Linux/macOS 7-Zip uses \x08 to erase and rewrite the progress line
# - Windows 7-Zip uses \r to overwrite the progress line
# Both need to be handled so the script works cross-platform.
_SPLIT_RE = re.compile(rb"[\x08\r]+")
def _move_up(n: int):
if SUPPORTS_COLOR:
sys.stdout.write(f"\033[{n}A")
def _clear_line():
if SUPPORTS_COLOR:
sys.stdout.write("\033[2K\r")
else:
sys.stdout.write("\r" + " " * 78 + "\r")
def _redraw(pct: int, elapsed: float, files_done: int,
current_file: str, speed_bps: float):
"""Overwrite the two reserved progress lines in-place."""
filled = int(BAR_WIDTH * pct / 100)
bar = "█" * filled + "░" * (BAR_WIDTH - filled)
color = green if pct == 100 else cyan
speed_s = f"{fmt_size(speed_bps)}/s" if speed_bps > 0 else "─"
_move_up(2)
# Line 1 — bar + stats
_clear_line()
sys.stdout.write(
f" {color(bar)} {bold(f'{pct:>3}%')}"
f" {dim('elapsed')} {dim(fmt_time(elapsed))}"
f" {yellow(speed_s)}"
f" {dim(f'files: {files_done}')}\n"
)
# Line 2 — current file
_clear_line()
label = current_file.strip() or "starting…"
if len(label) > 60:
label = "…" + label[-59:]
sys.stdout.write(f" {dim('↳ ' + label)}\n")
sys.stdout.flush()
# ── Compression with real-time progress ───────────────────────────────────────
def compress_folder(seven_zip: str, folder: Path, output_dir: Path) -> Path:
archive_path = output_dir / (folder.name + ".7z")
print(f"\n {dim('Calculating source size…')} ", end="", flush=True)
src_size = folder_size(folder)
print(fmt_size(src_size))
cmd = [
seven_zip,
"a",
"-t7z",
"-mx=0", # store no compression
"-ms=off", # no solid archive
"-bsp1", # stream progress to stdout
str(archive_path),
str(folder),
]
print(f"\n {dim('Command: ' + ' '.join(cmd))}")
print()
print(" " + "─" * 64)
# Reserve two blank lines for the live display
print()
print()
start_time = time.monotonic()
last_pct = 0
files_done = 0
current_file = ""
speed_bps = 0.0
last_bytes = 0.0
speed_ts = start_time
proc = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
# Raw binary — we parse bytes ourselves to handle \x08 and \r
)
buf = b""
while True:
chunk = proc.stdout.read(256)
if not chunk:
break
buf += chunk
elapsed = time.monotonic() - start_time
# Split on runs of \x08 (Linux/macOS) or \r (Windows).
# Each segment between separators is one "visible write" from 7-Zip.
segments = _SPLIT_RE.split(buf)
# Keep the last (potentially incomplete) segment for the next iteration
buf = segments[-1]
for seg in segments[:-1]:
text = seg.decode("utf-8", errors="replace").strip()
if not text:
continue
m = _PROGRESS_RE.search(text)
if m:
pct = min(int(m.group(1)), 100)
files_done = int(m.group(2)) if m.group(2) else files_done
fname = (m.group(3) or "").strip()
if fname:
current_file = fname
# Rolling speed estimate (recalculate every 0.4 s)
if src_size > 0 and pct > 0:
bytes_done = src_size * pct / 100
now = time.monotonic()
dt = now - speed_ts
if dt >= 0.4:
speed_bps = (bytes_done - last_bytes) / dt
last_bytes = bytes_done
speed_ts = now
last_pct = pct
_redraw(pct, elapsed, files_done, current_file, speed_bps)
proc.wait()
elapsed_total = time.monotonic() - start_time
# Lock bar at 100% on success
if proc.returncode == 0:
_redraw(100, elapsed_total, files_done, current_file, speed_bps)
print()
print(" " + "─" * 64)
if proc.returncode == 0:
arc_size = archive_path.stat().st_size
avg_speed = src_size / elapsed_total if elapsed_total > 0 else 0
print(f"\n {green('✅ Archive created successfully!')}")
print(f" {bold('Path')} : {archive_path}")
print(f" {bold('Source')} : {fmt_size(src_size)}")
print(f" {bold('Archive')} : {fmt_size(arc_size)}")
print(f" {bold('Time')} : {fmt_time(elapsed_total)}")
print(f" {bold('Avg speed')}: {fmt_size(avg_speed)}/s")
else:
print(f"\n {red(f'❌ 7-Zip exited with code {proc.returncode}.')}")
sys.exit(proc.returncode)
return archive_path
# ── Entry point ───────────────────────────────────────────────────────────────
def main():
scan_dir = os.path.abspath(sys.argv[1] if len(sys.argv) > 1 else os.getcwd())
if not os.path.isdir(scan_dir):
print(f"Error: '{scan_dir}' is not a valid directory.")
sys.exit(1)
print(f"\n{bold('📦 Folder Compressor')}")
print(f" Scanning: {cyan(scan_dir)}\n")
seven_zip = find_7zip()
if not seven_zip:
print(
f"\n{red('❌ 7-Zip not found.')}\n"
" Install it and make sure '7z' (or '7za') is on your PATH.\n"
" • Linux : sudo apt install p7zip-full\n"
" • macOS : brew install p7zip\n"
" • Windows: https://www.7-zip.org/download.html"
)
sys.exit(1)
print(f" {green('✔')} 7-Zip: {dim(seven_zip)}")
folders = list_folders(scan_dir)
if not folders:
print(f"\nNo sub-folders found in '{scan_dir}'.")
sys.exit(0)
chosen = prompt_folder_selection(folders)
print(f"\n {green('✔')} Selected: {bold(chosen.name)}")
compress_folder(seven_zip, chosen, chosen.parent)
print()
if __name__ == "__main__":
main()