Files
7Z_Python_Script/old_01/compress_folder.py
T
2026-09-13 19:48:16 +01:00

330 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()